Skip to content

Settings & Configuration System - Deep Dive Documentation

Generated: 2026-02-25 Scope: Backend Setting + AppConfig + UserSetting modules, entities, seeds, frontend API/store/UI, consumer components Files Analyzed: 40+ (22 backend, 18+ frontend) Lines of Code: ~5,800+ Workflow Mode: Exhaustive Deep-Dive Deep-Dive Area: #12 — Settings & Configuration


Overview

The Settings & Configuration system consists of three separate backend modules (Setting, AppConfig, UserSetting) that each serve a different purpose, with a frontend that consumes them through multiple access patterns (React Query, Redux, SSR server-side cache). The system manages global site configuration, encrypted application secrets, and per-user preferences.

Purpose: Provide configurable application behavior without code changes — site branding, contact info, auth page appearance, API keys, and per-user 2FA/language preferences.

Key Responsibilities: - Store and retrieve key-value site settings grouped by category (admin, site, contact, auth) - Store and retrieve encrypted application secrets (Stripe keys via AppConfig) - Manage per-user 2FA configuration and language preferences (UserSetting) - Serve settings to both authenticated (dashboard) and unauthenticated (public pages) contexts

Integration Points: Auth module (2FA flow), Payment module (Stripe key storage), Layout components (site name, logos, footer), SEO metadata, public pages (SSR settings)


Architecture: Three Separate Configuration Systems

Aspect Setting AppConfig UserSetting
Table settings app_configs user_setting
Purpose UI-configurable site settings (site name, logos, auth pages, contact info) Encrypted app secrets (Stripe keys) Per-user 2FA config + language preference
Scope Global (19 seeded rows) Global (no seed data) Per-user (1:1 with User)
API Path /api/v1/settings /api/v1/app-configs /api/v1/user-settings
Auth on GET PUBLIC (no auth) Auth required Auth required
Encryption None AES-256-GCM via isEncrypted flag None (secrets stored plaintext)
Soft Delete Yes (softDelete) Has @DeleteDateColumn but uses hard delete (bug) Yes (@DeleteDateColumn)
Timestamps Only deletedAt (no createdAt/updatedAt) createdAt, updatedAt, deletedAt createdAt, updatedAt, deletedAt
Key Uniqueness None (app-layer check only) @Index unique on key N/A (keyed by userId FK)
Column Naming snake_case (display_name) camelCase (displayName) camelCase (isTwoFactorEnabled)
Validation Empty file (no Zod schemas) Zod schemas for create/update Zod for 2FA token only
Permissions Seeded 4 (setting.create/view/update/delete) 0 (none seeded) 0 (none seeded)

Verdict: The three-system separation is intentional by design (general config / secrets vault / user prefs) but suffers from inconsistent implementation quality. Setting has public endpoints with no validation; AppConfig has validation but broken permissions; UserSetting has critical security gaps in 2FA secret handling.


Complete File Inventory

Backend — Setting Module (9 files)

saas-boilerplate/src/entity/Setting.ts (39 LOC)

Purpose: TypeORM entity for the settings table. Stores key-value configuration with grouping, ordering, display metadata, and soft delete.

Column Type Nullable Default Notes
id number No auto PK @PrimaryGeneratedColumn()
key string No varchar(255), NO unique constraint
display_name string Yes null varchar(255)
value string Yes null text
details string Yes null text
backupCodes string[] Yes null simple-arrayMISPLACED, belongs on UserSetting
type string Yes null varchar(255) — UI type: input, textarea, image, select_dropdown
order number Yes 1 Display ordering within group
group string Yes null varchar(255) — admin, contact, site, auth
deletedAt Date\|null Yes null @DeleteDateColumn

saas-boilerplate/src/app/modules/v1/Setting/setting.routes.ts (22 LOC)

Purpose: Express router for Settings CRUD. Public GET routes, authenticated mutations.

# Method Path Auth Middleware Handler
1 GET /api/v1/settings/ NO getAllSettings
2 GET /api/v1/settings/:prefix/prefix NO getSettingByPrefix
3 GET /api/v1/settings/:id NO getSettingById
4 POST /api/v1/settings/ Yes auth, context, upload.single('image') createSetting
5 PATCH /api/v1/settings/bulk-update Yes auth, context updateSettings
6 PATCH /api/v1/settings/:id Yes auth, context, upload.single('image') updateSettingById
7 DELETE /api/v1/settings/:id Yes auth, context deleteSetting

saas-boilerplate/src/app/modules/v1/Setting/setting.controller.ts (165 LOC)

Purpose: Request handling — extracts params, handles file upload, delegates to service. Has image upload support via handleFileUpload with cloudinary/S3/local storage support.

Key behavior: File upload writes image URL into body.value. File is saved to disk AFTER database write (inconsistency risk on file save failure).

saas-boilerplate/src/app/modules/v1/Setting/setting.service.ts (205 LOC)

Purpose: Business logic for Settings CRUD with permission checks on write operations.

