dorfteich/apps/api/src/common/zod-validation.pipe.ts
Claude Fable 5 bed9fc9307 Add authentication: signup, verification, sessions, password reset
AuthModule implements the M1 core as one coherent unit:

Signup (#13): POST signup/verify-email/resend-verification with shared
Zod validation (field-level error details), double opt-in via hashed
single-use tokens (24h, superseding reissue), registration_mode
enforcement, and per-IP rate limits.

Sessions (#14): opaque 32-byte cookie tokens stored as SHA-256 row
ids, sliding 30-day expiry (refresh at most hourly), global AuthGuard
with @Public() opt-out attaching the user to every request, CSRF
origin check on mutating requests, per-account login backoff (5/15min,
reset on success), generic 401 for wrong-vs-unknown credentials,
logout with immediate invalidation, GET /auth/me.

Reset (#15): forgot-password without account enumeration, one-hour
single-use tokens, reset destroys all existing sessions.

A 14-case supertest e2e suite drives every flow against the test
database, reading verification/reset links from the mail outbox.

Closes #13
Closes #14
Closes #15

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 05:22:31 +02:00

25 lines
903 B
TypeScript

import { BadRequestException, Injectable, PipeTransform } from '@nestjs/common';
import type { ZodTypeAny, z } from 'zod';
/**
* Validates request bodies against a shared Zod schema. Failures become a
* 400 with field-level details: { field: [i18nKey, …] } — the web client
* renders the keys next to the matching inputs.
*/
@Injectable()
export class ZodValidationPipe<Schema extends ZodTypeAny> implements PipeTransform {
constructor(private readonly schema: Schema) {}
transform(value: unknown): z.infer<Schema> {
const result = this.schema.safeParse(value);
if (result.success) return result.data;
const details: Record<string, string[]> = {};
for (const issue of result.error.issues) {
const field = issue.path.join('.') || '_';
(details[field] ??= []).push(issue.message);
}
throw new BadRequestException({ code: 'bad_request', details });
}
}