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,12 +5,14 @@
import type pg from 'pg';
import type { UserRepository } from '../domain/ports.js';
import type { NewUser, User } from '../domain/user.js';
import type { Role } from '../../../shared/auth.js';
import { EmailAlreadyRegisteredError } from '../domain/errors.js';
interface UserRow {
id: string;
email: string;
password_hash: string;
role: Role;
created_at: Date;
}
@@ -24,14 +26,14 @@ export class PgUserRepository implements UserRepository {
const result = await this.pool.query<UserRow>(
`INSERT INTO identity_users (email, password_hash)
VALUES ($1, $2)
RETURNING id, email, created_at`,
RETURNING id, email, role, created_at`,
[user.email, user.passwordHash],
);
const row = result.rows[0];
if (!row) {
throw new Error('identity_users INSERT returned no row');
}
return { id: row.id, email: row.email, createdAt: row.created_at };
return { id: row.id, email: row.email, role: row.role, createdAt: row.created_at };
} catch (error) {
if (isPgError(error) && error.code === UNIQUE_VIOLATION) {
throw new EmailAlreadyRegisteredError();
@@ -42,7 +44,7 @@ export class PgUserRepository implements UserRepository {
async findByEmail(email: string): Promise<(User & { passwordHash: string }) | undefined> {
const result = await this.pool.query<UserRow>(
`SELECT id, email, password_hash, created_at
`SELECT id, email, password_hash, role, created_at
FROM identity_users
WHERE email = $1`,
[email],
@@ -54,6 +56,7 @@ export class PgUserRepository implements UserRepository {
return {
id: row.id,
email: row.email,
role: row.role,
createdAt: row.created_at,
passwordHash: row.password_hash,
};