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:
- Registration flow (line 94-130):
- Checks invite-only mode via
SettingService.getSettingByKey("auth.website_type") - Hashes password, embeds hash+email+name in a JWT token (1d expiry)
- Emits
SEND_REGISTRATION_EMAILevent with verification URL - 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.
- Login flow (line 152-221):
- Loads user with relations: role, subscription, setting, role.permissions, userRoles
- Checks: user exists, not inactive, not suspended, not deleted, not OAuth-only
- Validates password via
validatePassword(handles lockout) - Super admin bypass: skips 2FA check entirely
- Email 2FA: generates OTP, emits event, saves to DB, returns
{method: "email"} - Google Authenticator 2FA: returns
{method: "google_auth"}(no server action) -
Normal: returns JWT + user info
-
Password validation & lockout (line 227-257):
- Gets or creates
LoginAttemptrecord - On failure: increments count, sets lock duration, blocks after 9 attempts
-
On success: resets all counters
-
Forgot password (line 299-327):
- Generates 64-byte random token, SHA-256 hashes it
- Stores hashed version in Token table (15min expiry)
-
Sends raw token via email (user gets unhashed link)
-
OTP-based passwordless login (line 593-731):
requestLoginOtp: Creates OTP even if user doesn't exist (prevents email enumeration)verifyLoginOtp: Uses DB transaction to create user if new, reactivates soft-deleted users-
Auto-creates user with
Provider.OTP, derives name from email prefix -
Refresh token (line 509-557):
- Verifies refresh token against separate secret
- 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 securehashSyncused in async function — blocks event loop- Login attempt logic has gap: attempts 7-8 have no lockout applied
authmiddleware checksuser.refreshTokenbut 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
optHashcolumn typo in OtpVerification entity- Password reset allows min 3 chars while registration requires min 6
-
refreshTokenendpoint 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.tslockout duration calculation - Unit test:
permissionNormalizer.tsformat 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 enrichesreq.user. -
Progressive Lockout: Login attempt tracking with escalating lock durations. State machine pattern: attempts → lock → block.
-
AsyncLocalStorage Context:
requestContext.tsprovides implicit request-scoped data passing. Avoids threadingreq.userthrough service layer function signatures. -
Per-Request Permission Cache:
permissionCache.tscaches 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).
AuthInitializersyncs 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) viapermissionNormalizer.ts. Bidirectional conversion with variant generation. - TOTP Secret:
speakeasy.generateSecret()→ base32 secret stored inUserSetting.tempTwoFactorSecretduring setup, moved totwoFactorSecreton verification. - User Payload Sanitization: Full user entity → JWT payload (id, email, role). Password explicitly excluded via
select: falseon 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 frontendsetCookieoverwrites 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:
stripeCustomerIdstored 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 frontendsetCookieinuseLoginoverwrites 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
rememberMecookie lifecycle:- Set by backend (
auth.contoroller.ts:53-57): When login returns 2FA (no token yet) ANDrememberMewas in request body → sets cookierememberMe=true, httpOnly: false, maxAge: 5 minutes - Read by backend (
auth.contoroller.ts:122):verifyOtpreadsreq.cookies.rememberMe - Used by backend (
auth.contoroller.ts:126-132): IfrememberMe === "true"→ clears the rememberMe cookie, callssetTokenCookie(20-day httpOnly cookie) - Cleared by backend (
auth.contoroller.ts:70): On non-rememberMe login,res.clearCookie("rememberMe")
- Set by backend (
-
Security note: The
rememberMecookie is non-httpOnly, so an XSS attacker could injectrememberMe=truebefore 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:
useProfilehook,useLogin/useLogout(invalidation) -
AsyncLocalStorage Context (
requestContext): - Type: Per-request server-side context
- Accessed By:
hasPermissionhelper,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.tsbut 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 aggregatorsaas-boilerplate/src/app/middlewares/auth.ts— Applied by route definitions outside auth modulesaas-boilerplate/src/app/middlewares/hasPermission.ts— Applied by route definitions outside auth modulesaas-boilerplate/src/app/cron/scheduleResetLoginAttempts.ts— Cron entry, invoked by schedulerSass-boilerplate-frontend-v1/src/middleware.js— Next.js Edge middleware, invoked by frameworkSass-boilerplate-frontend-v1/src/components/providers/auth-initializer.jsx— Mounted in root layoutSass-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 importssaas-boilerplate/src/helper/hashedPassword.ts— Pure utility, only bcryptsaas-boilerplate/src/helper/jwtHelpers.ts— Pure utility, only jsonwebtokensaas-boilerplate/src/entity/*.ts— All 10 entities are leaf nodes (import only TypeORM decorators)saas-boilerplate/src/app/events/eventTypes.ts— Constants onlySass-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.jsneeds Reduxdispatch(received viasetStoreDispatch)authSlice.jsis a Redux slice (no direct dependency)auth-initializer.jsximports 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.tshelper — 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
Related Code & Reuse Opportunities¶
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.tsline 22-29 for rate-limited route with validation → controller pipeline. - Adding a new 2FA method: Reference
auth.service.tsline 152-221 for the login branching pattern andverifyOtpfor verification pattern. - Adding a new entity: Reference
src/entity/User.tsfor TypeORM decorators, soft-delete, and index patterns. - Adding a new event: Reference
src/app/events/eventTypes.tsfor constant, create handler insrc/app/events/handlers/, register in event bootstrap. - Adding a new permission check: Reference
src/helper/hasPermission.tsfor 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
catchAsyncandsendResponseacross 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
sendEmailcall (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_MAPPINGlogic commented out —convertDotToRouteignores 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
handleBackupCodeSubmitfunction — remove. - login.jsx:626-636: Commented-out invite-only conditional for register link — decide and clean up.
- register.jsx:48: Commented-out
const websiteType = 2test 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.errorinhandleDisableError— 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 | — |
- Login attempt lockout gap:
handleFailedAttemptinauth.helpers.tsapplies lock for attempts 2-6 and blocks at 9+, but attempts 7-8 fall through with no lock applied (line 111-117). - Missing
refreshTokencolumn:authmiddleware (line 174) checksuser.refreshTokenbut the User entity has no such column — refresh token validation always fails. - Registration JWT exposes password hash:
registerAccount(line 111-115) embeds bcrypt hash in JWT payload — intercepted token reveals hash. Math.random()for OTPs:generateOtp.tsuses non-cryptographic RNG — predictable OTPs possible.hashSyncin async function:hashedPassword.tsblocks event loop during password hashing.- Axios interceptor operator precedence: Line 29 —
403check can throw iferror.responseis undefined. - 403 triggers logout: Axios interceptor logs out on permission denied (should only logout on 401).
- OAuth token in URL: Google OAuth callback passes JWT in URL query string — visible in browser history, logs, referrer headers.
- Password reset min length mismatch: Reset allows min 3 chars vs registration min 6 chars.
- Frontend refreshToken URL mismatch:
authService.jscalls/auth/refreshbut backend route is/auth/refresh-token. optHashcolumn typo: InOtp.tsentity — renaming requires database migration.- Duplicate token generation on login: Controller generates token AND
setTokenCookiegenerates another token with different expiry (20-day). - Double auth middleware:
authGuardandauth(default export) in same file — confusion about which to use. keepPreviousDatadeprecated: React Query 5 should useplaceholderDatainstead.- Old OTPs never cleaned up: No TTL-based deletion for
otp_verificationrecords. - Dual role-assignment tables:
User.ts@ManyToManycreates join tableuser_roles(plural), whileUserRoles.tsentity creates tableuser_role(singular) — two different tables serving the same purpose. Data splits across both, TypeORM cannot resolve both correctly. One mechanism must be removed. - Soft-delete blocks re-registration: Unique constraints on
User.email,Permission.route, andInviteUser.emaildo not exclude soft-deleted rows. A soft-deleted user blocks new registration with the same email. Needs PostgreSQL partial unique indexes. - Missing FK relations on Token and OTP: Both
TokenandOtpVerificationuse plainuserId/emailcolumns with no@ManyToOnerelation — no referential integrity, orphaned rows accumulate on user deletion. - No cascade cleanup:
LoginAttempt,Token,OtpVerification, andSubscriptionhave no cascade delete from User — all leave orphaned rows. OnlyUserSettingandUserRolecascade properly, but soft-delete never triggers DB-level cascades anyway. displayNamenullability ambiguity on Permission: Hasdefault: nullbut nonullable: true— may cause insert failures depending on TypeORM/PostgreSQL behavior.- Permission normalization commented out:
hasPermission()has normalization disabled (lines 57-63) buthasPermissionForUser()normalizes — inconsistent behavior means dot-notation permissions fail for current-user checks but work for user-by-ID checks. - OTP logged to console:
sendLoginOtp.handler.tsline 7 logs OTP in plaintext (console.log("Sending OTP to email:", email, "with OTP:", otp)) — security vulnerability in production. - Missing
awaitin subscription expiration:resetSubscribtionStatus.handler.tsline 19 does notawaitthe DB query — fire-and-forget, errors are unhandled promise rejections. - 3 dead event types:
OTP_VERIFIED,RESET_PASSWORD,VERIFY_EMAIL_REGare defined ineventTypes.tsbut have no corresponding handlers. checkPermissionAndThrowuses HTTP 400: Permission denial returnsBAD_REQUEST(400) instead ofFORBIDDEN(403) — semantically incorrect, confuses frontend error handling.- 3 event handlers missing error handling:
inviteOnlyLoginSendEmail.ts,sendForgotPass.handler.ts,sendLoginOtp.handler.tshave no try/catch — email send failures become unhandled promise rejections. - Plaintext password emailed:
inviteOnlyLoginSendEmail.tssends user password in cleartext in the email body — security anti-pattern. - Hardcoded
'super_admin'string:requestContext.tsisCurrentUserSuperAdmin()compares against literal string'super_admin'— if the role name changes in DB/seed, the entire super admin bypass silently breaks. - Debug
console.login production:hasPermission.tsline 68 logs{cachedResult}to stdout on every cache hit. - No rate limiting on sensitive endpoints:
/forgot-password,/verify-otp,/verify-2fa-google,/verify-backup-code,/reset-password,/registerhave no rate limiting — enables brute-force OTP guessing, email bombing, and mass registration. - Unhashed password on user restoration:
auth.service.tscreateAccount(line 60) sets raw password directly when restoring a soft-deleted user — critical security bug if User entity has no@BeforeUpdatehook (it doesn't). - Google OAuth logout doesn't clear cookie:
google-oauth.controller.tslogoutdestroys Passport session but does NOT clear thetokencookie — user retains valid JWT until natural expiry (7 days). verifyLoginOtphangs on no token:auth.contoroller.tslines 201-236 — no else branch forhasToken === false, request hangs forever with no response.verifyOtpdoesn't check OTP expiry:auth.service.tslines 360-407 — only checksisVerifiedflag, does not validateexpiresAttimestamp. Expired OTPs can still be used.- CSP
connectSrc: ['self']blocks external APIs:sequrity.tsCSP configuration blocks all cross-origin API calls (Stripe checkout, external services, CDNs). require-corpCOEP policy:sequrity.tssets Cross-Origin-Embedder-Policy torequire-corp— blocks loading any cross-origin resources (including Cloudinary images) that don't send CORP headers.- Three different token lifetimes:
setTokenCookie= 20 days, Google OAuth = 7 days, refresh-generated = 24 hours — inconsistent session durations depending on login method and refresh. verifyLoginOtpauto-registration bypasses invite-only: OTP-based passwordless login auto-creates users regardless ofauth.website_typesetting — circumvents invite-only mode.deserializeUsermissing relations: PassportdeserializeUserloads user withoutrole/settingrelations —req.user.roleisundefinedafter session deserialization.- Relative OAuth callback URL:
passport.tsusescallbackURL: "/api/v1/auth/google/callback"(relative) — may break behind reverse proxies with different host headers. - Google OAuth scope duplication:
google-oauth.controller.tsrequests scopes["profile", "email", "profile"]—profilelisted twice. handleBackupCodeSubmitis dead code:login.jsxline 122-124 setsisLoadingbut never actually submits anything — function body is empty.- 2FA modal re-trigger bug:
login.jsxline 263 — whenmethod === "google_auth",setIsVerifiedOnce(true)is never called, allowing the 2FA modal to re-open on re-renders. - 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.
- No server-side logout:
useLogouthook makes no API call (Promise.resolve()) — refresh tokens are never invalidated server-side, remaining valid until natural expiry. - Uncleaned
setTimeoutin 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. - 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.
- Cookie maxAge for non-remember-me is 7 days: Both
useLoginhook 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. - Forgot-password and reset-password use raw axios: Both pages bypass the shared
axiosInstanceinterceptor, using directaxios— duplicates base URL configuration and loses interceptor functionality. - 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.
createRolepermission check COMMENTED OUT:role.service.tsline 150 —checkPermissionAndThrow("role.create")is commented out, meaning any authenticated user can create roles. Critical security issue.getMySubscriptionsdata leakage:subscription.helpers.tsfindSubscriptionssilently ignores theuserIdfilter —getMySubscriptionsreturns ALL users' subscriptions instead of just the current user's.GET /user-permissionsroute unreachable:permission.routes.ts—GET /:permissionIdis defined beforeGET /user-permissions, so "user-permissions" is captured as a permissionId parameter. Dead route.updateProfilehangs on password change:user.controller.tslines 59-61 —return reswithout calling.json()or.end()when password is changed, causing the HTTP response to hang.- Duplicate userRoles validation bypasses permission check:
user.service.tslines 337-363 — userRoles validated twice; second unconditional validation makes the permission-guarded first check pointless. getAllPermissionshas no permission check:permission.service.ts— any authenticated user can list all system permissions without authorization.console.log(code)logs backup codes:user_setting.service.tsline 72 — logs backup code in plaintext to stdout during verification. Security vulnerability.- Missing seeded permissions:
subscription.view,users.assign.roles,users.update.roleare checked in service code but not in seed data — these permission checks always fail for non-super-admin users. - Email 2FA setup incomplete:
user_setting.service.tsstartEmail2FASetupis defined but never exported or called — email-based 2FA enable has no verification step, just enables immediately. console.log("2FA Setup Data:", data)leaks TOTP secret:setup-two-factor-verification.jsxline 35 logs the TOTP secret key and QR code data to the browser console. Security vulnerability.- Backup codes stored without delimiters:
user_setting.service.tsconcatenates SHA-512 hashes of backup codes into a single string with no delimiter — verification uses substring matching, which is architecturally fragile. createRolecache not invalidated on restore path:role.service.tscreateRole— when restoring a soft-deleted role,deleteCacheByPrefix("roles:")is NOT called, leaving stale cache.- Google OAuth cookie set before error check:
google-oauth.controller.tsline 60 sets the auth cookie BEFORE checkingloginErrat line 66 — on login failure, the cookie is already committed to the response. upsertRoleinherits missing permission check:role.service.tsupsertRolecallscreateRoleinternally, which has its permission check commented out — soupsertRoleis also unprotected.
Optimization Opportunities¶
- DB queries on every request: Both
authmiddleware andhasPermissionmiddleware do full DB lookups per request. Consider caching user + role data in Redis or JWT claims for frequently accessed endpoints. - Permission cache scope:
permissionCacheis 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 withhash(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.jsrouter.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:
isDeletedboolean 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) anduser_role(entity) — competing mechanisms for the same data. Must consolidate. - Missing FK constraints:
Token.userIdandOtpVerification.emailare plain columns with no foreign key relations — orphaned data on user deletion. - No entity-level password hashing: No
@BeforeInsert/@BeforeUpdatehooks 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 usesendEmail()helper directly — two code paths for the same operation. - Dead code in event/permission system:
expireSubscriptions()function,ACTION_MAPPINGconstant, unused imports (config,resetPassTemplateininviteOnlyLoginSendEmail.ts), 3 event types with no handlers. eventEmmitertypo: The EventEmitter singleton is namedeventEmmiterthroughout (should beeventEmitter).- Debug console.log statements:
hasPermission.tsline 68 andsendLoginOtp.handler.tsline 7 — leak internal state and OTP values to stdout. - Magic strings: Super admin role name hardcoded as
'super_admin'string literal inrequestContext.ts— not sourced from config or constants. - Three redundant NODE_ENV keys:
config.env,config.nod_env,config.mode,config.production+ directprocess.env.NODE_ENV— five ways to check the same value, invites inconsistent usage. - No env var validation:
config/index.tshas 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
sameSitesettings:setTokenCookieomitssameSite, 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) vsauth(lookup by id, no status check, omits name) — subtle bugs depending on which is used. - Frontend debug
console.logstatements: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-passwordimports from@/components/custom/input,reset-passwordimports from@/components/ui/input— different components with potentially different behavior. - Side effects in Redux reducers:
logoutreducer callsdeleteCookie(),initializeAuthcallsgetCookie()— violates Redux purity, breaks DevTools time-travel debugging. keepPreviousDatadeprecated in React Query v5:useProfilehook uses deprecated option — should useplaceholderData: keepPreviousDataimport.
Modification Guidance¶
To Add New Functionality¶
- New auth endpoint (e.g., magic link login):
- Add Zod schema to
auth.validation.ts - Add controller method to
auth.contoroller.ts(or new controller) - Add business logic to
auth.service.ts(or create new focused service) - Add route to
auth.route.tswith appropriate rate limiting - If emails needed: add event type to
eventTypes.ts, create handler insrc/app/events/handlers/ - If new DB state: create entity in
src/entity/, run TypeORM migration -
Frontend: add API method to
authService.js, React Query hook inapi/auth/index.js, UI component -
New 2FA method (e.g., SMS):
- Add enum value to
UserSetting.twoFactorProvider - Add setup logic in
auth.service.ts(reference Google Authenticator pattern) - Add verification branch in
AuthService.loginAccount()login flow - Add verification endpoint (route + controller + service)
-
Frontend: add setup UI (reference
setup-two-factor-verification.jsx) -
New permission type:
- Add permission to seed data (via admin UI or migration)
- Permission format:
/{resource}/{action}— automatically checked by middleware - For service-layer checks: use
hasPermission()fromsrc/helper/hasPermission.ts
To Modify Existing Functionality¶
-
Change token expiry: Edit
config/index.ts→jwt.expires_inandjwt.refresh_token_expires_in. Also update frontend cookie durations inapi/auth/index.js. -
Change lockout rules: Edit
auth.helpers.ts→calculateLockDuration()for durations,handleFailedAttempt()for block threshold. -
Change password requirements: Edit
auth.validation.ts→ update Zod.min()values. Ensure consistency acrossregisterValidation,loginValidation, andresetPasswordValidation. -
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¶
-
Remove Google OAuth: Delete
google-oauth.controller.ts, remove Passport config, remove Google routes fromauth.route.ts, removeProvider.GOOGLEhandling in service, remove frontendsocial-login.jsx. -
Remove OTP passwordless login: Delete
requestLoginOtp/verifyLoginOtpfrom service + controller + routes + validation, removeProvider.OTPhandling, remove frontend OTP login UI. -
Remove 2FA entirely: Remove all 2FA branches from
AuthService.loginAccount(), remove verify-otp/verify-2fa-google/verify-backup-code endpoints, removeUserSetting2FA 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