feat: add Zod validation for auth endpoints
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
12
package-lock.json
generated
12
package-lock.json
generated
@@ -29,7 +29,8 @@
|
||||
"react-datepicker": "^9.1.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-quill-new": "^3.8.3",
|
||||
"react-router-dom": "^6.30.3"
|
||||
"react-router-dom": "^6.30.3",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
@@ -3748,6 +3749,15 @@
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,8 @@
|
||||
"react-datepicker": "^9.1.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-quill-new": "^3.8.3",
|
||||
"react-router-dom": "^6.30.3"
|
||||
"react-router-dom": "^6.30.3",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
|
||||
@@ -7,6 +7,8 @@ import { LoginRequest, TotpVerifyRequest } from '../../types';
|
||||
import prisma from '../../config/database';
|
||||
import crypto from 'crypto';
|
||||
import { OTPAuth } from '../../utils/totp';
|
||||
import { parseBody } from '../../schemas/common';
|
||||
import { LoginSchema, TotpVerifySchema } from '../../schemas/auth.schema';
|
||||
|
||||
function setRefreshCookie(reply: import('fastify').FastifyReply, token: string, rememberMe: boolean) {
|
||||
const maxAge = rememberMe
|
||||
@@ -33,13 +35,11 @@ export default async function authRoutes(fastify: FastifyInstance): Promise<void
|
||||
},
|
||||
bodyLimit: 10240,
|
||||
}, async (request, reply) => {
|
||||
const { username, password, remember_me } = request.body;
|
||||
const parsed = parseBody(LoginSchema, request.body);
|
||||
if ('error' in parsed) return error(reply, parsed.error, 400);
|
||||
const { username, password, remember_me } = parsed.data;
|
||||
|
||||
if (!username || !password) {
|
||||
return error(reply, 'Uživatelské jméno a heslo jsou povinné', 400);
|
||||
}
|
||||
|
||||
const result = await login(username, password, !!remember_me, request);
|
||||
const result = await login(username, password, remember_me, request);
|
||||
|
||||
if (result.type === 'error') {
|
||||
await logAudit({
|
||||
@@ -64,7 +64,7 @@ export default async function authRoutes(fastify: FastifyInstance): Promise<void
|
||||
description: `Přihlášení uživatele ${result.user.username}`,
|
||||
});
|
||||
|
||||
setRefreshCookie(reply, result.refreshToken, !!remember_me);
|
||||
setRefreshCookie(reply, result.refreshToken, remember_me);
|
||||
return success(reply, {
|
||||
access_token: result.accessToken,
|
||||
user: result.user,
|
||||
@@ -73,11 +73,9 @@ export default async function authRoutes(fastify: FastifyInstance): Promise<void
|
||||
|
||||
// POST /api/admin/login/totp
|
||||
fastify.post<{ Body: TotpVerifyRequest }>('/login/totp', { bodyLimit: 10240 }, async (request, reply) => {
|
||||
const { login_token, totp_code } = request.body;
|
||||
|
||||
if (!login_token || !totp_code) {
|
||||
return error(reply, 'Login token a TOTP kód jsou povinné', 400);
|
||||
}
|
||||
const parsed = parseBody(TotpVerifySchema, request.body);
|
||||
if ('error' in parsed) return error(reply, parsed.error, 400);
|
||||
const { login_token, totp_code } = parsed.data;
|
||||
|
||||
const tokenHash = crypto.createHash('sha256').update(login_token).digest('hex');
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ import { encrypt } from '../../utils/encryption';
|
||||
import { OTPAuth } from '../../utils/totp';
|
||||
import * as OTPAuthLib from 'otpauth';
|
||||
import { logAudit } from '../../services/audit';
|
||||
import { parseBody } from '../../schemas/common';
|
||||
import { TotpBackupSchema } from '../../schemas/auth.schema';
|
||||
|
||||
export default async function totpRoutes(fastify: FastifyInstance): Promise<void> {
|
||||
// GET - generate new TOTP secret
|
||||
@@ -138,14 +140,11 @@ export default async function totpRoutes(fastify: FastifyInstance): Promise<void
|
||||
|
||||
// POST - verify backup code (pre-auth, no requireAuth)
|
||||
fastify.post('/backup-verify', { bodyLimit: 10240 }, async (request, reply) => {
|
||||
const body = request.body as Record<string, unknown>;
|
||||
const { login_token, code } = body;
|
||||
const parsed = parseBody(TotpBackupSchema, request.body);
|
||||
if ('error' in parsed) return error(reply, parsed.error, 400);
|
||||
const { login_token, backup_code: code } = parsed.data;
|
||||
|
||||
if (!login_token || !code) {
|
||||
return error(reply, 'Login token a záložní kód jsou povinné', 400);
|
||||
}
|
||||
|
||||
const tokenHash = crypto.createHash('sha256').update(String(login_token)).digest('hex');
|
||||
const tokenHash = crypto.createHash('sha256').update(login_token).digest('hex');
|
||||
|
||||
const storedToken = await prisma.totp_login_tokens.findFirst({
|
||||
where: { token_hash: tokenHash },
|
||||
|
||||
21
src/schemas/auth.schema.ts
Normal file
21
src/schemas/auth.schema.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const LoginSchema = z.object({
|
||||
username: z.string().min(1, 'Uživatelské jméno je povinné'),
|
||||
password: z.string().min(1, 'Heslo je povinné'),
|
||||
remember_me: z.boolean().optional().default(false),
|
||||
});
|
||||
|
||||
export const TotpVerifySchema = z.object({
|
||||
login_token: z.string().min(1, 'Token je povinný'),
|
||||
totp_code: z.string().length(6, 'Kód musí mít 6 číslic'),
|
||||
});
|
||||
|
||||
export const TotpBackupSchema = z.object({
|
||||
login_token: z.string().min(1, 'Token je povinný'),
|
||||
backup_code: z.string().min(1, 'Záložní kód je povinný'),
|
||||
});
|
||||
|
||||
export type LoginInput = z.infer<typeof LoginSchema>;
|
||||
export type TotpVerifyInput = z.infer<typeof TotpVerifySchema>;
|
||||
export type TotpBackupInput = z.infer<typeof TotpBackupSchema>;
|
||||
12
src/schemas/common.ts
Normal file
12
src/schemas/common.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { ZodSchema, ZodError } from 'zod';
|
||||
|
||||
export function parseBody<T>(schema: ZodSchema<T>, body: unknown): { data: T } | { error: string } {
|
||||
try {
|
||||
return { data: schema.parse(body) };
|
||||
} catch (e) {
|
||||
if (e instanceof ZodError) {
|
||||
return { error: e.errors.map(err => err.message).join(', ') };
|
||||
}
|
||||
return { error: 'Neplatný požadavek' };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user