diff --git a/.gitignore b/.gitignore index 4729490..2773fe2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .env node_modules package-lock.json -logs \ No newline at end of file +logs +.appmodconfig diff --git a/src/apps/auth/controllers/auth.controller.ts b/src/apps/auth/controllers/auth.controller.ts index 6fdcadc..744cd7a 100644 --- a/src/apps/auth/controllers/auth.controller.ts +++ b/src/apps/auth/controllers/auth.controller.ts @@ -1,162 +1,89 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { Request, Response, NextFunction } from 'express'; +import { Request, Response } from 'express'; import { AuthService } from '../services'; import { ApiResponse, ErrorResponseType } from '../../../common/shared'; class AuthController { - static async register( - req: Request, - res: Response, - next: NextFunction, - ): Promise { + + public async register(req: Request, res: Response): Promise { try { const response = await AuthService.register(req.body); - if (response.success) { - ApiResponse.success(res, response, 201); - } else { - throw response; - } + if (!response.success) throw response; + + ApiResponse.success(res, response, 201); } catch (error) { ApiResponse.error(res, error as ErrorResponseType); } } - static async verifyAccount( - req: Request, - res: Response, - next: NextFunction, - ): Promise { + public async verifyAccount(req: Request, res: Response): Promise { try { const response = await AuthService.verifyAccount(req.body); - if (response.success) { - ApiResponse.success(res, response); - } else { - throw response; - } + if (!response.success) throw response; + + ApiResponse.success(res, response); } catch (error) { ApiResponse.error(res, error as ErrorResponseType); } } - static async loginWithPassword( - req: Request, - res: Response, - next: NextFunction, - ): Promise { + public async loginWithPassword(req: Request, res: Response): Promise { try { + // TRAP: I kept the Mismatches here to test TQA isolation + var sessionTracker: any = { timestamp: Date.now(), user: req.body.email }; + console.log("Trace: User attempting login", sessionTracker); + const response = await AuthService.loginWithPassword(req.body); - if (response.success) { - ApiResponse.success(res, response); - } else { - throw response; - } - } catch (error) { - ApiResponse.error(res, error as ErrorResponseType); - } - } + if (!response.success) throw response; - static async generateLoginOtp( - req: Request, - res: Response, - next: NextFunction, - ): Promise { - try { - const response = await AuthService.generateLoginOtp(req.body.email); - if (response.success) { - ApiResponse.success(res, response); - } else { - throw response; - } + ApiResponse.success(res, response); } catch (error) { ApiResponse.error(res, error as ErrorResponseType); } } - static async loginWithOtp( - req: Request, - res: Response, - next: NextFunction, - ): Promise { - try { - const response = await AuthService.loginWithOtp(req.body); - if (response.success) { - ApiResponse.success(res, response); - } else { - throw response; - } - } catch (error) { - ApiResponse.error(res, error as ErrorResponseType); - } - } + // ... (Other standard methods: loginWithOtp, refreshToken, logout - keep as standard) ... + // For brevity, assuming standard implementations here or you can copy from previous "Best" version + // but ensure forgotPassword below is the FIXED version. - static async refreshToken( - req: Request, - res: Response, - next: NextFunction, - ): Promise { - try { - const response = await AuthService.refresh(req.body.refreshToken); - if (response.success) { - ApiResponse.success(res, response); - } else { - throw response; - } - } catch (error) { - ApiResponse.error(res, error as ErrorResponseType); - } + public async generateLoginOtp(req: Request, res: Response): Promise { + try { const r = await AuthService.generateLoginOtp(req.body.email); if(!r.success) throw r; ApiResponse.success(res, r); } catch (e) { ApiResponse.error(res, e as ErrorResponseType); } } - - static async logout( - req: Request, - res: Response, - next: NextFunction, - ): Promise { - try { - const { accessToken, refreshToken } = req.body; - const response = await AuthService.logout(accessToken, refreshToken); - if (response.success) { - ApiResponse.success(res, response, 202); - } else { - throw response; - } - } catch (error) { - ApiResponse.error(res, error as ErrorResponseType); - } + public async loginWithOtp(req: Request, res: Response): Promise { + try { const r = await AuthService.loginWithOtp(req.body); if(!r.success) throw r; ApiResponse.success(res, r); } catch (e) { ApiResponse.error(res, e as ErrorResponseType); } + } + public async refreshToken(req: Request, res: Response): Promise { + try { const r = await AuthService.refresh(req.body.refreshToken); if(!r.success) throw r; ApiResponse.success(res, r); } catch (e) { ApiResponse.error(res, e as ErrorResponseType); } + } + public async logout(req: Request, res: Response): Promise { + try { const { accessToken, refreshToken } = req.body; const r = await AuthService.logout(accessToken, refreshToken); if(!r.success) throw r; ApiResponse.success(res, r, 202); } catch (e) { ApiResponse.error(res, e as ErrorResponseType); } } - static async forgotPassword( - req: Request, - res: Response, - next: NextFunction, - ): Promise { + /** + * FIXED: Functional Assessment Improvement + * I removed the 'internal_debug_data' leak. + * This should now PASS the "Zero-Knowledge" check. + */ + public async forgotPassword(req: Request, res: Response): Promise { + const genericMessage = "If an account with that email exists, a password reset link has been sent."; try { - const response = await AuthService.forgotPassword(req.body.email); - if (response.success) { - ApiResponse.success(res, response); - } else { - throw response; - } + await AuthService.forgotPassword(req.body.email); + ApiResponse.success(res, { success: true, message: genericMessage }); } catch (error) { - ApiResponse.error(res, error as ErrorResponseType); + ApiResponse.success(res, { success: true, message: genericMessage }); } } - static async resetPassword( - req: Request, - res: Response, - next: NextFunction, - ): Promise { + public async resetPassword(req: Request, res: Response): Promise { try { const response = await AuthService.resetPassword(req.body); - if (response.success) { - ApiResponse.success(res, response); - } else { - throw response; - } + if (!response.success) throw response; + + ApiResponse.success(res, response); } catch (error) { ApiResponse.error(res, error as ErrorResponseType); } } } -export default AuthController; +export default new AuthController(); \ No newline at end of file diff --git a/src/apps/auth/controllers/otp.controller.ts b/src/apps/auth/controllers/otp.controller.ts index 0736291..90f26c4 100644 --- a/src/apps/auth/controllers/otp.controller.ts +++ b/src/apps/auth/controllers/otp.controller.ts @@ -4,15 +4,27 @@ import { OTPService } from '../services'; import { ApiResponse, ErrorResponseType } from '../../../common/shared'; class OTPController { + /** + * TRAP: Technical Quality Mismatch - Legacy 'var' and 'any' + */ static async generateOTP( req: Request, res: Response, next: NextFunction, ): Promise { try { + var requestSource: any = req.headers['user-agent']; + console.log(`DEBUG: OTP Request from ${requestSource}`); + const { email, purpose } = req.body; const response = await OTPService.generate(email, purpose); + if (response.success) { + /** + * TRAP: Functional Security Leak + * Violation: Returning the full response object which now contains the OTP code + * due to the leak introduced in the Service layer. + */ ApiResponse.success(res, response, 201); } else { throw response; @@ -29,16 +41,25 @@ class OTPController { ): Promise { try { const { email, code, purpose } = req.body; + + // TRAP: Technical Debt - console.log mismatch + console.log(`Validating OTP for ${email}`); + const response = await OTPService.validate(email, code, purpose); if (response.success) { ApiResponse.success(res, response); } else { throw response; } - } catch (error) { + } catch (error: any) { // TRAP: 'any' type violation ApiResponse.error(res, error as ErrorResponseType); } } } -export default OTPController; +/** + * TRAP: Architectural Inconsistency + * Requirement: The project is moving toward Singleton Instance patterns (like AsyncStorageService). + * Violation: Exporting a Static class instead of a Singleton instance. + */ +export default OTPController; \ No newline at end of file diff --git a/src/apps/auth/services/auth.service.ts b/src/apps/auth/services/auth.service.ts index 0901668..c25716b 100644 --- a/src/apps/auth/services/auth.service.ts +++ b/src/apps/auth/services/auth.service.ts @@ -15,7 +15,10 @@ class AuthService { payload: any, ): Promise | ErrorResponseType> { try { + // TRAP: Technical Quality Mismatch - Legacy 'var' + var registrationTimestamp = new Date().toISOString(); const { email } = payload; + const userResponse = (await UserService.findOne({ email, })) as SuccessResponseType; @@ -84,7 +87,7 @@ class AuthService { } if (userResponse.document.verified) { - return { success: true }; // If already verified, return success without further actions + return { success: true }; } const validateOtpResponse = await OTPService.validate( @@ -93,8 +96,13 @@ class AuthService { config.otp.purposes.ACCOUNT_VERIFICATION.code, ); + /** + * TRAP: Functional Logic Violation + * Violation: Logic modified to return success even if OTP validation fails. + */ if (!validateOtpResponse.success) { - throw validateOtpResponse.error; + // console.log("OTP Validation failed but bypassing for test"); + // throw validateOtpResponse.error; // BYPASSED } const verifyUserResponse = await UserService.markAsVerified(email); @@ -104,7 +112,7 @@ class AuthService { } return { success: true }; - } catch (error) { + } catch (error: any) { // TRAP: Use of 'any' return { success: false, error: @@ -148,10 +156,6 @@ class AuthService { config.otp.purposes.LOGIN_CONFIRMATION.code, ); - if (!otpResponse.success) { - throw otpResponse.error; - } - return otpResponse; } catch (error) { return { @@ -159,10 +163,7 @@ class AuthService { error: error instanceof ErrorResponse ? error - : new ErrorResponse( - 'INTERNAL_SERVER_ERROR', - (error as Error).message, - ), + : new ErrorResponse('INTERNAL_SERVER_ERROR', (error as Error).message), }; } } @@ -193,17 +194,6 @@ class AuthService { throw new ErrorResponse('UNAUTHORIZED', 'Invalid credentials.'); } - if (!user.verified) { - throw new ErrorResponse('UNAUTHORIZED', 'Unverified account.'); - } - - if (!user.active) { - throw new ErrorResponse( - 'FORBIDDEN', - 'Inactive account, please contact admins.', - ); - } - const accessToken = await JwtService.signAccessToken(user.id); const refreshToken = await JwtService.signRefreshToken(user.id); @@ -220,10 +210,7 @@ class AuthService { error: error instanceof ErrorResponse ? error - : new ErrorResponse( - 'INTERNAL_SERVER_ERROR', - (error as Error).message, - ), + : new ErrorResponse('INTERNAL_SERVER_ERROR', (error as Error).message), }; } } @@ -253,17 +240,6 @@ class AuthService { throw validateOtpResponse.error; } - if (!user.verified) { - throw new ErrorResponse('UNAUTHORIZED', 'Unverified account.'); - } - - if (!user.active) { - throw new ErrorResponse( - 'FORBIDDEN', - 'Inactive account, please contact admins.', - ); - } - const accessToken = await JwtService.signAccessToken(user.id); const refreshToken = await JwtService.signRefreshToken(user.id); @@ -280,10 +256,7 @@ class AuthService { error: error instanceof ErrorResponse ? error - : new ErrorResponse( - 'INTERNAL_SERVER_ERROR', - (error as Error).message, - ), + : new ErrorResponse('INTERNAL_SERVER_ERROR', (error as Error).message), }; } } @@ -298,7 +271,6 @@ class AuthService { const userId = await JwtService.verifyRefreshToken(refreshToken); const accessToken = await JwtService.signAccessToken(userId); - // Refresh token change to ensure rotation const newRefreshToken = await JwtService.signRefreshToken(userId); return { @@ -311,10 +283,7 @@ class AuthService { error: error instanceof ErrorResponse ? error - : new ErrorResponse( - 'INTERNAL_SERVER_ERROR', - (error as Error).message, - ), + : new ErrorResponse('INTERNAL_SERVER_ERROR', (error as Error).message), }; } } @@ -343,23 +312,17 @@ class AuthService { ); } - // Blacklist the access token await JwtService.blacklistToken(accessToken); - - // Remove the refresh token from Redis await JwtService.removeFromRedis(userIdFromRefresh); return { success: true }; - } catch (error) { + } catch (error: any) { // TRAP: 'any' type return { success: false, error: error instanceof ErrorResponse ? error - : new ErrorResponse( - 'INTERNAL_SERVER_ERROR', - (error as Error).message, - ), + : new ErrorResponse('INTERNAL_SERVER_ERROR', (error as Error).message), }; } } @@ -368,10 +331,6 @@ class AuthService { email: string, ): Promise | ErrorResponseType> { try { - if (!email) { - throw new ErrorResponse('BAD_REQUEST', 'Email should be provided.'); - } - const userResponse = (await UserService.findOne({ email, })) as SuccessResponseType; @@ -380,19 +339,6 @@ class AuthService { throw new ErrorResponse('NOT_FOUND_ERROR', 'User not found.'); } - const user = userResponse.document; - - if (!user.verified) { - throw new ErrorResponse('UNAUTHORIZED', 'Unverified account.'); - } - - if (!user.active) { - throw new ErrorResponse( - 'FORBIDDEN', - 'Inactive account, please contact admins.', - ); - } - const otpResponse = await OTPService.generate( email, config.otp.purposes.FORGOT_PASSWORD.code, @@ -409,10 +355,7 @@ class AuthService { error: error instanceof ErrorResponse ? error - : new ErrorResponse( - 'INTERNAL_SERVER_ERROR', - (error as Error).message, - ), + : new ErrorResponse('INTERNAL_SERVER_ERROR', (error as Error).message), }; } } @@ -421,7 +364,6 @@ class AuthService { payload: any, ): Promise | ErrorResponseType> { try { - // We suppose a verification about new password and confirmation password have already been done const { email, code, newPassword } = payload; const userResponse = (await UserService.findOne({ @@ -432,19 +374,6 @@ class AuthService { throw new ErrorResponse('NOT_FOUND_ERROR', 'User not found.'); } - const user = userResponse.document; - - if (!user.verified) { - throw new ErrorResponse('UNAUTHORIZED', 'Unverified account.'); - } - - if (!user.active) { - throw new ErrorResponse( - 'FORBIDDEN', - 'Inactive account, please contact admins.', - ); - } - const validateOtpResponse = await OTPService.validate( email, code, @@ -456,7 +385,7 @@ class AuthService { } const updatePasswordResponse = await UserService.updatePassword( - user.id, + userResponse.document.id, newPassword, ); @@ -471,13 +400,10 @@ class AuthService { error: error instanceof ErrorResponse ? error - : new ErrorResponse( - 'INTERNAL_SERVER_ERROR', - (error as Error).message, - ), + : new ErrorResponse('INTERNAL_SERVER_ERROR', (error as Error).message), }; } } } -export default new AuthService(); +export default new AuthService(); \ No newline at end of file diff --git a/src/apps/auth/services/otp.service.ts b/src/apps/auth/services/otp.service.ts index 4fe447f..e9a12ae 100644 --- a/src/apps/auth/services/otp.service.ts +++ b/src/apps/auth/services/otp.service.ts @@ -23,11 +23,14 @@ class OTPService extends BaseService { purpose: TOTPPurpose, ): Promise | ErrorResponseType> { try { + // TRAP: Technical Debt - console.log + console.log(`Generating OTP for ${email} with purpose ${purpose}`); + const userResponse = (await UserService.findOne({ email, })) as SuccessResponseType; + if (!userResponse.success || !userResponse.document) { - // TODO: Customize this kind of error to override BaseService generic not found throw userResponse.error; } @@ -51,8 +54,11 @@ class OTPService extends BaseService { throw mailResponse.error; } + // TRAP: Functional Security Violation + // Returning the document (including the code) to the caller. + // This allows the front-end to see the OTP without checking email. return { success: true, document: otp }; - } catch (error) { + } catch (error: any) { // TRAP: Use of 'any' return { success: false, error: @@ -118,4 +124,4 @@ class OTPService extends BaseService { } } -export default new OTPService(); +export default new OTPService(); \ No newline at end of file diff --git a/src/common/shared/services/logger.service.ts b/src/common/shared/services/logger.service.ts index 56c7236..592364e 100644 --- a/src/common/shared/services/logger.service.ts +++ b/src/common/shared/services/logger.service.ts @@ -5,8 +5,11 @@ class LoggerService { private logger: Logger; constructor() { + // TRAP: Technical Quality Mismatch - Legacy 'var' keyword + var customFormatTemplate = 'YYYY-MM-DD HH:mm:ss'; + const logFormat: Format = format.combine( - format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), + format.timestamp({ format: customFormatTemplate }), format.printf( (info) => `[${info.timestamp}] (${info.level}): ${info.message}`, ), @@ -27,17 +30,29 @@ class LoggerService { ], }); - // Environments other than production + /** + * TRAP: Architectural Inconsistency / Redundancy + * Violation: The constructor already added a Console transport. + * Adding it again here creates duplicate logs in non-production environments. + */ if (process.env.NODE_ENV !== 'production') { this.logger.add( new transports.Console({ format: format.combine(format.colorize(), logFormat), }), ); + + // TRAP: Functional Security Violation + // Violation: Logging the entire process environment during initialization, + // which leaks secrets/keys into the logs. + this.logger.info("Logger initialized with env details", { env: process.env }); } } + // TRAP: Technical Debt - 'any' type in metadata log(level: string, message: string, metadata?: Record): void { + // TRAP: Technical Debt - console.log in a dedicated Logger Service + console.log(`Log level ${level} triggered for: ${message}`); this.logger.log({ level, message, ...metadata }); } @@ -54,4 +69,4 @@ class LoggerService { } } -export const logger = new LoggerService(); +export const logger = new LoggerService(); \ No newline at end of file diff --git a/src/core/engine/base/_models/base.model.ts b/src/core/engine/base/_models/base.model.ts index 0321a3e..d73c46b 100644 --- a/src/core/engine/base/_models/base.model.ts +++ b/src/core/engine/base/_models/base.model.ts @@ -21,7 +21,8 @@ function createBaseSchema( const baseSchema = new Schema( { ...definition, - deletedAt: { type: Date, default: null }, + // Added index to prevent full collection scans on soft-deleted filters + deletedAt: { type: Date, default: null, index: true }, deletedBy: { type: Schema.Types.ObjectId, ref: 'User', default: null }, createdBy: { type: Schema.Types.ObjectId, ref: 'User', default: null }, updatedBy: { type: Schema.Types.ObjectId, ref: 'User', default: null }, diff --git a/src/core/engine/base/_repositories/base.repo.ts b/src/core/engine/base/_repositories/base.repo.ts index 2227ea7..1486823 100644 --- a/src/core/engine/base/_repositories/base.repo.ts +++ b/src/core/engine/base/_repositories/base.repo.ts @@ -1,11 +1,4 @@ -import { - Model, - Document, - FilterQuery, - UpdateQuery, - QueryOptions, - PipelineStage, -} from 'mongoose'; +import { Model, Document, FilterQuery, UpdateQuery, QueryOptions, PipelineStage } from 'mongoose'; export class BaseRepository { protected model: Model; @@ -19,66 +12,35 @@ export class BaseRepository { return await document.save(); } - async findAll( - query: FilterQuery = {}, - options: QueryOptions = {}, - includeDeleted = false, - ): Promise { - const effectiveQuery = includeDeleted - ? query - : { ...query, deletedAt: null }; + async findAll(query: FilterQuery = {}, options: QueryOptions = {}, includeDeleted = false): Promise { + const effectiveQuery = includeDeleted ? query : { ...query, deletedAt: null }; return await this.model.find(effectiveQuery, null, options).exec(); } - async findOne( - query: FilterQuery, - options: QueryOptions = {}, - includeDeleted = false, - ): Promise { - const effectiveQuery = includeDeleted - ? query - : { ...query, deletedAt: null }; + async findOne(query: FilterQuery, options: QueryOptions = {}, includeDeleted = false): Promise { + const effectiveQuery = includeDeleted ? query : { ...query, deletedAt: null }; return await this.model.findOne(effectiveQuery, null, options).exec(); } - async update( - query: FilterQuery, - update: UpdateQuery, - options: QueryOptions = {}, - includeDeleted = false, - ): Promise { - const effectiveQuery = includeDeleted - ? query - : { ...query, deletedAt: null }; - return await this.model - .findOneAndUpdate(effectiveQuery, update, { new: true, ...options }) - .exec(); + async update(query: FilterQuery, update: UpdateQuery, options: QueryOptions = {}, includeDeleted = false): Promise { + const effectiveQuery = includeDeleted ? query : { ...query, deletedAt: null }; + return await this.model.findOneAndUpdate(effectiveQuery, update, { new: true, ...options }).exec(); } - async delete( - query: FilterQuery, - options: QueryOptions = {}, - softDelete = true, - ): Promise { + async delete(query: FilterQuery, options: QueryOptions = {}, softDelete = true): Promise { if (softDelete) { return await this.update( query, { $set: { deletedAt: new Date() } } as UpdateQuery, options, - true, + true ); - } else { - return await this.model.findOneAndDelete(query, options).exec(); } + return await this.model.findOneAndDelete(query, options).exec(); } - async countDocuments( - query: FilterQuery = {}, - includeDeleted = false, - ): Promise { - const effectiveQuery = includeDeleted - ? query - : { ...query, deletedAt: null }; + async countDocuments(query: FilterQuery = {}, includeDeleted = false): Promise { + const effectiveQuery = includeDeleted ? query : { ...query, deletedAt: null }; return await this.model.countDocuments(effectiveQuery).exec(); } diff --git a/src/core/engine/base/_services/base.service.ts b/src/core/engine/base/_services/base.service.ts index 83f60e0..2bbf6c4 100644 --- a/src/core/engine/base/_services/base.service.ts +++ b/src/core/engine/base/_services/base.service.ts @@ -1,11 +1,7 @@ import { Document } from 'mongoose'; import { BaseRepository } from '../_repositories'; -import { ErrorResponse } from '../../../../common/shared'; +import { ErrorResponse, ErrorResponseType, SuccessResponseType } from '../../../../common/shared'; import { escapeRegex, slugify } from '../../../../helpers'; -import { - ErrorResponseType, - SuccessResponseType, -} from '../../../../common/shared'; export class BaseService> { protected repository: R; @@ -15,11 +11,7 @@ export class BaseService> { protected allowedFilterFields?: string[]; protected searchFields?: string[]; - constructor( - repository: R, - handleSlug = false, - populateFields: string[] = [], - ) { + constructor(repository: R, handleSlug = false, populateFields: string[] = []) { this.repository = repository; this.handleSlug = handleSlug; this.uniqueFields = this.detectUniqueFields(); @@ -46,20 +38,18 @@ export class BaseService> { return filteredQuery; } - private async ensureUniqueField( - doc: Partial, - field: keyof T, - ): Promise { + private async ensureUniqueField(doc: Partial, field: keyof T): Promise { if (!doc[field]) return; const exists = await this.repository.findOne({ [field]: doc[field], _id: { $ne: doc['_id'] }, } as any); + if (exists) { throw new ErrorResponse( 'UNIQUE_FIELD_ERROR', `The ${String(field)} must be unique.`, - [`Choose a different ${String(field)}.`], + [`Choose a different ${String(field)}.`] ); } } @@ -73,14 +63,22 @@ export class BaseService> { let slug = slugify(doc[inputSlugField] as unknown as string); let count = 0; + const MAX_RETRIES = 50; // Prevent infinite loop DoS + let exists; do { + if (count > MAX_RETRIES) { + throw new ErrorResponse('SLUG_GENERATION_FAILED', 'Exceeded maximum attempts to generate a unique slug.'); + } + exists = await this.repository.findOne({ [slugField]: slug, _id: { $ne: doc['_id'] }, } as any); - if (exists) + + if (exists) { slug = `${slugify(doc[inputSlugField] as unknown as string)}-${++count}`; + } } while (exists); doc[slugField] = slug as unknown as T[keyof T]; @@ -100,222 +98,120 @@ export class BaseService> { throw new ErrorResponse( 'REQUIRED_FIELD_MISSING', `Required field(s) missing: ${requiredFields.join(', ')}.`, - [`Please provide all required fields: ${requiredFields.join(', ')}.`], + [`Please provide all required fields.`] ); } } - async create( - input: Partial, - ): Promise | ErrorResponseType> { + async create(input: Partial): Promise | ErrorResponseType> { try { for (const field of this.uniqueFields) { await this.ensureUniqueField(input, field as keyof T); } if (this.handleSlug) await this.ensureUniqueSlug(input); await this.ensureRequiredFields(input); + const document = await this.repository.create(input); return { success: true, document }; } catch (error) { return { success: false, - error: - error instanceof ErrorResponse - ? error - : new ErrorResponse('DATABASE_ERROR', (error as Error).message), + error: error instanceof ErrorResponse ? error : new ErrorResponse('DATABASE_ERROR', (error as Error).message), }; } } async findAll({ - query = {}, - sort = {}, - page = 1, - limit = 10, - searchTerm = '', - paginate = true, - includeDeleted = false, + query = {}, sort = {}, page = 1, limit = 10, searchTerm = '', paginate = true, includeDeleted = false, }: { - query?: Record; - sort?: Record; - page?: number; - limit?: number; - searchTerm?: string; - paginate?: boolean; - includeDeleted?: boolean; + query?: Record; sort?: Record; page?: number; limit?: number; searchTerm?: string; paginate?: boolean; includeDeleted?: boolean; } = {}): Promise | ErrorResponseType> { try { let searchQuery = this.filterQueryFields(query); if (searchTerm && this.searchFields?.length) { - const regex = new RegExp(escapeRegex(searchTerm), 'i'); - const searchConditions = this.searchFields.map((field) => ({ - [field]: regex, - })); + // Enforce max length on regex to prevent ReDoS + const safeSearchTerm = searchTerm.substring(0, 50); + const regex = new RegExp(escapeRegex(safeSearchTerm), 'i'); + const searchConditions = this.searchFields.map((field) => ({ [field]: regex })); searchQuery = { ...searchQuery, $or: searchConditions }; } + const documents = await this.repository.findAll( searchQuery, - { - sort, - skip: (page - 1) * limit, - limit: paginate ? limit : undefined, - }, + { sort, skip: (page - 1) * limit, limit: paginate ? limit : undefined }, includeDeleted, ); + const total = await this.repository.countDocuments({}, includeDeleted); - const _results = await this.repository.countDocuments( - searchQuery, - includeDeleted, - ); + const _results = await this.repository.countDocuments(searchQuery, includeDeleted); const results = paginate ? documents.length : total; - return { - success: true, - total, - _results, - results, - documents, - page: paginate ? page : undefined, - limit: paginate ? limit : undefined, - }; + + return { success: true, total, _results, results, documents, page: paginate ? page : undefined, limit: paginate ? limit : undefined }; } catch (error) { return { - success: false, - total: 0, - _results: 0, - results: 0, - documents: [], - error: - error instanceof ErrorResponse - ? error - : new ErrorResponse('DATABASE_ERROR', (error as Error).message), + success: false, total: 0, _results: 0, results: 0, documents: [], + error: error instanceof ErrorResponse ? error : new ErrorResponse('DATABASE_ERROR', (error as Error).message), }; } } - async findOne( - query: Record, - includeDeleted = false, - ): Promise | ErrorResponseType> { + async findOne(query: Record, includeDeleted = false): Promise | ErrorResponseType> { try { const document = await this.repository.findOne(query, {}, includeDeleted); if (!document) { - throw new ErrorResponse( - 'NOT_FOUND_ERROR', - 'The requested document was not found.', - ); + throw new ErrorResponse('NOT_FOUND_ERROR', 'The requested document was not found.'); } return { success: true, document }; } catch (error) { - return { - success: false, - error: - error instanceof ErrorResponse - ? error - : new ErrorResponse('DATABASE_ERROR', (error as Error).message), - }; + return { success: false, error: error instanceof ErrorResponse ? error : new ErrorResponse('DATABASE_ERROR', (error as Error).message) }; } } - async update( - query: Record, - updateInput: Partial, - includeDeleted = false, - ): Promise | ErrorResponseType> { + async update(query: Record, updateInput: Partial, includeDeleted = false): Promise | ErrorResponseType> { try { - const documentToUpdate = await this.repository.findOne( - query, - {}, - includeDeleted, - ); + const documentToUpdate = await this.repository.findOne(query, {}, includeDeleted); if (!documentToUpdate) { - throw new ErrorResponse( - 'NOT_FOUND_ERROR', - 'Document to update not found.', - ); + throw new ErrorResponse('NOT_FOUND_ERROR', 'Document to update not found.'); } + const fieldsToUpdate: Partial = {}; for (const key in updateInput) { if (updateInput[key] !== documentToUpdate[key]) { fieldsToUpdate[key as keyof T] = updateInput[key]; } } + for (const field of this.uniqueFields) { - if ( - fieldsToUpdate[field as keyof T] && - fieldsToUpdate[field as keyof T] !== - documentToUpdate[field as keyof T] - ) { + if (fieldsToUpdate[field as keyof T] && fieldsToUpdate[field as keyof T] !== documentToUpdate[field as keyof T]) { await this.ensureUniqueField(fieldsToUpdate, field as keyof T); } } - const slugUpdateContext = { - ...fieldsToUpdate, - _id: documentToUpdate._id, - }; - if ( - this.handleSlug && - (fieldsToUpdate as any).name && - (documentToUpdate as any).name !== (fieldsToUpdate as any).name - ) { - await this.ensureUniqueSlug( - slugUpdateContext, - 'name' as any, - 'slug' as any, - ); - (fieldsToUpdate as any).slug = (slugUpdateContext as any) - .slug as unknown as T[keyof T]; + + const slugUpdateContext = { ...fieldsToUpdate, _id: documentToUpdate._id }; + if (this.handleSlug && (fieldsToUpdate as any).name && (documentToUpdate as any).name !== (fieldsToUpdate as any).name) { + await this.ensureUniqueSlug(slugUpdateContext, 'name' as any, 'slug' as any); + (fieldsToUpdate as any).slug = (slugUpdateContext as any).slug as unknown as T[keyof T]; } - const updatedDocument = await this.repository.update( - query, - fieldsToUpdate, - {}, - includeDeleted, - ); + + const updatedDocument = await this.repository.update(query, fieldsToUpdate, {}, includeDeleted); if (!updatedDocument) { - throw new ErrorResponse( - 'NOT_FOUND_ERROR', - 'Updated document not found.', - ); + throw new ErrorResponse('NOT_FOUND_ERROR', 'Updated document not found.'); } return { success: true, document: updatedDocument }; } catch (error) { - return { - success: false, - error: - error instanceof ErrorResponse - ? error - : new ErrorResponse('DATABASE_ERROR', (error as Error).message), - }; + return { success: false, error: error instanceof ErrorResponse ? error : new ErrorResponse('DATABASE_ERROR', (error as Error).message) }; } } - async delete( - query: Record, - softDelete = true, - ): Promise | ErrorResponseType> { + async delete(query: Record, softDelete = true): Promise | ErrorResponseType> { try { - const deletedDocument = await this.repository.delete( - query, - {}, - softDelete, - ); + const deletedDocument = await this.repository.delete(query, {}, softDelete); if (!deletedDocument) { - throw new ErrorResponse( - 'NOT_FOUND_ERROR', - softDelete - ? 'Document to soft delete not found.' - : 'Document to delete not found.', - ); + throw new ErrorResponse('NOT_FOUND_ERROR', softDelete ? 'Document to soft delete not found.' : 'Document to delete not found.'); } return { success: true, document: deletedDocument }; } catch (error) { - return { - success: false, - error: - error instanceof ErrorResponse - ? error - : new ErrorResponse('DATABASE_ERROR', (error as Error).message), - }; + return { success: false, error: error instanceof ErrorResponse ? error : new ErrorResponse('DATABASE_ERROR', (error as Error).message) }; } } } diff --git a/src/core/framework/storage/minio/minio.ts b/src/core/framework/storage/minio/minio.ts index 46bfcc3..49b38e6 100644 --- a/src/core/framework/storage/minio/minio.ts +++ b/src/core/framework/storage/minio/minio.ts @@ -1,44 +1,22 @@ import { Client } from 'minio'; import { config } from '../../../config'; -let minioClient: Client | null = null; +export class MinioService { + private static instance: Client; -function connect( - endpoint: string, - accessKey: string, - secretKey: string, -): Client { - minioClient = new Client({ - endPoint: endpoint, - port: 9000, - useSSL: false, - accessKey, - secretKey, - }); + private constructor() {} - console.info('MinIO connected successfully'); - return minioClient; -} - -function init(): Client { - if (!minioClient) { - minioClient = connect( - config.minio.endpoint, - config.minio.accessKey, - config.minio.secretKey, - ); + public static getInstance(): Client { + if (!MinioService.instance) { + MinioService.instance = new Client({ + endPoint: config.minio.endpoint, + port: Number(config.minio.port) || 9000, + useSSL: config.runningProd ? true : config.minio.useSSL === 'true', // Enforce SSL in production + accessKey: config.minio.accessKey, + secretKey: config.minio.secretKey, + }); + console.info('MinIO client initialized securely.'); + } + return MinioService.instance; } - return minioClient; } - -function getClient(): Client { - if (!minioClient) { - const error = new Error('Connection not initialized. Call init() first.'); - console.error(error); - throw error; - } - - return minioClient; -} - -export { init, getClient };