Phase 0 research resolves the library choices (jsonwebtoken + bcryptjs, chosen partly to avoid native-build friction on Windows dev environments), the Redis-backed revocation-denylist shape (reusing 002's own jti-replay-protection pattern exactly), a 4-hour token lifetime, and why fastify.authenticate populating the already-shared reqContext.actorId/actorType retroactively makes every audit trail since 007 accurate for real agent/admin actions instead of always 'unknown'. Also surfaces and scopes a real gap found along the way: User (login identity) and Agent (routing/skills profile) have never been linked. Adds Agent.userId as a nullable FK now (cheap, additive) without building the actual linking workflow, which belongs in 006's own identity/agents admin screens as a later, separate piece of work. Phase 1 adds data-model.md, the login/self-identity/account-creation/ logout contract, and five quickstart scenarios including a specific requirement to re-verify at least one already-shipped admin route per module (002-009), not just this feature's own new endpoints. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2.7 KiB
2.7 KiB
Quickstart: Validating Identity and Authentication
Prerequisites: migrations applied; npm run prisma:seed run so the two demo accounts exist
with their new real passwords (documented in the seed script itself, local/dev use only).
Scenario 1 — login (User Story 1)
POST /auth/loginwith the seeded admin's correct email/password. Expected:200, a token, and{id, email, name, role: 'ADMIN'}.- Repeat with the correct email but a wrong password. Expected:
401. - Repeat with an email that doesn't exist at all. Expected: the exact same
401body/ status as step 2 — diff the two responses to confirm they're indistinguishable.
Scenario 2 — route gating and role enforcement (User Story 2)
- Call an existing admin route (e.g.
POST /admin/teams) with noAuthorizationheader. Expected:401. - Repeat with a malformed token (
Bearer not-a-real-token). Expected:401. - Log in as the seeded agent (role
AGENT); call an admin-only route gated byrequireRole('ADMIN'). Expected:403. - Log in as the seeded admin; repeat step 3's call. Expected:
200/201(whatever that route normally returns on success).
Scenario 3 — self-identity (User Story 3)
- Log in; call
GET /auth/mewith the resulting token. Expected:200, identity matches the login response exactly. - Directly deactivate that account (
active: false) via a direct DB update (simulating an admin action no UI exists for yet); repeat the sameGET /auth/mecall with the same, still-unexpired token. Expected:401— re-validated against current account state, not the token's own claims.
Scenario 4 — admin creates an account (User Story 4)
- Log in as admin;
POST /admin/userswith a new email/name/roleAGENT/password. Expected:201, response never includes the password or its hash. - Log in as a non-admin (the seeded agent); repeat step 1. Expected:
403. - Immediately log in as the newly-created account with the password from step 1. Expected:
200— no manual step needed in between. - Repeat step 1 with an email already in use. Expected:
409.
Scenario 5 — logout (User Story 5)
- Log in; call
POST /auth/logoutwith the resulting token. Expected:200. - Immediately reuse that same token on any gated route. Expected:
401— rejected even though it hasn't naturally expired.
What "done" looks like
All five scenarios pass, and Scenario 2 is additionally verified against at least one already-shipped admin route from each of 002-009 (not just a route this feature itself adds), proving the real gate actually protects what the no-op stub never did.