Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .husky/pre-push
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env sh
# Global hooks run via ~/.config/husky/init.sh
1 change: 0 additions & 1 deletion apps/api/src/account/account.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
* guard configuration is required.
*
* @layer account
* @see docs/DEVELOPMENT_PLAN.md §Phase 14 P14-2
*/

import { Module } from '@nestjs/common';
Expand Down
46 changes: 15 additions & 31 deletions apps/api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,16 @@
* @file app.module.ts
* @description Root NestJS module for `@nest-auth-example/api`.
*
* Phase 7 adds:
* - `AuthModule` — wires `BymaxAuthModule.registerAsync` with all four
* implementation bindings and mounts `/api/auth/*` controllers.
* - `ThrottlerModule.forRoot(AUTH_THROTTLE_CONFIGS)` — rate-limiting applied
* globally (auth routes use the library's throttle configs).
* - `TenantsModule` and `ProjectsModule` — example domain modules that
* demonstrate RBAC, multi-tenant scoping, and library decorators.
* - `UsersModule` — exposes `PATCH /api/users/:id/status` for the admin
* suspension demo (FCM row #23).
* - `PlatformModule` — exposes `/api/platform/*` endpoints protected by
* `JwtPlatformGuard` + `PlatformRolesGuard` (FCM row #22).
* - `DebugModule` (non-production only) — dev helper for brute-force lockout
* demo (FCM row #16).
* - `NotificationsModule` — WebSocket gateway at `/ws/notifications` protected by
* `WsJwtGuard`; includes the dev-only `POST /api/debug/notify/:userId` trigger
* (FCM row #24).
* - Five global `APP_GUARD` providers registered in the exact order mandated by
* `docs/guidelines/nest-auth-guidelines.md`: JwtAuthGuard → UserStatusGuard →
* MfaRequiredGuard → TenantMfaPolicyGuard → RolesGuard. The new
* `TenantMfaPolicyGuard` is app-owned (see `auth/tenant-mfa-policy.guard.ts`)
* and forces every user in the tenants listed in `MFA_REQUIRED_TENANT_SLUGS`
* to enrol in MFA before they can touch protected endpoints; it composes
* with the lib's `MfaRequiredGuard` rather than replacing it. Order must
* not be changed without an ADR.
* Wires infrastructure, auth, and all feature modules. Registers five global
* `APP_GUARD` providers in the exact order mandated by
* `docs/guidelines/nest-auth-guidelines.md`: JwtAuthGuard → UserStatusGuard →
* MfaRequiredGuard → TenantMfaPolicyGuard → RolesGuard. Order must not be
* changed without an ADR.
*
* `TenantMfaPolicyGuard` is app-owned and forces every user in the tenants
* listed in `MFA_REQUIRED_TENANT_SLUGS` to enrol in MFA before reaching
* protected endpoints; it composes with the library's `MfaRequiredGuard`
* rather than replacing it.
*
* Import order is intentional:
* 1. `AppConfigModule` must be first — registers `ConfigService` globally.
Expand All @@ -37,7 +23,6 @@
*
* @layer root
* @see docs/guidelines/nest-auth-guidelines.md §Decorators & guards
* @see docs/DEVELOPMENT_PLAN.md §Phase 7 P7-2
*/

import { Module } from '@nestjs/common';
Expand Down Expand Up @@ -101,13 +86,12 @@ import { DebugModule } from './debug/debug.module.js';
TenantsModule,
ProjectsModule,
UsersModule,
// Phase 9 — Platform admin context (FCM #22). Mounts /api/platform/* routes
// that are protected by JwtPlatformGuard + PlatformRolesGuard.
// Platform admin endpoints under /api/platform/*, protected by
// JwtPlatformGuard + PlatformRolesGuard.
PlatformModule,
// Phase 10 — WebSocket notifications gateway (FCM #24). Mounts the
// /ws/notifications WebSocket endpoint guarded by WsJwtGuard. The dev-only
// POST /api/debug/notify/:userId controller is included by NotificationsModule
// itself when NODE_ENV !== 'production'.
// WebSocket notifications gateway at /ws/notifications, guarded by WsJwtGuard.
// The dev-only POST /api/debug/notify/:userId trigger is included by
// NotificationsModule itself when NODE_ENV !== 'production'.
NotificationsModule,
// DebugModule is conditionally included only outside of production to
// keep brute-force demo helpers out of production deployments.
Expand Down
8 changes: 3 additions & 5 deletions apps/api/src/auth/app-auth.hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@
* oauth.login |
* invitation.accepted
*
* Covers FCM row #30 (audit / lifecycle hooks).
*
* @layer auth
* @see docs/guidelines/observability-guidelines.md
* @see docs/guidelines/nest-auth-guidelines.md
Expand All @@ -51,7 +49,7 @@ import { isBlockedStatus } from './auth.constants.js';
/**
* Auth lifecycle hooks that write immutable `AuditLog` rows for every event.
*
* Injected via `BYMAX_AUTH_HOOKS` token in Phase 7's `AuthModule`. The hooks
* Injected via `BYMAX_AUTH_HOOKS` token in `AuthModule`. The hooks
* class also dispatches the new-session security email — the library does not
* call `IEmailProvider.sendNewSessionAlert` automatically; consumers wire it
* inside their own `onNewSession` hook.
Expand Down Expand Up @@ -232,7 +230,7 @@ export class AppAuthHooks implements IAuthHooks {
device: sessionInfo.device,
});

// Dispatch the new-session security email (FCM #15). The library never
// Dispatch the new-session security email. The library never
// calls `sendNewSessionAlert` itself — consumers are responsible for the
// dispatch, typically from this hook. Wrap in try/catch so an email
// failure never blocks the login response.
Expand Down Expand Up @@ -283,7 +281,7 @@ export class AppAuthHooks implements IAuthHooks {
* to `PrismaUserRepository.createWithOAuth`, which performs an upsert on
* `(tenantId, email)`. If a user registered via email/password with the same
* address, their OAuth fields are updated in-place rather than creating a
* duplicate row — implementing the account-linking guarantee (FCM #12).
* duplicate row — implementing the account-linking guarantee.
*
* @param profile - Normalised OAuth profile from the provider.
* @param existingUser - Existing user found by OAuth provider ID, or null.
Expand Down
3 changes: 0 additions & 3 deletions apps/api/src/auth/auth-exception.filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,8 @@
* NestJS's built-in exception handler. Stack traces and internal Prisma/Redis
* diagnostics are never included in the response body.
*
* Covers FCM row #29 (shared error codes, anti-enumeration).
*
* @layer auth
* @see docs/guidelines/security-privacy-guidelines.md
* @see docs/DEVELOPMENT_PLAN.md §Phase 7 P7-7
*/

import type { ArgumentsHost, ExceptionFilter } from '@nestjs/common';
Expand Down
10 changes: 2 additions & 8 deletions apps/api/src/auth/auth.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* @file auth.config.ts
* @description Factory that builds `BymaxAuthModuleOptions` from the Zod-validated
* environment, consumed by `BymaxAuthModule.registerAsync` in Phase 7.
* environment, consumed by `BymaxAuthModule.registerAsync`.
*
* Keeps configuration concerns separate from module wiring: this file answers
* "what are the options?" while `auth.module.ts` answers "how is the module wired?".
Expand All @@ -14,7 +14,6 @@
*
* @layer auth
* @see docs/guidelines/nest-auth-guidelines.md
* @see docs/DEVELOPMENT_PLAN.md §Phase 6.1
*/

import type { Request } from 'express';
Expand All @@ -27,12 +26,7 @@ import { BLOCKED_USER_STATUSES } from './auth.constants.js';
/**
* Builds the `BymaxAuthModuleOptions` object for `BymaxAuthModule.registerAsync`.
*
* Every option group directly maps to the development plan §6.1 spec. The function
* is pure — no side effects, no logging, no secret values emitted anywhere.
*
* FCM rows covered: #3 (refresh grace), #5 (email verification), #13 (sessions),
* #14 (FIFO eviction), #16 (brute-force), #18/#19 (RBAC), #20 (tenant resolver),
* #23 (blocked statuses).
* The function is pure — no side effects, no logging, no secret values emitted anywhere.
*
* @param config - Zod-validated `ConfigService<Env, true>`. Every required variable
* is guaranteed present because the app refuses to start on an invalid config.
Expand Down
5 changes: 1 addition & 4 deletions apps/api/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* @file auth.module.ts
* @description Phase 7 module that wires `BymaxAuthModule.registerAsync` with all
* @description NestJS module that wires `BymaxAuthModule.registerAsync` with all
* five required implementation bindings: user repository, platform user repository,
* Redis client, email provider, and auth hooks.
*
Expand All @@ -15,11 +15,8 @@
* validates this eagerly and does not check `imports`. The global `RedisModule` provides
* a separate client instance for other feature modules (e.g. NotificationsModule).
*
* Covers FCM rows #1–#5, #13–#20, #23, #29–#32 (module-level wiring layer).
*
* @layer auth
* @see docs/guidelines/nest-auth-guidelines.md
* @see docs/DEVELOPMENT_PLAN.md §Phase 7 P7-1
*/

import { Module } from '@nestjs/common';
Expand Down
3 changes: 0 additions & 3 deletions apps/api/src/auth/mailpit-email.provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@
* - Template rendering uses simple `{{var}}` string replacement; no eval or
* dynamic code execution.
*
* Covers FCM rows #5 (email verification), #6/#7 (password reset), #15 (new-session
* alert), #21 (invitations), #31 (custom email provider).
*
* @layer auth
* @see docs/guidelines/email-guidelines.md
* @see docs/guidelines/logging-guidelines.md
Expand Down
1 change: 0 additions & 1 deletion apps/api/src/auth/noop-fallbacks.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
* linking to an existing user (if found) or creating a new account.
* `NoOpEmailProvider` — resolves all delivery methods without doing anything.
*
* @see docs/DEVELOPMENT_PLAN.md §Appendix B — Library Export → Example File Map
* @layer test
*/

Expand Down
4 changes: 2 additions & 2 deletions apps/api/src/auth/prisma-platform-user.repository.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* - `findByEmail`: found/not-found, lower-case normalisation.
* - `updateLastLogin`, `updateMfa`, `updatePassword`, `updateStatus`.
*
* Security-critical invariants validated here (FCM row #22):
* Security-critical invariants validated here:
* - `mfaSecret` is absent (not undefined) when null in the DB.
* - `mfaRecoveryCodes` is absent when `mfaEnabled=false`.
* - `platformId` is absent when null in the DB.
Expand Down Expand Up @@ -77,7 +77,7 @@ describe('PrismaPlatformUserRepository.findById', () => {

it('returns a mapped AuthPlatformUser when the row exists', async () => {
// Happy path — the row must be mapped to AuthPlatformUser with all required
// fields populated correctly (FCM #22).
// fields populated correctly.
platformUserFindUnique.mockResolvedValue(makePlatformUserRow());

const result = await repo.findById('platform-user-1');
Expand Down
4 changes: 1 addition & 3 deletions apps/api/src/auth/prisma-platform-user.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@
* - Platform users are never mixed with tenant users — different Prisma models,
* different JWT payloads, different guards.
*
* Covers FCM row #22 (platform admin backing repository).
*
* @layer auth
* @see docs/guidelines/prisma-guidelines.md
* @see docs/guidelines/nest-auth-guidelines.md
Expand All @@ -31,7 +29,7 @@ import { PrismaService } from '../prisma/prisma.service.js';
/**
* Prisma-backed repository for the platform admin auth context.
*
* Injected via `BYMAX_AUTH_PLATFORM_USER_REPOSITORY` token in Phase 7's `AuthModule`.
* Injected via `BYMAX_AUTH_PLATFORM_USER_REPOSITORY` token in `AuthModule`.
* No `tenantId` filtering — platform users are not tenant-scoped.
*
* @public
Expand Down
4 changes: 2 additions & 2 deletions apps/api/src/auth/prisma-user.repository.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
* - `findByOAuthId` — provider + providerId scoped to tenant.
* - `linkOAuth` — OAuth fields update call.
*
* These paths are security-critical (FCM #12, #23, #32): regressions here would
* These paths are security-critical: regressions here would
* break tenant isolation, allow blocked accounts to receive tokens, or corrupt
* user credentials.
*
Expand Down Expand Up @@ -350,7 +350,7 @@ describe('PrismaUserRepository.findById', () => {

it('returns AuthUser scoped to tenantId when tenantId is provided', async () => {
// findById must pass both `id` and `tenantId` in the WHERE clause when
// tenantId is supplied — this is the tenant-isolation requirement (FCM #32).
// tenantId is supplied — this is the tenant-isolation requirement.
userFindFirst.mockResolvedValue(makeUserRow());

const result = await repo.findById('user-1', 'acme');
Expand Down
4 changes: 1 addition & 3 deletions apps/api/src/auth/prisma-user.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@
* - Every query that returns a user is scoped by `tenantId` to prevent cross-tenant leaks.
* - Email is stored lower-case on write and returned as-is from the DB.
*
* Covers FCM row #32 (custom user repository).
*
* @layer auth
* @see docs/guidelines/prisma-guidelines.md
* @see docs/guidelines/nest-auth-guidelines.md
Expand All @@ -36,7 +34,7 @@ import { BLOCKED_USER_STATUSES } from './auth.constants.js';
/**
* Prisma-backed user repository for the tenant (dashboard) auth context.
*
* Injected via `BYMAX_AUTH_USER_REPOSITORY` token in Phase 7's `AuthModule`.
* Injected via `BYMAX_AUTH_USER_REPOSITORY` token in `AuthModule`.
* Repositories are the only layer that imports `PrismaService` directly.
*
* @public
Expand Down
4 changes: 1 addition & 3 deletions apps/api/src/auth/resend-email.provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@
* - Template rendering uses simple `{{var}}` string replacement; no eval or
* dynamic code execution.
*
* Covers FCM row #31 (custom email provider — production variant).
*
* @layer auth
* @see docs/guidelines/email-guidelines.md
* @see docs/guidelines/logging-guidelines.md
Expand Down Expand Up @@ -44,7 +42,7 @@ const TEMPLATE_DIR = resolve(dirname(fileURLToPath(import.meta.url)), 'email-tem
* Resend SDK-backed email provider for production deployments.
*
* Injected in place of `MailpitEmailProvider` when `EMAIL_PROVIDER=resend`.
* Enabled and registered by the `AuthModule` in Phase 7.
* Enabled and registered by the `AuthModule`.
*
* @public
*/
Expand Down
3 changes: 1 addition & 2 deletions apps/api/src/config/env.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
*
* @layer config
* @see docs/guidelines/environment-guidelines.md
* @see docs/DEVELOPMENT_PLAN.md Appendix A
*/

import { z } from 'zod';
Expand Down Expand Up @@ -179,7 +178,7 @@ const base = z.object({
* Password reset delivery method.
*
* `token` (default) sends a signed link; `otp` sends a short numeric code.
* Both modes are available in this example to cover FCM rows #6 and #7.
* Both modes are available in this example to demonstrate token-link and OTP flows.
*/
PASSWORD_RESET_METHOD: z
.enum(['token', 'otp'])
Expand Down
3 changes: 1 addition & 2 deletions apps/api/src/debug/debug.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,12 @@
* Endpoints:
* - `POST /api/debug/lockout` — forces brute-force lockout for a given
* `(tenantId, email)` pair so QA can demo the lockout flow without
* manually exhausting failed attempts. Covers FCM row #16.
* manually exhausting failed attempts.
*
* The lockout key format mirrors the library's internal Redis key:
* `<redisNamespace>:lf:<sha256(tenantId + ':' + email.toLowerCase())>`
*
* @layer debug
* @see docs/DEVELOPMENT_PLAN.md §Phase 7 P7-5
*/

import {
Expand Down
1 change: 0 additions & 1 deletion apps/api/src/debug/debug.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
*
* @layer debug
* @see debug.controller.ts
* @see docs/DEVELOPMENT_PLAN.md §Phase 7 P7-5
*/

import { Module } from '@nestjs/common';
Expand Down
20 changes: 10 additions & 10 deletions apps/api/src/health/health.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,18 @@
* @file health.controller.ts
* @description Aggregate readiness probe for `apps/api`.
*
* Phase 5 upgrade: checks Postgres (`SELECT 1`), Redis (`PING`), and reads the
* Checks Postgres (`SELECT 1`), Redis (`PING`), and reads the
* installed `@bymax-one/nest-auth` version via a walk-up package.json strategy
* that is robust against library restructuring. Individual dependency failures
* downgrade `status` to `'degraded'` but still return HTTP 200 so orchestrators
* can distinguish a degraded-but-alive process from a crash.
*
* Also exposes `GET /api/health/throttle-demo` decorated with
* `@Throttle(AUTH_THROTTLE_CONFIGS.login)` to demonstrate FCM row #17 (IP-based
* rate limiting) without touching any auth state.
* `@Throttle(AUTH_THROTTLE_CONFIGS.login)` to demonstrate IP-based rate limiting
* without touching any auth state.
*
* @layer infrastructure
* @see health.types.ts
* @see docs/DEVELOPMENT_PLAN.md §Phase 5 P5-4
*/

import { createRequire } from 'node:module';
Expand Down Expand Up @@ -89,7 +88,7 @@ const LIB_VERSION: string = resolveLibraryVersion();
*
* Mounted under the global `/api` prefix:
* - `GET /api/health` — aggregate readiness probe.
* - `GET /api/health/throttle-demo` — throttled endpoint for FCM #17 demo.
* - `GET /api/health/throttle-demo` — throttled endpoint for rate-limiting demo.
*
* @public
*/
Expand All @@ -110,8 +109,8 @@ export class HealthController {
* status without throwing, so HTTP 200 is always returned with a body
* that orchestrators can inspect.
*
* Marked `@Public()` so that once the global `JwtAuthGuard` is registered
* in Phase 7 this route remains accessible to liveness probes without a token.
* Marked `@Public()` so the route remains accessible to liveness probes without
* a JWT (the global `JwtAuthGuard` would otherwise reject it).
*
* @returns Aggregate health status with per-dependency details.
*/
Expand Down Expand Up @@ -140,15 +139,16 @@ export class HealthController {
}

/**
* Throttled demo endpoint for FCM row #17.
* Throttled demo endpoint for IP-based rate limiting.
*
* Applies the `login` throttle tier (5 requests per 60 s per IP) so the
* frontend can demonstrate the HTTP 429 response without touching any auth
* state. `@UseGuards(ThrottlerGuard)` enables throttling for this route;
* `@Throttle` overrides the module-level default with the login tier.
*
* Marked `@Public()` so it remains reachable after Phase 7 registers the
* global `JwtAuthGuard`. The ThrottlerGuard provides IP-level rate limiting.
* Marked `@Public()` so it remains reachable without a JWT
* (the global `JwtAuthGuard` would otherwise reject it). The ThrottlerGuard
* provides IP-level rate limiting.
*
* @returns Timestamp object confirming the request was served.
*/
Expand Down
Loading
Loading