fix: use transaction in refreshTokens to prevent race conditions

- 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>
This commit is contained in:
Consultoria AS
2026-01-31 03:09:32 +00:00
parent ba012254db
commit 1fe462764f

View File

@@ -147,52 +147,56 @@ export async function login(data: LoginRequest): Promise<LoginResponse> {
}
export async function refreshTokens(token: string): Promise<{ accessToken: string; refreshToken: string }> {
const storedToken = await prisma.refreshToken.findUnique({
where: { token },
});
// 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) {
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');
}
if (storedToken.expiresAt < new Date()) {
await tx.refreshToken.deleteMany({ where: { id: storedToken.id } });
throw new AppError(401, 'Token expirado');
}
const payload = verifyToken(token);
const payload = verifyToken(token);
const user = await prisma.user.findUnique({
where: { id: payload.userId },
include: { tenant: true },
});
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');
}
if (!user || !user.active) {
throw new AppError(401, 'Usuario no encontrado o desactivado');
}
await prisma.refreshToken.delete({ where: { id: storedToken.id } });
// 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 prisma.refreshToken.create({
data: {
const newTokenPayload = {
userId: user.id,
token: refreshToken,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
},
});
email: user.email,
role: user.role,
tenantId: user.tenantId,
schemaName: user.tenant.schemaName,
};
return { accessToken, refreshToken };
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> {