Files
mercadodevida/work/artifacts/F-006/implementer.md
rikrdo 546971280f 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
2026-08-15 09:28:15 +02:00

4.6 KiB
Raw Blame History

done -> work/artifacts/F-006/implementer.md

F-006 — Users: profile, addresses, RBAC (Build)

Agent: implementer Stage: build Feature: F-006

Scope delivered

Profile + address management with owner-or-admin authorization, on top of the F-005 identity foundation. No new npm dependencies were added.

Files

Migrations (immutable; new migrations only)

  • project/migrations/003_identity_roles.js — adds identity_users.role (customer | admin, default customer, NOT NULL) + index.
  • project/migrations/004_users.jsusers_profiles (PK user_id FK → identity_users, display_name, phone, timestamps) and users_addresses (id uuid PK, user_id FK ON DELETE CASCADE, address fields, is_default, timestamps, user_id index).

Shared auth contract

  • project/src/shared/auth.tsRole, CurrentUser, Authenticate, requireRole, requireOwnerOrAdmin. Shared (not module-owned) so both identity and users use one authorization vocabulary without cross-module imports.

Identity additions

  • project/src/modules/identity/infrastructure/session-authenticator.ts — resolves the session cookie into CurrentUser from the DB (expiry + revocation enforced in SQL). Exported through the module index so the composition root can inject it.
  • Role added to User, PgUserRepository (SELECT/RETURNING role), and the register/login responses.

Users module (hexagonal)

  • domain/profile.ts, domain/address.ts, domain/ports.ts
  • application/profile-use-cases.ts, application/address-use-cases.ts
  • infrastructure/pg-profile-repository.ts, infrastructure/pg-address-repository.ts
  • api/users.routes.ts — registers the endpoints below.
  • index.ts — public API.

Composition root

  • project/src/app/build-app.ts — registers @fastify/cookie once at root (cross-module infrastructure), wires identity routes, builds the Authenticate from identity and injects it into the users routes.

Tests

  • project/src/shared/tests/auth.test.ts (unit)
  • project/src/app/tests/users.itest.ts (integration)
  • project/src/infrastructure/db/tests/migrations.itest.ts — full rollback now asserts users_addresses, users_profiles, identity and baseline all drop.

Endpoints

Method Path Auth
GET /users admin only
GET /users/:id owner or admin
PATCH /users/:id owner or admin
GET /users/:id/addresses owner or admin
POST /users/:id/addresses owner or admin
PATCH /users/:id/addresses/:addressId owner or admin
DELETE /users/:id/addresses/:addressId owner or admin

Authorization runs BEFORE existence checks: a non-owner gets 403 regardless of whether the target resource exists. Address operations are additionally scoped by user_id in SQL, so a caller cannot touch another user's address even with a valid id.

Verification

  • npm run lint, npm run lint:boundaries, npm run typecheck, npm run build: clean.
  • npm test: 52 passed, 22 skipped (integration skipped without TEST_DATABASE_URL).
  • npm run test:integration: 22 passed against real PostgreSQL 16.
  • ./scripts/verify.sh: green.
  • Live smoke (PORT=3995): register/login 201/200; PATCH own profile 200; GET own 200; cross-user GET 403; GET /users as customer 403; after promotion GET /users as admin 200; address CRUD 201/204; cross-user address 403; no-cookie GET 401. Smoke data removed afterwards.

Deviations from TESTS.md (documented, not silent)

  • The planned users/tests/ownership.test.ts was merged into shared/tests/auth.test.ts, because the owner-or-admin decision lives in the shared contract (shared/auth.ts), not in the users module. Coverage is equivalent.
  • @fastify/cookie registration moved from identity's route scope to the app root. Fastify encapsulation meant the request.cookies decorator was not visible in the users scope (caused 500s); cookies are now cross-module infrastructure registered once at the composition root.

Notes for gates

  • Reviewer: check the SQL column whitelist in pg-profile-repository.upsert and pg-address-repository.update (only hardcoded columns; values always parameterized), and that every users route calls authenticate then requireOwnerOrAdmin before touching data.
  • Security: session resolution is DB-backed (revocation + expiry in SQL); role is read from identity_users per request, so promotion/demotion is reflected immediately without trusting the client.
  • QA: integration suite exercises every acceptance criterion (AC1AC4) plus the F-005 logout regression.