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:
saqib mir
2026-09-07 11:15:45 +05:30
co-authored by Claude Sonnet 5
parent 3b4c220a45
commit 8327fafac2
+285
View File
@@ -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