Skip to content

Authentication System - Deep Dive Documentation

Generated: 2026-02-13 | Validated: 2026-02-13 | Review: All 21 findings resolved — see deep-dive-auth-review-findings.md Scope: Full-Stack Authentication (Backend + Frontend) Files Analyzed: 70+ Lines of Code: ~5,500+ Workflow Mode: Exhaustive Deep-Dive

Overview

The authentication system is the backbone of the SaaS boilerplate, spanning both the Express.js backend and Next.js 15 frontend. It implements multi-provider authentication (email/password, Google OAuth, OTP-based passwordless login), two-factor authentication (email OTP + Google Authenticator), JWT access tokens with automatic access token refresh, role-based access control (RBAC) with route-level permissions, and progressive account lockout with login attempt tracking.

Purpose: Secure user identity management, session handling, and authorization across the entire application.

Key Responsibilities: - User registration with email verification (invite-only mode supported) - Multi-method login (password, OAuth, OTP) - JWT token lifecycle (access + refresh tokens) - Two-factor authentication (email OTP, TOTP via Google Authenticator, backup codes) - Password reset via email with hashed tokens - Role-based access control with cached permission checking - Login attempt tracking with progressive lockout - Frontend route protection and session persistence

Integration Points: Every protected API endpoint, every frontend page behind the dashboard, payment subscription checks, user profile management, admin operations.


Core Auth File Inventory

Backend — Auth Module (saas-boilerplate/src/app/modules/v1/Auth/)


saas-boilerplate/src/app/modules/v1/Auth/auth.route.ts

Purpose: Defines all authentication HTTP routes. Applies rate limiting to login and OTP endpoints, wires Zod validation, and delegates to controllers. LOC: 99 File Type: Route definition

What Future Contributors Must Know: Rate limits are per-route (login: 100/15min, OTP request: 5/15min, OTP verify: 10/15min). Google OAuth routes have no rate limiting. No auth middleware on any route — these are all public endpoints.

Exports: - default (authRoutes: Router) — Express Router with 16 route registrations

Dependencies: - AuthValidation — Zod schemas for each endpoint - AuthController — Handler functions - validateRequest — Zod validation middleware - rateLimiter — Rate limiting middleware - GoogleOAuthController — OAuth flow handlers

Used By: - src/app/routes/v1/index.ts — Route aggregator

Key Implementation Details: - 16 endpoints: /google, /google/callback, /profile, /logout, /refresh-token, /failure, /login, /register, /forgot-password, /verify-otp, /verify-2fa-google, /verify-backup-code, /login-otp, /verify-login-otp, /verify-email, /reset-password - Rate limiting applied to /login (100 req/15min), /login-otp (5 req/15min), /verify-login-otp (10 req/15min)

Patterns Used: - Express Router chaining with middleware pipeline per route

Side Effects: None (pure routing)

Error Handling: Delegated to validation middleware and controllers

Testing: No test file exists

Comments/TODOs: Rate limit comment says "limit each IP to 3 requests" but actual value is 100 (line 22)


saas-boilerplate/src/app/modules/v1/Auth/auth.contoroller.ts

Purpose: HTTP request/response handlers for all auth endpoints. Handles cookie management, JWT generation, and delegates business logic to AuthService. Note: filename has typo ("contoroller"). LOC: 261 File Type: Controller

What Future Contributors Must Know: Login flow has complex branching: normal login, email 2FA (returns method "otp_verification"), Google Authenticator 2FA (returns method "google_auth"). With rememberMe, the JWT in the response body gets a 15-day expiry ("15d") and setTokenCookie sets a 20-day httpOnly cookie. The token cookie is set as httpOnly by setTokenCookie, but the frontend's setCookie call overwrites it with a non-httpOnly cookie — so in practice the token is accessible to client JavaScript. refresh_token is httpOnly.

Exports: - AuthController object with: createAccount, loginAccount, forgotPassword, resetPassword, verifyOtp, verifyEmailForRegistration, verify2faGoogleAuthenticator, refereshToken (typo), requestLoginOtp, verifyLoginOtp, verifyBackupCode

Dependencies: - config — JWT secrets - jwtHelpers — Token generation/verification - catchAsync — Error wrapper - sendResponse — Response formatter - AuthService — Business logic - UserSettingService — Backup code verification - setTokenCookie — Cookie utility

Used By: - auth.route.ts — Route handler binding

Key Implementation Details: - loginAccount (line 21-107): Generates access token + refresh token, sets cookie. Returns different shapes for 2FA vs normal login - verifyOtp (line 120-143): Reads rememberMe from cookies, sets token cookie if remember was true - refereshToken (line 179-188): Reads refresh_token from httpOnly cookie, generates new access token - verifyLoginOtp (line 201-236): Full token + refresh token + cookie flow for OTP-based login

Patterns Used: - catchAsync wrapper for all handlers - Consistent sendResponse for success cases - Direct res.status().json() for 2FA partial responses (inconsistent)

Side Effects: - Sets cookies: token, refresh_token, rememberMe - Clears cookies on non-remember login

Error Handling: All wrapped in catchAsync, errors thrown as ApiError

Risks: - setTokenCookie generates a separate token with 20-day expiry, different from the response token - Login generates token in BOTH controller AND setTokenCookie — double token generation - rememberMe cookie set as non-httpOnly (line 54) - Empty block at line 124-125 (dead code)

Testing: No test file

Comments/TODOs: None


saas-boilerplate/src/app/modules/v1/Auth/auth.service.ts

Purpose: Core business logic for all authentication operations: registration, login (with 2FA branching), password reset, OTP verification, email verification, Google Authenticator TOTP verification, refresh token, and OTP-based passwordless login with auto-registration. LOC: 746 File Type: Service (business logic)

What Future Contributors Must Know: This is the most critical file in the auth system. The loginAccount function has complex branching for super_admin bypass, email 2FA, Google Authenticator 2FA, and normal login. verifyLoginOtp (passwordless) creates users inside a DB transaction if they don't exist. registerAccount sends verification email via event system (not direct). Registration stores hashed password in the JWT verification token itself (security consideration: password in JWT payload).

Exports: - AuthService object with: createAccount, loginAccount, forgotPassword, resetPassword, verifyOtp, registerAccount, verifyEmailForRegistration, verify2faGoogleAuthenticator, refreshToken, requestLoginOtp, verifyLoginOtp, logininfoWithToken

Dependencies: - Entities: User, Token, OtpVerification, LoginAttempt, InviteUser, UserSetting - Helpers: jwtHelpers, hashedPassword, verifyPassword, generateOTP, getTimeDifferenceLabel - Services: RoleService, UserSerivce (typo), SettingService, InviteUserService - Event system: eventEmmiter, EVENT_TYPES - External: speakeasy (TOTP), crypto

Key Implementation Details:

  1. Registration flow (line 94-130):
  2. Checks invite-only mode via SettingService.getSettingByKey("auth.website_type")
  3. Hashes password, embeds hash+email+name in a JWT token (1d expiry)
  4. Emits SEND_REGISTRATION_EMAIL event with verification URL
  5. Does NOT create user yet — deferred to email verification

createAccount vs registerAccount: These are two distinct functions. registerAccount (line 94-130) is the public-facing flow described above — it checks invite-only mode, hashes the password, generates a JWT verification token, and emits an email event without creating a user. createAccount (line 46-78) is the internal DB function — it inserts or restores the user record after email verification (called by verifyEmailForRegistration). The controller handler named createAccount calls AuthService.registerAccount() (not AuthService.createAccount()), which is a naming confusion.

  1. Login flow (line 152-221):
  2. Loads user with relations: role, subscription, setting, role.permissions, userRoles
  3. Checks: user exists, not inactive, not suspended, not deleted, not OAuth-only
  4. Validates password via validatePassword (handles lockout)
  5. Super admin bypass: skips 2FA check entirely
  6. Email 2FA: generates OTP, emits event, saves to DB, returns {method: "email"}
  7. Google Authenticator 2FA: returns {method: "google_auth"} (no server action)
  8. Normal: returns JWT + user info

  9. Password validation & lockout (line 227-257):

  10. Gets or creates LoginAttempt record
  11. On failure: increments count, sets lock duration, blocks after 9 attempts
  12. On success: resets all counters

  13. Forgot password (line 299-327):

  14. Generates 64-byte random token, SHA-256 hashes it
  15. Stores hashed version in Token table (15min expiry)
  16. Sends raw token via email (user gets unhashed link)

  17. OTP-based passwordless login (line 593-731):

  18. requestLoginOtp: Creates OTP even if user doesn't exist (prevents email enumeration)
  19. verifyLoginOtp: Uses DB transaction to create user if new, reactivates soft-deleted users
  20. Auto-creates user with Provider.OTP, derives name from email prefix

  21. Refresh token (line 509-557):

  22. Verifies refresh token against separate secret
  23. Generates new access token only (no rotation of refresh token)

Patterns Used: - Repository pattern via getDbRepository - Event-driven email sending - Transaction for OTP user creation

Side Effects: - DB: Creates/updates User, Token, OtpVerification, LoginAttempt - Events: SEND_REGISTRATION_EMAIL, SEND_LOGIN_OTP, FORGOT_PASSWORD

Error Handling: Throws ApiError with HTTP status codes

Risks: - Registration JWT contains hashed password in payload (line 111-115) — if JWT is intercepted, hash is exposed - verifyLoginOtp creates users with no password — these users cannot do password login - requestLoginOtp doesn't verify user exists before sending OTP (by design for privacy, but could be abused) - logininfoWithToken calls UserSerivce.getProfile twice in some code paths (performance) - hashassword typo at line 352

Testing: No test file

Comments/TODOs: - Line 117-121: Commented-out direct sendEmail call (replaced by event system) - Line 122: console.log("event emit register email") — debug log left in


saas-boilerplate/src/app/modules/v1/Auth/auth.validation.ts

Purpose: Zod validation schemas for all auth request bodies. LOC: 88 File Type: Validation

What Future Contributors Must Know: Password minimum is 6 chars for login/register but only 3 chars for reset password (line 29). OTP is always 6 digits. Google 2FA token requires exactly 6 digits via regex.

Exports: - AuthValidation object with 10 schemas: registerValidation, loginValidation, forgotPasswordValidation, verifyOtpValidation, resetPasswordValidation, verifyEmailValidation, verify2faGoogleValidation, loginOtpRequestValidation, loginOtpVerifyValidation, verifyBackupCodeValidation

Risks: - resetPasswordValidation has password: z.string().min(3) — inconsistent with register's min(6)


saas-boilerplate/src/app/modules/v1/Auth/auth.helpers.ts

