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

106 lines
4.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 (AC1AC4) plus the
F-005 logout regression.