feat(F-006): users profile, addresses and RBAC

- users module: profile + address CRUD behind use cases (users_profiles,
  users_addresses)
- roles customer/admin on identity_users; role resolved from DB per request
- shared auth contract (Authenticate, requireRole, requireOwnerOrAdmin)
  injected from composition root; users never imports identity
- authorization runs before existence checks; address SQL scoped by user_id
- @fastify/cookie registered once at app root (cross-module)
- migrations 003_identity_roles + 004_users (reversible)
- no new npm dependencies; tests: unit 52, integration 22

Gates: reviewer/security/qa APPROVED; verify.sh green
This commit is contained in:
rikrdo
2026-08-15 09:27:38 +02:00
parent 75293f39bc
commit 546971280f
37 changed files with 1732 additions and 161 deletions

View File

@@ -5,11 +5,13 @@
import { randomUUID } from 'node:crypto';
import { performance } from 'node:perf_hooks';
import Fastify, { type FastifyInstance } from 'fastify';
import fastifyCookie from '@fastify/cookie';
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 { registerIdentityRoutes, createSessionAuthenticator } from '../modules/identity/index.js';
import { registerUsersRoutes } from '../modules/users/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';
@@ -113,6 +115,9 @@ export async function buildApp(deps: BuildAppDeps = {}): Promise<FastifyInstance
await registerHealthRoutes(instance);
});
// Cookie infrastructure is cross-module (identity + users): register once at root.
await app.register(fastifyCookie);
if (deps.pool) {
await app.register(async (instance) => {
await registerIdentityRoutes(instance, {
@@ -120,6 +125,16 @@ export async function buildApp(deps: BuildAppDeps = {}): Promise<FastifyInstance
cookieSecure: deps.cookieSecure,
});
});
// Session resolution is identity's; users receives it by injection so no
// module ever imports another module.
const authenticate = createSessionAuthenticator(deps.pool);
await app.register(async (instance) => {
await registerUsersRoutes(instance, {
pool: deps.pool as pg.Pool,
authenticate,
});
});
}
return app;