Purpose: Login attempt management — progressive lockout logic with escalating durations. LOC: 144 File Type: Helper

What Future Contributors Must Know: Lock durations escalate: 1min (4th), 5min (5th), 15min (6th), 30min (7th), 1h (8th), 2h (9th), 4h (10th), 8h (11th), 24h (12th), 7 days (13th+). Account blocks permanently at 9+ attempts (isBlocked=true). There's a logic gap: calculateLockDuration handles attempts 4-13+ but handleFailedAttempt blocks at 9+ with isBlocked=true, so the 7th and 8th failed attempts fall through with no lock applied.

Exports: - getOrCreateLoginAttempt(userId: number) — Gets or creates LoginAttempt record - checkIfBlockedOrLocked(attempt: LoginAttempt) — Throws if blocked or locked - handleFailedAttempt(attempt: LoginAttempt) — Increments counter, applies lock - resetLoginAttempt(attempt: LoginAttempt) — Resets all counters

Risks: - Logic bug: handleFailedAttempt applies lock for attempts 2-6 and blocks at 9+, but attempts 7-8 fall through with no lock (line 111-117) - checkIfBlockedOrLocked has unreachable code: checks isBlocked at line 81, then isBlocked && lockUntil at line 88 — the second check never runs if blocked


saas-boilerplate/src/app/modules/v1/Auth/auth.utils.ts

Purpose: Utility functions: save OTP to database (hashed), set JWT cookie, encode error messages. LOC: 43 File Type: Utility

Exports: - saveOtpToDb(email, otp, expiryMinutes=15) — Hashes OTP with bcrypt and saves to OtpVerification table - setTokenCookie(res, jwtPayload) — Generates 20-day JWT and sets as cookie - encodeErrorMessage(errorMessage) — URL-encodes error for redirect params

Risks: - setTokenCookie uses httpOnly: true and checks NODE_ENV for secure — but secure defaults to false in development - Cookie maxAge is 20 days (long-lived), separate from the JWT that may have a shorter expires_in


saas-boilerplate/src/app/modules/v1/Auth/google-oauth.controller.ts

Purpose: Handles Google OAuth flow: initiates auth, processes callback, manages profile retrieval, logout, and failure redirect. LOC: 161 File Type: Controller (OAuth)

What Future Contributors Must Know: OAuth callback uses Passport's callback pattern (not redirect). On success, sets httpOnly cookie and redirects to frontend with token in URL. If user has Google Authenticator 2FA enabled, redirects with method=google_auth query param instead of token. Token is passed in URL query string (security consideration).

Exports: - default GoogleAuthController with: googleAuth, googleCallback, getProfile, logout, authFailure

Key Implementation Details: - googleCallback (line 16-89): Sets token cookie (httpOnly, secure, strict sameSite, 7-day), redirects to frontend with ?success=true&token=...&email=... - Uses config.nod_env (typo: should be node_env or env) - Handles 2FA: if user has Google Authenticator, redirects without token, just email and method

Risks: - JWT token passed in redirect URL query string — visible in browser history, server logs, referrer headers - Session-based logout (req.logout, req.session.destroy) mixed with JWT auth


Backend — Middleware (saas-boilerplate/src/app/middlewares/)


saas-boilerplate/src/app/middlewares/auth.ts

Purpose: Two auth middleware variants: authGuard (role-based with catchAsync) and auth (default export with automatic access token refresh). Both extract JWT from cookie or Authorization header, verify against DB, and attach user to request. LOC: 235 File Type: Middleware

What Future Contributors Must Know: Two separate auth middleware exist in this file! authGuard is the simpler version (role check only). auth (default export) handles token expiration by checking the refresh token cookie and auto-generating a new access token. Both do a DB lookup on every request to verify user exists and is active.

Exports: - authGuard(...roles: string[]) — Named export, role-based guard - default auth(...roles: string[]) — Default export, with automatic access token refresh

Key Implementation Details: - authGuard (line 16-78): Checks cookie first, then Authorization header. Verifies token, loads user from DB, validates role matches JWT claim. Has debug error message "You are not authorized!33" (line 62) - auth (line 134-233): Same token extraction, but on TokenExpiredError catches and attempts refresh. Validates refresh token against user.refreshToken in DB (but User entity has no refreshToken column!) - Both add roleId to req.user

Risks: - auth middleware checks user.refreshToken !== refreshToken (line 174) but the User entity has no refreshToken column — this will always be undefined !== string, causing refresh to fail - Two competing auth middlewares could confuse developers about which to use - DB query on every authenticated request (no caching) - Commented-out old implementation still in file (line 80-132)


saas-boilerplate/src/app/middlewares/hasPermission.ts

Purpose: RBAC middleware that checks if the authenticated user's role has the required permission for the requested route/action. LOC: 79 File Type: Middleware

What Future Contributors Must Know: Permission checking is path-based. Normalizes URL by removing /api/v1 prefix, maps HTTP methods to actions (GET=read, POST=create, PATCH=edit, DELETE=delete). Super admin bypasses all checks. Loads role with permissions from DB on every request.

Exports: - default hasPermission() — Returns Express middleware function

Key Implementation Details: - normalizePath (line 9-18): Strips query params and /api/v1, keeps action suffixes (edit, delete, create) - getActionFromMethod (line 20-36): Maps HTTP method + URL action segment to permission action - Permission format: /{resource} for read, /{resource}/{action} for mutations - DB query per request: loads Role with permissions relation

Risks: - DB query on every protected request (no caching at middleware level) - Only checks primary role permissions, not userRoles (multi-role)


Backend — Config & Helpers


saas-boilerplate/src/config/passport.ts

Purpose: Configures Passport.js Google OAuth strategy. Handles user lookup/creation, session serialization. LOC: 95 File Type: Configuration

What Future Contributors Must Know: Creates new users with Provider.GOOGLE and no password. Checks if email already exists as credential-based user and rejects (unless invite-only). Includes soft-delete awareness (withDeleted: true). Error handling returns structured {message, isAuthError} object to done callback.

Risks: - profile.emails?.[0]?.value! — Non-null assertion on optional value (line 50) - Soft-deleted users with same email cannot re-register via Google OAuth - Session-based serialize/deserialize configured but JWT is primary auth mechanism — potential conflict


saas-boilerplate/src/config/index.ts

Purpose: Centralized environment variable configuration. Groups all env vars into typed sections. LOC: 95 File Type: Configuration

Key Auth-Related Config: - jwt.jwt_secret — Main JWT signing secret - jwt.expires_in — Access token TTL - jwt.refresh_token_secret — Refresh token secret - jwt.refresh_token_expires_in — Refresh token TTL - jwt.reset_pass_secret — Password reset token secret - sesseion_secret (typo) — Express session secret - oauth.googleClientId/googleClientSecret — Google OAuth credentials - client_url — Frontend URL for redirects - reset_pass_link — Password reset base URL - nod_env (typo) — NODE_ENV reference - invite_user_link — Invitation URL - admin.email — Admin account email (ADMIN_EMAIL env var, used for admin seeding) - admin.password — Admin account password (ADMIN_PASSWORD env var, used for admin seeding) - masterKey — Master key for elevated operations (MASTER_KEY env var)

Risks: - Typos: sesseion_secret, nod_env — must match .env file keys exactly - No validation — missing env vars silently become undefined (some defaults exist: mail type, image storage, NODE_ENV)


saas-boilerplate/src/helper/jwtHelpers.ts

Purpose: JWT token generation, verification, decoding, and refresh token operations. LOC: 43 File Type: Helper

Exports: - jwtHelpers.generateToken(payload, secret, expiresIn) — Creates JWT with HS256 - jwtHelpers.verifyToken(token, secret) — Verifies and returns payload - jwtHelpers.decodeToken(token) — Decodes without verification - jwtHelpers.generateRefreshToken(payload) — Creates refresh token with separate secret/TTL - jwtHelpers.verifyRefreshToken(token) — Verifies refresh token

Risks: - decodeToken does NOT verify signature — if used alone, tokens could be forged - Algorithm hardcoded to HS256 — no key rotation support


saas-boilerplate/src/helper/hashedPassword.ts

Purpose: Bcrypt password hashing and verification. LOC: 12 File Type: Helper

Exports: - hashedPassword(password: string) — Hashes with 10 salt rounds (uses hashSync despite async function signature) - verifyPassword(password, hash) — Compares with bcrypt

Risks: - Uses hashSync inside an async function — blocks event loop during hashing - Also used to hash OTPs (via saveOtpToDb) — bcrypt for 6-digit numbers is overkill but secure


saas-boilerplate/src/helper/generateOtp.ts

Purpose: Generates random 6-digit OTP. LOC: 3

Exports: - generateOTP(): string — Returns random 6-digit number as string

Risks: - Uses Math.random() — not cryptographically secure. Should use crypto.randomInt() for security-critical OTPs


saas-boilerplate/src/helper/hasPermission.ts

Purpose: Programmatic permission checking for service-layer code. Supports checking permissions for the current request user (via AsyncLocalStorage context) or for any user by ID. Implements permission caching, multi-role support, and both route-format and dot-notation permission strings. LOC: 295 File Type: Helper (service-layer permission checking)

Exports: - hasPermission(permissionName: string): Promise<boolean> — Checks current user - hasPermissionForUser(userId, permissionName): Promise<boolean> — Checks specific user - getCurrentUserPermissions(): Promise<string[]> — Gets all permissions for current user - hasMultiplePermissions(permissions): Promise<Record<string, boolean>> — Batch check - hasAnyPermission(permissions): Promise<boolean> — OR check - hasAllPermissions(permissions): Promise<boolean> — AND check - PermissionError class

Key Implementation Details: - Uses requestContext (AsyncLocalStorage) to get current user - Super admin returns true for all permissions - Cache-first: checks permissionCache before DB - Loads user with primary role + userRoles (multi-role) - Supports both /users/create and users.create formats


Backend — Shared Utilities


saas-boilerplate/src/shared/requestContext.ts

Purpose: AsyncLocalStorage-based request context for sharing user data across the call chain without passing it through function arguments. LOC: 122 File Type: Shared utility

Exports: - RequestUser, PermissionCache, RequestContext interfaces - getRequestContext() — Gets current context - getCurrentUser() — Gets user from context (throws if missing) - runWithContext(context, fn) — Runs function within context - setPermissionCache/getPermissionCache/clearPermissionCache — Cache management - isCurrentUserSuperAdmin() — Super admin check - Error classes: RequestContextError, UserNotFoundError, ContextNotFoundError


saas-boilerplate/src/shared/permissionCache.ts

Purpose: Per-request permission cache backed by request context. Validates cache ownership against current user to prevent cross-user cache leaks. LOC: 183 File Type: Shared utility

