import { prisma } from '../config/database.js'; import { hashPassword, verifyPassword } from '../utils/password.js'; import { generateAccessToken, generateRefreshToken, verifyToken } from '../utils/token.js'; import { createTenantSchema } from '../utils/schema-manager.js'; import { AppError } from '../middlewares/error.middleware.js'; import { PLANS } from '@horux/shared'; import type { LoginRequest, RegisterRequest, LoginResponse } from '@horux/shared'; export async function register(data: RegisterRequest): Promise { const existingUser = await prisma.user.findUnique({ where: { email: data.usuario.email }, }); if (existingUser) { throw new AppError(400, 'El email ya está registrado'); } const existingTenant = await prisma.tenant.findUnique({ where: { rfc: data.empresa.rfc }, }); if (existingTenant) { throw new AppError(400, 'El RFC ya está registrado'); } const schemaName = `tenant_${data.empresa.rfc.toLowerCase().replace(/[^a-z0-9]/g, '')}`; const tenant = await prisma.tenant.create({ data: { nombre: data.empresa.nombre, rfc: data.empresa.rfc.toUpperCase(), plan: 'starter', schemaName, cfdiLimit: PLANS.starter.cfdiLimit, usersLimit: PLANS.starter.usersLimit, }, }); await createTenantSchema(schemaName); const passwordHash = await hashPassword(data.usuario.password); const user = await prisma.user.create({ data: { tenantId: tenant.id, email: data.usuario.email.toLowerCase(), passwordHash, nombre: data.usuario.nombre, role: 'admin', }, }); const tokenPayload = { userId: user.id, email: user.email, role: user.role, tenantId: tenant.id, schemaName: tenant.schemaName, }; const accessToken = generateAccessToken(tokenPayload); const refreshToken = generateRefreshToken(tokenPayload); await prisma.refreshToken.create({ data: { userId: user.id, token: refreshToken, expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), }, }); return { accessToken, refreshToken, user: { id: user.id, email: user.email, nombre: user.nombre, role: user.role, tenantId: tenant.id, tenantName: tenant.nombre, tenantRfc: tenant.rfc, }, }; } export async function login(data: LoginRequest): Promise { const user = await prisma.user.findUnique({ where: { email: data.email.toLowerCase() }, include: { tenant: true }, }); if (!user) { throw new AppError(401, 'Credenciales inválidas'); } if (!user.active) { throw new AppError(401, 'Usuario desactivado'); } if (!user.tenant.active) { throw new AppError(401, 'Empresa desactivada'); } const isValidPassword = await verifyPassword(data.password, user.passwordHash); if (!isValidPassword) { throw new AppError(401, 'Credenciales inválidas'); } await prisma.user.update({ where: { id: user.id }, data: { lastLogin: new Date() }, }); const tokenPayload = { userId: user.id, email: user.email, role: user.role, tenantId: user.tenantId, schemaName: user.tenant.schemaName, }; const accessToken = generateAccessToken(tokenPayload); const refreshToken = generateRefreshToken(tokenPayload); await prisma.refreshToken.create({ data: { userId: user.id, token: refreshToken, expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), }, }); return { accessToken, refreshToken, user: { id: user.id, email: user.email, nombre: user.nombre, role: user.role, tenantId: user.tenantId, tenantName: user.tenant.nombre, tenantRfc: user.tenant.rfc, }, }; } export async function refreshTokens(token: string): Promise<{ accessToken: string; refreshToken: string }> { // Use a transaction to prevent race conditions return await prisma.$transaction(async (tx) => { const storedToken = await tx.refreshToken.findUnique({ where: { token }, }); if (!storedToken) { throw new AppError(401, 'Token inválido'); } if (storedToken.expiresAt < new Date()) { await tx.refreshToken.deleteMany({ where: { id: storedToken.id } }); throw new AppError(401, 'Token expirado'); } const payload = verifyToken(token); const user = await tx.user.findUnique({ where: { id: payload.userId }, include: { tenant: true }, }); if (!user || !user.active) { throw new AppError(401, 'Usuario no encontrado o desactivado'); } // Use deleteMany to avoid error if already deleted (race condition) await tx.refreshToken.deleteMany({ where: { id: storedToken.id } }); const newTokenPayload = { userId: user.id, email: user.email, role: user.role, tenantId: user.tenantId, schemaName: user.tenant.schemaName, }; const accessToken = generateAccessToken(newTokenPayload); const refreshToken = generateRefreshToken(newTokenPayload); await tx.refreshToken.create({ data: { userId: user.id, token: refreshToken, expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), }, }); return { accessToken, refreshToken }; }); } export async function logout(token: string): Promise { await prisma.refreshToken.deleteMany({ where: { token }, }); }