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.js` — `users_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.ts` — `Role`, `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 (AC1–AC4) plus the F-005 logout regression.