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:
53
api-gateway/scripts/seed-usuarios.test.ts
Normal file
53
api-gateway/scripts/seed-usuarios.test.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
123
api-gateway/scripts/seed-usuarios.ts
Normal file
123
api-gateway/scripts/seed-usuarios.ts
Normal 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())
|
||||
}
|
||||
Reference in New Issue
Block a user