security: fix all Critical and High findings from FLAWS_REPORT audit

- Auth: pessimistic locking on login tokens and refresh token rotation,
  backup code attempt counter, rate limiting verification
- Schema: unique constraints on business numbers, FK relations,
  unsigned/signed alignment, attendance duplicate prevention
- Invoices/PDFs: DOMPurify sanitization, bounded queries in stats
  and alerts, VAT rounding, Puppeteer error handling
- Orders/Offers: transactional parent+child creation, Zod NaN
  refinement, status enums, uniqueness checks
- Projects/Files: path traversal protection, streamed uploads,
  permission guards, query param validation
- Attendance/HR: duplicate checks, ownership validation, GPS
  restrictions, trip distance validation
- Frontend: modal lock reference counting, XSS escaping in print
  HTML, ref mutation fixes, accessibility attributes

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-04-24 00:58:35 +02:00
parent 122eee175e
commit 528e55991b
57 changed files with 2355 additions and 1010 deletions

View File

@@ -92,7 +92,15 @@ export default async function authRoutes(
// POST /api/admin/login/totp
fastify.post<{ Body: TotpVerifyRequest }>(
"/login/totp",
{ bodyLimit: 10240 },
{
config: {
rateLimit: {
max: 20,
timeWindow: "1 minute",
},
},
bodyLimit: 10240,
},
async (request, reply) => {
const parsed = parseBody(TotpVerifySchema, request.body);
if ("error" in parsed) return error(reply, parsed.error, 400);
@@ -106,20 +114,42 @@ export default async function authRoutes(
.update(login_token)
.digest("hex");
const storedToken = await prisma.totp_login_tokens.findFirst({
where: { token_hash: tokenHash },
const totpResult = await prisma.$transaction(async (tx) => {
const tokens = await tx.$queryRaw<
Array<{ id: number; user_id: number; expires_at: Date }>
>`
SELECT id, user_id, expires_at FROM totp_login_tokens WHERE token_hash = ${tokenHash} FOR UPDATE
`;
const storedToken = tokens[0] ?? null;
if (!storedToken || new Date(storedToken.expires_at) < new Date()) {
return { error: "Neplatný nebo expirovaný login token", status: 401 };
}
await tx.totp_login_tokens.delete({ where: { id: storedToken.id } });
const user = await tx.users.findUnique({
where: { id: storedToken.user_id },
include: { roles: true },
});
if (!user || !user.totp_secret) {
return { error: "Uživatel nenalezen", status: 401 };
}
if (user.locked_until && new Date(user.locked_until) > new Date()) {
return { error: "Účet je dočasně uzamčen", status: 429 };
}
return { user };
});
if (!storedToken || new Date(storedToken.expires_at) < new Date()) {
return error(reply, "Neplatný nebo expirovaný login token", 401);
if ("error" in totpResult) {
return error(reply, totpResult.error!, totpResult.status!);
}
const user = await prisma.users.findUnique({
where: { id: storedToken.user_id },
include: { roles: true },
});
if (!user || !user.totp_secret) {
const user = totpResult.user;
if (!user.totp_secret) {
return error(reply, "Uživatel nenalezen", 401);
}
@@ -128,8 +158,6 @@ export default async function authRoutes(
return error(reply, "Neplatný TOTP kód", 401);
}
await prisma.totp_login_tokens.delete({ where: { id: storedToken.id } });
// Reset failed attempts and update last login (TOTP verified = successful login)
await prisma.users.update({
where: { id: user.id },
@@ -186,31 +214,43 @@ export default async function authRoutes(
);
// POST /api/admin/refresh
fastify.post("/refresh", { bodyLimit: 10240 }, async (request, reply) => {
const refreshTokenRaw = request.cookies.refresh_token;
if (!refreshTokenRaw) {
return error(reply, "Refresh token chybí", 401);
}
fastify.post(
"/refresh",
{
config: {
rateLimit: {
max: 10,
timeWindow: "1 minute",
},
},
bodyLimit: 10240,
},
async (request, reply) => {
const refreshTokenRaw = request.cookies.refresh_token;
if (!refreshTokenRaw) {
return error(reply, "Refresh token chybí", 401);
}
const result = await refreshAccessToken(refreshTokenRaw, request);
const result = await refreshAccessToken(refreshTokenRaw, request);
if (result.type === "error") {
reply.clearCookie("refresh_token", {
path: "/api/admin",
httpOnly: true,
secure: config.isProduction,
sameSite: "strict",
if (result.type === "error") {
reply.clearCookie("refresh_token", {
path: "/api/admin",
httpOnly: true,
secure: config.isProduction,
sameSite: "strict",
});
return error(reply, result.message, result.status);
}
// Preserve the original remember_me flag so long-lived sessions stay long-lived after rotation
setRefreshCookie(reply, result.refreshToken, result.rememberMe);
return success(reply, {
access_token: result.accessToken,
user: result.user,
});
return error(reply, result.message, result.status);
}
// Preserve the original remember_me flag so long-lived sessions stay long-lived after rotation
setRefreshCookie(reply, result.refreshToken, result.rememberMe);
return success(reply, {
access_token: result.accessToken,
user: result.user,
});
});
},
);
// POST /api/admin/logout
fastify.post("/logout", async (request, reply) => {