Exports: - initializePermissionCache(user, roles) — Creates cache from user roles - getCachedPermissions() / hasPermissionInCache(permission) / getCachedRoles() — Cache lookups - addPermissionToCache(permission) — Adds single permission - isCacheValid() / validateCacheOwnership() / invalidateCache() — Cache management - getCacheStats() — Debug info


saas-boilerplate/src/shared/permissionNormalizer.ts

Purpose: Normalizes permission strings between route format (/users/create) and dot notation (users.create). Provides bidirectional conversion and variant generation for flexible permission matching. LOC: 215 File Type: Shared utility

Exports: - normalizePermissionString(permission) — Canonical normalization - getPermissionVariants(permission) — Returns all format variants - detectPermissionFormat(permission) — Route vs dot notation - parseRouteFormat/parseDotNotation — Format-specific parsers - convertDotToRoute/convertRouteToDot — Format converters - extractResource/extractAction — Component extractors


Backend — Entities


saas-boilerplate/src/entity/User.ts

Purpose: Core user entity. Central to all auth operations. LOC: 134 Table: user Columns: id (PK), name, email (unique), password (select:false, nullable), roleId (FK, nullable), avatar, stripeCustomerId, lemonSqueezyCustomerId, phone, address, status (enum: active/inactive/suspend), provider (enum: credential/google/facebook/github/otp), inviteOnly (boolean), isDeleted (boolean), createdAt, updatedAt, deletedAt Relations: role (ManyToOne->Role), userRoles (ManyToMany->Role via user_roles), purchases (OneToMany), subscription (OneToOne), blogs (OneToMany), setting (OneToOne->UserSetting) Indexes: 6 compound indexes on status/deletedAt, email/deletedAt, roleId/deletedAt, status/roleId/deletedAt, provider/email, inviteOnly/status

Risks: - isDeleted boolean coexists with @DeleteDateColumn — redundant soft-delete mechanism - password is select: false but explicitly selected in auth queries — easy to accidentally expose - No refreshToken column despite auth middleware checking user.refreshToken


saas-boilerplate/src/entity/Otp.ts (OtpVerification)

Purpose: Stores hashed OTP codes for email verification and login OTP. LOC: 38 Table: otp_verification Columns: id, email (indexed), optHash (indexed, typo: should be otpHash), isVerified (default false), createdAt, updatedAt, expiresAt (timestamp, nullable), deletedAt

Risks: - optHash column name has typo — renaming requires migration - Old OTPs are never cleaned up (no TTL-based deletion)


saas-boilerplate/src/entity/LoginAttempt.ts

Purpose: Tracks failed login attempts per user for progressive lockout. LOC: 49 Table: login_attempt Columns: id, userId (FK, indexed), failedAttemptsCount (default 0), lastFailedAt (timestamp, nullable), lockUntil (timestamp, nullable), isBlocked (default false, indexed), createdAt, updatedAt (indexed), deletedAt


saas-boilerplate/src/entity/Role.ts

Purpose: Role definitions with hierarchical permissions. LOC: 57 Table: role Columns: id, name (indexed), displayName, isDeleted (default false), createdAt, updatedAt, deletedAt Relations: users (OneToMany from User), userRoles (ManyToMany from User), permissions (ManyToMany->Permission via role_permission, cascade true) Indexes: Partial index IDX_ROLE_NAME_ACTIVE on name where deletedAt IS NULL


saas-boilerplate/src/entity/Permission.ts

Purpose: Route-based permission definitions. LOC: 37 Table: permission Columns: id, route (unique, indexed), displayName, readonly (default false), isDeleted (default false), createdAt, updatedAt, deletedAt


saas-boilerplate/src/entity/Token.ts

Purpose: Stores hashed password-reset tokens with expiration. LOC: 35 Table: token Columns: id, userId (indexed), token (indexed, SHA-256 hash), expiresAt, createdAt, updatedAt, deletedAt


saas-boilerplate/src/entity/UserRoles.ts (UserRole)

Purpose: Junction entity for many-to-many user-role relationship. LOC: 37 Table: user_role Columns: id, FK to User (cascade delete), FK to Role (cascade delete), createdAt, updatedAt, deletedAt Constraint: Unique on [user, role]


saas-boilerplate/src/entity/UserSetting.ts

Purpose: Per-user settings, primarily 2FA configuration. LOC: 64 Table: user_setting Columns: id, userId (FK, indexed), isTwoFactorEnabled (default false), twoFactorProvider (enum: email/google_authenticator/sms, nullable), twoFactorSecret (text, nullable), tempTwoFactorSecret (text, nullable), backupCodes (text, nullable), language (varchar, default "en"), createdAt, updatedAt, deletedAt Relation: OneToOne with User (cascade delete)


saas-boilerplate/src/entity/InviteUser.ts

Purpose: Tracks invited users for invite-only registration mode. LOC: 38 Table: user_invites Columns: id, email (unique), status (enum: PENDING/ACCEPTED), createdAt, updatedAt, deletedAt


saas-boilerplate/src/entity/Subscription.ts

Purpose: User subscription records linking users to packages. LOC: 68 Table: subscription Columns: id, userId (FK, indexed), packageId (FK, indexed), startDate, endDate, status (enum: active/expired/pending/cancelled), stripeSubscriptionId, createdAt, updatedAt, deletedAt Relations: OneToOne with User, ManyToOne with Package Why included in auth scope: Subscription data (status, package name, billing cycle) is loaded via user.subscription relation in the login query (auth.service.ts:167-174) and returned in the login response payload.


Backend — Event Handlers


saas-boilerplate/src/app/events/eventTypes.ts

Purpose: Constants for all event type strings. LOC: 13 Auth-related events (9 of 10 total): SEND_LOGIN_OTP, OTP_VERIFIED, FORGOT_PASSWORD, RESET_PASSWORD, VERIFY_EMAIL_REG, RESET_LOGIN_ATTEMPTS, INVITEONLY_USER_LOGIN, SEND_REGISTRATION_EMAIL, INVITE_USER_CREATED (10th event RESET_SUBSCRIPTION_STATUS is subscription-related)


Event Handlers (7 files)

File Event Action LOC
registerEmailHandler.ts SEND_REGISTRATION_EMAIL Sends registration verification email via EmailFactory 18
sendForgotPass.handler.ts FORGOT_PASSWORD Sends password reset email with link 14
sendLoginOtp.handler.ts SEND_LOGIN_OTP Sends OTP email for login verification 13
resetLoginAttempts.handler.ts RESET_LOGIN_ATTEMPTS Bulk resets login attempts older than 7 days 28
inviteOnlyLoginSendEmail.ts INVITEONLY_USER_LOGIN Sends invite-only login credentials 14
inviteUserCreate.handler.ts INVITE_USER_CREATED Sends invitation email to new invited user 21
resetSubscribtionStatus.handler.ts RESET_SUBSCRIPTION_STATUS Resets subscription status (auth-adjacent)

saas-boilerplate/src/app/cron/scheduleResetLoginAttempts.ts

Purpose: Daily cron job (midnight) that emits RESET_LOGIN_ATTEMPTS event. LOC: 13 Risk: Cron jobs don't work on Vercel serverless. This job will never execute in production if deployed to Vercel.


Frontend — Auth System


Sass-boilerplate-frontend-v1/src/services/authService.js

Purpose: API service functions for authentication — thin wrappers around axios calls. LOC: 54 Exports: authAPI object with: login, register, verifyEmail, getProfile, refreshToken Key Detail: getProfile calls /users/profile (not /auth/profile). refreshToken calls /auth/refresh (but backend route is /auth/refresh-token — mismatch!).

Risks: - refreshToken endpoint URL may not match backend route - Error handling strips error to error.response?.data — loses HTTP status info


Sass-boilerplate-frontend-v1/src/api/auth/index.js

Purpose: React Query hooks for all auth operations. LOC: 153 Exports: useLogin, useRegister, useLogout, useProfile, useVerifyEmail, useRefreshToken

