Helper Functions, Config & DB — Deep Dive Documentation¶
Generated: 2026-03-29
Scope: saas-boilerplate/src/helper/, saas-boilerplate/src/config/, saas-boilerplate/src/db/
Files Analyzed: 33 (24 helper + 8 config + 1 db)
Lines of Code: ~1,460
Workflow Mode: Exhaustive Deep-Dive
Issues Found: 113 (20 Critical, 28 High, 39 Medium, 26 Low)
Dead Code: ~380 LOC across 5 files (+ 150 LOC duplicate AppConfig.ts)
Overview¶
The helper, config, and database connection layer forms the utility backbone of the SaaS boilerplate backend. Every API module, middleware, and service depends on these files for configuration values, authentication tokens, permission checks, file uploads, email sending, encryption, and database access.
Purpose: Cross-cutting utilities consumed by all 30 API modules Key Responsibilities: JWT management, RBAC permission checks, password hashing, file upload orchestration, email dispatch, AES-256-GCM encryption, environment configuration, database connection, pagination, boolean parsing Integration Points: 40+ consumers for config, 24+ for permission checking, 20+ for pagination, 9+ for password hashing, 7+ for Stripe, 5+ for file uploads
Issue Summary¶
| Severity | Count | Key Examples |
|---|---|---|
| Critical | 20 | Hardcoded Cloudinary credentials, 3x path traversal, zero file type validation, MASTER_KEY not validated at startup, AppConfig.ts 150-LOC duplicate, dual Stripe initialization, hardcoded localhost in LemonSqueezy, synchronize:true via typo key, encryption crash on missing env var, saveBufferToFile arg swap with LocalSotrage.ts |
| High | 28 | checkPermissionAndThrow uses HTTP 400 not 403, hasPermission no isDeleted check, debug console.log in production, req.user leaked to logs, 3 dead code files, passport invite-only takeover, no key rotation for encryption, decodeToken hardcodes access secret, refresh_token_secret unsafe cast |
| Medium | 39 | Hardcoded "Sass Template" from address, no TLS in email transport, no pagination limit cap, boolean duplicate files, redundant JWT decode, no AAD in encryption, sequential batch permission checks, pluralization bug, S3 client recreated per call |
| Low | 26 | Filename typo hashedPassword, Buffer.slice deprecated, mixed export styles, 4 config typos propagated to type system, unused baseUrl param, CommonJS require in TS |
Dead Code Inventory (~530 LOC removable)¶
| File | LOC | Evidence | Risk |
|---|---|---|---|
helper/fileUploader.ts |
43 | 0 imports anywhere | CRITICAL: Contains hardcoded Cloudinary API key + secret. Delete and rotate credentials immediately. |
helper/calculateDiscount.ts |
26 | 0 imports anywhere | Safe to delete |
helper/paginationHelper.ts |
37 | 0 imports, superseded by parsePaginationQuery.ts |
Safe to delete |
config/AppConfig.ts |
150 | Character-for-character duplicate of config/index.ts wrapped in singleton class. 0 meaningful imports (only self-import pattern). |
Safe to delete — config/index.ts is the canonical config |
helper/stringToBoolean.ts |
4 | 1 consumer only, inferior to parseBoolean.ts (case-sensitive, no null-safety) |
Replace consumer with parseBoolean, then delete |
helper/email/resend.ts |
~40 | 0 imports (Resend handled by shared/email/ResendService.ts) |
Verify no runtime dynamic import, then delete |
| Subtotal | ~300 |
Additionally, DeleteObjectCommand (imported but unused in cloud/s3.ts), configureCloudinary (imported but never called in uploadToCloudinary.ts), and baseUrl param (accepted but ignored in transformImageUrl.ts) are dead code within active files.
Complete File Inventory¶
helper/checkPermissionAndThrow.ts¶
Purpose: Thin wrapper around hasPermission() that throws a structured ApiError when permission is denied. The PRIMARY permission enforcement mechanism used by service-layer code.
Lines of Code: 23
File Type: Permission utility
What Future Contributors Must Know: This file is imported by 24 service modules — every protected endpoint in the system. Changing the error format, status code, or function signature is an extremely high-blast-radius change that will affect frontend error handling (Axios interceptors parse status codes).
Exports:
- export const checkPermissionAndThrow = async (path: string, actionDescription?: string): Promise<void> — Checks permission via hasPermission(path), throws ApiError(400, message) on denial
Dependencies:
- http-status — HTTP status code constants
- ./hasPermission — Default import; delegates actual permission check
- ../app/errors/ApiError — Custom error class
Used By: 24 service files: Auth, User, Role, Permission, Blog, BlogCategory, BlogTag, Contact, Cupon, EmailTemplate, GenericPage, Image, InviteUser, Menu, MenuItem, Package, PackageCategory, Payment, Product, ProductCategory, Public, Setting, Subscription, UserSetting
Known Issues:
1. HIGH: Uses HTTP 400 (Bad Request) instead of 403 (Forbidden) — Semantically incorrect. Security audit tools scanning for 403s will miss all permission denials. Frontend Axios interceptors may rely on 400 vs 403 distinction.
2. LOW: No null/undefined check on path parameter — Passes raw input to hasPermission()
helper/hasPermission.ts¶
Purpose: Core RBAC permission-checking engine. Determines whether the current user has a given permission by checking cache first, then falling back to database queries. Supports both route format (/users/create) and dot notation (users.create). The most complex and critical file in the helper directory.
Lines of Code: 296
File Type: RBAC engine
What Future Contributors Must Know: This is the most complex file in the helper directory with 6 exports and 2 internal functions. It has 3 direct importers + 24 indirect via checkPermissionAndThrow = affects every protected endpoint. The commented-out normalization (lines 57-63) was likely disabled during debugging and never re-enabled. The cache layer uses AsyncLocalStorage and is request-scoped. Both user.role (single FK) and user.userRoles (M2M) are loaded and deduplicated.
Exports:
- export class PermissionError extends Error — Custom error for permission system failures
- export const hasPermission = async (permissionName: string): Promise<boolean> — Main function, checks current user's permission via request context (also default export)
- export const hasPermissionForUser = async (userId: number, permissionName: string): Promise<boolean> — Checks a specific user's permission (for background jobs)
- export const getCurrentUserPermissions = async (): Promise<string[]> — Returns all permissions for current user (['*'] for super_admin)
- export const hasMultiplePermissions = async (permissions: string[]): Promise<Record<string, boolean>> — Batch check (sequential)
- export const hasAnyPermission = async (permissions: string[]): Promise<boolean> — OR logic
- export const hasAllPermissions = async (permissions: string[]): Promise<boolean> — AND logic
Dependencies:
- ../shared/requestContext — getCurrentUser(), isCurrentUserSuperAdmin()
- ../shared/permissionCache — hasPermissionInCache(), getCachedRoles()
- ../shared/permissionNormalizer — normalizePermissionString(), getPermissionVariants()
- ../shared/getDbRepository — TypeORM repository factory
- ../entity/Role, ../entity/Permission, ../entity/User — Entity types
Used By: checkPermissionAndThrow.ts (→ 24 services), hasPermission middleware (1), plus 2 direct service imports
Known Issues:
1. CRITICAL: Normalization asymmetry — hasPermission() has normalization commented out (lines 57-63), but hasPermissionForUser() uses it (line 137). Same permission string yields different results depending on which function is called.
2. HIGH: Debug console.log({cachedResult}) on line 68 — Leaks permission check results to server logs on every permission check in production.
3. HIGH: No isDeleted check — Main hasPermission() path loads user via loadUserRoles() without filtering soft-deleted users. A deleted user with a valid JWT passes permission checks.
4. MEDIUM: Sequential permission checks — hasMultiplePermissions, hasAnyPermission, hasAllPermissions use sequential await in for loops instead of Promise.all.
helper/hashedPassword.ts¶
Purpose: Bcrypt password hashing and verification utility for the auth system. Lines of Code: 12 File Type: Crypto utility
What Future Contributors Must Know: Used by 9 files across auth, registration, seeding, and settings. Changing salt rounds affects only new hashes (existing ones still verify). The function name hashedPassword is a noun (misleading — should be verb hashPassword).
Exports:
- hashedPassword = async (password: string): Promise<string> — Hashes plaintext with bcrypt (10 rounds)
- verifyPassword = async (password: string, hash: string): Promise<boolean> — Timing-safe comparison
Dependencies:
- bcrypt — Password hashing library
Used By: 9 files: auth.service, auth.utils, UserHandler, LoginAttemptHandler, OtpHandler, OtpPolicy, seed.ts, setting.service, user.service
Known Issues:
1. MEDIUM: Salt rounds 10 hardcoded — OWASP recommends 12+ for 2026. Not configurable.
2. LOW: 72-byte bcrypt truncation — Passwords longer than 72 bytes are silently truncated. No pre-hashing (SHA-256) mitigation.
3. LOW: Misleading filename — hashedPassword (noun) used as function name; should be hashPassword (verb).
4. LOW: No input validation — Empty string, null, undefined passed directly to bcrypt.
helper/jwtHelpers.ts¶
Purpose: JWT token generation, verification, and decoding for access tokens and refresh tokens. Central auth token utility. Lines of Code: 45 File Type: Auth utility
What Future Contributors Must Know: Used by 5 files in the auth pipeline. generateToken is generic (caller provides secret + expiry), while generateRefreshToken is hardcoded to config values. decodeToken verify-then-decodes (redundant but safe). Changing the algorithm from HS256 invalidates all existing tokens immediately.
Exports:
- jwtHelpers.generateToken(payload: any, secret: Secret, expiresIn: string): string — Signs JWT with HS256
- jwtHelpers.verifyToken(token: string, secret: Secret): JwtPayload — Verifies and returns payload
- jwtHelpers.decodeToken(token: string): JwtPayload — Verifies with access secret, then decodes (redundant)
- jwtHelpers.generateRefreshToken(payload: object): string — Signs with refresh secret from config
- jwtHelpers.verifyRefreshToken(token: string): JwtPayload — Verifies with refresh secret
Dependencies:
- jsonwebtoken — JWT library
- ../config — JWT secrets and expiration values
Used By: 5 files: auth.service, auth.utils, auth.controller, auth middleware, TokenHandler
Known Issues:
1. HIGH: decodeToken hardcodes access token secret — Cannot be used for refresh tokens, but name doesn't indicate this.
2. HIGH: refresh_token_secret as string — Unsafe cast; if env var is undefined, becomes string "undefined" (valid but insecure secret).
3. MEDIUM: payload: any — No type safety on token payload contents.
4. MEDIUM: Redundant decode — verifyToken already returns the decoded payload, then decodeToken calls jwt.decode() again.
5. LOW: Mixed function declaration styles — Arrow functions vs function declarations in same object.
helper/generateOtp.ts¶
Purpose: Generates a cryptographically secure 6-digit one-time password. Lines of Code: 5 File Type: Crypto utility
What Future Contributors Must Know: Cleanest file in the helper directory. Range [100000, 1000000) guarantees exactly 6 digits. crypto.randomInt is cryptographically secure (uses crypto.randomFillSync internally).
Exports:
- export function generateOTP(): string — Returns 6-digit numeric string
Dependencies:
- crypto — Node.js built-in randomInt
Used By: 1 file: OtpHandler.ts
Known Issues: None. This file is correct and well-implemented.
helper/encryption.ts¶
Purpose: AES-256-GCM symmetric encryption/decryption for sensitive data at rest (AppConfig secrets, Stripe keys). Lines of Code: 31 File Type: Crypto utility
What Future Contributors Must Know: MASTER_KEY must be exactly 64 hex characters (32 bytes). Losing this key = all encrypted AppConfig values permanently unrecoverable. No key rotation mechanism exists. The module reads process.env.MASTER_KEY directly (bypasses config/index.ts). Output format: [IV 12 bytes | Auth Tag 16 bytes | Ciphertext] base64-encoded.
Exports:
- export function encrypt(plaintext: string): string — AES-256-GCM encrypt to base64
- export function decrypt(data: string): string — AES-256-GCM decrypt from base64
Dependencies:
- crypto — Node.js built-in
Used By: 2 files: config/getStripe.ts (decrypts Stripe keys), appConfig.service.ts (encrypts/decrypts stored secrets)
Known Issues:
1. CRITICAL: MASTER_KEY not in requiredEnv — App starts without it, crashes only when encryption.ts is first imported (module-scope Buffer.from(process.env.MASTER_KEY!, 'hex')).
2. CRITICAL: No key rotation support — Changing MASTER_KEY invalidates all encrypted data with no migration path.
3. HIGH: No key length validation — Malformed MASTER_KEY produces cryptic createCipheriv error.
4. MEDIUM: No AAD (Additional Authenticated Data) — Encrypted values not bound to context; values could be swapped between config keys undetected.
5. LOW: Buffer.slice deprecated — Should use Buffer.subarray (Node 17.5+).
helper/email.ts¶
Purpose: Email dispatch router that branches between nodemailer and Resend based on config. Lines of Code: 29 File Type: Email utility
What Future Contributors Must Know: This is the legacy email pipeline (Pipeline #1). A separate modern pipeline exists in shared/email/ with factory pattern. Both coexist — this one uses config.mail.type while the factory uses config.mail.service. 4 consumers (event handlers + user settings).
Exports:
- export const sendEmail = async (payload: { to: string; subject: string; html: string }): Promise<void> — Sends email via configured provider
Dependencies:
- http-status, ../app/errors/ApiError — Error handling
- ../config — Provider selection (config.mail.type)
- ./email/resend — Resend provider
- ./transporterMail — Nodemailer transport
Used By: 4 files: event handlers (registerEmailHandler, sendForgotPass, sendLoginOtp, inviteOnlyLoginSendEmail) + userSetting.service
Known Issues:
1. MEDIUM: Hardcoded "Sass Template" <[email protected]> from address — Typo ("Sass" vs "SaaS") and ignores config.mail.from.
2. MEDIUM: Dual email infrastructure — This pipeline coexists with shared/email/EmailFactory using different config keys.
helper/transporterMail.ts¶
Purpose: Creates and exports a nodemailer SMTP transport at module load time. Lines of Code: 14 File Type: Email transport
What Future Contributors Must Know: Transport is created on import regardless of whether nodemailer is the configured provider. No TLS/SSL options configured.
Exports:
- export default transporter — nodemailer.Transporter instance
Dependencies:
- nodemailer — Email library
- ../config — SMTP credentials
Used By: 1 file: helper/email.ts
Known Issues:
1. MEDIUM: No TLS/SSL configuration — SMTP transport created without secure, tls, or requireTLS options.
2. LOW: Created at module load time — Even when Resend is configured as the provider.
helper/email/resend.ts¶
Purpose: Sends email via Resend SDK with attachment support. Lines of Code: 53 File Type: Email provider
What Future Contributors Must Know: This file has 0 imports — it's the legacy Resend implementation. The active one is shared/email/ResendService.ts. The Resend client is instantiated at module load with potentially undefined API key.
Exports:
- export async function resendEmail({ to, subject, html, attachments? }): Promise<...> — Sends via Resend SDK
Dependencies:
- resend — Resend SDK
- fs, path — File system (synchronous attachment reading)
- ../../config — API key, mail user
Used By: 0 files (dead code — superseded by shared/email/ResendService.ts)
Known Issues:
1. CRITICAL: Likely dead code — 0 imports found. Verify no dynamic imports before deletion.
2. HIGH: Resend SDK instantiated at module load — new Resend(config.resend.api_key) at line 7, before runtime validation at line 29.
3. MEDIUM: Synchronous fs.readFileSync for attachments — blocks event loop.
helper/calculateDiscount.ts¶
Purpose: Calculates price after applying percentage or fixed discount. Lines of Code: 26 File Type: Business logic utility
What Future Contributors Must Know: Dead code — 0 imports anywhere. Safe to delete.
Exports:
- export default calculateDiscount({ originalPrice, discountType, discountValue }): { originalPrice, discountPrice, amountPaid } — Discount calculation
Dependencies: None
Used By: 0 files
Known Issues: 1. HIGH: Entirely dead code — 0 consumers. Safe to delete.
helper/getTimeDifferenceLabel.ts¶
Purpose: Returns human-readable time difference string ("5 minutes ago", "2 hours ago", "3 days ago"). Lines of Code: 24 File Type: Formatting utility
Exports:
- export function getTimeDifferenceLabel(dateString: string): string — Time difference formatter
Dependencies: None
Used By: Analytic service (dashboard display)
Known Issues:
1. MEDIUM: Pluralization bug — When diff rounds to 0 minutes, displays "1 minutes" instead of "1 minute". The display value is overridden to 1 but pluralization check uses the original minutes variable (which is 0).
helper/httpLogger.ts¶
Purpose: Express-winston HTTP request logger middleware with response time tracking. Lines of Code: 43 File Type: Logging middleware
What Future Contributors Must Know: Logs the ENTIRE req.user object (including password hashes, 2FA secrets) to the log transport. The _startAt property must be set by upstream middleware or process.hrtime() crashes.
Exports:
- export const httpLogger — Express middleware instance (express-winston)
Dependencies:
- express-winston — Express logging integration
- ../app/middlewares/logger — Winston logger instance
Used By: app.ts (mounted globally)
Known Issues:
1. HIGH: Full req.user object logged — Password hashes, 2FA secrets, and other sensitive fields leak to log files/transport.
2. HIGH: req._startAt may be undefined — process.hrtime(undefined) throws TypeError, crashing the response.
3. MEDIUM: baseMsg set to empty string — Log messages contain no meaningful text.
helper/paginationHelper.ts¶
Purpose: Pagination parameter calculator (page/limit/skip/sort). Lines of Code: 37 File Type: Query utility
What Future Contributors Must Know: Dead code — 0 imports. Superseded by parsePaginationQuery.ts.
Exports:
- export const paginationHelper = { calculatePagination(options): IOptionsResult } — Pagination calculator
Dependencies: None
Used By: 0 files
Known Issues:
1. HIGH: Entirely dead code — Superseded by parsePaginationQuery.ts. Safe to delete.
helper/parseBoolean.ts¶
Purpose: Robust boolean parser with null-safety, case-insensitivity, and trimming. Lines of Code: 14 File Type: Parsing utility
What Future Contributors Must Know: The BEST-DESIGNED helper in the folder. Has JSDoc, handles null/undefined, trims whitespace, case-insensitive comparison. 13 consumers. Use this instead of stringToBoolean.ts.
Exports:
- export const parseBoolean = (value: string | boolean | null | undefined): boolean — Robust boolean parser
Dependencies: None
Used By: 13 service files (soft-delete/filter logic across many modules)
Known Issues: None — this file is well-implemented.
helper/parsePaginationQuery.ts¶
Purpose: Extracts pagination parameters (page, limit, searchTerm) from Express query object. Lines of Code: 15 File Type: Query utility
What Future Contributors Must Know: Used by 20 service files — second-highest consumer count in helper/. No upper bound on limit parameter.
Exports:
- export const getPaginationParams = (query: Record<string, any>): PaginationParams — Extracts page, limit, searchTerm
Dependencies: None
Used By: 20 service files (every paginated endpoint)
Known Issues:
1. MEDIUM: No upper bound on limit — Client can pass ?limit=999999 (DoS risk). No minimum bound either.
helper/stringToBoolean.ts¶
Purpose: Strict boolean string parser (case-sensitive, no trimming). Lines of Code: 4 File Type: Parsing utility
What Future Contributors Must Know: Inferior duplicate of parseBoolean.ts. Only 1 consumer. Should be replaced.
Exports:
- export function stringToBoolean(value: string): boolean — Strict === 'true' check
Dependencies: None
Used By: 1 file
Known Issues:
1. MEDIUM: Duplicate of parseBoolean.ts — Case-sensitive, no null-safety, no trimming. Inconsistent behavior with the 13-consumer version.
helper/deleteFile.ts¶
Purpose: Deletes a file from the local filesystem by relative path. Lines of Code: 20 File Type: File I/O utility
What Future Contributors Must Know: Path traversal vulnerability — relativeImagePath is joined with process.cwd() without sanitization. A ../../etc/passwd value would escape the project directory.
Exports:
- export default deleteFileByPathName(relativeImagePath: string): void — Deletes file synchronously
Dependencies:
- fs, path — Node.js built-ins (CommonJS require)
Used By: 4 files: blog.service, generic-page.service, user.service, LocalSotrage.ts
Known Issues:
1. CRITICAL: Path traversal — No sanitization of relativeImagePath. Can delete arbitrary files.
2. LOW: Uses CommonJS require — Inconsistent with ES imports used elsewhere.
helper/fileUploader.ts¶
Purpose: Multer disk storage + Cloudinary upload utility. Lines of Code: 43 File Type: File upload utility
What Future Contributors Must Know: DEAD CODE with LEAKED CREDENTIALS. Contains hardcoded Cloudinary API key and secret in plaintext. 0 imports anywhere. Delete immediately and rotate the Cloudinary credentials.
Exports:
- export const fileUploader = { upload, uploadToCloudinary } — Multer instance + Cloudinary uploader
Dependencies:
- multer, path, fs, cloudinary — File handling
Used By: 0 files
Known Issues:
1. CRITICAL: Hardcoded Cloudinary credentials — api_key: '173484379744282', api_secret: 'eHKsVTxIOLl5oaO_BHxBQWAK3GA' at lines 8-12.
2. CRITICAL: Dead code — 0 consumers. Delete and rotate credentials.
3. HIGH: fs.unlinkSync before error check — Original file deleted even on upload failure (data loss).
helper/handleFileUpload.ts¶
Purpose: Main file upload orchestrator that routes to local storage, Cloudinary, or S3 based on config. Lines of Code: 55 File Type: File upload orchestrator
What Future Contributors Must Know: The local branch constructs a path but does NOT write the file — callers must separately call saveBufferToFile. This is mitigated architecturally (4 controllers do call it), but the function itself is incomplete. 5 consumers.
Exports:
- export interface IReturnedFileResponse { imageUrl: string | string[] | undefined; fileName: string | null }
- export async function handleFileUpload(payload: IPayload): Promise<IReturnedFileResponse> — Routes to storage backend
Dependencies:
- http-status, ../app/errors/ApiError — Error handling
- ../config — Storage config
- ./cloud/s3 — S3 upload
- ./uploadImage — Cloudinary upload
Used By: 5 files: Blog, GenericPage, User, Setting controllers + Image service
Known Issues:
1. CRITICAL: Path traversal in local branch — subFolder used unsanitized in path construction.
2. HIGH: Local branch doesn't save file — Constructs path but no fs.writeFile. Callers must handle separately.
3. MEDIUM: Zero file type validation — Any file type accepted.
helper/saveBufferToFile.ts¶
Purpose: Writes a buffer to the filesystem with directory creation. Lines of Code: 37 File Type: File I/O utility
What Future Contributors Must Know: LocalSotrage.ts calls this with swapped arguments — folder name as fileName and filename as subFolder, resulting in incorrect file paths. 6 consumers.
Exports:
- export async function saveBufferToFile(buffer: Buffer, fileName: string, subFolder?: string): Promise<string> — Writes buffer, returns path
Dependencies:
- path, fs/promises — File system
Used By: 6 files: Blog, GenericPage, User, Product, Setting controllers + LocalSotrage.ts
Known Issues:
1. CRITICAL: Path traversal — Neither fileName nor subFolder sanitized.
2. HIGH: Argument order mismatch with LocalSotrage.ts — Caller passes (buffer, folderName, fileName) but signature is (buffer, fileName, subFolder).
3. MEDIUM: JSDoc mismatch — Doc says originalName but param is fileName.
helper/transformImageUrl.ts¶
Purpose: Prepends API base URL to local upload paths. Lines of Code: 13 File Type: URL utility
Exports:
- export const transformImageUrl = (image: string | undefined | null, baseUrl?: string): string | null — Transforms upload paths to full URLs
Dependencies:
- ../config — API base URL
Used By: 5 files: Blog, Product, Analytic, GenericPage, Public services
Known Issues:
1. LOW: baseUrl parameter is dead code — Accepted but never used in function body. Callers pass it, function ignores it.
2. LOW: Redundant null check — !image and !Boolean(image) are logically identical.
helper/uploadImage.ts¶
Purpose: Storage router for image uploads (currently only Cloudinary implemented). Lines of Code: 25 File Type: File upload router
Exports:
- export const uploadImage = async (storage: string, file: File | File[]): Promise<string[] | string> — Routes to storage backend
Dependencies:
- ./uploadToCloudinary — Cloudinary upload
Used By: 3 files: handleFileUpload.ts, Image service, Product controller
Known Issues: 1. MEDIUM: S3 case commented out — Only Cloudinary works; S3 branch is dead code within this file.
helper/uploadToCloudinary.ts¶
Purpose: Uploads buffer to Cloudinary with WebP conversion. Lines of Code: 25 File Type: Cloud upload utility
Exports:
- export default uploadToCloudinary(buffer: Buffer, filename: string): Promise<string> — Returns secure_url
Dependencies:
- ../config/cloudinary — Configured Cloudinary instance
- ../config/cloudinary — configureCloudinary (imported but never called)
Used By: 1 file: uploadImage.ts
Known Issues:
1. MEDIUM: Hardcoded image/jpeg MIME type — Data URI always uses image/jpeg regardless of actual file type.
2. LOW: configureCloudinary imported but never called — Credential validation bypassed.
helper/cloud/s3.ts¶
Purpose: AWS S3 upload utility using @aws-sdk/client-s3.
Lines of Code: 79
File Type: Cloud upload utility
Exports:
- export { uploadToS3 } — async (options: UploadToS3Options): Promise<UploadToS3Result> — Uploads buffer to S3
Dependencies:
- @aws-sdk/client-s3 — S3 SDK (PutObjectCommand, DeleteObjectCommand)
- ../../config — AWS credentials
Used By: 1 file: handleFileUpload.ts
Known Issues:
1. MEDIUM: S3 client recreated on every upload — createS3Client() called per invocation, no caching.
2. LOW: DeleteObjectCommand imported but never used — Dead import.
3. LOW: Location URL manually constructed — Could break with non-standard S3 endpoints.
config/index.ts¶
Purpose: Central configuration loader. Reads 55+ environment variables and exports a typed IConfig object. The hub of the entire application — 40+ direct consumers.
Lines of Code: 112
File Type: Configuration
What Future Contributors Must Know: This is the most-imported file in the entire codebase (40+ consumers). Only 6 of 55+ env vars are validated as required (JWT_SECRET, REFRESH_TOKEN_SECRET, DB_URL, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, SESSION_SECRET). All others silently undefined if missing. Contains 4 typos baked into the type system.
Exports:
- export default config — IConfig object with all application configuration
Dependencies:
- dotenv — Environment variable loading
- path — Path resolution
Used By: 40+ files across the entire codebase
Known Issues:
1. CRITICAL: Only 6 of 55+ env vars validated — 49 env vars silently undefined if missing.
2. HIGH: Admin credentials on config object — config.admin.email and config.admin.password accessible from any module.
3. MEDIUM: 4 typos in type system — nod_env (for NODE_ENV), sesseion_secret (for SESSION_SECRET), SSL_VALIDATIOIN_API, "SASS Template" (for "SaaS Template").
config/AppConfig.ts¶
Purpose: Singleton class wrapper around the exact same config loading logic as index.ts.
Lines of Code: 150
File Type: Configuration (DEAD CODE)
What Future Contributors Must Know: Character-for-character duplicate of config/index.ts wrapped in a singleton class. The loadConfig() method (lines 40-128) is identical to index.ts lines 23-109. 0 meaningful consumers. Safe to delete.
Exports:
- export class AppConfig — Singleton with getInstance(), value, isProduction, isDevelopment
- export default AppConfig.getInstance().value — Same config object as config/index.ts
Dependencies:
- dotenv, path, IConfig — Same as index.ts
Used By: 0 files (dead code)
Known Issues:
1. CRITICAL: 150 LOC duplicate — Identical to config/index.ts. Delete.
config/cloudinary.ts¶
Purpose: Configures Cloudinary SDK with credentials from environment. Lines of Code: 25 File Type: Cloud service config
Exports:
- export const configureCloudinary = () => cloudinary.ConfigOptions — Validates and configures
- export default cloudinary — Pre-configured v2 instance
Dependencies:
- cloudinary — Cloudinary SDK
- ./ (config/index.ts) — Credentials
- ../app/errors/ApiError, http-status — Error handling
Used By: 1 file: helper/uploadToCloudinary.ts
Known Issues:
1. MEDIUM: Double configuration — Module-level cloudinary.config() (lines 6-10) then configureCloudinary() does the same call again (lines 16-20). The named export adds validation but the default export already configures without validation.
config/dbConfig.ts¶
Purpose: TypeORM DataSource factory with lazy singleton pattern. Lines of Code: 21 File Type: Database configuration
Exports:
- export const getAppDataSource = (): DataSource — Lazy singleton DataSource factory
Dependencies:
- typeorm — DataSource
- ./ (config/index.ts) — DB URL, nod_env
- path — Entity glob resolution
Used By: 4 files: db/connection.ts, shared/getDbRepository.ts, shared/getDataSource.ts, app/builder/getQueryBuilder.ts
Known Issues:
1. CRITICAL: synchronize: true controlled by typo key nod_env — config.nod_env === 'development' works because nod_env maps to NODE_ENV, but if the typo is ever "fixed" without updating dbConfig, production databases get synchronize: true (schema auto-modification).
config/getLemonsqueezy.ts¶
Purpose: LemonSqueezy SDK initialization AND checkout session creation (config + business logic mixed). Lines of Code: 117 File Type: Payment config + business logic
Exports:
- export const getLemonsqueezy = () => void — Initializes SDK
- export async function createSubscriptionCheckout({ variantId, email, userId, planId }): Promise<string> — Creates checkout session
Dependencies:
- @lemonsqueezy/lemonsqueezy.js — LemonSqueezy SDK
- ./ (config/index.ts) — API key, store ID
- ../app/errors/ApiError, http-status — Error handling
Used By: 2 files: payment.service.ts, payment.route.ts
Known Issues:
1. CRITICAL: Hardcoded http://localhost:5500 redirect URL — Line 107. Production blocker.
2. HIGH: custom.user_id set to email instead of userId — Line 98. Webhook data mismatch.
3. MEDIUM: Business logic in config file — createSubscriptionCheckout() belongs in a service.
config/getStripe.ts¶
Purpose: Async lazy Stripe instance with env-only + DB fallback initialization. Lines of Code: 37 File Type: Payment config
What Future Contributors Must Know: This is the async alternative to stripe.ts. Falls back to loading the Stripe key from the database (encrypted via helper/encryption.ts). 7 consumers import this. Some modules import BOTH stripe.ts and getStripe.ts.
Exports:
- export const getStripe = async (): Promise<Stripe | null> — Async lazy singleton with DB fallback
Dependencies:
- stripe — Stripe SDK
- ./ (config/index.ts) — Stripe secret from env
- ../app/modules/v1/stripe.service — DB lookup for Stripe key
- ../helper/encryption — Decrypts DB-stored Stripe key
Used By: 7 files: StripeService, requireStripe, cupon.service, webhook.controller, payment.service, payment.controller, stripe.service
Known Issues:
1. CRITICAL: Dual Stripe initialization — Coexists with stripe.ts (sync). Some modules import both, potentially creating 2 different Stripe instances with different API keys.
2. MEDIUM: "SASS Template" typo — appInfo.name should be "SaaS Template".
config/stripe.ts¶
Purpose: Synchronous Stripe instance from environment variable only. Lines of Code: 15 File Type: Payment config
Exports:
- export default stripe — Stripe | null — Synchronous instance
Dependencies:
- stripe — Stripe SDK
- ./ (config/index.ts) — Stripe secret
Used By: 6 files: requireStripe, cupon.service, webhook.controller, payment.service, payment.controller, stripe.service
Known Issues:
1. CRITICAL: Part of dual Stripe initialization — See getStripe.ts above.
2. MEDIUM: "SASS Template" typo — Same as getStripe.ts.
config/passport.ts¶
Purpose: Passport.js Google OAuth strategy configuration with user creation/linking. Lines of Code: 102 File Type: Auth config
What Future Contributors Must Know: Contains the invite-only account takeover vulnerability. Google OAuth can claim credential-based invite-only users without password verification. Includes withDeleted: true in user lookup (finds soft-deleted users).
Exports:
- export default passport — Configured passport instance
Dependencies:
- passport, passport-google-oauth20 — Auth libraries
- ./ (config/index.ts) — OAuth credentials
- ../entity/User — User entity
- ../shared/getDbRepository — DB access
- ../app/modules/v1/Role/role.service — Role assignment
Used By: 2 files: app.ts, google-oauth.controller.ts
Known Issues:
1. HIGH: Invite-only account takeover — Google OAuth can claim credential-based invite-only users' accounts. The condition provider === CREDENTIAL && inviteOnly === false blocks only non-invited credential users; invited credential users fall through.
2. MEDIUM: withDeleted: true in user lookup — Soft-deleted users found and get error message "This account is removed" instead of generic "not found".
db/connection.ts¶
Purpose: Singleton database connection manager using global.appDataSource for serverless/hot-reload compatibility.
Lines of Code: 27
File Type: Database utility
Exports:
- export const connectToDB = async (): Promise<DataSource> — Lazy init + connect
Dependencies:
- typeorm — DataSource
- ../config/dbConfig — getAppDataSource() factory
Used By: 2 files: server.ts, seed/seed.ts
Known Issues:
1. HIGH: Race condition — No mutex; concurrent .initialize() calls possible during startup.
2. MEDIUM: Dual singleton — global.appDataSource vs dbConfig.ts module-level singleton. Other consumers (getDbRepository, getDataSource, getQueryBuilder) bypass connectToDB() entirely.
3. MEDIUM: Credential leak risk — console.error(error) may dump DB URL with password.
Architecture & Design Patterns¶
Code Organization¶
The 33 files are organized into 3 directories by concern:
- helper/ (24 files): Pure utilities and service-layer helpers. No consistent internal organization — auth, file upload, email, formatting, and parsing utilities are all at the same level.
- config/ (8 files): Environment configuration loading + external service initialization. Mixes pure config (index.ts, dbConfig.ts) with service initialization (passport.ts, getLemonsqueezy.ts).
- db/ (1 file): Database connection lifecycle.
Design Patterns¶
- Hub-and-spoke config:
config/index.tsis the central hub (40+ consumers). All other config files and most helpers import from it. - Factory routing:
handleFileUploadandemail.tsboth use switch/branch to route to different storage/email backends. Not formalized as a factory pattern. - Lazy singleton:
getStripe.ts,dbConfig.ts,db/connection.tsall use lazy singleton patterns (but implemented differently — module-level variable, global variable, or class singleton). - Wrapper pattern:
checkPermissionAndThrowwrapshasPermissionadding error throwing.
Error Handling Philosophy¶
Inconsistent. Three approaches coexist:
1. Throw ApiError: Used by checkPermissionAndThrow, handleFileUpload, email.ts, cloudinary.ts
2. Silent return false/null: Used by hasPermission (fail-safe deny), getStripe (graceful degradation)
3. No error handling: Used by hashedPassword, jwtHelpers, encryption (errors propagate to callers)
Testing Strategy¶
0% coverage. No test files exist for any of the 33 files. No test framework configured.
Data Flow¶
Entry Points¶
- Permission check: Controller →
checkPermissionAndThrow(path)→hasPermission→ cache/DB → boolean - File upload: Controller →
handleFileUpload(payload)→uploadImage/uploadToS3→ cloud service → URL string - Email send: Event handler →
sendEmail(payload)→transporter.sendMail/resendEmail→ SMTP/API - Token create: Auth service →
jwtHelpers.generateToken(payload, secret, expiry)→ JWT string - Encrypt: AppConfig service →
encrypt(plaintext)→ base64 ciphertext - DB connect:
server.ts→connectToDB()→getAppDataSource()→DataSource.initialize()→ PostgreSQL
Data Exit Points¶
- Permission result:
booleanreturned to service layer - Upload URL:
string(Cloudinary secure_url or S3 location) returned to controller - Email: Side effect (SMTP/API call), no return value
- JWT:
stringtoken returned to auth flow → sent to client via cookie - Ciphertext:
stringstored inAppConfigDB table - DB connection:
DataSourceinstance used globally for all queries
Integration Points¶
External Services¶
- Cloudinary: Image upload + WebP conversion (
uploadToCloudinary.ts) - AWS S3: Object storage (
cloud/s3.ts) - Stripe: Payment processing (
stripe.ts,getStripe.ts) - LemonSqueezy: Alternative payment processing (
getLemonsqueezy.ts) - SMTP server: Email via nodemailer (
transporterMail.ts) - Resend API: Email via Resend SDK (
email/resend.ts) - Google OAuth: Authentication via passport (
passport.ts) - PostgreSQL: Database via TypeORM (
dbConfig.ts,connection.ts)
Shared State¶
global.appDataSource— TypeORM DataSource singleton (survives hot reloads)stripeInstance(module-level ingetStripe.ts) — Cached Stripe instance- Module-level
KEYinencryption.ts— AES key loaded once at import - Module-level
transporterintransporterMail.ts— SMTP transport created at import - Module-level
resendinemail/resend.ts— Resend client created at import
Dependency Graph¶
config/index.ts (HUB - 40+ consumers)
├── config/cloudinary.ts
│ └── helper/uploadToCloudinary.ts
│ └── helper/uploadImage.ts
│ └── helper/handleFileUpload.ts (also → cloud/s3.ts)
├── config/dbConfig.ts
│ └── db/connection.ts
├── config/stripe.ts
├── config/getStripe.ts (also → helper/encryption.ts)
├── config/getLemonsqueezy.ts
├── config/passport.ts
├── helper/jwtHelpers.ts
├── helper/transformImageUrl.ts
├── helper/transporterMail.ts
│ └── helper/email.ts (also → helper/email/resend.ts)
└── helper/cloud/s3.ts
STANDALONE (no in-scope dependencies):
helper/checkPermissionAndThrow.ts → helper/hasPermission.ts (→ shared/ modules, not in scope)
helper/hashedPassword.ts
helper/generateOtp.ts
helper/encryption.ts
helper/parseBoolean.ts
helper/parsePaginationQuery.ts
helper/deleteFile.ts
helper/saveBufferToFile.ts
helper/stringToBoolean.ts
helper/getTimeDifferenceLabel.ts
helper/httpLogger.ts
helper/calculateDiscount.ts (DEAD)
helper/paginationHelper.ts (DEAD)
helper/fileUploader.ts (DEAD)
config/AppConfig.ts (DEAD)
Circular Dependencies¶
None detected. The dependency graph is a clean DAG.
Related Code & Reuse Opportunities¶
TRIPLE Permission Implementation (Critical Consolidation Target)¶
Three separate permission-checking systems exist with incompatible logic:
| System | Location | Logic | Consumers |
|---|---|---|---|
helper/hasPermission.ts |
Service layer | Cache → normalizer → DB, checks all userRoles | 24 (via wrapper) |
middlewares/hasPermission.ts |
Legacy middleware | Direct DB query, checks single role only | 1 (payment.route) |
middlewares/permissionMiddleware.ts |
New middleware | Pre-loaded Set from contextMiddleware, no normalization | 1 (user.routes) |
Recommendation: Consolidate to helper/hasPermission.ts as the single source of truth. The middleware versions should call the helper.
DUAL Email Infrastructure¶
Two completely separate email pipelines coexist:
| Pipeline | Config Key | Providers | Used By |
|---|---|---|---|
helper/email.ts |
config.mail.type |
nodemailer, Resend | 4 event handlers |
shared/email/EmailFactory |
config.mail.service |
nodemailer, Resend, Brevo | 1 file (newer code) |
Recommendation: Migrate all consumers to shared/email/EmailFactory and delete helper/email.ts, helper/transporterMail.ts, helper/email/resend.ts.
Missing Abstractions¶
- File upload controller pattern: 4 controllers duplicate 30-line upload blocks. Extract to shared middleware.
- Soft-delete query helper: Multiple services manually add
isDeleted: falsefilters. - Sort alias prefixer: Multiple services inline
entity.${sortBy}prefixing. - Super admin bypass: 4 places check
user.role?.name === 'super_admin'inline instead of usingisCurrentUserSuperAdmin().
Contributor Checklist¶
- Risks & Gotchas:
config/index.tshas 40+ consumers — any key rename breaks the entire appcheckPermissionAndThrowhas 24 consumers — changing error format breaks all frontend error handlingencryption.tscrash at import time ifMASTER_KEYis unset — will break test environmentsfileUploader.tshas LEAKED Cloudinary credentials that need rotation regardless of code changes- Permission normalization is silently disabled — re-enabling without testing could break existing checks
saveBufferToFilearg swap withLocalSotrage.tscreates incorrect file paths silently-
Dual Stripe instances may use different API keys depending on import path
-
Pre-change Verification Steps:
- Check consumer count before modifying any export signature
- Verify env vars are set in
.env.examplebefore adding new config dependencies - Test permission checks with both route-format and dot-notation strings
- Verify file upload works for local, Cloudinary, and S3 storage backends
-
Test email sending with both nodemailer and Resend providers
-
Suggested Tests Before PR:
- Permission check round-trip (grant → check → deny → check)
- JWT generate → verify → decode round-trip for both access and refresh tokens
- Encrypt → decrypt round-trip with various string lengths
- File upload to each storage backend with various file types
- Email dispatch via each configured provider
- Pagination with edge cases (limit=0, limit=MAX, negative page)
- Boolean parsing with all input types (string, boolean, null, undefined)
- Path traversal prevention (../../../etc/passwd in file paths)
- Config loading with missing required env vars (should throw at startup)
- DB connection race condition (concurrent connectToDB calls)
Modification Guidance¶
To Add New Functionality¶
- New helper: Create in
helper/at root level. Use named exports (not default). Add JSDoc. FollowparseBoolean.tsas the reference implementation (clean, typed, documented). - New config value: Add to
config/index.tsIConfig shape ANDAppConfig.ts(if not deleted) ANDIConfiginterface. Add torequiredEnvarray if critical. - New storage backend: Add case to
handleFileUpload.tsswitch ANDuploadImage.tsswitch. Followcloud/s3.tspattern.
To Modify Existing Functionality¶
- Changing permission logic: Test with ALL 24 consumer services. Verify both route-format and dot-notation inputs. Check cache invalidation.
- Changing error status codes: Verify frontend Axios interceptors handle the new code.
checkPermissionAndThrowuses 400 — changing to 403 affects 24 modules. - Changing encryption: All existing encrypted data in DB becomes unreadable if algorithm/format changes. Migration required.
To Remove/Deprecate¶
- Delete
fileUploader.tsimmediately (dead + leaked credentials) - Delete
calculateDiscount.ts(dead, 0 consumers) - Delete
paginationHelper.ts(dead, superseded) - Delete
config/AppConfig.ts(150 LOC duplicate) - Delete
stringToBoolean.tsafter migrating 1 consumer toparseBoolean.ts - Delete
helper/email/resend.tsafter verifying no dynamic imports
Testing Checklist for Changes¶
- All 24
checkPermissionAndThrowconsumers still work - JWT tokens from before the change still verify (backward compat)
- Encrypted config values from before the change still decrypt
- File uploads succeed for all 3 storage backends
- Emails send via both nodemailer and Resend
- Pagination works with edge case inputs
- No new
console.logstatements in production paths - No path traversal possible in file operations
- All 6 required env vars still validated at startup
- Stripe instance is consistent regardless of import path
Generated by document-project workflow (deep-dive mode)
Base Documentation: docs/index.md
Scan Date: 2026-03-29
Analysis Mode: Exhaustive — 33 files, ~1,460 LOC, 113 issues (20C, 28H, 39M, 26L)
5 parallel subagents + re-verification pass after git pull