Method Permission DB Query Notes
createSetting(payload) setting.create findOne (dup check) + create + save Throws raw Error (not ApiError) for duplicate
updateSettingById(id, payload) setting.update findOne + merge + save
bulkUpdateSettings(payloads) setting.bulkUpdate Promise.all per-key updates Permission NOT seeded
deleteSetting(id) setting.delete softDelete(id)
getAllSettings() NONE find() No permission check, public
getSettingById(id) NONE findOneBy({ id }) No permission check, public
getSettingByKey(key) NONE findOneBy({ key })
getImageSizeByKey() NONE findOneBy({ key: "admin.image_size" }) Key NOT seeded, always returns default 5
getSettingsByPrefix(prefix) NONE Like(\${prefix}%`)` LIKE-pattern injectable

Dead code: generateBackupCodes (22-32), verifyBackupCode (170-188) — copy-pasted from UserSetting, would crash (queries userId column that doesn't exist). Unused imports: Not, hashedPassword, verifyPassword, bcrypt, crypto.

saas-boilerplate/src/app/modules/v1/Setting/setting.validation.ts (1 LOC)

Purpose: Should contain Zod validation schemas. File is completely empty. No request body validation exists for any Setting endpoint.

saas-boilerplate/src/seed/data/setting.ts (208 LOC)

Purpose: Seed data for 19 settings across 4 groups.

Group Keys Count
admin site_name, test_5, favicon, meta_description, meta_image 5
contact contact_address, page_desc, contact_phone, contact_email, page_title 5
site footer_copyright, site_logo, site_logo_dark, footer_text 4
auth login_image, register_image, login_page_type, register_page_type, website_type 5

Issues: "SASS Boilerplate" typo (should be SaaS), "Headquaters" typo, 6 hardcoded Cloudinary URLs, admin.test_5 is test data in production seed, all entries have backupCodes: null confirming misplaced column, duplicate order values within groups.

saas-boilerplate/src/seed/setting.seed.ts (27 LOC)

Purpose: Seeds settings table. Skip-if-any-exist logic: if even ONE setting exists, entire seed is skipped (new seed settings in future releases won't be inserted).

saas-boilerplate/src/seed/data/permission.ts (partial)

Setting-related permissions seeded:

Permission String Display Name Used in Code?
setting.create Setting Management Yes
setting.view Setting Management NEVER (seeded but unchecked)
setting.update Setting Management Yes
setting.delete Setting Management Yes
setting.bulkUpdate NOT SEEDED (checked in code but doesn't exist)

Backend — AppConfig Module (5 files)

saas-boilerplate/src/entity/AppConfig.ts (38 LOC)

Purpose: TypeORM entity for app_configs table. Encrypted key-value store for application secrets.

Column Type Nullable Default Notes
id number No auto PK
key string No varchar(150), unique index
displayName string No varchar(255), no DB default
value string No text (encrypted ciphertext when isEncrypted=true)
isEncrypted boolean No true Defaults to encrypting everything
createdAt Date No auto @CreateDateColumn
updatedAt Date No auto @UpdateDateColumn
deletedAt Date\|null Yes null @DeleteDateColumn — but hard delete used in service

saas-boilerplate/src/app/modules/v1/AppConfig/app_config.routes.ts (32 LOC)

# Method Path Auth Middleware Handler
1 GET /api/v1/app-configs/ Yes auth, context loadAllConfigs
2 GET /api/v1/app-configs/:id Yes auth, context getConfigById
3 POST /api/v1/app-configs/ Yes auth, context, validateRequest createAppConfig
4 PATCH /api/v1/app-configs/:id Yes auth, context, validateRequest updateAppConfig
5 DELETE /api/v1/app-configs/:id Yes auth, context deleteConfigById

saas-boilerplate/src/app/modules/v1/AppConfig/app_config.service.ts (130 LOC)

Purpose: CRUD for encrypted app config with permission checks.

Method Permission String Notes
loadAllConfigs() app_config.read
getConfigById(id) app_config.read
getConfigByKey(key) COMMENTED OUT Used by StripeService internally
createAppConfig(body) create_app_config.create WRONG FORMAT
updateAppConfig(id, body) update_app_config.update WRONG FORMAT
deleteAppConfig(id) app_config.delete Uses hard delete() not softDelete()

Critical: Three different permission naming conventions: app_config.read, create_app_config.create, update_app_config.update. None are seeded — module is permanently inaccessible to non-super_admin users.

saas-boilerplate/src/app/modules/v1/AppConfig/app_config.validation.ts (25 LOC)

Purpose: Zod schemas for AppConfig create/update.

Schema Fields
createConfig key: string.min(1), displayName: string.min(1), value: string.min(1), isEncrypted: boolean.optional()
updateConfig All fields partial

Issue: No max length on key (DB is varchar(150) — would fail at DB level).


Backend — UserSetting Module (5 files)

saas-boilerplate/src/entity/UserSetting.ts (64 LOC)

Purpose: Per-user 2FA configuration and language preference. 1:1 relationship with User entity.

Column Type Nullable Default Notes
id number No auto PK
user User No @OneToOne with onDelete: CASCADE
userId number No FK, indexed
isTwoFactorEnabled boolean No false
twoFactorProvider TwoFactorProvider\|null Yes enum: email, google_authenticator, sms
twoFactorSecret string\|null Yes select: true — returned in all queries!
tempTwoFactorSecret string\|null Yes Temporary secret during setup
backupCodes string Yes (DB) Concatenated SHA-512 hashes as single text string
language string No "en" varchar(10)
createdAt/updatedAt/deletedAt Date auto Standard timestamps

Enum: TwoFactorProvider = EMAIL \| GOOGLE_AUTHENTICATOR \| SMS (SMS is defined but never used anywhere).

saas-boilerplate/src/app/modules/v1/UserSetting/user_setting.routes.ts (33 LOC)

# Method Path Auth Middleware Handler
1 PUT /api/v1/user-settings/2fa/email Yes auth enableEmail2fa
2 PUT /api/v1/user-settings/2fa/google/setup Yes auth start2FASetup
3 POST /api/v1/user-settings/2fa/generate-backup-codes Yes auth generateBackupCodes
4 PATCH /api/v1/user-settings/2fa/google/verify Yes auth, validateRequest verifyAnd2FAGoogleSetup
5 PATCH /api/v1/user-settings/2fa/google/disable Yes auth, validateRequest disableGoogle2fa
6 GET /api/v1/user-settings/ Yes auth, context getUserSetting
7 GET /api/v1/user-settings/me Yes auth, context getMySetting
8 GET /api/v1/user-settings/:id Yes auth, context getSettingById

saas-boilerplate/src/app/modules/v1/UserSetting/user_setting.service.ts (325 LOC)

Purpose: 2FA management (Google Authenticator + Email) and backup code generation/verification.

Key behavior: Uses speakeasy for TOTP, qrcode for QR generation. Backup codes are generated with Math.random() (not cryptographically secure), hashed with SHA-512, concatenated into a single string, and matched via String.includes().


Frontend — API Layer (3 files)

Sass-boilerplate-frontend-v1/src/api/settings/index.js (202 LOC)

Purpose: React Query hooks for Settings CRUD.

Hook Method Endpoint Notes
useSettings() GET /settings Transforms array into grouped object by dot-notation key split
useCreateSetting() POST /settings FormData with image support
useUpdateSetting() PATCH /settings/${id} FormData with image support
useDeleteSetting() DELETE /settings/${id} Dev-mode swallows errors
useSettingByGroup(group) GET /settings/${group}/prefix Same dot-split transform

Key data transform: Setting key "admin.site_name" is split on "." → grouped as { admin: { site_name: "value" } }. Keys without dots produce undefined field name. Keys with 2+ dots silently truncate.

Sass-boilerplate-frontend-v1/src/api/settings/account-setting.js (123 LOC)

Purpose: React Query hooks for UserSetting / 2FA operations.

Hook Method Endpoint Notes
useAccountSettings() GET /user-settings/me Returns 2FA status
useSetupTwoFactor() PUT via useQuery /user-settings/2fa/google/setup BUG: side-effecting query
useVerifyOTP(params) Multi-action Various (switch on action) Default calls non-existent /2fa/verify
useBackupCodes({ enabled }) POST via useQuery /user-settings/2fa/generate-backup-codes BUG: regenerates on refetch

Sass-boilerplate-frontend-v1/src/api/public/settings.js (37 LOC)

Purpose: Server-side settings fetch with unstable_cache for public SSR pages.

Function Endpoint Cache Notes
getSettingByGroup(group) GET ${API_URL}/settings/${group}/prefix unstable_cache with "settings-group" tag Error returns [] (type mismatch with success {})

Frontend — State Management (2 files)

Sass-boilerplate-frontend-v1/src/store/slices/settingSlice.js (27 LOC)

Purpose: Redux slice that mirrors React Query settings data. Single reducer setSettings. Selectors: getAllSettings, getSettingByGroup(group) (curried).

Architecture concern: Redundant mirror of React Query cache. Data flows: API → React Query → useEffect dispatch → Redux. Creates dual source of truth with timing gap.

Sass-boilerplate-frontend-v1/src/store/index.js (17 LOC)

Purpose: Redux store config. Contains settinReducer typo (missing "g"). Has phantom redux-persist ignored actions for a library that isn't installed.


Frontend — UI Components (8 settings-specific files)

components/pages/settings/site-settings-page.jsx (~113 LOC)

Purpose: Main site settings admin page. Groups settings by group field into tabs (admin, contact, site, auth). Uses PermissionWrapper with setting.view permission. Renders SettingField for each setting and includes CreateSettingForm for new settings.

components/pages/settings/setting-field.jsx (~115 LOC)

Purpose: Individual setting row with inline editing. Supports 4 field types: input (text field), textarea, image (with upload), select_dropdown (hardcoded options). Uses useUpdateSetting mutation. Has edit/save/cancel toggle.

Issues: Hardcoded dropdown options (["option_1", "option_2", "option_3"]) — not dynamic. Image preview uses transformImageUrl but broken fallbacks.

components/pages/settings/create-setting-form.jsx (~95 LOC)

Purpose: Modal form to create new settings. Fields: key, display_name, value, type, group. Uses useCreateSetting mutation. Opens via sheet/drawer component.

Issues: No image upload support in create form (only in edit via SettingField). Type dropdown hardcoded: input, textarea, image, select_dropdown.

components/pages/settings/account-setting-page.jsx (~145 LOC)

Purpose: User account settings page showing 2FA status, enable/disable buttons, backup code generation. Uses useAccountSettings, useSetupTwoFactor, useVerifyOTP, useBackupCodes hooks.

Route Pages

  • dashboard/settings/site-settings/page.js — renders SiteSettingsPage
  • dashboard/settings/account-settings/page.js — renders AccountSettingPage
  • dashboard/settings/profile/page.jsx — renders profile editing page (covered in User Mgmt DD)

Frontend — Consumer Components (5+ files reading settings)

Consumer Settings Used Access Pattern
Dashboard layout.js All settings (dispatches to Redux) useSettings()dispatch(setSettings())
top-nav.jsx admin.site_name, admin.favicon Redux getSettingByGroup("admin")
sidebar-old.jsx admin.site_name, admin.favicon, admin.admin_site_title Redux getSettingByGroup("admin")
footer-credit.jsx site.footer_copyright Redux getSettingByGroup("site")
Root layout.js admin.meta_description, admin.meta_image, admin.meta_title Direct fetch() with next.revalidate
Login/Register pages auth.login_image, auth.login_page_type, etc. SSR getSettingByGroup("auth")
Contact page contact.* keys SSR getSettingByGroup("contact")
Header/Footer site.site_logo, site.site_logo_dark, site.footer_text SSR getSettingByGroup("site")

Setting keys consumed but NEVER seeded: - admin.meta_title — used in root layout for page title → renders undefined - admin.admin_site_title — used in sidebar for dashboard title → renders undefined - admin.image_size — used in getImageSizeByKey() → returns hardcoded default 5


Data Flow Analysis

┌─────────────────────────────────────────────────────────────────────┐
│                     SETTINGS DATA FLOW                              │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  PostgreSQL `settings` table (19 rows, 4 groups)                   │
│       │                                                             │
│       ├── GET /settings (PUBLIC, no auth)                          │
│       │     ├── [CSR] useSettings() → React Query ["settings"]     │
│       │     │     └── useEffect → dispatch(setSettings()) → Redux  │
│       │     │           ├── top-nav (admin.site_name, favicon)     │
│       │     │           ├── sidebar (admin.site_name, favicon)     │
│       │     │           └── footer-credit (site.footer_copyright)  │
│       │     └── [SSR] fetchMetaData() → root layout metadata       │
│       │           └── admin.meta_description, meta_image           │
│       │                                                             │
│       ├── GET /settings/:prefix/prefix (PUBLIC, no auth)           │
│       │     ├── [CSR] useSettingByGroup(group)                     │
│       │     └── [SSR] getSettingByGroup(group) → unstable_cache    │
│       │           ├── Login page (auth.login_image, login_type)    │
│       │           ├── Register page (auth.register_image, type)    │
│       │           ├── Contact page (contact.*)                     │
│       │           └── Header/Footer (site.logo, footer_text)      │
│       │                                                             │
│       ├── POST/PATCH/DELETE /settings (AUTH + permission)          │
│       │     └── [CSR] useCreateSetting/useUpdateSetting/useDelete  │
│       │           └── Site Settings admin page                     │
│       │                                                             │
│  PostgreSQL `app_configs` table (0 seeded rows)                    │
│       │                                                             │
│       ├── GET/POST/PATCH/DELETE /app-configs (AUTH + permission)   │
│       │     └── No frontend UI exists for this module              │
│       │                                                             │
│       └── Internal: getConfigByKey("stripe_secret_key")            │
│             └── StripeService → decrypt() → Stripe SDK             │
│                                                                     │
│  PostgreSQL `user_setting` table (per-user 2FA)                    │
│       │                                                             │
│       ├── GET /user-settings/me (AUTH)                             │
│       │     └── useAccountSettings() → Account Settings page       │
│       │                                                             │
│       ├── 2FA endpoints (AUTH)                                     │
│       │     ├── PUT /2fa/email → enableEmail2fa                    │
│       │     ├── PUT /2fa/google/setup → start2FASetup              │
│       │     ├── PATCH /2fa/google/verify → verify setup            │
│       │     ├── PATCH /2fa/google/disable → disable 2FA            │
│       │     └── POST /2fa/generate-backup-codes                    │
│       │                                                             │
│       └── Cross-module: Auth login flow checks user.setting        │
│             └── TwoFactorPolicy.ts → verify TOTP token             │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Three Frontend Access Patterns

  1. SSR via unstable_cache — Public pages (login, register, contact, header, footer) through getSettingByGroup() with tag "settings-group". Revalidated via revalidateTag("settings-group") on setting mutations.

  2. SSR via direct fetch() — Root layout's fetchMetaData() uses its own fetch() with next: { revalidate }. Separate caching — not invalidated when settings are updated via admin UI (stale metadata until TTL expires).

  3. CSR via React Query + Redux — Dashboard layout fetches ALL settings via useSettings(), dispatches to Redux store. Consumer components use useSelector(getSettingByGroup("group")). Creates dual source of truth between React Query cache and Redux store.


Dependency Graph

Circular Dependency

UserSetting.service → imports AuthService (for logininfoWithToken in backup code verification) Auth.controller → imports UserSettingService (for verifyBackupCode)

This creates a circular dependency chain that works at runtime due to Node.js module caching but is an architectural smell.

Entry Points (route files)

  • setting.routes.ts → registered at /api/v1/settings
  • app_config.routes.ts → registered at /api/v1/app-configs
  • user_setting.routes.ts → registered at /api/v1/user-settings

Leaf Nodes (entities, no outbound imports within scope)

  • entity/Setting.ts
  • entity/AppConfig.ts
  • entity/UserSetting.ts (imports User entity)

External Dependencies

  • speakeasy — TOTP token generation/verification (UserSetting)
  • qrcode — QR code generation for authenticator app setup
  • multer — File upload handling (Setting image uploads)
  • helper/encryption.ts — AES-256-GCM encrypt/decrypt (AppConfig)
  • helper/handleFileUpload.ts — Cloudinary/S3/local file upload
  • helper/checkPermissionAndThrow.ts — Permission enforcement

Testing Analysis

Test Files: NONE. Zero test files exist for any of the three modules (backend or frontend).

Test Coverage: 0% across all statements, branches, functions, and lines.

Testing Gaps: - No unit tests for service layer business logic - No integration tests for API endpoints - No tests for permission enforcement - No tests for encryption/decryption roundtrip - No tests for 2FA setup/verification flow - No tests for backup code generation/verification - No frontend component tests - No E2E tests for settings admin flow


Known Issues — Consolidated & Deduplicated

CRITICAL (18)

# ID Subsystem File:Line Description
1 SET-C1 Setting routes:10-12 All GET endpoints are fully public. No auth, no permission check. Anyone can read all 19 site settings including admin config, contact details, auth page types. The seeded setting.view permission is completely unused.
2 SET-C2 Setting service:82 setting.bulkUpdate permission NOT seeded. Service checks checkPermissionAndThrow("setting.bulkUpdate") but this permission doesn't exist in seed data. Bulk update is permanently broken for non-super_admin users.
3 SET-C3 Setting entity:13 No unique DB constraint on key column. Uniqueness only enforced at app layer (TOCTOU race). AppConfig does this correctly with @Index unique.
4 SET-C4 Setting service:170-188 Dead code verifyBackupCode queries non-existent userId column. Would crash with EntityColumnNotFound if ever called. Copy-pasted from UserSetting module.
5 SET-C5 Setting validation:1 Completely empty validation file. No Zod schemas. POST/PATCH endpoints accept arbitrary body content — extra fields could write unexpected columns (e.g., injecting backupCodes).
6 AC-C1 AppConfig service:17,43,72 Three different permission naming conventions. app_config.read vs create_app_config.create vs update_app_config.update — creates 3 separate resources. Should all be app_config.*.
7 AC-C2 AppConfig permission seed Zero AppConfig permissions seeded. No app_config.*, create_app_config.*, or update_app_config.* in seed data. Module is permanently inaccessible to non-super_admin users.
8 AC-C3 AppConfig service:117 Hard delete() bypasses soft delete. Entity has @DeleteDateColumn but service uses configRepo.delete(id) (permanent) instead of configRepo.softDelete(id). Data is irrecoverably lost.
9 AC-C4 AppConfig service:34 getConfigByKey permission check commented out. Used internally by StripeService.loadStripeKeyFromDb() — reads encrypted Stripe secret key without authorization. If a route is ever added for this method, it's fully unprotected.
10 AC-C5 AppConfig service:19-20,64,107 Encrypted ciphertext returned in API responses. All service methods return full AppConfig including the value field. When isEncrypted: true, the AES-256-GCM ciphertext is sent to the client. Leaks ciphertext for replay attacks if MASTER_KEY is compromised.
11 US-C1 UserSetting service:74,82 Backup code verification uses String.includes() on concatenated hashes. Fundamentally flawed data structure — should use array with exact equality. After code removal via string.replace(), boundary artifacts could create false positives.
12 US-C2 UserSetting entity:44, service:221,257 twoFactorSecret stored plaintext and returned in API responses. select: true means it's included in all queries. verify2FAGoogleSetup and disableGoogle2fa return full entity including the TOTP secret. If leaked, attacker can generate valid 2FA tokens indefinitely.
13 US-C3 UserSetting routes:28, service:260 GET /user-settings/ lists ALL users' 2FA settings. No permission check, no ownership filter. Any authenticated user can paginate through every user's 2FA configuration including twoFactorSecret.
14 US-C4 UserSetting service:287-288 GET /user-settings/:id permission check commented out. Any authenticated user can fetch any other user's setting record by ID, including twoFactorSecret, tempTwoFactorSecret, and backupCodes.
15 US-C5 UserSetting service:27 Backup codes generated with Math.random(). Not cryptographically secure. Should use crypto.randomBytes() or crypto.randomInt().
16 FE-C1 Frontend API account-setting.js:24-40 useSetupTwoFactor uses useQuery for side-effecting PUT. Fires automatically on mount/re-render/refetch. Each call generates a new TOTP secret and overwrites the previous one. StrictMode double-renders cause two PUT calls. Should be useMutation.
17 FE-C2 Frontend API account-setting.js:102-121 useBackupCodes uses useQuery for side-effecting POST. Regenerates backup codes on every mount/refetch, overwriting existing codes in the database. Users' saved backup codes become useless when component re-mounts.
18 FE-C3 Frontend API account-setting.js:73-75 Default case in useVerifyOTP calls non-existent endpoint. PATCH /user-settings/2fa/verify does not exist in backend routes. Any call without explicit action parameter will 404.

HIGH (15)

# ID Subsystem File:Line Description
19 SET-H1 Setting entity:25-26 backupCodes column on Setting entity is architecturally misplaced — belongs on UserSetting. All 19 seed entries have backupCodes: null.
20 SET-H2 Setting service:1,7-9,22-32 Dead code: 4 unused imports (Not, hashedPassword, verifyPassword, bcrypt, crypto) + 2 dead functions (generateBackupCodes, verifyBackupCode).
21 SET-H3 Setting service:126-167 No permission checks on any read method. Seeded setting.view permission is never used anywhere in codebase.
22 SET-H4 Setting service:47 createSetting throws raw Error instead of ApiError for duplicate key — returns 500 instead of 409. updateSettingById correctly uses ApiError.
23 SET-H5 Setting routes:14,16 No file-size limit on multer upload. memoryStorage() with no limits.fileSize allows OOM attacks via arbitrarily large file uploads.
24 AC-H1 AppConfig service:61 Default isEncrypted: true silently encrypts all values. Admin creating simple config without explicitly setting isEncrypted: false gets ciphertext stored.
25 AC-H2 AppConfig encryption.ts:6 Missing MASTER_KEY env var crashes entire app at import time. Buffer.from(process.env.MASTER_KEY!, 'hex') with non-null assertion throws on undefined.
26 AC-H3 AppConfig service:84 Duplicate key check in updateAppConfig doesn't exclude current record (TOCTOU race). Unused Not import was intended for this.
27 US-H1 UserSetting service:105-126 Email 2FA enabled instantly without email verification. startEmail2FASetup (which sends verification OTP) is defined but never exported or called from any route.
28 US-H2 UserSetting service:173,185 TOTP secret returned in API response body. Comment acknowledges "usually don't expose" but returns it anyway.
29 US-H3 UserSetting service:72 console.log(code) leaks plaintext backup code to server logs.
30 US-H4 UserSetting service:205-209 Inconsistent TOTP window param: setup verification uses default 0, login verification (TwoFactorPolicy.ts:46-51) uses window: 1.
31 FE-H1 Frontend API account-setting.js:11-17,31-37 Errors returned instead of thrown in 3 query hooks. React Query sees error as success data. isError is always false, error UI never renders.
32 FE-H2 Frontend API account-setting.js:96-97 invalidateQueries uses React Query v4 array syntax. v5 requires { queryKey: [...] }. Queries may not properly invalidate.
33 FE-H3 Frontend API settings/index.js:25,189 keepPreviousData: true deprecated/no-op in React Query v5. Replaced with placeholderData: keepPreviousData.

MEDIUM (27)

# ID Subsystem File:Line Description
34 SET-M1 Setting controller:78-80 updateSettingById returns wrong message ("created") and wrong status (201 not 200). Copy-paste from createSetting.
35 SET-M2 Setting controller:91 Bulk update returns string literal "updates" instead of actual data variable. Client never receives updated settings.
36 SET-M3 Setting service:154 getImageSizeByKey references non-existent key "admin.image_size". Always returns hardcoded default 5.
37 SET-M4 Setting service:163 LIKE-pattern injection via prefix parameter. User can inject % wildcard to match all settings (not full SQL injection but bypasses filtering).
38 SET-M5 Setting service:85-108 bulkUpdateSettings has no transaction. Promise.all concurrent updates to same key have race condition.
39 SET-M6 Setting seed data:7 "SASS Boilerplate" typo (should be "SaaS").
40 SET-M7 Setting seed data:16 "Headquaters" typo (should be "Headquarters").
41 SET-M8 Setting seed data:multiple 6 hardcoded Cloudinary URLs won't work for new deployments with different accounts.
42 SET-M9 Setting seed:13-16 Skip-if-any-exist logic prevents adding new seed settings in future releases.
43 SET-M10 Setting permission:128 setting.view is seeded but never checked anywhere in code.
44 AC-M1 AppConfig service:19 No pagination on loadAllConfigs. Returns all rows with no limit, no ordering.
45 AC-M2 AppConfig service (all) No decryption utility for read operations. Clients get raw ciphertext. decrypt exists but only used by getStripe.ts.
46 AC-M3 AppConfig validation:7-8 key max length (150 chars) not enforced in Zod. Would fail at DB level with truncation error.
47 AC-M4 AppConfig service:79 Double space in error message: "Config not found".
48 US-M1 UserSetting service:128-155 Dead code: startEmail2FASetup function never exported or called.
49 US-M2 UserSetting service:41,44 generateBackupCodes(count) accepts count param but never forwards it. Always generates exactly 10.
50 US-M3 UserSetting service:163 Typo: "Sass Boilerplate" in authenticator app name (should be "SaaS Boilerplate").
51 US-M4 UserSetting entity:17 TwoFactorProvider.SMS enum value defined but never used. Dead enum value creates false expectations.
52 US-M5 UserSetting service:114 Switching to email 2FA silently destroys Google Authenticator secret without confirmation.
53 US-M6 UserSetting entity:43 Unnecessary @Index() on twoFactorSecret column. No query searches by secret value.
54 FE-M1 Frontend API settings/index.js:162 useDeleteSetting missing revalidate("settings-group"). SSR-cached settings stale after deletion.
55 FE-M2 Frontend API settings/index.js:154-158 Dev-mode mock swallows real delete errors — masks bugs during development.
56 FE-M3 Frontend API multiple Keys without "." produce undefined field name in grouped settings object.
57 FE-M4 Frontend API public/settings.js:25,29 Error returns [] (array) but success returns {} (object). Type mismatch.
58 FE-M5 Frontend API public/settings.js:16,28 Error messages say "navigation links" instead of "settings" (copy-paste).
59 FE-M6 Frontend settingSlice.js:22 vs public/settings.js:3 Name collision: both export getSettingByGroup with different semantics (Redux selector vs async fetch).
60 FE-M7 Frontend layout.js Dashboard infinite loading if settings API fails — useSettings error not handled, layout depends on success.

LOW (25)

# ID Subsystem File:Line Description
61 SET-L1 Setting controller:15 Unused authorId variable. Setting entity has no authorId column.
62 SET-L2 Setting controller:31-38 File save after DB write creates inconsistency on file save failure.
63 SET-L3 Setting entity No createdAt/updatedAt timestamps (unlike AppConfig and UserSetting).
64 SET-L4 Setting entity No index on key or group columns. Full table scans for lookups.
65 SET-L5 Setting seed data:130-139 admin.test_5 / test_dropdown is test data in production seed.
66 SET-L6 Setting seed data:multiple Duplicate order values within same group (unpredictable display order).
67 SET-L7 Setting seed:1 Unused DataSource import in seed file.
68 SET-L8 Setting service:138-145 Commented-out old implementation dead code.
69 SET-L9 Setting routes:11 Unconventional /:prefix/prefix route pattern (confusing design).
70 AC-L1 AppConfig validation:19, routes:5 Inconsistent naming: AppconfigValidation (lowercase 'c') vs AppConfig everywhere else.
71 AC-L2 AppConfig service:46 Stale placeholder comment // Implementation here.
72 AC-L3 AppConfig service (all) No audit logging for secret management CRUD.
73 AC-L4 AppConfig validation:9 No max length on value field (unbounded text storage).
74 US-L1 UserSetting service:303-310 Inconsistent response shape between "has setting" and "no setting" cases for getMySetting.
75 US-L2 UserSetting entity:50-51 TypeScript type string but DB column nullable: true. Should be string | null.
76 US-L3 UserSetting service:60-63 verifyBackupCode loads unnecessary subscription relation; package sub-relation missing (always undefined).
77 US-L4 UserSetting routes:2 Unused import: authGuard.
78 US-L5 UserSetting controller:5 Unused import: checkPermissionAndThrow in controller.
79 US-L6 UserSetting service:7 Unused import: get from http.
80 US-L7 UserSetting service:17-19 Unused interface IUserSettingPayload.
81 FE-L1 Frontend API account-setting.js:48 Debug console.log of OTP params left in production code.
82 FE-L2 Frontend API account-setting.js:107 useBackupCodes returns res.data instead of res.data.data (inconsistent with all other hooks).
83 FE-L3 Frontend store/index.js:3 Typo: settinReducer (missing "g").
84 FE-L4 Frontend store/index.js:11-13 Phantom redux-persist ignored actions; redux-persist not installed.
85 FE-L5 Frontend public/settings.js:26 Dead commented-out code // return data.data || [];.

Severity Summary

Severity Count
Critical 18
High 15
Medium 27
Low 25
Total 85

Contributor Checklist

Risks & Gotchas

  • Setting module GET routes are public — intentional for SSR pages but means ALL settings are world-readable. Never store sensitive data in the Setting entity.
  • AppConfig module is super_admin-only by accident (no permissions seeded). Any admin UI for AppConfig won't work for regular admins.
  • UserSetting twoFactorSecret leaks in API responses — any endpoint returning the full UserSetting entity exposes the TOTP secret.
  • backupCodes exists on BOTH Setting and UserSetting entities — the one on Setting is wrong. Don't use it.
  • Three different caching strategies on frontend — React Query, Redux, and unstable_cache can all get out of sync.
  • MASTER_KEY env var is required even if AppConfig module isn't actively used — missing it crashes the entire app at startup.

Pre-change Verification Steps

  1. Check if the setting key you're adding/modifying exists in seed data (seed/data/setting.ts)
  2. Verify permission strings match exactly between service code and seed/data/permission.ts
  3. Test with non-super_admin user — many features are accidentally super_admin-only
  4. Verify frontend key-splitting works for your setting key format (must be exactly group.field)
  5. Check all 3 caching layers after setting updates (React Query, Redux, unstable_cache)

Suggested Tests Before PR

  • Unit: Setting service CRUD with permission mocking
  • Unit: AppConfig encryption/decryption roundtrip
  • Unit: UserSetting backup code generation + verification
  • Integration: Setting API with auth/non-auth requests
  • Integration: AppConfig API with permission checks
  • E2E: Site Settings page — create, edit, delete, image upload
  • E2E: Account Settings page — 2FA enable/disable flow
  • Security: Verify twoFactorSecret not returned in API responses after fix

Similar Patterns Elsewhere

  • Blog module uses similar CRUD pattern (routes → controller → service → entity) with checkPermissionAndThrow
  • EmailTemplate module has same "permissions seeded but not checked" pattern (Issue C2/C3 in email DD)
  • User module's PATCH /users/profile has similar self-role-change vulnerability documented in API review

Reusable Utilities Available

  • shared/getSortQuery.ts — safe sort query builder (use instead of raw orderBy to prevent SQL injection)
  • shared/catchAsync.ts — async error wrapper (already used correctly)
  • shared/sendResponse.ts — standardized response formatting (already used)
  • helper/paginationHelper.ts — pagination utilities (should be added to AppConfig loadAllConfigs)

Patterns to Follow

  • Permission seeding: Follow Role module's pattern of seeding all CRUD permissions (create/read/update/delete) consistently
  • Validation: Follow AppConfig's validation pattern (Zod schemas) and apply to Setting module
  • Entity timestamps: Follow AppConfig's pattern with @CreateDateColumn + @UpdateDateColumn
  • Unique constraints: Follow AppConfig's @Index(["key"], { unique: true }) for Setting entity

Modification Guidance

To Add a New Setting

  1. Add entry to seed/data/setting.ts with proper key (format: group.field_name), display_name, value, type, order, group
  2. Problem: Existing databases won't get the new setting (seed skips if any exist). Workaround: manually INSERT or change seed logic to upsert.
  3. Frontend: Setting will auto-appear on Site Settings page if grouped correctly
  4. For SSR consumption: use getSettingByGroup(group) and access result.field_name

To Add AppConfig Admin UI

  1. Create frontend components under components/pages/app-config/
  2. Create API hooks file at api/app-configs/index.js
  3. Critical: First seed the permissions (app_config.create, app_config.read, app_config.update, app_config.delete) and fix the naming inconsistency
  4. Add decryption for read operations (currently returns ciphertext)
  5. Add decrypt() call in a new getDecryptedConfigById service method

To Fix 2FA Secret Leakage

  1. Add select: false to twoFactorSecret column in UserSetting entity
  2. Add explicit select arrays to all service queries that return UserSetting
  3. Ensure verify2FAGoogleSetup and disableGoogle2fa do NOT return the full entity
  4. Add @Exclude() decorator from class-transformer as defense-in-depth

Testing Checklist for Changes

  • All 4 Setting permissions match between code and seed data
  • AppConfig permissions seeded if non-super_admin access needed
  • UserSetting twoFactorSecret not included in any API response
  • Setting validation schemas added and applied to routes
  • Frontend hooks use correct React Query v5 API ({ queryKey: [...] })
  • useSetupTwoFactor and useBackupCodes converted from useQuery to useMutation
  • Default action in useVerifyOTP either removed or mapped to valid endpoint
  • MASTER_KEY env var documented in deployment guide
  • All 3 cache layers invalidate on setting changes

Cross-References to Other Deep-Dives

Issue Related Deep-Dive Notes
US-C2, US-C3, US-C4 (2FA secrets leak) Auth DD #51-64 2FA issues documented but owned by UserSetting module
AC-H2 (MASTER_KEY crash) Middleware DD #C1 App startup crash pattern
SET-C1 (public settings) API DD All GET routes documented as public
checkPermissionAndThrow returns 400 not 403 RBAC DD #H5 Cross-cutting issue affects all modules
Permission format mismatch RBAC DD #C5 dot.notation vs route_format inconsistency

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