From a9bc82fac52db6f8b8668409a7b44d1df1b9d856 Mon Sep 17 00:00:00 2001 From: BOHA Date: Fri, 24 Apr 2026 11:18:38 +0200 Subject: [PATCH] fix: Prisma $queryRaw MySQL type coercion for BigInt and Boolean $queryRaw on MySQL returns BigInt for integer columns and 0/1 for booleans. Passing these raw values back to Prisma client methods causes validation errors: - Expected Int, provided BigInt - Expected Boolean, provided Int Fixed in auth refresh, TOTP login, and TOTP backup code flows by wrapping storedToken.id, storedToken.user_id with Number() and remember_me with Boolean(). Co-Authored-By: Claude Opus 4.7 --- src/routes/admin/auth.ts | 8 ++++++-- src/routes/admin/totp.ts | 8 ++++++-- src/services/auth.ts | 15 +++++++++++---- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/routes/admin/auth.ts b/src/routes/admin/auth.ts index 2e2bad3..8625f28 100644 --- a/src/routes/admin/auth.ts +++ b/src/routes/admin/auth.ts @@ -126,10 +126,14 @@ export default async function authRoutes( return { error: "Neplatný nebo expirovaný login token", status: 401 }; } - await tx.totp_login_tokens.delete({ where: { id: storedToken.id } }); + // $queryRaw on MySQL may return BigInt for integer columns + const storedTokenId = Number(storedToken.id); + const storedUserId = Number(storedToken.user_id); + + await tx.totp_login_tokens.delete({ where: { id: storedTokenId } }); const user = await tx.users.findUnique({ - where: { id: storedToken.user_id }, + where: { id: storedUserId }, include: { roles: true }, }); diff --git a/src/routes/admin/totp.ts b/src/routes/admin/totp.ts index 6fa04d2..81f17d6 100644 --- a/src/routes/admin/totp.ts +++ b/src/routes/admin/totp.ts @@ -282,8 +282,12 @@ export default async function totpRoutes( return { error: "Neplatný nebo expirovaný login token", status: 401 }; } + // $queryRaw on MySQL may return BigInt for integer columns + const storedTokenId = Number(storedToken.id); + const storedUserId = Number(storedToken.user_id); + const user = await tx.users.findUnique({ - where: { id: storedToken.user_id }, + where: { id: storedUserId }, include: { roles: true }, }); @@ -315,7 +319,7 @@ export default async function totpRoutes( const newFailedAttempts = (user.failed_login_attempts ?? 0) + 1; if (newFailedAttempts >= settings.max_login_attempts) { await tx.totp_login_tokens.delete({ - where: { id: storedToken.id }, + where: { id: storedTokenId }, }); await tx.users.update({ where: { id: user.id }, diff --git a/src/services/auth.ts b/src/services/auth.ts index c10d167..2e70541 100644 --- a/src/services/auth.ts +++ b/src/services/auth.ts @@ -274,7 +274,11 @@ export async function refreshAccessToken( return { type: "error", message: "Neplatný refresh token", status: 401 }; } - const authData = await loadAuthData(storedToken.user_id); + // $queryRaw on MySQL may return BigInt for integer columns + const storedTokenId = Number(storedToken.id); + const storedUserId = Number(storedToken.user_id); + + const authData = await loadAuthData(storedUserId); if (!authData) { return { type: "error", message: "Uživatel nenalezen", status: 401 }; } @@ -286,16 +290,19 @@ export async function refreshAccessToken( ? config.jwt.refreshTokenRememberExpiry : config.jwt.refreshTokenSessionExpiry; + // $queryRaw on MySQL returns 0/1 for booleans; Prisma expects true/false + const rememberMe = Boolean(storedToken.remember_me); + await tx.refresh_tokens.update({ - where: { id: storedToken.id }, + where: { id: storedTokenId }, data: { replaced_at: new Date(), replaced_by_hash: newRefreshTokenHash }, }); await tx.refresh_tokens.create({ data: { - user_id: storedToken.user_id, + user_id: storedUserId, token_hash: newRefreshTokenHash, expires_at: new Date(Date.now() + expiresIn * 1000), - remember_me: storedToken.remember_me ?? false, + remember_me: rememberMe, ip_address: request.ip, user_agent: request.headers["user-agent"] ?? null, },