feat(auth): login JWT en API y las 4 apps

- Modelo Usuario + seed desde lista de vendedores (18 usuarios, bcrypt cost 12)
- POST /api/auth/login + /api/auth/me; middleware Bearer en todo /api
  excepto health, login y webhook Monday; flag AUTH_ENABLED
- Username = nombre completo normalizado (case/acentos insensible),
  password inicial Frasa2026, admin vía ADMIN_PASSWORD
- Apps: LoginScreen + AuthGate (StudioBlank), token en localStorage,
  header Authorization en todas las llamadas, logout en 401
- Tests: api 136, cotizador 71, alta 65, cobranza 71, agente-ia 13, landing 4
This commit is contained in:
2026-08-27 08:20:25 +00:00
parent b39fe6334f
commit de11af985e
40 changed files with 2085 additions and 38 deletions

View File

@@ -0,0 +1,53 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { upsertUsuario, type DatosUsuarioSeed } from './seed-usuarios.js'
// ---------- Prisma mockeado (seed idempotente sin red ni BD) ----------
const prismaMock = vi.hoisted(() => ({
usuario: {
findUnique: vi.fn(),
create: vi.fn(),
update: vi.fn(),
count: vi.fn(),
},
$disconnect: vi.fn(),
}))
vi.mock('../src/services/prisma.js', () => ({ prisma: prismaMock }))
const datos: DatosUsuarioSeed = {
username: 'erick israel pena ramos',
nombre: 'ERICK ISRAEL PEÑA RAMOS',
rol: 'vendedor',
asignacion: 'MONTEBELLO',
empleadoId: 13676,
passwordHash: 'hash-inicial',
}
beforeEach(() => {
vi.clearAllMocks()
})
describe('upsertUsuario (seed idempotente)', () => {
it('crea el usuario si no existe', async () => {
prismaMock.usuario.findUnique.mockResolvedValue(null)
const resultado = await upsertUsuario(datos, false)
expect(resultado).toBe('creado')
expect(prismaMock.usuario.create).toHaveBeenCalledWith({ data: datos })
})
it('si ya existe actualiza datos sin tocar el password', async () => {
prismaMock.usuario.findUnique.mockResolvedValue({ id: 'u-1' })
const resultado = await upsertUsuario({ ...datos, passwordHash: 'hash-nuevo' }, false)
expect(resultado).toBe('actualizado')
const update = prismaMock.usuario.update.mock.calls[0][0]
expect(update.data).not.toHaveProperty('passwordHash')
})
it('con --reset-passwords sí restablece el password', async () => {
prismaMock.usuario.findUnique.mockResolvedValue({ id: 'u-1' })
await upsertUsuario({ ...datos, passwordHash: 'hash-nuevo' }, true)
const update = prismaMock.usuario.update.mock.calls[0][0]
expect(update.data.passwordHash).toBe('hash-nuevo')
})
})

View File

@@ -0,0 +1,123 @@
/**
* Siembra los usuarios del ecosistema FRASA.
*
* - Lee reference/vendedores.json (gitignored). Si no existe, solo crea el
* usuario admin y avisa.
* - Username = nombre completo normalizado (minúsculas, sin acentos, espacios
* simples). Password inicial de todos: 'Frasa2026' (bcrypt cost 12).
* - Upsert por username: NO resetea el password de usuarios existentes.
* Con `--reset-passwords` sí lo restablece al inicial.
* - Admin: username 'admin', password de env ADMIN_PASSWORD (default
* 'Frasa2026'), rol 'admin'.
*
* Uso: npm run seed:usuarios [-- --reset-passwords]
*/
import fs from 'node:fs'
import path from 'node:path'
import bcrypt from 'bcryptjs'
import { prisma } from '../src/services/prisma.js'
import { normalizarUsername } from '../src/services/auth.service.js'
import { config } from '../src/config/index.js'
const PASSWORD_INICIAL = 'Frasa2026'
const BCRYPT_COST = 12
export interface DatosUsuarioSeed {
username: string
nombre: string
rol: string
asignacion: string | null
empleadoId: number | null
passwordHash: string
}
/**
* Upsert por username. NO toca el password de un usuario existente salvo que
* resetPasswords sea true (flag --reset-passwords).
*/
export async function upsertUsuario(
datos: DatosUsuarioSeed,
resetPasswords: boolean,
): Promise<'creado' | 'actualizado'> {
const existente = await prisma.usuario.findUnique({ where: { username: datos.username } })
if (!existente) {
await prisma.usuario.create({ data: datos })
return 'creado'
}
await prisma.usuario.update({
where: { username: datos.username },
data: {
nombre: datos.nombre,
rol: datos.rol,
asignacion: datos.asignacion,
empleadoId: datos.empleadoId,
...(resetPasswords ? { passwordHash: datos.passwordHash } : {}),
},
})
return 'actualizado'
}
interface VendedorJson {
empleadoId: number | null
nombre: string
asignacion: string | null
rol: string
}
async function main(): Promise<void> {
const resetPasswords = process.argv.includes('--reset-passwords')
const rutaVendedores = path.resolve(process.cwd(), '..', 'reference', 'vendedores.json')
const hashVendedores = bcrypt.hashSync(PASSWORD_INICIAL, BCRYPT_COST)
const hashAdmin = bcrypt.hashSync(config.auth.adminPassword, BCRYPT_COST)
let creados = 0
let actualizados = 0
if (fs.existsSync(rutaVendedores)) {
const json = JSON.parse(fs.readFileSync(rutaVendedores, 'utf8')) as { vendedores: VendedorJson[] }
for (const v of json.vendedores) {
const resultado = await upsertUsuario(
{
username: normalizarUsername(v.nombre),
nombre: v.nombre,
rol: v.rol,
asignacion: v.asignacion,
empleadoId: v.empleadoId,
passwordHash: hashVendedores,
},
resetPasswords,
)
if (resultado === 'creado') creados++
else actualizados++
}
console.log(`[seed-usuarios] vendedores: ${creados} creados, ${actualizados} actualizados`)
} else {
console.warn(`[seed-usuarios] AVISO: no existe ${rutaVendedores}; se crea solo el admin`)
}
const admin = await upsertUsuario(
{
username: 'admin',
nombre: 'Administrador FRASA',
rol: 'admin',
asignacion: null,
empleadoId: null,
passwordHash: hashAdmin,
},
resetPasswords,
)
console.log(`[seed-usuarios] admin ${admin}${resetPasswords ? ' (password restablecido)' : ''}`)
const total = await prisma.usuario.count()
console.log(`[seed-usuarios] total de usuarios en BD: ${total}`)
}
// Solo se ejecuta como script (npm run seed:usuarios); al importarlo en tests no corre
if (process.argv[1]?.endsWith('seed-usuarios.ts')) {
main()
.catch((err) => {
console.error('[seed-usuarios] falló:', err)
process.exitCode = 1
})
.finally(() => prisma.$disconnect())
}