feat: implement JWT authentication system

Add complete authentication infrastructure including:
- Password hashing utilities with bcrypt
- JWT token generation and verification
- Auth service with register, login, refresh, and logout
- Auth controller with Zod validation
- Auth middleware for route protection
- Auth routes mounted at /api/auth

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Consultoria AS
2026-01-22 01:54:13 +00:00
parent 18bdb24478
commit e54019ba01
7 changed files with 378 additions and 2 deletions

View File

@@ -0,0 +1,200 @@
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<LoginResponse> {
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,
},
};
}
export async function login(data: LoginRequest): Promise<LoginResponse> {
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,
},
};
}
export async function refreshTokens(token: string): Promise<{ accessToken: string; refreshToken: string }> {
const storedToken = await prisma.refreshToken.findUnique({
where: { token },
});
if (!storedToken) {
throw new AppError(401, 'Token inválido');
}
if (storedToken.expiresAt < new Date()) {
await prisma.refreshToken.delete({ where: { id: storedToken.id } });
throw new AppError(401, 'Token expirado');
}
const payload = verifyToken(token);
const user = await prisma.user.findUnique({
where: { id: payload.userId },
include: { tenant: true },
});
if (!user || !user.active) {
throw new AppError(401, 'Usuario no encontrado o desactivado');
}
await prisma.refreshToken.delete({ 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 prisma.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<void> {
await prisma.refreshToken.deleteMany({
where: { token },
});
}