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-array — MISPLACED, 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— rendersSiteSettingsPagedashboard/settings/account-settings/page.js— rendersAccountSettingPagedashboard/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¶
-
SSR via
unstable_cache— Public pages (login, register, contact, header, footer) throughgetSettingByGroup()with tag"settings-group". Revalidated viarevalidateTag("settings-group")on setting mutations. -
SSR via direct
fetch()— Root layout'sfetchMetaData()uses its ownfetch()withnext: { revalidate }. Separate caching — not invalidated when settings are updated via admin UI (stale metadata until TTL expires). -
CSR via React Query + Redux — Dashboard layout fetches ALL settings via
useSettings(), dispatches to Redux store. Consumer components useuseSelector(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/settingsapp_config.routes.ts→ registered at/api/v1/app-configsuser_setting.routes.ts→ registered at/api/v1/user-settings
Leaf Nodes (entities, no outbound imports within scope)¶
entity/Setting.tsentity/AppConfig.tsentity/UserSetting.ts(imports User entity)
External Dependencies¶
speakeasy— TOTP token generation/verification (UserSetting)qrcode— QR code generation for authenticator app setupmulter— File upload handling (Setting image uploads)helper/encryption.ts— AES-256-GCM encrypt/decrypt (AppConfig)helper/handleFileUpload.ts— Cloudinary/S3/local file uploadhelper/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
twoFactorSecretleaks in API responses — any endpoint returning the full UserSetting entity exposes the TOTP secret. backupCodesexists 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_cachecan all get out of sync. MASTER_KEYenv var is required even if AppConfig module isn't actively used — missing it crashes the entire app at startup.
Pre-change Verification Steps¶
- Check if the setting key you're adding/modifying exists in seed data (
seed/data/setting.ts) - Verify permission strings match exactly between service code and
seed/data/permission.ts - Test with non-super_admin user — many features are accidentally super_admin-only
- Verify frontend key-splitting works for your setting key format (must be exactly
group.field) - 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
twoFactorSecretnot returned in API responses after fix
Related Code & Reuse Opportunities¶
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/profilehas similar self-role-change vulnerability documented in API review
Reusable Utilities Available¶
shared/getSortQuery.ts— safe sort query builder (use instead of raworderByto 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 AppConfigloadAllConfigs)
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¶
- Add entry to
seed/data/setting.tswith properkey(format:group.field_name),display_name,value,type,order,group - Problem: Existing databases won't get the new setting (seed skips if any exist). Workaround: manually INSERT or change seed logic to upsert.
- Frontend: Setting will auto-appear on Site Settings page if grouped correctly
- For SSR consumption: use
getSettingByGroup(group)and accessresult.field_name
To Add AppConfig Admin UI¶
- Create frontend components under
components/pages/app-config/ - Create API hooks file at
api/app-configs/index.js - Critical: First seed the permissions (
app_config.create,app_config.read,app_config.update,app_config.delete) and fix the naming inconsistency - Add decryption for read operations (currently returns ciphertext)
- Add
decrypt()call in a newgetDecryptedConfigByIdservice method
To Fix 2FA Secret Leakage¶
- Add
select: falsetotwoFactorSecretcolumn in UserSetting entity - Add explicit
selectarrays to all service queries that return UserSetting - Ensure
verify2FAGoogleSetupanddisableGoogle2fado NOT return the full entity - Add
@Exclude()decorator fromclass-transformeras 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
twoFactorSecretnot included in any API response - Setting validation schemas added and applied to routes
- Frontend hooks use correct React Query v5 API (
{ queryKey: [...] }) -
useSetupTwoFactoranduseBackupCodesconverted fromuseQuerytouseMutation - Default action in
useVerifyOTPeither removed or mapped to valid endpoint -
MASTER_KEYenv 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