2026-09-07 11:15:45 +05:30
|
|
|
---
|
|
|
|
|
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
|
|
|
|
|
|
2026-09-07 12:45:37 +05:30
|
|
|
- [x] T001 [P] Add `jsonwebtoken` and `bcryptjs` (plus `@types/jsonwebtoken`,
|
2026-09-07 11:15:45 +05:30
|
|
|
`@types/bcryptjs`) to `package.json`
|
2026-09-07 12:45:37 +05:30
|
|
|
- [x] T002 [P] Add `AUTH_JWT_SECRET` (required, no default — never a committed secret) and
|
2026-09-07 11:15:45 +05:30
|
|
|
`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
|
2026-09-07 12:45:37 +05:30
|
|
|
- [x] T003 [P] Populate `src/modules/identity/auth/` with the full standard shape around its
|
2026-09-07 11:15:45 +05:30
|
|
|
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.
|
|
|
|
|
|
2026-09-07 12:45:37 +05:30
|
|
|
- [x] T004 Add `User.passwordHash` (`String`, required) and `User.active` (`Boolean
|
2026-09-07 11:15:45 +05:30
|
|
|
@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)
|
2026-09-07 12:45:37 +05:30
|
|
|
- [x] T005 Run `npm run prisma:generate` and create the migration (`npm run prisma:migrate`)
|
2026-09-07 11:15:45 +05:30
|
|
|
for T004 (depends on T004)
|
2026-09-07 12:45:37 +05:30
|
|
|
- [x] T006 Update `prisma/seed/roles.seed.ts` to set a real bcryptjs-hashed password on both
|
2026-09-07 11:15:45 +05:30
|
|
|
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
|
|
|
|
|
|
2026-09-07 12:45:37 +05:30
|
|
|
- [x] T007 [P] [US1] Unit test: given a found user with a matching/non-matching password, and
|
2026-09-07 11:15:45 +05:30
|
|
|
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`
|
2026-09-07 12:45:37 +05:30
|
|
|
- [x] T008 [US1] Integration test covering Quickstart Scenario 1 (correct login succeeds with a
|
2026-09-07 11:15:45 +05:30
|
|
|
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
|
|
|
|
|
|
2026-09-07 12:45:37 +05:30
|
|
|
- [x] T009 [US1] Add `hashPassword`/`verifyPassword` (bcryptjs) and `signToken`/`verifyToken`
|
2026-09-07 11:15:45 +05:30
|
|
|
(jsonwebtoken, embedding `sub`/`email`/`role`/`actorType`/`jti`/`iat`/`exp` per
|
|
|
|
|
data-model.md) in `identity/auth/mapper/` (depends on T002)
|
2026-09-07 12:45:37 +05:30
|
|
|
- [x] T010 [US1] Add `AuthRepository.findActiveByEmail` (replacing `findByEmail`) in
|
2026-09-07 11:15:45 +05:30
|
|
|
`identity/auth/repository/` (depends on T005)
|
2026-09-07 12:45:37 +05:30
|
|
|
- [x] T011 [US1] Add `AuthService.login(email, password)`: looks up the user, compares against
|
2026-09-07 11:15:45 +05:30
|
|
|
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)
|
2026-09-07 12:45:37 +05:30
|
|
|
- [x] T012 [US1] Replace `POST /auth/login`'s schema (`email` + `password`, replacing the
|
2026-09-07 11:15:45 +05:30
|
|
|
email-only schema) and controller in `identity/auth/schema/` + `controller/`, registered
|
|
|
|
|
from `src/api/routes.ts` (depends on T011)
|
2026-09-07 12:45:37 +05:30
|
|
|
- [x] T013 [US1] Run Quickstart Scenario 1 locally and confirm all 3 steps pass
|
2026-09-07 11:15:45 +05:30
|
|
|
|
|
|
|
|
**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
|
|
|
|
|
|
2026-09-07 12:45:37 +05:30
|
|
|
- [x] T014 [P] [US2] Unit test for `requireRole`'s matching logic (allowed role passes, wrong
|
2026-09-07 11:15:45 +05:30
|
|
|
role throws `AuthorizationError`, no `request.user` at all throws) in
|
|
|
|
|
`tests/unit/identity/require-role.test.ts`
|
2026-09-07 12:45:37 +05:30
|
|
|
- [x] T015 [US2] Integration test covering Quickstart Scenario 2 (no header, malformed token,
|
2026-09-07 11:15:45 +05:30
|
|
|
wrong-role token, correct-role token) against a real Postgres/Redis in
|
|
|
|
|
`tests/integration/identity-auth-flow.test.ts` (depends on T008)
|
2026-09-07 12:45:37 +05:30
|
|
|
- [x] T016 [US2] Integration test spot-checking at least one existing admin route per module
|
2026-09-07 11:15:45 +05:30
|
|
|
(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
|
|
|
|
|
|
2026-09-07 12:45:37 +05:30
|
|
|
- [x] T017 [US2] Add revocation-denylist helpers (`isTokenRevoked`, `revokeToken`) in
|
2026-09-07 11:15:45 +05:30
|
|
|
`src/infrastructure/cache/`, alongside the existing `hasSeenJti`/`markJtiSeen` (same
|
|
|
|
|
Redis-key-with-TTL shape, research.md) (depends on T002)
|
2026-09-07 12:45:37 +05:30
|
|
|
- [x] T018 [US2] Replace `auth.plugin.ts`'s `authenticate` stub: verify the JWT signature and
|
2026-09-07 11:15:45 +05:30
|
|
|
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)
|
2026-09-07 12:45:37 +05:30
|
|
|
- [x] T019 [US2] Add `requireRole(...allowedRoles: string[])` preHandler factory (checks
|
2026-09-07 11:15:45 +05:30
|
|
|
`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)
|
2026-09-07 12:45:37 +05:30
|
|
|
- [x] T020 [US2] Add `requireRole('ADMIN')` to every existing write/config admin route across
|
2026-09-07 11:15:45 +05:30
|
|
|
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)
|
2026-09-07 12:45:37 +05:30
|
|
|
- [x] T021 [US2] Run Quickstart Scenario 2 locally and confirm all 4 steps pass
|
2026-09-07 11:15:45 +05:30
|
|
|
|
|
|
|
|
**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
|
|
|
|
|
|
2026-09-07 12:45:37 +05:30
|
|
|
- [x] T022 [US3] Integration test covering Quickstart Scenario 3 (identity matches login;
|
2026-09-07 11:15:45 +05:30
|
|
|
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
|
|
|
|
|
|
2026-09-07 12:45:37 +05:30
|
|
|
- [x] T023 [US3] Add `AuthService.getCurrentUser(userId)`: re-fetches the `User` row, throws
|
2026-09-07 11:15:45 +05:30
|
|
|
`AuthenticationError` if it no longer exists or `active: false` — in `identity/auth/
|
|
|
|
|
service/` (depends on T010)
|
2026-09-07 12:45:37 +05:30
|
|
|
- [x] T024 [US3] Add `GET /auth/me` route (gated by `fastify.authenticate`) in `identity/auth/
|
2026-09-07 11:15:45 +05:30
|
|
|
controller/` + `routes/` (depends on T023)
|
2026-09-07 12:45:37 +05:30
|
|
|
- [x] T025 [US3] Run Quickstart Scenario 3 locally and confirm both steps pass
|
2026-09-07 11:15:45 +05:30
|
|
|
|
|
|
|
|
**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
|
|
|
|
|
|
2026-09-07 12:45:37 +05:30
|
|
|
- [x] T026 [US4] Integration test covering Quickstart Scenario 4 (admin creates an account and
|
2026-09-07 11:15:45 +05:30
|
|
|
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
|
|
|
|
|
|
2026-09-07 12:45:37 +05:30
|
|
|
- [x] T027 [US4] Add `UsersService.create(email, name, role, password)` (resolve-or-409 on
|
2026-09-07 11:15:45 +05:30
|
|
|
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)
|
2026-09-07 12:45:37 +05:30
|
|
|
- [x] T028 [US4] Add `POST /admin/users` route (gated by `fastify.authenticate` +
|
2026-09-07 11:15:45 +05:30
|
|
|
`requireRole('ADMIN')`) in `identity/agents/controller/` + `routes/`, registered from
|
|
|
|
|
`src/api/routes.ts` — response never includes the password or hash (depends on T019, T027)
|
2026-09-07 12:45:37 +05:30
|
|
|
- [x] T029 [US4] Run Quickstart Scenario 4 locally and confirm all 4 steps pass
|
2026-09-07 11:15:45 +05:30
|
|
|
|
|
|
|
|
**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
|
|
|
|
|
|
2026-09-07 12:45:37 +05:30
|
|
|
- [x] T030 [US5] Integration test covering Quickstart Scenario 5 (logout succeeds; the same
|
2026-09-07 11:15:45 +05:30
|
|
|
token is rejected immediately afterward) — in `tests/integration/identity-auth-flow.test.ts`
|
|
|
|
|
(depends on T021)
|
|
|
|
|
|
|
|
|
|
### Implementation for User Story 5
|
|
|
|
|
|
2026-09-07 12:45:37 +05:30
|
|
|
- [x] T031 [US5] Add `AuthService.logout(jti, remainingTtlSeconds)`: calls T017's `revokeToken`
|
2026-09-07 11:15:45 +05:30
|
|
|
— in `identity/auth/service/` (depends on T017)
|
2026-09-07 12:45:37 +05:30
|
|
|
- [x] T032 [US5] Add `POST /auth/logout` route (gated by `fastify.authenticate`) in
|
2026-09-07 11:15:45 +05:30
|
|
|
`identity/auth/controller/` + `routes/` (depends on T031)
|
2026-09-07 12:45:37 +05:30
|
|
|
- [x] T033 [US5] Run Quickstart Scenario 5 locally and confirm both steps pass
|
2026-09-07 11:15:45 +05:30
|
|
|
|
|
|
|
|
**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
|
|
|
|
|
|
2026-09-07 12:45:37 +05:30
|
|
|
- [x] T034 [P] Update `specs/010-identity-auth/checklists/requirements.md` Notes with any
|
2026-09-07 11:15:45 +05:30
|
|
|
implementation-time findings
|
2026-09-07 12:45:37 +05:30
|
|
|
- [x] T035 Run `npx tsx scripts/check-architecture.ts` and `npm run lint`/`npm run typecheck`
|
|
|
|
|
- [x] T036 Full regression: `npm run test:unit` (scoped to `tests/unit`) to confirm nothing
|
2026-09-07 11:15:45 +05:30
|
|
|
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
|