- Wrap token refresh logic in Prisma transaction - Use deleteMany instead of delete to handle race conditions gracefully Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
207 lines
5.3 KiB
TypeScript
207 lines
5.3 KiB
TypeScript
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,
|
|
tenantRfc: tenant.rfc,
|
|
},
|
|
};
|
|
}
|
|
|
|
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,
|
|
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<void> {
|
|
await prisma.refreshToken.deleteMany({
|
|
where: { token },
|
|
});
|
|
}
|