8.0 KiB
8.0 KiB
Task 2 Report: Backend admin mutations and authentication enforcement
Status
Implemented and verified on top of Task 1 commit 4105460.
Delivered API
- Added versioned global-admin controller at
admin/users(effective path follows the existing global/apiprefix and URI versioning). - Added narrow mutations:
PATCH admin/users/:id/profile(firstName,lastNameonly)PATCH admin/users/:id/role(strict numericRoleEnum.admin|userID)PATCH admin/users/:id/status(strict numericStatusEnum.active|inactiveID)PUT admin/users/:userId/players/:playerIdDELETE admin/users/:userId/players/:playerId
- Added
GET admin/users/playerswith search, optionalteamId,all|assigned|unassigned, page, and limit. - Kept
GET users/directoryas the user list source. Removed superseded generic user create/read/update/delete handlers that exposed unsafe/raw shapes or bypassed the narrow mutation safeguards. - Removed
DELETE auth/me, which could race with a promotion and bypass last-admin protection.
TDD RED evidence
The following failures were observed before their production implementations:
npm test -- --runInBand admin-users.controller.spec.ts- Failed to compile because
AdminUsersControllerand the narrow DTOs did not exist.
- Failed to compile because
npm test -- --runInBand admin-users.service.spec.ts- Failed to compile because
AdminUsersServicedid not exist.
- Failed to compile because
npm test -- --runInBand auth.service.spec.ts jwt.strategy.spec.ts- Inactive password login produced the ordinary password failure, social login issued a token, logs contained email/token values, and
JwtStrategyaccepted only the JWT snapshot.
- Inactive password login produced the ordinary password failure, social login issued a token, logs contained email/token values, and
npm test -- --runInBand AddPlayerLookupIndexes.spec.ts- Failed to compile because the reversible lookup-index migration did not exist.
npm test -- --runInBand auth.service.spec.ts auth.controller.spec.tsGET auth/mehad no JWT guard and the service accepted/refreshed an inactive user.
npm test -- --runInBand users.controller.security.spec.ts- Generic
UsersControllermutations were still present and bypassed the new invariants.
- Generic
npm test -- --runInBand admin-users.service.spec.ts -t "updates only names"- The locked user lookup did not use alias-scoped
FOR UPDATE, exposing a PostgreSQL outer-join runtime failure.
- The locked user lookup did not use alias-scoped
npm test -- --runInBand admin-users.controller.spec.ts -t "reverse-map"- Numeric enum reverse-map names such as
"admin"passed validation.
- Numeric enum reverse-map names such as
npm test -- --runInBand logging.service.spec.ts admin-users.service.spec.ts -t "caller transaction manager|updates only names"- Audit logging had no transaction-manager support and ran after commit.
npm test -- --runInBand auth.service.spec.ts admin-users.service.spec.ts -t "serializes email confirmation|explicit paginated player"- Email confirmation had no row-lock transaction, and a numeric driver boolean was returned as
1instead oftrue.
- Email confirmation had no row-lock transaction, and a numeric driver boolean was returned as
Every production behavior above was added only after the corresponding expected RED was captured.
Final GREEN evidence
- Focused backend tests:
- Command:
npm test -- --runInBand users.service.spec.ts users.controller.security.spec.ts admin-users.controller.spec.ts admin-users.service.spec.ts auth.controller.spec.ts auth.service.spec.ts jwt.strategy.spec.ts logging.service.spec.ts AddPlayerLookupIndexes.spec.ts - Result: 9 suites passed, 40 tests passed, 0 failed.
- Command:
- Targeted lint across every touched backend TypeScript file:
- Command: direct project ESLint invocation over 21 touched source/spec files.
- Result: exit 0, no findings.
- Backend build:
- Command:
npm run build - Result: exit 0.
- Command:
- Migration up/down smoke coverage:
- Exact
CREATE INDEXand reverse-orderDROP INDEXSQL asserted inAddPlayerLookupIndexes.spec.ts. - TypeORM entity index metadata asserted to match both migration names.
- Exact
- Diff checks:
git diff --check: exit 0.- Both frontend directories: no changes.
Security and concurrency design
- The controller is class-level protected by JWT auth,
RolesGuard, andRoles([RoleEnum.admin]). - Mutation DTOs are narrow and whitelisted. Role/status accept only strict numeric IDs, avoiding class-validator numeric-enum reverse-map strings.
- Role and status changes execute in transactions and lock the active-admin set in stable user-ID order. This serializes concurrent demotions/deactivations so the last active admin cannot be lost.
- Self-demotion and self-deactivation are rejected inside the locked transaction.
- User row locks use explicit query builders with
FOR UPDATE OFthe user alias. Role/status are loaded with left joins, preserving support for nullable relations without asking PostgreSQL to lock nullable joined rows. - Deactivation changes only
User.statusand revokes any outstanding confirmation hash; it does not alterPlayer.user. - Email confirmation locks the same user row and re-checks the hash inside its transaction. This serializes confirmation against administrative deactivation and prevents an old/racing confirmation link from reactivating a deactivated account.
- Assignment and reassignment lock the player row before changing
Player.user; unlink verifies the locked row is still linked to the requested user. - All mutation responses are explicit Task 1-compatible admin summaries. Player search uses its own explicit player/team/current-user projection. Password, hash, social ID, and tokens are never mapped.
- Admin audit events contain actor ID in
userIdand target/action IDs in details. Audit insertion uses the same transaction manager as the mutation, so an audit failure rolls back the security-sensitive change. - Password and social login reject inactive accounts.
JwtStrategyreloads the non-deleted database user on every request, rejects inactive/missing users, and returns the current database role/status rather than trusting token role claims. GET auth/meis JWT guarded and independently checks current active status before any refresh behavior.
Files
Added
src/users/admin-users.controller.tssrc/users/admin-users.controller.spec.tssrc/users/admin-users.service.tssrc/users/admin-users.service.spec.tssrc/users/users.controller.security.spec.tssrc/users/dto/admin-user.dto.tssrc/users/dto/admin-player-response.dto.tssrc/auth/auth.controller.spec.tssrc/auth/auth.service.spec.tssrc/auth/strategies/jwt.strategy.spec.tssrc/database/migrations/1785517200000-AddPlayerLookupIndexes.tssrc/database/migrations/AddPlayerLookupIndexes.spec.ts
Modified
src/users/users.controller.tssrc/users/users.module.tssrc/auth/auth.controller.tssrc/auth/auth.service.tssrc/auth/strategies/jwt.strategy.tssrc/database/logging/logging.service.tssrc/database/logging/logging.service.spec.tssrc/database/logging/model/logging-event.type.tssrc/players/entities/player.entity.ts
Self-review
- Checked every endpoint for server-side global-admin authorization and removed legacy mutation bypasses.
- Checked response construction for password/hash/social-ID/token leakage.
- Checked role/status races, lock acquisition order, nullable-relation SQL shape, player reassignment ownership, and confirmation/deactivation ordering.
- Checked all touched logging details for email, password, token, hash, or social-ID values.
- Checked migration names against entity metadata and down ordering.
- Confirmed no frontend changes.
Concerns / follow-up
- The lock/concurrency and migration tests are focused unit/SQL-shape tests; no live PostgreSQL instance was available for a two-connection race test or an actual migration run/revert. A database-backed integration test remains advisable before production rollout.
- Removing superseded generic user CRUD/read routes and
DELETE auth/meis intentionally security-hardening and may affect undocumented external clients. Repository frontend searches showed no use of those removed routes. - Full unrelated backend test-suite repair was intentionally out of scope; the focused Task 1 + Task 2 suite and backend build are green.