tasks: task breakdown for identity and authentication feature (010)
36 tasks across 8 phases (5 user stories + setup/foundational/polish).
US1 (real login) and US2 (real route/role gating) are the P1 MVP; the
one task that touches code outside identity/* (T020, adding
requireRole('ADMIN') across 002-009's existing admin routes) is called
out explicitly to run each touched module's own test suite immediately
after, not only in the final regression pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
3b4c220a45
commit
8327fafac2
@@ -0,0 +1,285 @@
|
||||
---
|
||||
description: "Task list for 010-identity-auth"
|
||||
---
|
||||
|
||||
# Tasks: Identity and Authentication
|
||||
|
||||
**Input**: Design documents from `specs/010-identity-auth/`
|
||||
|
||||
**Prerequisites**: [plan.md](./plan.md), [spec.md](./spec.md), [research.md](./research.md),
|
||||
[data-model.md](./data-model.md),
|
||||
[contracts/identity-auth-contract.md](./contracts/identity-auth-contract.md),
|
||||
[quickstart.md](./quickstart.md)
|
||||
|
||||
**Tests**: Included as first-class tasks. Pure logic worth a unit test: the identical-failure-
|
||||
response behavior (FR-002/SC-003) and the `requireRole` matching logic. Everything else is
|
||||
best proven end-to-end against a real Postgres/Redis, including a specific pass re-verifying
|
||||
existing 002-009 admin routes now actually reject an invalid session.
|
||||
|
||||
**Organization**: Tasks are grouped by user story (US1 = P1 login, US2 = P1 route/role gating,
|
||||
US3 = P2 self-identity, US4 = P2 admin-created accounts, US5 = P3 logout).
|
||||
|
||||
## Format: `[ID] [P?] [Story] Description`
|
||||
|
||||
All file paths are relative to `supporthub-api/` (repo root).
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Setup
|
||||
|
||||
- [ ] T001 [P] Add `jsonwebtoken` and `bcryptjs` (plus `@types/jsonwebtoken`,
|
||||
`@types/bcryptjs`) to `package.json`
|
||||
- [ ] T002 [P] Add `AUTH_JWT_SECRET` (required, no default — never a committed secret) and
|
||||
`AUTH_TOKEN_LIFETIME_HOURS` (`z.coerce.number().default(4)`) to `src/config/env.ts`,
|
||||
exposed via a new `src/config/auth.ts` (`authConfig.jwtSecret`,
|
||||
`authConfig.tokenLifetimeHours`), matching `orchestrationConfig`'s exact shape
|
||||
- [ ] T003 [P] Populate `src/modules/identity/auth/` with the full standard shape around its
|
||||
existing files, replacing the email-only `AuthService.validateCredentials`/
|
||||
`AuthRepository.findByEmail`-only stub content
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Foundational (Blocking Prerequisites)
|
||||
|
||||
**Purpose**: Schema for the entities every user story needs.
|
||||
|
||||
**⚠️ CRITICAL**: No user-story stage work can begin until this phase is complete.
|
||||
|
||||
- [ ] T004 Add `User.passwordHash` (`String`, required) and `User.active` (`Boolean
|
||||
@default(true)`) to `prisma/schema.prisma`, plus `Agent.userId` (`String? @unique`, FK to
|
||||
`User.id` — research.md's additive, not-yet-consumed link) (depends on T001-T003)
|
||||
- [ ] T005 Run `npm run prisma:generate` and create the migration (`npm run prisma:migrate`)
|
||||
for T004 (depends on T004)
|
||||
- [ ] T006 Update `prisma/seed/roles.seed.ts` to set a real bcryptjs-hashed password on both
|
||||
seeded accounts (`admin@supporthub.internal`, `agent@supporthub.internal`), documenting
|
||||
the plaintext dev password in a comment directly above the hash call (local/dev use only,
|
||||
per spec.md Edge Cases) (depends on T005)
|
||||
|
||||
**Checkpoint**: Schema migrated, demo accounts have real passwords. User stories can now be
|
||||
built.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: User Story 1 - An agent or admin logs in and receives a session (Priority: P1) 🎯 MVP (part 1)
|
||||
|
||||
**Goal**: Real password verification and JWT issuance, with an identical failure response
|
||||
regardless of which reason login failed.
|
||||
|
||||
**Independent Test**: Quickstart Scenario 1.
|
||||
|
||||
### Tests for User Story 1
|
||||
|
||||
- [ ] T007 [P] [US1] Unit test: given a found user with a matching/non-matching password, and
|
||||
given no user found at all, the login-failure path produces byte-identical response
|
||||
shape/status in the non-matching and no-user cases — in
|
||||
`tests/unit/identity/login-failure-parity.test.ts`
|
||||
- [ ] T008 [US1] Integration test covering Quickstart Scenario 1 (correct login succeeds with a
|
||||
token + identity; wrong password and nonexistent email produce the same `401`) against a
|
||||
real Postgres in `tests/integration/identity-auth-flow.test.ts` (depends on T006)
|
||||
|
||||
### Implementation for User Story 1
|
||||
|
||||
- [ ] T009 [US1] Add `hashPassword`/`verifyPassword` (bcryptjs) and `signToken`/`verifyToken`
|
||||
(jsonwebtoken, embedding `sub`/`email`/`role`/`actorType`/`jti`/`iat`/`exp` per
|
||||
data-model.md) in `identity/auth/mapper/` (depends on T002)
|
||||
- [ ] T010 [US1] Add `AuthRepository.findActiveByEmail` (replacing `findByEmail`) in
|
||||
`identity/auth/repository/` (depends on T005)
|
||||
- [ ] T011 [US1] Add `AuthService.login(email, password)`: looks up the user, compares against
|
||||
either the found hash or a fixed dummy hash when not found (FR-002's timing/shape
|
||||
parity), returns `{ token, user }` or throws a single, identical `AuthenticationError` for
|
||||
every failure branch — in `identity/auth/service/` (depends on T009, T010)
|
||||
- [ ] T012 [US1] Replace `POST /auth/login`'s schema (`email` + `password`, replacing the
|
||||
email-only schema) and controller in `identity/auth/schema/` + `controller/`, registered
|
||||
from `src/api/routes.ts` (depends on T011)
|
||||
- [ ] T013 [US1] Run Quickstart Scenario 1 locally and confirm all 3 steps pass
|
||||
|
||||
**Checkpoint**: Login works and never leaks account existence through its failure response.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: User Story 2 - Protected routes require a valid session; admin-only routes require the admin role (Priority: P1) 🎯 MVP (part 2)
|
||||
|
||||
**Goal**: `fastify.authenticate` actually verifies; `requireRole` enforces role on top of it;
|
||||
every existing gated route is re-verified.
|
||||
|
||||
**Independent Test**: Quickstart Scenario 2.
|
||||
|
||||
### Tests for User Story 2
|
||||
|
||||
- [ ] T014 [P] [US2] Unit test for `requireRole`'s matching logic (allowed role passes, wrong
|
||||
role throws `AuthorizationError`, no `request.user` at all throws) in
|
||||
`tests/unit/identity/require-role.test.ts`
|
||||
- [ ] T015 [US2] Integration test covering Quickstart Scenario 2 (no header, malformed token,
|
||||
wrong-role token, correct-role token) against a real Postgres/Redis in
|
||||
`tests/integration/identity-auth-flow.test.ts` (depends on T008)
|
||||
- [ ] T016 [US2] Integration test spot-checking at least one existing admin route per module
|
||||
(002's product-integration admin route, 004's knowledge admin route, 006's team-creation
|
||||
route, 007's manual-assignment route, 008's SLA-policy route, 009's investigation route)
|
||||
now rejects a missing/invalid session — in `tests/integration/identity-auth-flow.test.ts`
|
||||
(depends on T015)
|
||||
|
||||
### Implementation for User Story 2
|
||||
|
||||
- [ ] T017 [US2] Add revocation-denylist helpers (`isTokenRevoked`, `revokeToken`) in
|
||||
`src/infrastructure/cache/`, alongside the existing `hasSeenJti`/`markJtiSeen` (same
|
||||
Redis-key-with-TTL shape, research.md) (depends on T002)
|
||||
- [ ] T018 [US2] Replace `auth.plugin.ts`'s `authenticate` stub: verify the JWT signature and
|
||||
expiry, check T017's revocation denylist, and on success set `request.user` (the full
|
||||
`AuthUser`) and `request.reqContext.actorId`/`actorType` — throw `AuthenticationError` on
|
||||
any failure, never pass through as anonymous (depends on T009, T017)
|
||||
- [ ] T019 [US2] Add `requireRole(...allowedRoles: string[])` preHandler factory (checks
|
||||
`request.user?.role`, throws `AuthorizationError` if it doesn't match) in
|
||||
`identity/auth/service/` (or a dedicated `identity/auth/guards/` file), exported from
|
||||
`identity/auth`'s public `index.ts` (depends on T018)
|
||||
- [ ] T020 [US2] Add `requireRole('ADMIN')` to every existing write/config admin route across
|
||||
002-009 that doesn't already distinguish agent-vs-admin access (product-integration
|
||||
admin, knowledge admin, teams/hierarchy admin, SLA/escalation-policy admin) — read-only
|
||||
routes and ticket-working routes an agent legitimately uses stay `fastify.authenticate`-
|
||||
only (research.md's own scoping: this is a mechanical pass applying an existing judgment,
|
||||
not a new design) (depends on T019)
|
||||
- [ ] T021 [US2] Run Quickstart Scenario 2 locally and confirm all 4 steps pass
|
||||
|
||||
**Checkpoint**: Every P1 user story is complete — a session is real, and it's actually checked
|
||||
everywhere it's supposed to be. This is the feature's MVP.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: User Story 3 - An authenticated user can identify themselves (Priority: P2)
|
||||
|
||||
**Goal**: A self-identity endpoint that re-validates against current account state.
|
||||
|
||||
**Independent Test**: Quickstart Scenario 3.
|
||||
|
||||
### Tests for User Story 3
|
||||
|
||||
- [ ] T022 [US3] Integration test covering Quickstart Scenario 3 (identity matches login;
|
||||
deactivating the account rejects a still-unexpired token's use of this endpoint
|
||||
specifically) — in `tests/integration/identity-auth-flow.test.ts` (depends on T021)
|
||||
|
||||
### Implementation for User Story 3
|
||||
|
||||
- [ ] T023 [US3] Add `AuthService.getCurrentUser(userId)`: re-fetches the `User` row, throws
|
||||
`AuthenticationError` if it no longer exists or `active: false` — in `identity/auth/
|
||||
service/` (depends on T010)
|
||||
- [ ] T024 [US3] Add `GET /auth/me` route (gated by `fastify.authenticate`) in `identity/auth/
|
||||
controller/` + `routes/` (depends on T023)
|
||||
- [ ] T025 [US3] Run Quickstart Scenario 3 locally and confirm both steps pass
|
||||
|
||||
**Checkpoint**: A session can be introspected and is re-validated against live account state.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: User Story 4 - An admin creates additional agent/admin accounts (Priority: P2)
|
||||
|
||||
**Goal**: Admin-only account creation, immediately usable to log in.
|
||||
|
||||
**Independent Test**: Quickstart Scenario 4.
|
||||
|
||||
### Tests for User Story 4
|
||||
|
||||
- [ ] T026 [US4] Integration test covering Quickstart Scenario 4 (admin creates an account and
|
||||
it logs in immediately; non-admin rejected; duplicate email rejected) — in
|
||||
`tests/integration/identity-auth-flow.test.ts` (depends on T021)
|
||||
|
||||
### Implementation for User Story 4
|
||||
|
||||
- [ ] T027 [US4] Add `UsersService.create(email, name, role, password)` (resolve-or-409 on
|
||||
duplicate email, hashes the password via T009) in `identity/agents/service/` (research.md
|
||||
— account creation lives alongside `identity/agents`'s own roster CRUD, not
|
||||
`identity/auth`) (depends on T009)
|
||||
- [ ] T028 [US4] Add `POST /admin/users` route (gated by `fastify.authenticate` +
|
||||
`requireRole('ADMIN')`) in `identity/agents/controller/` + `routes/`, registered from
|
||||
`src/api/routes.ts` — response never includes the password or hash (depends on T019, T027)
|
||||
- [ ] T029 [US4] Run Quickstart Scenario 4 locally and confirm all 4 steps pass
|
||||
|
||||
**Checkpoint**: New staff accounts can be provisioned without a manual database write.
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: User Story 5 - A user logs out (Priority: P3)
|
||||
|
||||
**Goal**: Explicit, immediate session revocation.
|
||||
|
||||
**Independent Test**: Quickstart Scenario 5.
|
||||
|
||||
### Tests for User Story 5
|
||||
|
||||
- [ ] T030 [US5] Integration test covering Quickstart Scenario 5 (logout succeeds; the same
|
||||
token is rejected immediately afterward) — in `tests/integration/identity-auth-flow.test.ts`
|
||||
(depends on T021)
|
||||
|
||||
### Implementation for User Story 5
|
||||
|
||||
- [ ] T031 [US5] Add `AuthService.logout(jti, remainingTtlSeconds)`: calls T017's `revokeToken`
|
||||
— in `identity/auth/service/` (depends on T017)
|
||||
- [ ] T032 [US5] Add `POST /auth/logout` route (gated by `fastify.authenticate`) in
|
||||
`identity/auth/controller/` + `routes/` (depends on T031)
|
||||
- [ ] T033 [US5] Run Quickstart Scenario 5 locally and confirm both steps pass
|
||||
|
||||
**Checkpoint**: All five user stories work independently and together — real login, real
|
||||
gating, self-identity, admin-provisioned accounts, and logout form one coherent auth system.
|
||||
|
||||
---
|
||||
|
||||
## Phase 8: Polish & Cross-Cutting Concerns
|
||||
|
||||
- [ ] T034 [P] Update `specs/010-identity-auth/checklists/requirements.md` Notes with any
|
||||
implementation-time findings
|
||||
- [ ] T035 Run `npx tsx scripts/check-architecture.ts` and `npm run lint`/`npm run typecheck`
|
||||
- [ ] T036 Full regression: `npm run test:unit` (scoped to `tests/unit`) to confirm nothing
|
||||
broke elsewhere, then the full integration suite (including 002-009's own suites, since
|
||||
T020 adds `requireRole` to their existing routes) against real Docker-provisioned
|
||||
Postgres/Redis
|
||||
|
||||
---
|
||||
|
||||
## Dependencies & Execution Order
|
||||
|
||||
### Phase Dependencies
|
||||
|
||||
- **Setup (Phase 1)**: No dependencies
|
||||
- **Foundational (Phase 2)**: Depends on Setup — BLOCKS all user stories
|
||||
- **User Story 1 (Phase 3)**: Depends on Foundational — no dependency on US2-US5
|
||||
- **User Story 2 (Phase 4)**: Depends on US1 (a real token to verify)
|
||||
- **User Story 3 (Phase 5)**: Depends on US2 (the gate US3's own route sits behind)
|
||||
- **User Story 4 (Phase 6)**: Depends on US2 (`requireRole('ADMIN')`)
|
||||
- **User Story 5 (Phase 7)**: Depends on US2 (the gate logout's own route sits behind) and
|
||||
US1's token shape (`jti`)
|
||||
- **Polish (Phase 8)**: Depends on all five user stories
|
||||
|
||||
### Parallel Opportunities
|
||||
|
||||
- T001-T003 (independent scaffolding)
|
||||
- T007 (unit test) alongside T009-T011 (the implementation it tests)
|
||||
- T014 (unit test) alongside T019 (the implementation it tests)
|
||||
- T034 in Polish
|
||||
|
||||
### Sequencing Note
|
||||
|
||||
T020 (adding `requireRole('ADMIN')` across 002-009's existing routes) is the one task in this
|
||||
feature that touches code outside `identity/*` — run each touched module's own existing test
|
||||
suite immediately after, not only in T036's final regression pass, so a role-gating regression
|
||||
in, say, 007's own suite is caught close to its cause rather than at the very end.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Strategy
|
||||
|
||||
### MVP First (User Stories 1-2 Only)
|
||||
|
||||
1. Setup + Foundational (T001-T006)
|
||||
2. User Story 1 (T007-T013) → login works, no account-existence leak
|
||||
3. User Story 2 (T014-T021) → the gate is real everywhere it already existed
|
||||
4. **STOP and VALIDATE**: Quickstart Scenarios 1-2 pass, including the cross-module spot-check
|
||||
(T016). This is the feature's MVP — every other user story is a smaller addition on top of a
|
||||
now-real auth system.
|
||||
|
||||
### Incremental Delivery
|
||||
|
||||
1. Setup + Foundational → schema migrated, demo accounts have real passwords
|
||||
2. Add User Story 1 → login is real
|
||||
3. Add User Story 2 → the gate is real everywhere (P1-complete, MVP)
|
||||
4. Add User Story 3 → self-identity, re-validated against live account state
|
||||
5. Add User Story 4 → admins can provision new accounts
|
||||
6. Add User Story 5 → explicit logout
|
||||
7. Polish → full regression across every feature this touches
|
||||
Reference in New Issue
Block a user