Key Implementation Details: - useLogin: On success, sets token cookie (30d if rememberMe, 7d otherwise). Handles 2FA response (doesn't set cookie, returns early). Uses window.location.href for redirect (full page reload). - useLogout: No API call — just clears cookie, Redux state, and React Query cache - useProfile: Enabled only when token cookie exists. 5min stale time, 15min refetch interval. Skips retry on 401. - useRefreshToken: Sets new token cookie on success, clears everything on failure

Risks: - useLogin uses window.location.href = "/dashboard" — triggers full page reload instead of Next.js router navigation - useLogout has no server-side logout — refresh tokens aren't invalidated - keepPreviousData is deprecated in React Query 5 (should use placeholderData)


Sass-boilerplate-frontend-v1/src/store/slices/authSlice.js

Purpose: Redux Toolkit slice for global auth state. LOC: 47 State shape: { user, token, isAuthenticated } Reducers: setAuth, updateUser, logout, initializeAuth

Key Detail: initializeAuth reads token from cookie and sets isAuthenticated: true but does NOT set user — user data comes later from useProfile query.


Sass-boilerplate-frontend-v1/src/interceptors/axiosInstance.js

Purpose: Axios instance with auth interceptors. LOC: 48 Key Details: - Request interceptor: Adds Authorization: Bearer {token} from cookie - Response interceptor: On 401/403 — deletes cookie, dispatches Redux logout, redirects to /login - setStoreDispatch allows wiring Redux dispatch externally

Risks: - Line 29: error.response.status === 401 || error.response.status === 403 — missing parentheses around OR. Actually reads as (error.response && error.response.status === 401) || error.response.status === 403 — the 403 check can throw if error.response is undefined - 403 (forbidden) triggers logout — permission denied shouldn't log user out


Sass-boilerplate-frontend-v1/src/middleware.js

Purpose: Next.js Edge middleware for route protection. LOC: 23 Key Details: Checks token cookie. Redirects /dashboard/* to /login if no token. Redirects authenticated users away from /login and /register. Matcher: /login, /register, /dashboard/:path*


Sass-boilerplate-frontend-v1/src/components/providers/auth-initializer.jsx

Purpose: Client component that initializes auth state on app mount. LOC: 20 Key Details: Calls dispatch(initializeAuth()) and setStoreDispatch(dispatch) on mount. Must be in provider hierarchy above all auth-dependent components.


Sass-boilerplate-frontend-v1/src/components/providers/protected-route.jsx

Purpose: Client-side route guard component. LOC: 28 Key Details: Checks both Redux isAuthenticated and cookie presence. Redirects to /login if neither. Returns null during auth check (flash of empty content).

Risks: - Double protection with middleware.js — redundant but provides defense in depth - getCookie('token') called both in effect and render — could cause hydration mismatch


Sass-boilerplate-frontend-v1/src/components/pages/auth/login.jsx

Purpose: Full login page component with form, Google OAuth, and inline 2FA verification modal. LOC: 688 Key Details: - Handles OAuth callback via URL params (?success=true&token=...&email=...) - Validates JWT from URL before setting cookie (checks email match, expiry) - 2FA modal with OTP input (6-digit) and backup code fallback - Google Auth 2FA detected via method=google_auth URL param - Three configurable login page layouts (type ½/3) - Contains inline LoginTwoFactorVerification component

Risks: - JWT token accepted from URL query string — could be bookmarked/shared - Uses jwtDecode client-side which doesn't verify signature


Sass-boilerplate-frontend-v1/src/components/pages/auth/register.jsx

Purpose: Registration page with invite-only mode support. LOC: 424 Key Details: - Supports 3 website types: open (1), invite-only (2), approval-required (3) - For type 2: checks invite status via debounced API call on email input - Pre-fills email from URL param for invited users - Requires terms agreement checkbox


Sass-boilerplate-frontend-v1/src/components/pages/auth/social-login.jsx

Purpose: Google OAuth social login button. LOC: 80 Key Detail: Redirects to NEXT_PUBLIC_API_URL/auth/google?withCredentials=true — direct navigation, not API call.


Sass-boilerplate-frontend-v1/src/hooks/useCheckPermission.js

Purpose: React hook for client-side permission checking against Redux-stored permissions. LOC: 11 Key Detail: Checks if ALL required permissions exist in state.auth.user.permissions array. Returns true if no permissions required.


Frontend — 2FA Settings Components


settings/setup-two-factor-verification.jsx

Purpose: Modal wizard for enabling Google Authenticator 2FA. Step 1: Display QR code + manual secret. Step 2: OTP verification. LOC: 277

settings/two-factor-verification.jsx

Purpose: Reusable OTP input component for 2FA verify/setup/disable actions. LOC: 261

settings/disable-two-factor-modal.jsx, two-factor-backup.jsx, account-setting-page.jsx

Purpose: Additional 2FA management UI components (disable modal, backup code display, account settings page).


Frontend — Auth Pages (Next.js App Router)


File Purpose LOC
(auth)/layout.js Simple wrapper layout with footer credit 15
(auth)/login/page.js Page component that renders LoginPage 17
(auth)/register/page.js Page component that renders Register ~20
(auth)/verify-email/[token]/page.js Email verification with auto-redirect to login 128
forgot-password/page.js Forgot password form (uses raw axios, not authService) 122
reset-password/[token]/page.js Password reset form with token validation 187

Contributor Checklist

  • Risks & Gotchas:
  • Registration embeds hashed password in JWT payload — if token intercepted, hash exposed
  • Math.random() used for OTP generation — not cryptographically secure
  • hashSync used in async function — blocks event loop
  • Login attempt logic has gap: attempts 7-8 have no lockout applied
  • auth middleware checks user.refreshToken but User entity has no such column
  • Axios interceptor has operator precedence bug that can crash on 403 without response
  • 403 errors trigger full logout instead of just permission denied
  • OAuth token passed in URL query string — browser history/log exposure
  • Cron job for login attempt reset won't work on Vercel serverless
  • optHash column typo in OtpVerification entity
  • Password reset allows min 3 chars while registration requires min 6
  • refreshToken endpoint URL mismatch between frontend service and backend route

  • Pre-change Verification Steps:

  • Check that all auth routes still work after changes (16 endpoints)
  • Verify cookie behavior in both development and production (httpOnly, secure flags)
  • Test 2FA flow for both email and Google Authenticator
  • Test invite-only registration mode
  • Verify login attempt lockout progression
  • Check that permission caching doesn't leak between requests

  • Suggested Tests Before PR:

  • Unit test: auth.helpers.ts lockout duration calculation
  • Unit test: permissionNormalizer.ts format conversion
  • Integration test: Full login flow (password, 2FA, OTP)
  • Integration test: Registration + email verification
  • Integration test: Password reset flow
  • Integration test: Access token refresh via refresh token
  • E2E test: OAuth redirect flow

Architecture & Design Patterns

Code Organization

The authentication system follows a layered modular architecture across both backend and frontend:

Backend (Express.js + TypeScript):

Route → Validation (Zod) → Controller → Service → Repository (TypeORM) → Database
                                ↕                       ↕
                          Middleware Layer         Event System
                       (auth, hasPermission)    (EventEmitter)

Each auth domain is colocated in src/app/modules/v1/Auth/ following the module pattern: route.ts → controller.ts → service.ts → validation.ts → helpers.ts → utils.ts. Cross-cutting concerns (middleware, events, shared utilities) are in separate top-level directories.

Frontend (Next.js 15 + React 19):

Edge Middleware → App Router Pages → Components → Hooks → Services → Axios Instance
                                         ↕            ↕
                                    Redux Store    React Query Cache

Auth pages live in route groups ((auth)/), business logic in components/pages/auth/, state in Redux slice + React Query hooks, and API calls in services/authService.js + api/auth/index.js.

Design Patterns

  • Service Layer Pattern: All business logic isolated in auth.service.ts (746 LOC). Controllers are thin HTTP handlers that delegate to services. This is the most critical design decision — services are reusable and testable independent of HTTP.

  • Event-Driven Side Effects: Email sending is decoupled via EventEmitter. Auth operations emit events (SEND_REGISTRATION_EMAIL, SEND_LOGIN_OTP, FORGOT_PASSWORD), handlers subscribe and send emails. This prevents blocking the auth response on email delivery.

  • Repository Pattern: TypeORM repositories accessed via getDbRepository() utility. No custom repository classes — direct use of TypeORM's generic repository methods.

  • Middleware Pipeline: Express middleware chain for auth (auth.ts) → permissions (hasPermission.ts) → route handler. Each middleware enriches req.user.

  • Progressive Lockout: Login attempt tracking with escalating lock durations. State machine pattern: attempts → lock → block.

  • AsyncLocalStorage Context: requestContext.ts provides implicit request-scoped data passing. Avoids threading req.user through service layer function signatures.

  • Per-Request Permission Cache: permissionCache.ts caches permission lookups within a single request lifecycle. Cache is validated against current user to prevent cross-user leaks.

  • Cookie + Redux Dual State: Frontend maintains auth state in both cookies (for SSR/middleware) and Redux (for client components). AuthInitializer syncs cookie → Redux on mount.

  • React Query for Server State: Auth-related API data (profile, etc.) managed via TanStack React Query hooks with configurable stale times and retry logic.

State Management Strategy

Backend: - Request-scoped state: AsyncLocalStorage context holds user info and permission cache per request - Database as source of truth: User, roles, permissions, tokens, OTPs all persisted in PostgreSQL - No in-memory session store: JWT-based stateless auth (no sticky sessions needed)

Frontend: - Redux Toolkit (authSlice): Global auth state (user, token, isAuthenticated). Written on login, read everywhere, cleared on logout. - React Query (useProfile, useRefreshToken): Server state with automatic refetching (15min interval), stale-while-revalidate pattern (5min stale time). - Cookies (js-cookie): Persistent token storage accessible by both client JavaScript and Next.js Edge middleware. Cookie durations: 7 days (default), 30 days (remember me). - URL params: OAuth callback passes token via query string (security concern — tokens visible in browser history).

Error Handling Philosophy

Backend: - Custom ApiError class with HTTP status codes. Services throw ApiError for business logic failures. - catchAsync HOF wraps all controllers — unhandled errors pass to Express error middleware. - Zod validation errors processed by validateRequest middleware before reaching controllers. - Auth-specific errors: 401 Unauthorized (bad credentials, expired token), 403 Forbidden (role/permission denied), 409 Conflict (duplicate email), 423 Locked (account locked). - Password validation returns user-friendly lock duration messages (e.g., "Account locked for 5 minutes").

Frontend: - Axios interceptor catches 401/403 globally → auto-logout + redirect to /login. - React Query onError callbacks show toast notifications. - Form-level errors shown via React Hook Form + toast. - No error boundaries for auth-specific failures.

Testing Strategy

Current state: No tests exist for the authentication system. Neither backend nor frontend has any test files for auth functionality. This is a significant gap given the system's complexity (746 LOC in the core service alone, 16 API endpoints, multiple auth flows).


Data Flow

Authentication Data Flow Diagram

┌──────────────────── REGISTRATION FLOW ────────────────────┐
│                                                            │
│  Client Form → POST /auth/register                        │
│       → Zod validates body                                │
│       → AuthService.registerAccount()                     │
│            → Check invite-only mode (SettingService)      │
│            → Hash password (bcrypt)                       │
│            → Generate JWT with {email, name, hashPass}    │
│            → Emit SEND_REGISTRATION_EMAIL event           │
│            → Return success (no user created yet)         │
│                                                            │
│  Email Link → Frontend /verify-email/[token]              │
│       → POST /auth/verify-email {token}                   │
│       → AuthService.verifyEmailForRegistration()          │
│            → Verify JWT → extract email, name, hashPass   │
│            → Create User in DB (with hashed password)     │
│            → Create default role assignment               │
│            → Return success → frontend redirects to login │
└────────────────────────────────────────────────────────────┘

┌──────────────────── LOGIN FLOW ───────────────────────────┐
│                                                            │
│  Client Form → POST /auth/login                           │
│       → Zod validates {email, password}                   │
│       → AuthController.loginAccount()                     │
│            → AuthService.loginAccount()                   │
│                 → Load user + role + subscription          │
│                 → Check status (active/inactive/suspended) │
│                 → Validate password (bcrypt compare)       │
│                 → Track login attempts (progressive lock)  │
│                 │                                          │
│                 ├─ 2FA Email? → Generate OTP              │
│                 │   → Hash OTP → Save to otp_verification │
│                 │   → Emit SEND_LOGIN_OTP event           │
│                 │   → Return {method: "email"}            │
│                 │                                          │
│                 ├─ 2FA Google? → Return {method:           │
│                 │   "google_auth"}                         │
│                 │                                          │
│                 └─ Normal → Generate JWT access token      │
│                     → Generate refresh token               │
│                     → Return {token, user, refreshToken}   │
│                                                            │
│  Controller:                                              │
│       → Set `token` cookie (httpOnly, 20d via setTokenCookie) │
│       → Set `refresh_token` cookie (httpOnly)             │
│       → Return response JSON                              │
│                                                            │
│  Frontend (useLogin hook):                                │
│       → Set token cookie via js-cookie (30d/7d)           │
│       → Dispatch setAuth(user, token) to Redux            │
│       → window.location.href = "/dashboard" (full reload) │
└────────────────────────────────────────────────────────────┘

┌──────────────────── TOKEN REFRESH FLOW ───────────────────┐
│                                                            │
│  Axios interceptor sends request with expired token        │
│       → Backend auth middleware catches TokenExpiredError  │
│       → Reads refresh_token from httpOnly cookie          │
│       → Verifies refresh token with separate secret       │
│       → Generates new access token                        │
│       → Attaches to response header / sets new cookie     │
│       → Continues with original request                   │
│                                                            │
│  Frontend useRefreshToken hook:                           │
│       → Calls POST /auth/refresh-token                    │
│       → On success: updates cookie with new token         │
│       → On failure: clears auth state, redirect to login  │
└────────────────────────────────────────────────────────────┘

┌──────────────────── PERMISSION CHECK FLOW ────────────────┐
│                                                            │
│  Request → auth middleware (verify JWT, load user)         │
│       → hasPermission middleware                          │
│            → Normalize path (strip /api/v1)               │
│            → Map HTTP method to action                    │
│            → Construct permission: /{resource}/{action}   │
│            → Super admin? → Allow                         │
│            → Load role permissions from DB                │
│            → Check permission against role.permissions    │
│            → Allow or 403                                 │
│                                                            │
│  Service-layer (hasPermission helper):                    │
│       → Get user from AsyncLocalStorage context           │
│       → Check permissionCache first                       │
│       → Normalize permission format                       │
│       → Load all user roles (primary + userRoles)         │
│       → Check ANY role has permission → Allow/Deny        │
└────────────────────────────────────────────────────────────┘

Data Entry Points

  • POST /auth/register: {name, email, password} — New user registration
  • POST /auth/login: {email, password} — Credential-based login
  • POST /auth/login-otp: {email} — Request passwordless OTP
  • POST /auth/verify-login-otp: {email, otp} — Verify passwordless OTP
  • POST /auth/verify-otp: {email, otp} — Verify 2FA email OTP
  • POST /auth/verify-2fa-google: {email, token} — Verify Google Authenticator TOTP
  • POST /auth/verify-backup-code: {email, backupCode} — Verify 2FA backup code
  • POST /auth/forgot-password: {email} — Request password reset
  • POST /auth/reset-password: {token, password} — Reset password with token
  • POST /auth/verify-email: {token} — Email verification
  • GET /auth/google: Initiate Google OAuth
  • GET /auth/google/callback: Google OAuth callback
  • GET /auth/profile: Session-based profile retrieval (Google OAuth sessions)
  • POST /auth/logout: Session destroy + logout response (Google OAuth sessions)
  • GET /auth/failure: Redirect to frontend with error message (Google OAuth failure)
  • POST /auth/refresh-token: Refresh access token (from httpOnly cookie)

Data Transformations

  • Password Hashing: Plaintext → bcrypt hash (10 salt rounds) via hashedPassword(). Happens at registration, password reset, and OTP hashing.
  • JWT Generation: User payload → signed JWT (HS256) via jwtHelpers.generateToken(). Used for access tokens, refresh tokens, and email verification links.
  • OTP Generation: Math.random() → 6-digit string → bcrypt hash. Stored as hash, sent as plaintext via email.
  • Password Reset Token: crypto.randomBytes(64) → hex string → SHA-256 hash. Hash stored in DB, raw token sent via email.
  • Permission Normalization: Route format (/users/create) ↔ dot notation (users.create) via permissionNormalizer.ts. Bidirectional conversion with variant generation.
  • TOTP Secret: speakeasy.generateSecret() → base32 secret stored in UserSetting.tempTwoFactorSecret during setup, moved to twoFactorSecret on verification.
  • User Payload Sanitization: Full user entity → JWT payload (id, email, role). Password explicitly excluded via select: false on entity.

Data Exit Points

  • JWT Tokens: Access token returned in response body AND set as cookie. Refresh token set as httpOnly cookie.
  • Email Events: Registration verification, login OTP, password reset, invite emails — all sent via EventEmitter → email handlers.
  • OAuth Redirects: Token + email passed as URL query parameters to frontend callback.
  • API Responses: User profile data (excluding password), auth status, error messages.
  • Cookies: token (backend sets httpOnly, but frontend setCookie overwrites with non-httpOnly — net result: non-httpOnly), refresh_token (httpOnly), rememberMe (non-httpOnly, 5-minute TTL).

Integration Points

APIs Consumed

  • Google OAuth API (via Passport.js): User authentication and profile retrieval
  • Method: OAuth 2.0 authorization code flow
  • Authentication: Client ID + Client Secret
  • Response: Google profile (email, name, avatar)

  • Stripe API (auth-adjacent): Customer creation on user registration

  • Method: REST API
  • Authentication: Stripe Secret Key
  • Response: stripeCustomerId stored on User entity

APIs Exposed

  • POST /api/v1/auth/register: User registration
  • Request: {name: string, email: string, password: string}
  • Response: {message: "Verification email sent"}

  • POST /api/v1/auth/login: User login

  • Request: {email: string, password: string}
  • Response (normal): {token: string, refreshToken: string, user: UserPayload}
  • Response (2FA): {method: "email"|"google_auth", email: string}

  • POST /api/v1/auth/verify-otp: Verify 2FA email OTP

  • Request: {email: string, otp: string}
  • Response: {token: string, user: UserPayload}

  • POST /api/v1/auth/verify-2fa-google: Verify TOTP

  • Request: {email: string, token: string} (6-digit code)
  • Response: {token: string, user: UserPayload}

  • POST /api/v1/auth/verify-backup-code: Verify backup code

  • Request: {email: string, backupCode: string}
  • Response: {token: string, user: UserPayload}

  • POST /api/v1/auth/login-otp: Request passwordless OTP

  • Request: {email: string}
  • Response: {message: "OTP sent"}

  • POST /api/v1/auth/verify-login-otp: Verify passwordless OTP

  • Request: {email: string, otp: string}
  • Response: {token: string, user: UserPayload} (auto-creates user if new)

  • POST /api/v1/auth/forgot-password: Request password reset

  • Request: {email: string}
  • Response: {message: "Reset email sent"}

  • POST /api/v1/auth/reset-password: Reset password

  • Request: {token: string, password: string}
  • Response: {message: "Password reset successful"}

  • POST /api/v1/auth/verify-email: Verify email from registration link

  • Request: {token: string} (JWT from email verification link)
  • Response: {message: "Email verified"} (creates user)

  • GET /api/v1/auth/google: Initiate Google OAuth (redirect to Google)

  • GET /api/v1/auth/google/callback: OAuth callback (redirect to frontend with token)

  • GET /api/v1/auth/profile: Get current user profile from Google OAuth session

  • Request: Session cookie (Passport session)
  • Response: {success: true, user: {id, name, email, avatar}, token: string} or {success: false, message: "Not authenticated"} (401)

  • POST /api/v1/auth/logout: Destroy Google OAuth session

  • Request: Session cookie
  • Response: {success: true, message: "Logged out successfully"} or 500 on error

  • GET /api/v1/auth/failure: Google OAuth authentication failure handler

  • Request: None (redirect target)
  • Response: Redirect to ${CLIENT_URL}/login?success=false&error=Authentication%20failed

  • POST /api/v1/auth/refresh-token: Refresh access token

  • Request: httpOnly cookie with refresh token
  • Response: {token: string}

Shared State

  • Cookies (token, refresh_token, rememberMe):
  • Type: HTTP cookies (token: backend sets httpOnly via setTokenCookie, but frontend setCookie in useLogin overwrites with non-httpOnly — net result: non-httpOnly; refresh_token: httpOnly)
  • Accessed By: Frontend JS (cookies-next), Next.js Edge Middleware, Axios interceptor, Backend auth middleware
  • rememberMe cookie lifecycle:
    1. Set by backend (auth.contoroller.ts:53-57): When login returns 2FA (no token yet) AND rememberMe was in request body → sets cookie rememberMe=true, httpOnly: false, maxAge: 5 minutes
    2. Read by backend (auth.contoroller.ts:122): verifyOtp reads req.cookies.rememberMe
    3. Used by backend (auth.contoroller.ts:126-132): If rememberMe === "true" → clears the rememberMe cookie, calls setTokenCookie (20-day httpOnly cookie)
    4. Cleared by backend (auth.contoroller.ts:70): On non-rememberMe login, res.clearCookie("rememberMe")
  • Security note: The rememberMe cookie is non-httpOnly, so an XSS attacker could inject rememberMe=true before OTP verification to force a 20-day persistent session

  • Redux Auth Slice (state.auth):

  • Type: In-memory client state
  • Accessed By: All authenticated frontend components, AuthInitializer, Axios interceptor (via setStoreDispatch)

  • React Query Cache (["profile"], ["auth"]):

  • Type: In-memory client cache
  • Accessed By: useProfile hook, useLogin/useLogout (invalidation)

  • AsyncLocalStorage Context (requestContext):

  • Type: Per-request server-side context
  • Accessed By: hasPermission helper, permissionCache, any service needing current user

Events

  • SEND_REGISTRATION_EMAIL: Published by AuthService.registerAccount(). Payload: {name, email, link}. Handler sends verification email.
  • SEND_LOGIN_OTP: Published by AuthService.loginAccount() (2FA email). Payload: {email, otp}. Handler sends OTP email.
  • FORGOT_PASSWORD: Published by AuthService.forgotPassword(). Payload: {name, email, link}. Handler sends reset email.
  • RESET_LOGIN_ATTEMPTS: Published by cron job (daily). Payload: none. Handler bulk-resets old LoginAttempt records.
  • INVITEONLY_USER_LOGIN: Published when invite-only user logs in. Payload: {email, credentials}. Handler sends credentials email.
  • INVITE_USER_CREATED: Published when admin creates invite. Payload: {email, link}. Handler sends invitation email.
  • OTP_VERIFIED: Published after OTP verification. Payload: user info.
  • RESET_PASSWORD: Published after password reset. Payload: user info.
  • VERIFY_EMAIL_REG: Dead event type — defined in eventTypes.ts but never emitted or handled anywhere in the codebase. See Known Issue #24.

Database Access

  • user (SELECT, INSERT, UPDATE): Login lookup (with password), profile, creation on verify-email and OTP login. Indexes: email+deletedAt, status+deletedAt, provider+email.
  • otp_verification (SELECT, INSERT, DELETE): OTP storage and verification. Lookups by email (indexed). Old OTPs deleted on new generation.
  • login_attempt (SELECT, INSERT, UPDATE): Per-user tracking. Lookup by userId (indexed). Bulk reset by handler.
  • token (SELECT, INSERT, DELETE): Password reset tokens. Lookup by hashed token (indexed). Deleted after use.
  • role (SELECT): Loaded via user relation. Joined with permissions for RBAC checks.
  • permission (SELECT): Loaded via role.permissions. Matched against normalized request path.
  • user_role (SELECT): Multi-role junction table. Loaded for comprehensive permission checks.
  • user_setting (SELECT, INSERT, UPDATE): 2FA configuration. Loaded via user.setting relation.
  • user_invites (SELECT, UPDATE): Invite-only mode. Lookup by email (unique).
  • subscription (SELECT): Loaded via user.subscription for login response payload.

Dependency Graph

                          ┌─────────────────┐
                          │  auth.route.ts   │ ← ENTRY POINT
                          └────────┬────────┘
                    ┌──────────────┼──────────────┐
                    ▼              ▼               ▼
           ┌──────────────┐ ┌──────────┐  ┌───────────────────┐
           │auth.controller│ │auth.valid.│  │google-oauth.ctrl  │
           └───────┬──────┘ └──────────┘  └────────┬──────────┘
                   │                                │
                   ▼                                ▼
           ┌──────────────┐               ┌──────────────┐
           │auth.service.ts│               │passport.ts   │
           │   (746 LOC)   │               │(Google OAuth)│
           └───┬───┬───┬──┘               └──────────────┘
               │   │   │
    ┌──────────┘   │   └──────────┐
    ▼              ▼              ▼
┌────────┐  ┌──────────┐  ┌──────────┐
│auth.    │  │auth.     │  │Events    │
│helpers  │  │utils     │  │System    │
│(lockout)│  │(OTP,cook)│  │(emitter) │
└────────┘  └────┬─────┘  └────┬─────┘
                 │              │
                 ▼              ▼
          ┌──────────┐  ┌──────────────┐
          │Shared     │  │Event Handlers│
          │Helpers    │  │(7 files)     │
          │(jwt,hash, │  └──────────────┘
          │ otp)      │
          └──────────┘

MIDDLEWARE CHAIN (independent entry):
┌───────────┐    ┌────────────────┐    ┌──────────────────┐
│auth.ts    │ →  │hasPermission.ts│ →  │Route Handler     │
│(middleware)│    │(middleware)     │    │                  │
└─────┬─────┘    └───────┬────────┘    └──────────────────┘
      │                  │
      ▼                  ▼
┌──────────┐    ┌──────────────────┐
│jwtHelpers│    │helper/           │
│          │    │hasPermission.ts  │
└──────────┘    │+ permissionCache │
                │+ permNormalizer  │
                │+ requestContext  │
                └──────────────────┘

FRONTEND DEPENDENCY CHAIN:
┌───────────────┐    ┌──────────────┐
│middleware.js   │    │auth-init.jsx │ ← App mount
│(Edge, cookies) │    │(Redux sync)  │
└───────────────┘    └──────┬───────┘
         ┌──────────────────┼──────────────────┐
         ▼                  ▼                   ▼
  ┌──────────────┐  ┌─────────────┐    ┌───────────────┐
  │authSlice.js  │  │axiosInstance│    │api/auth/      │
  │(Redux)       │  │(interceptor)│    │index.js       │
  └──────────────┘  └──────┬──────┘    │(React Query)  │
                           │           └───────┬───────┘
                           ▼                   ▼
                    ┌─────────────┐    ┌───────────────┐
                    │authService  │    │Auth Pages     │
                    │.js          │    │(login,register│
                    └─────────────┘    │,forgot,reset) │
                                       └───────────────┘

Entry Points (Not Imported by Others in Scope)

  • saas-boilerplate/src/app/modules/v1/Auth/auth.route.ts — Route entry point, mounted by route aggregator
  • saas-boilerplate/src/app/middlewares/auth.ts — Applied by route definitions outside auth module
  • saas-boilerplate/src/app/middlewares/hasPermission.ts — Applied by route definitions outside auth module
  • saas-boilerplate/src/app/cron/scheduleResetLoginAttempts.ts — Cron entry, invoked by scheduler
  • Sass-boilerplate-frontend-v1/src/middleware.js — Next.js Edge middleware, invoked by framework
  • Sass-boilerplate-frontend-v1/src/components/providers/auth-initializer.jsx — Mounted in root layout
  • Sass-boilerplate-frontend-v1/src/app/(auth)/layout.js — Route group layout

Leaf Nodes (Don't Import Others in Scope)

  • saas-boilerplate/src/helper/generateOtp.ts — Pure utility, no auth imports
  • saas-boilerplate/src/helper/hashedPassword.ts — Pure utility, only bcrypt
  • saas-boilerplate/src/helper/jwtHelpers.ts — Pure utility, only jsonwebtoken
  • saas-boilerplate/src/entity/*.ts — All 10 entities are leaf nodes (import only TypeORM decorators)
  • saas-boilerplate/src/app/events/eventTypes.ts — Constants only
  • Sass-boilerplate-frontend-v1/src/hooks/useCheckPermission.js — Reads Redux, no auth imports

Circular Dependencies

No direct circular dependencies detected within the core auth file inventory. (Note: Analysis is limited to files in the Core Auth File Inventory section — files referenced in findings 51–64 were not included.) However, there is a logical circular dependency in the frontend:

  • axiosInstance.js needs Redux dispatch (received via setStoreDispatch)
  • authSlice.js is a Redux slice (no direct dependency)
  • auth-initializer.jsx imports both, acting as the mediator

This is resolved at runtime via the setStoreDispatch injection pattern, but it creates a tight coupling between the HTTP layer and state management.


Testing Analysis

Test Coverage Summary

  • Statements: 0%
  • Branches: 0%
  • Functions: 0%
  • Lines: 0%

No test files exist for any part of the authentication system. Neither backend nor frontend has unit tests, integration tests, or E2E tests for auth functionality.

Test Files

None.

Test Utilities Available

  • Backend has no test framework configured in the auth module
  • Frontend has no testing setup visible in auth-related code
  • No mocking utilities, test helpers, or fixtures exist for auth

Testing Gaps

  • Critical: No tests for auth.service.ts (746 LOC of core business logic)
  • Critical: No tests for login attempt lockout progression (auth.helpers.ts)
  • Critical: No tests for JWT token generation/verification (jwtHelpers.ts)
  • Critical: No tests for 2FA flows (email OTP, Google Authenticator, backup codes)
  • Critical: No tests for permission checking (hasPermission.ts helper — 295 LOC)
  • Critical: No tests for permission normalization (permissionNormalizer.ts — 215 LOC)
  • High: No tests for OAuth flow (google-oauth.controller.ts, passport.ts)
  • High: No tests for password reset flow (token generation, hashing, verification)
  • High: No tests for OTP-based passwordless login with auto-registration
  • Medium: No tests for middleware auth flow (token extraction, access token refresh)
  • Medium: No tests for frontend auth hooks (useLogin, useLogout, useProfile)
  • Medium: No tests for Redux auth slice state transitions
  • Low: No tests for permission cache correctness and cross-user leak prevention

Similar Features Elsewhere

  • User Management Module (src/app/modules/v1/User/)
  • Similarity: Same module structure (controller → service → routes → validation)
  • Can Reference For: Pattern for adding new auth-adjacent endpoints (profile update, user listing)

  • Subscription Module (src/app/modules/v1/Subscription/)

  • Similarity: Uses same auth middleware chain, accesses User entity
  • Can Reference For: How to add package-gated auth features

  • Settings Module (src/app/modules/v1/Setting/)

  • Similarity: Key-value settings pattern used for invite-only mode, 2FA config
  • Can Reference For: Adding new auth configuration flags

  • RoleAccess Module (src/app/modules/v1/RoleAccess/)

  • Similarity: Direct RBAC management (CRUD for role permissions)
  • Can Reference For: Understanding how permissions are initially seeded

Reusable Utilities Available

  • catchAsync (src/shared/catchAsync.ts): HOF wrapping async handlers — used by all auth controllers, available for new endpoints.
  • sendResponse (src/shared/sendResponse.ts): Standardized response format — ensures consistent API responses.
  • validateRequest (src/app/middlewares/validateRequest.ts): Zod schema validation middleware — apply to any new auth endpoint.
  • rateLimiter (src/app/middlewares/rateLimiter.ts): Configurable rate limiting — apply to abuse-prone auth endpoints.
  • EmailFactory (src/app/modules/v1/Email/): Email template system — used by all auth event handlers for sending emails.
  • getDbRepository (src/db/connection.ts): TypeORM repository accessor — used throughout for DB operations.
  • eventEmmiter (src/app/events/): Global EventEmitter instance — add new event types + handlers for auth actions.

Patterns to Follow

  • Adding a new auth endpoint: Reference auth.route.ts line 22-29 for rate-limited route with validation → controller pipeline.
  • Adding a new 2FA method: Reference auth.service.ts line 152-221 for the login branching pattern and verifyOtp for verification pattern.
  • Adding a new entity: Reference src/entity/User.ts for TypeORM decorators, soft-delete, and index patterns.
  • Adding a new event: Reference src/app/events/eventTypes.ts for constant, create handler in src/app/events/handlers/, register in event bootstrap.
  • Adding a new permission check: Reference src/helper/hasPermission.ts for service-layer permission checking with cache.

Implementation Notes

Code Quality Observations

  • Core auth service (auth.service.ts, 746 LOC) is monolithic — all auth operations in one file. Could benefit from splitting by auth flow (registration, login, password-reset, otp-login).
  • Consistent use of catchAsync and sendResponse across controllers provides uniform error handling and response format.
  • Event-driven email architecture is well-separated — auth logic doesn't block on email delivery.
  • Permission system (3 files, ~693 LOC combined) is well-architected with caching, normalization, and multi-role support.
  • TypeORM entities use proper indexing for auth queries (compound indexes on frequently queried column combinations).
  • Frontend auth code is split across too many abstraction layers (authService + api/auth hooks + Redux slice + axiosInstance + cookies) — increases cognitive load for simple operations.

TODOs and Future Work

  • auth.route.ts:22: Rate limit comment says "limit each IP to 3 requests" but actual value is 100 — either fix comment or fix value.
  • auth.service.ts:117-121: Commented-out direct sendEmail call (replaced by event system) — clean up dead code.
  • auth.service.ts:122: console.log("event emit register email") — remove debug log.
  • auth.controller.ts:124-125: Empty block (dead code) — remove.
  • auth.controller.ts:82: Method name typo refereshToken — rename.
  • auth.middleware.ts:62: Debug error message "You are not authorized!33" — fix message.
  • auth.middleware.ts:80-132: Commented-out old auth implementation — remove.
  • hasPermission.ts:57-63: Permission normalization commented out with no explanation — either re-enable or document why disabled.
  • hasPermission.ts:68: console.log({cachedResult}) — remove debug log.
  • sendLoginOtp.handler.ts:7: console.log("Sending OTP to email:", email, "with OTP:", otp) — remove OTP logging (security).
  • sendLoginOtp.handler.ts:10: Email subject has leading space: " Email Verification" — fix.
  • eventTypes.ts:1: Legacy path comment doesn't match actual file path.
  • permissionNormalizer.ts:94-99: ACTION_MAPPING logic commented out — convertDotToRoute ignores action aliases.
  • inviteOnlyLoginSendEmail.ts: Unused imports (config, resetPassTemplate) — remove.
  • resetSubscribtionStatus.handler.ts: Dead expireSubscriptions() function (lines 7-15) — remove.
  • resetLoginAttempts.handler.ts: Variable typo loginAttempRepo — fix.
  • Frontend debug console.logs (6 files): api/auth/index.js:19, register.jsx:52, account-setting-page.jsx:27, setup-two-factor-verification.jsx:35, two-factor-backup.jsx:21, login.jsx (multiple) — remove all debug logging.
  • login.jsx:122-124: Dead handleBackupCodeSubmit function — remove.
  • login.jsx:626-636: Commented-out invite-only conditional for register link — decide and clean up.
  • register.jsx:48: Commented-out const websiteType = 2 test override — remove.
  • register.jsx:90-94: Commented-out existing user check logic — remove or implement.
  • social-login.jsx:9-14: Commented-out alternative Axios implementation — remove.
  • social-login.jsx:51-74: Commented-out Facebook/Twitter/Github/Mail buttons — remove or implement.
  • account-setting-page.jsx:77: Commented-out toast.error in handleDisableError — error silently swallowed.

Known Issues

Note: This is the canonical list of all known issues. Some issues are also mentioned in individual file inventory "Risks" subsections for per-file context. If an issue is updated or resolved, update it here — file-level mentions are informational cross-references.

Severity Breakdown (64 issues total, 50 in-scope + 14 out-of-scope):

Severity In-Scope Issues Out-of-Scope Issues
Critical (security/data) #2, #3, #4, #8, #22, #27, #30, #31, #34, #38, #44 #51, #52, #57, #60
High (broken flows/auth gaps) #1, #5, #10, #12, #16, #17, #21, #25, #26, #33, #35, #36, #37, #39, #45 #53, #54, #55, #56, #63, #64
Medium (bugs with workarounds) #6, #7, #9, #11, #13, #15, #18, #19, #23, #24, #28, #29, #32, #40, #41, #42, #43, #48 #58, #59, #61, #62
Low (cosmetic/minor) #14, #20, #46, #47, #49, #50
  1. Login attempt lockout gap: handleFailedAttempt in auth.helpers.ts applies lock for attempts 2-6 and blocks at 9+, but attempts 7-8 fall through with no lock applied (line 111-117).
  2. Missing refreshToken column: auth middleware (line 174) checks user.refreshToken but the User entity has no such column — refresh token validation always fails.
  3. Registration JWT exposes password hash: registerAccount (line 111-115) embeds bcrypt hash in JWT payload — intercepted token reveals hash.
  4. Math.random() for OTPs: generateOtp.ts uses non-cryptographic RNG — predictable OTPs possible.
  5. hashSync in async function: hashedPassword.ts blocks event loop during password hashing.
  6. Axios interceptor operator precedence: Line 29 — 403 check can throw if error.response is undefined.
  7. 403 triggers logout: Axios interceptor logs out on permission denied (should only logout on 401).
  8. OAuth token in URL: Google OAuth callback passes JWT in URL query string — visible in browser history, logs, referrer headers.
  9. Password reset min length mismatch: Reset allows min 3 chars vs registration min 6 chars.
  10. Frontend refreshToken URL mismatch: authService.js calls /auth/refresh but backend route is /auth/refresh-token.
  11. optHash column typo: In Otp.ts entity — renaming requires database migration.
  12. Duplicate token generation on login: Controller generates token AND setTokenCookie generates another token with different expiry (20-day).
  13. Double auth middleware: authGuard and auth (default export) in same file — confusion about which to use.
  14. keepPreviousData deprecated: React Query 5 should use placeholderData instead.
  15. Old OTPs never cleaned up: No TTL-based deletion for otp_verification records.
  16. Dual role-assignment tables: User.ts @ManyToMany creates join table user_roles (plural), while UserRoles.ts entity creates table user_role (singular) — two different tables serving the same purpose. Data splits across both, TypeORM cannot resolve both correctly. One mechanism must be removed.
  17. Soft-delete blocks re-registration: Unique constraints on User.email, Permission.route, and InviteUser.email do not exclude soft-deleted rows. A soft-deleted user blocks new registration with the same email. Needs PostgreSQL partial unique indexes.
  18. Missing FK relations on Token and OTP: Both Token and OtpVerification use plain userId/email columns with no @ManyToOne relation — no referential integrity, orphaned rows accumulate on user deletion.
  19. No cascade cleanup: LoginAttempt, Token, OtpVerification, and Subscription have no cascade delete from User — all leave orphaned rows. Only UserSetting and UserRole cascade properly, but soft-delete never triggers DB-level cascades anyway.
  20. displayName nullability ambiguity on Permission: Has default: null but no nullable: true — may cause insert failures depending on TypeORM/PostgreSQL behavior.
  21. Permission normalization commented out: hasPermission() has normalization disabled (lines 57-63) but hasPermissionForUser() normalizes — inconsistent behavior means dot-notation permissions fail for current-user checks but work for user-by-ID checks.
  22. OTP logged to console: sendLoginOtp.handler.ts line 7 logs OTP in plaintext (console.log("Sending OTP to email:", email, "with OTP:", otp)) — security vulnerability in production.
  23. Missing await in subscription expiration: resetSubscribtionStatus.handler.ts line 19 does not await the DB query — fire-and-forget, errors are unhandled promise rejections.
  24. 3 dead event types: OTP_VERIFIED, RESET_PASSWORD, VERIFY_EMAIL_REG are defined in eventTypes.ts but have no corresponding handlers.
  25. checkPermissionAndThrow uses HTTP 400: Permission denial returns BAD_REQUEST (400) instead of FORBIDDEN (403) — semantically incorrect, confuses frontend error handling.
  26. 3 event handlers missing error handling: inviteOnlyLoginSendEmail.ts, sendForgotPass.handler.ts, sendLoginOtp.handler.ts have no try/catch — email send failures become unhandled promise rejections.
  27. Plaintext password emailed: inviteOnlyLoginSendEmail.ts sends user password in cleartext in the email body — security anti-pattern.
  28. Hardcoded 'super_admin' string: requestContext.ts isCurrentUserSuperAdmin() compares against literal string 'super_admin' — if the role name changes in DB/seed, the entire super admin bypass silently breaks.
  29. Debug console.log in production: hasPermission.ts line 68 logs {cachedResult} to stdout on every cache hit.
  30. No rate limiting on sensitive endpoints: /forgot-password, /verify-otp, /verify-2fa-google, /verify-backup-code, /reset-password, /register have no rate limiting — enables brute-force OTP guessing, email bombing, and mass registration.
  31. Unhashed password on user restoration: auth.service.ts createAccount (line 60) sets raw password directly when restoring a soft-deleted user — critical security bug if User entity has no @BeforeUpdate hook (it doesn't).
  32. Google OAuth logout doesn't clear cookie: google-oauth.controller.ts logout destroys Passport session but does NOT clear the token cookie — user retains valid JWT until natural expiry (7 days).
  33. verifyLoginOtp hangs on no token: auth.contoroller.ts lines 201-236 — no else branch for hasToken === false, request hangs forever with no response.
  34. verifyOtp doesn't check OTP expiry: auth.service.ts lines 360-407 — only checks isVerified flag, does not validate expiresAt timestamp. Expired OTPs can still be used.
  35. CSP connectSrc: ['self'] blocks external APIs: sequrity.ts CSP configuration blocks all cross-origin API calls (Stripe checkout, external services, CDNs).
  36. require-corp COEP policy: sequrity.ts sets Cross-Origin-Embedder-Policy to require-corp — blocks loading any cross-origin resources (including Cloudinary images) that don't send CORP headers.
  37. Three different token lifetimes: setTokenCookie = 20 days, Google OAuth = 7 days, refresh-generated = 24 hours — inconsistent session durations depending on login method and refresh.
  38. verifyLoginOtp auto-registration bypasses invite-only: OTP-based passwordless login auto-creates users regardless of auth.website_type setting — circumvents invite-only mode.
  39. deserializeUser missing relations: Passport deserializeUser loads user without role/setting relations — req.user.role is undefined after session deserialization.
  40. Relative OAuth callback URL: passport.ts uses callbackURL: "/api/v1/auth/google/callback" (relative) — may break behind reverse proxies with different host headers.
  41. Google OAuth scope duplication: google-oauth.controller.ts requests scopes ["profile", "email", "profile"]profile listed twice.
  42. handleBackupCodeSubmit is dead code: login.jsx line 122-124 sets isLoading but never actually submits anything — function body is empty.
  43. 2FA modal re-trigger bug: login.jsx line 263 — when method === "google_auth", setIsVerifiedOnce(true) is never called, allowing the 2FA modal to re-open on re-renders.
  44. Social login bypasses invite-only: Google OAuth button is shown for all website types including invite-only (type 2) — users can bypass invite requirement by using social login.
  45. No server-side logout: useLogout hook makes no API call (Promise.resolve()) — refresh tokens are never invalidated server-side, remaining valid until natural expiry.
  46. Uncleaned setTimeout in verify-email and reset-password pages: 3-second auto-redirect timers are not cleared on component unmount — if user navigates away early, the redirect fires causing double navigation.
  47. Login form label mismatch: Email field label says "Email or Username" but validation pattern only accepts email format — username login will always fail client-side validation.
  48. Cookie maxAge for non-remember-me is 7 days: Both useLogin hook and social auth set 7-day cookies for non-remember-me sessions — unexpectedly long for a session that's supposed to expire on browser close.
  49. Forgot-password and reset-password use raw axios: Both pages bypass the shared axiosInstance interceptor, using direct axios — duplicates base URL configuration and loses interceptor functionality.
  50. Backup codes can be regenerated without warning: two-factor-backup.jsx — each click on "Generate Backup Codes" fetches new codes, potentially invalidating previous ones with no confirmation dialog.

Out-of-Scope Observations

The following issues were discovered during auth analysis but involve files outside the Core Auth File Inventory (role.service.ts, subscription.helpers.ts, permission.routes.ts, user.controller.ts, user.service.ts, permission.service.ts, user_setting.service.ts). They are retained here for visibility but should be tracked in their respective module deep-dives.

  1. createRole permission check COMMENTED OUT: role.service.ts line 150 — checkPermissionAndThrow("role.create") is commented out, meaning any authenticated user can create roles. Critical security issue.
  2. getMySubscriptions data leakage: subscription.helpers.ts findSubscriptions silently ignores the userId filter — getMySubscriptions returns ALL users' subscriptions instead of just the current user's.
  3. GET /user-permissions route unreachable: permission.routes.tsGET /:permissionId is defined before GET /user-permissions, so "user-permissions" is captured as a permissionId parameter. Dead route.
  4. updateProfile hangs on password change: user.controller.ts lines 59-61 — return res without calling .json() or .end() when password is changed, causing the HTTP response to hang.
  5. Duplicate userRoles validation bypasses permission check: user.service.ts lines 337-363 — userRoles validated twice; second unconditional validation makes the permission-guarded first check pointless.
  6. getAllPermissions has no permission check: permission.service.ts — any authenticated user can list all system permissions without authorization.
  7. console.log(code) logs backup codes: user_setting.service.ts line 72 — logs backup code in plaintext to stdout during verification. Security vulnerability.
  8. Missing seeded permissions: subscription.view, users.assign.roles, users.update.role are checked in service code but not in seed data — these permission checks always fail for non-super-admin users.
  9. Email 2FA setup incomplete: user_setting.service.ts startEmail2FASetup is defined but never exported or called — email-based 2FA enable has no verification step, just enables immediately.
  10. console.log("2FA Setup Data:", data) leaks TOTP secret: setup-two-factor-verification.jsx line 35 logs the TOTP secret key and QR code data to the browser console. Security vulnerability.
  11. Backup codes stored without delimiters: user_setting.service.ts concatenates SHA-512 hashes of backup codes into a single string with no delimiter — verification uses substring matching, which is architecturally fragile.
  12. createRole cache not invalidated on restore path: role.service.ts createRole — when restoring a soft-deleted role, deleteCacheByPrefix("roles:") is NOT called, leaving stale cache.
  13. Google OAuth cookie set before error check: google-oauth.controller.ts line 60 sets the auth cookie BEFORE checking loginErr at line 66 — on login failure, the cookie is already committed to the response.
  14. upsertRole inherits missing permission check: role.service.ts upsertRole calls createRole internally, which has its permission check commented out — so upsertRole is also unprotected.

Optimization Opportunities

  • DB queries on every request: Both auth middleware and hasPermission middleware do full DB lookups per request. Consider caching user + role data in Redis or JWT claims for frequently accessed endpoints.
  • Permission cache scope: permissionCache is per-request only. A short-TTL shared cache (Redis/memory) could eliminate repeated DB permission lookups across requests from the same user.
  • bcrypt hashSync: Replace with hash (async) to avoid blocking the event loop during password and OTP hashing.
  • Math.random()crypto.randomInt(): Swap for cryptographically secure OTP generation with minimal code change.
  • Monolithic service split: Break auth.service.ts (746 LOC) into focused services: LoginService, RegistrationService, PasswordResetService, OtpLoginService.
  • Frontend full-page reload: Replace window.location.href = "/dashboard" with Next.js router.push("/dashboard") to enable client-side navigation.

Technical Debt

  • No tests: Zero test coverage across 70+ files handling security-critical authentication logic.
  • Typos in code identifiers: contoroller, refereshToken, optHash, nod_env, sesseion_secret, hashassword, sequrity.ts, subscribtionExpire.ts, Cupon, UserSerivce — require coordinated rename + migration.
  • Redundant soft-delete: isDeleted boolean column alongside TypeORM's @DeleteDateColumn() — pick one mechanism.
  • Commented-out code: Old auth middleware implementation (50+ lines), old email sending code — remove dead code.
  • Non-httpOnly token cookie: Access token cookie readable by client JavaScript — XSS vulnerability vector.
  • Session + JWT hybrid: Passport session serialization configured alongside JWT auth — unused session infrastructure adds confusion.
  • No CSRF protection: Cookie-based auth without CSRF tokens on mutation endpoints.
  • Dual role tables: user_roles (JoinTable) and user_role (entity) — competing mechanisms for the same data. Must consolidate.
  • Missing FK constraints: Token.userId and OtpVerification.email are plain columns with no foreign key relations — orphaned data on user deletion.
  • No entity-level password hashing: No @BeforeInsert/@BeforeUpdate hooks on User entity — hashing relies entirely on service code. If any code path bypasses the service, plaintext passwords get stored.
  • Soft-delete + unique constraint conflicts: Unique constraints don't exclude soft-deleted rows, blocking re-use of emails/routes after soft deletion.
  • Inconsistent email sending: 2 handlers use getEmailService() factory, 3 handlers use sendEmail() helper directly — two code paths for the same operation.
  • Dead code in event/permission system: expireSubscriptions() function, ACTION_MAPPING constant, unused imports (config, resetPassTemplate in inviteOnlyLoginSendEmail.ts), 3 event types with no handlers.
  • eventEmmiter typo: The EventEmitter singleton is named eventEmmiter throughout (should be eventEmitter).
  • Debug console.log statements: hasPermission.ts line 68 and sendLoginOtp.handler.ts line 7 — leak internal state and OTP values to stdout.
  • Magic strings: Super admin role name hardcoded as 'super_admin' string literal in requestContext.ts — not sourced from config or constants.
  • Three redundant NODE_ENV keys: config.env, config.nod_env, config.mode, config.production + direct process.env.NODE_ENV — five ways to check the same value, invites inconsistent usage.
  • No env var validation: config/index.ts has no validation — missing JWT secrets or DB credentials cause cryptic runtime failures instead of startup errors. Some defaults exist (mail type, image storage, NODE_ENV) but critical auth values have none.
  • Inconsistent cookie sameSite settings: setTokenCookie omits sameSite, Google OAuth uses "strict" — inconsistent CSRF protection across cookie-setting paths.
  • Two auth middlewares with different behavior: authGuard (lookup by email, checks status, includes name) vs auth (lookup by id, no status check, omits name) — subtle bugs depending on which is used.
  • Frontend debug console.log statements: api/auth/index.js:19, register.jsx:52, account-setting-page.jsx:27, setup-two-factor-verification.jsx:35, two-factor-backup.jsx:21 — all log sensitive auth data to browser console.
  • Frontend unused imports: 15+ unused imports across login.jsx (Facebook, Github, Mail, Twitter), register.jsx (same + NotFound, AlertIcon), disable-two-factor-modal.jsx (AlertTriangle), two-factor-verification.jsx (Loading), setup-two-factor-verification.jsx (QrCode, Smartphone), forgot-password (Label).
  • Inconsistent Input component imports: forgot-password imports from @/components/custom/input, reset-password imports from @/components/ui/input — different components with potentially different behavior.
  • Side effects in Redux reducers: logout reducer calls deleteCookie(), initializeAuth calls getCookie() — violates Redux purity, breaks DevTools time-travel debugging.
  • keepPreviousData deprecated in React Query v5: useProfile hook uses deprecated option — should use placeholderData: keepPreviousData import.

Modification Guidance

To Add New Functionality

  1. New auth endpoint (e.g., magic link login):
  2. Add Zod schema to auth.validation.ts
  3. Add controller method to auth.contoroller.ts (or new controller)
  4. Add business logic to auth.service.ts (or create new focused service)
  5. Add route to auth.route.ts with appropriate rate limiting
  6. If emails needed: add event type to eventTypes.ts, create handler in src/app/events/handlers/
  7. If new DB state: create entity in src/entity/, run TypeORM migration
  8. Frontend: add API method to authService.js, React Query hook in api/auth/index.js, UI component

  9. New 2FA method (e.g., SMS):

  10. Add enum value to UserSetting.twoFactorProvider
  11. Add setup logic in auth.service.ts (reference Google Authenticator pattern)
  12. Add verification branch in AuthService.loginAccount() login flow
  13. Add verification endpoint (route + controller + service)
  14. Frontend: add setup UI (reference setup-two-factor-verification.jsx)

  15. New permission type:

  16. Add permission to seed data (via admin UI or migration)
  17. Permission format: /{resource}/{action} — automatically checked by middleware
  18. For service-layer checks: use hasPermission() from src/helper/hasPermission.ts

To Modify Existing Functionality

  1. Change token expiry: Edit config/index.tsjwt.expires_in and jwt.refresh_token_expires_in. Also update frontend cookie durations in api/auth/index.js.

  2. Change lockout rules: Edit auth.helpers.tscalculateLockDuration() for durations, handleFailedAttempt() for block threshold.

  3. Change password requirements: Edit auth.validation.ts → update Zod .min() values. Ensure consistency across registerValidation, loginValidation, and resetPasswordValidation.

  4. Switch from cookie to header-only auth: Remove cookie setting in controller, update frontend to store token in memory/localStorage, update middleware to check Authorization header only, remove Next.js middleware cookie check.

To Remove/Deprecate

  1. Remove Google OAuth: Delete google-oauth.controller.ts, remove Passport config, remove Google routes from auth.route.ts, remove Provider.GOOGLE handling in service, remove frontend social-login.jsx.

  2. Remove OTP passwordless login: Delete requestLoginOtp/verifyLoginOtp from service + controller + routes + validation, remove Provider.OTP handling, remove frontend OTP login UI.

  3. Remove 2FA entirely: Remove all 2FA branches from AuthService.loginAccount(), remove verify-otp/verify-2fa-google/verify-backup-code endpoints, remove UserSetting 2FA columns, remove frontend 2FA components.

Testing Checklist for Changes

  • All 16 auth API endpoints still return expected responses
  • Login works with email/password, Google OAuth, and OTP
  • 2FA flows work for email OTP and Google Authenticator
  • Backup code verification works
  • Registration with email verification completes end-to-end
  • Invite-only registration mode correctly restricts access
  • Password reset flow sends email and accepts valid token
  • Login attempt lockout progresses correctly through all thresholds
  • Refresh token generates new valid access token
  • Permission checks correctly allow/deny based on role
  • Super admin bypasses all permission checks
  • Frontend correctly handles auth state (cookie + Redux + React Query)
  • Edge middleware protects dashboard routes
  • Logout clears all auth state (cookies, Redux, React Query cache)
  • 401 errors trigger automatic logout in frontend
  • Cookie security flags are correct per environment (httpOnly, secure, sameSite)

Generated by document-project workflow (deep-dive mode) Base Documentation: docs/index.md Scan Date: 2026-02-13 Analysis Mode: Exhaustive