feat(identity): F-005 register/login/logout with argon2 sessions and rate limiting

- Hexagonal identity module: domain ports, use cases, argon2id hasher, pg repos
- Migration 002_identity: identity_users + identity_sessions (token hash only)
- Opaque 512-bit session tokens; DB stores SHA-256 hash; 7-day TTL in SQL
- Cookie HttpOnly + Secure (COOKIE_SECURE, default true) + SameSite=Lax
- LoginRateLimiter: 10 failures -> 429 + Retry-After, 15-min cooldown
- Anti-enumeration: identical generic 401 + dummy-hash timing equalization
- buildApp gains optional pool/cookieSecure; foundation-only app preserved
- 47 unit + 14 integration tests; live smoke covers all acceptance criteria
This commit is contained in:
rikrdo
2026-08-14 22:58:32 +02:00
parent 4851692031
commit 75293f39bc
46 changed files with 1734 additions and 54 deletions

View File

@@ -7,7 +7,9 @@ import { performance } from 'node:perf_hooks';
import Fastify, { type FastifyInstance } from 'fastify';
import type { FastifyError, FastifyReply, FastifyRequest } from 'fastify';
import type { IncomingMessage } from 'node:http';
import type pg from 'pg';
import { registerHealthRoutes } from '../modules/health/index.js';
import { registerIdentityRoutes } from '../modules/identity/index.js';
import { createFlagStore, type FeatureFlagProvider } from '../modules/flags/index.js';
import { AppError, errorEnvelope } from '../shared/errors.js';
import { createLogger, type Logger } from '../infrastructure/logging/logger.js';
@@ -26,6 +28,10 @@ export interface BuildAppDeps {
logger?: Logger;
/** Feature flags. Default: empty store, every flag OFF (fail-safe). */
flags?: FeatureFlagProvider;
/** Database pool. When present, DB-backed modules (identity) are wired. */
pool?: pg.Pool;
/** Secure cookie flag forwarded to identity routes. */
cookieSecure?: boolean;
}
function generateRequestId(raw: IncomingMessage): string {
@@ -107,5 +113,14 @@ export async function buildApp(deps: BuildAppDeps = {}): Promise<FastifyInstance
await registerHealthRoutes(instance);
});
if (deps.pool) {
await app.register(async (instance) => {
await registerIdentityRoutes(instance, {
pool: deps.pool as pg.Pool,
cookieSecure: deps.cookieSecure,
});
});
}
return app;
}