import Mongoose = require('mongoose'); import Express = require('express'); /* import Mongoose from 'mongoose'; import Express from 'express'; */ import * as BodyParser from 'body-parser'; import * as FileSystem from 'fs'; import * as Http from 'http'; import * as Https from 'https'; import * as Path from 'path'; import * as dotenv from 'dotenv' /* import { version } from '../../../package.json'; */ import { DyFM_AnyError, DyFM_Array, DyFM_Async, DyFM_Error, DyFM_error_defaults, DyFM_Error_Settings, DyFM_ErrorLevel, DyFM_Log, DyFM_resolveRetentionTtlSeconds, megabyte, second } from '@futdevpro/fsm-dynamo'; import { DyNTS_defaultFallbackCacheMaxAge } from '../../_collections/default-fallback-cache-max-age.const'; import { DyNTS_defaultNotFoundPageHtml } from '../../_collections/default-not-found-page.const'; import { DyNTS_global_settings } from '../../_collections/global-settings.const'; import { DyNTS_ClientSafeError_Util } from '../../_collections/client-safe-error.util'; import { DyNTS_getEnvironment_settings } from '../../_collections/get-environment-settings.util'; import { DyNTS_createSecurityHeadersMiddleware } from '../../_collections/security-headers.util'; import { DyNTS_createLoadShedMiddleware } from '../../_collections/load-shed.util'; import { DyNTS_SecurityHeaders_Settings } from '../../_models/interfaces/security-headers-settings.interface'; import { startMongoReconnectGuard } from '../../_collections/mongo-reconnect-guard.util'; import { DyNTS_resolveStaticCacheControlHeader } from '../../_collections/static-cache-control.util'; import { DyNTS_RouteSecurity } from '../../_enums/route-security.enum'; import { DyNTS_App_Params } from '../../_models/control-models/app-params.control-model'; import { DyNTS_AppSystemControls } from '../../_models/control-models/app-system-controls.control-model'; import { DyNTS_Endpoint_Params } from '../../_models/control-models/endpoint-params.control-model'; import { DyNTS_Http_Settings } from '../../_models/control-models/http-settings.control-model'; import { DyNTS_Certification_Settings } from '../../_models/interfaces/certification-settings.interface'; import { DyNTS_GlobalService_Settings } from '../../_models/interfaces/global-service-settings.interface'; import { DyNTS_StaticClient_Settings } from '../../_models/interfaces/static-client-settings.interface'; import { DyNTS_Cors_Settings } from '../../_models/interfaces/cors-settings.interface'; import { DyNTS_DBService } from '../base/db.service'; import { DyNTS_SingletonService } from '../base/singleton.service'; import { DyNTS_GlobalService } from '../core/global.service'; import { DyNTS_CollectionGrowthMonitor } from '../core/collection-growth-monitor.service'; import { DyNTS_MemoryGuard } from '../core/memory-guard.service'; import { DyNTS_EventLoopDiag } from '../core/event-loop-diag.service'; import { DyNTS_RoutingModule } from '../route/routing-module.service'; import { DyNTS_getStarRoute } from '../../_collections/star.controller'; /** * * function MyDecorator(config: any) { return function (target: Function) { // attach metadata or modify target target.prototype.myMeta = config; }; } @MyDecorator({ role: 'admin' }) class User { printRole() { console.log((this as any).myMeta.role); // → "admin" } } */ /** * This will be the MAIN service of our server project, * follow the types and type instructions while setting up your project * * In this service, there are abstract functions that you will need to implement, * where you need to set up the main params for your application. * * (after the example, you can find the list of services you can/should setup) * * @example * export class App extends DyNTS_AppExtended { * * ... * * // Setting up App params, and preparing project global settings * setupAppParams(): void { * this.params = new DyNTS_AppParams({ * name: 'Warbots Server', * title: warbotsTitleLog, * version: version, * dbName: 'warbots', * }); * * // dynamoNTS_GlobalSettings.logRequestsContent = false; * } * * ... * * // Setting up DBServices * setGlobalServiceCollection(): void { * DyNTS_GlobalService.setServices({ * authService: AuthService.getInstance(), * emailServiceCollection: EmailServiceCollectionService.getInstance(), * dbModels: [ * userModelParams, * userDataModelParams, * userOptionsModelParams, * userStatisticsModelParams, * userAchievementsModelParams, * userNotificationsModelParams, * * matchStatisticsModelParams, * matchDataModelParams, * DyFM_usageSession_dataParams, * DyFM_customData_dataParams, * ] * }); * } * * ... * * // Setting up Routes * setupRoutingModules(): void { * this.httpPort = env.port; * this.routingModules = [ * new DyNTS_RoutingModule({ * route: '/user', * controllers: [ * UserController.getInstance(), * UserDataController.getInstance(), * UserOptionsController.getInstance(), * UserStatisticsController.getInstance(), * UserAchievementsController.getInstance(), * UserNotificationsController.getInstance() * ] * }), * new DyNTS_RoutingModule({ * route: '/match', * controllers: [ * MatchController.getInstance(), * MatchDistributionController.getInstance(), * MatchStatisticsController.getInstance(), * ] * }), * new DyNTS_RoutingModule({ * route: '/server', * controllers: [ * ServerController.getInstance(), * ] * }), * getTestRoutingModule(), * getUsageRoutingModule() * ]; * } * } * * // * // The Services available * // * // Authentication Service * // A commonly used basic service, * // which is necessary fur certain functions (such as registering call issuers) * // * // This will handle Authentication Token checking/refreshing, * // checking issuer's identifier and routeParams, * // handling JWT Token, or maybe with OAuth2 or other commonly used security procedures * // * // You can create one with this Dynamo Object: * // * * @example * // follow the instructions on the abstract class (DyNTS_AuthService) * export class AuthService extends DyNTS_AuthService {} * * * * // */ /** * This will be the MAIN service of our server project, * follow the types and type instructions while setting up your project * * In this service, there are abstract functions that you will need to implement, * where you need to set up the main params for your application. * * (after the example, you can find the list of services you can/should setup) * * You need to setup the following functions: * ```ts * // this is where you set up the main params for your application * getAppParams(): DyNTS_AppParams * * // this is where you connect your main services * getGlobalServiceSettings(): DyNTS_GlobalService_Settings * * // this is where you set up your ports * getPorts(): DyNTS_PortSettings * * // this is where you set up your routes * getRoutingModules(): DyNTS_RoutingModule[] * * * * ``` * optionally you can setup the following functions: * ```ts * // this is where you set up your certifications * getCertificationSettings(): DyNTS_CertificationSettings * * // this is where you set up additional root services * getRootServices(): DyNTS_SingletonService[] * * // this is where you set up your initial db entries * createEntries(): void * * // this is where you can define post setup processes * postProcess(): void * * * * ``` * */ /** FR-258 / SR-2 — one entry from `collection.indexes()` (only the fields the TTL-installer reads). */ export interface DyNTS_TtlIndexInfo { name?: string; key?: Record; expireAfterSeconds?: number; } /** * FR-258 / SR-2 — the minimal native-driver collection surface the TTL-installer needs. Declared * structurally (not importing mongodb types) so it stays dependency-light and is trivially mockable * in unit tests. */ export interface DyNTS_TtlIndexCollection { indexes?: () => Promise; createIndex: (keys: Record, options: { expireAfterSeconds: number }) => Promise; dropIndex?: (name: string) => Promise; } /** * FR-258 / SR-2 — outcome of {@link DyNTS_App.ensureRetentionTtlIndex}. * - `created`: no `__created` TTL index existed → one was created. * - `noop`: an index with the SAME TTL already existed → nothing done. * - `differs`: an index exists with a DIFFERENT TTL (or a non-TTL `__created` index) → left UNTOUCHED, * only a warning is emitted. We deliberately DO NOT drop+recreate at boot: rebuilding a TTL index on a * multi-GB production collection is a heavy server-side op that can starve concurrent boot queries (e.g. * the health-probe) and trip a fail-safe deploy revert. A deliberate retention CHANGE must be applied via * maintenance, not silently at every startup. */ export type DyNTS_TtlIndexAction = 'created' | 'differs' | 'noop'; export abstract class DyNTS_App extends DyNTS_SingletonService { protected systemControls: DyNTS_AppSystemControls = new DyNTS_AppSystemControls(); get started(): boolean { return this.systemControls.app.started; } protected get superStarted(): boolean { return this.systemControls.app.started; } protected constructErrors: (Error | DyFM_Error)[] = []; /* removed since cant use version from package.json private readonly _ntsVersion: string = 'v01.07.18'; protected get ntsVersion(): string { return this._ntsVersion; } */ get serverName(): string { return this.params.name; } private _params: DyNTS_App_Params; protected get params(): DyNTS_App_Params { return this._params; } protected mongoose = Mongoose; private _security: DyNTS_RouteSecurity; protected get security(): DyNTS_RouteSecurity { return this._security; } protected _portSettings: DyNTS_Http_Settings = new DyNTS_Http_Settings(); protected get portSettings(): DyNTS_Http_Settings { return this._portSettings; } private _cert?: DyNTS_Certification_Settings; protected get cert(): DyNTS_Certification_Settings { return this._cert; } protected openExpress: Express.Application; protected secureExpress: Express.Application; protected httpsServer: Https.Server; protected httpServer: Http.Server; private globalService: DyNTS_GlobalService; private _rootServices: DyNTS_SingletonService[] = []; private _routingModules: DyNTS_RoutingModule[] = []; protected readonly defaultReadyTimeout: number = 30 * second; override readonly defaultErrorUserMsg = `We encountered an unhandled Server Error, ` + `\nplease contact the responsible development team.` + `\n(Internal Server error)`; get logSetup(): boolean { return DyNTS_global_settings.log_settings.setup; } get deepLog(): boolean { return DyNTS_global_settings.log_settings.deep; } get fnLogs(): boolean { return DyNTS_global_settings.log_settings.functions; } debugLog: boolean = DyNTS_global_settings.log_settings.server_debug; constructor(/* extended?: boolean */){ super(); /* dotenv.config() */ process.on( 'unhandledRejection', (reason_theError: object, p_passWhatIsThis_maybeThePromise: any): void => { if (reason_theError instanceof DyFM_Error) { reason_theError.logSimple('Unhandled Rejection'); } else { DyFM_Log.H_error( 'Unhandled Rejection:', (p_passWhatIsThis_maybeThePromise as Promise)?.toString(), '\n Rejection reason:', (reason_theError as Error)?.stack?.split('at')?.[0], /* '\n ErrorCode:', (reason as any)?.code, */ '\n\n Stack:', (reason_theError as Error)?.stack?.replaceAll?.('\n at', '\n at'), ); } try { DyNTS_GlobalService.globalErrorHandler?.( new DyFM_Error({ errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-AS0-BASE-UR`, message: `Unhandled Rejection!: "${(reason_theError as Error)?.stack?.split('at')?.[0]}"`, userMessage: this.defaultErrorUserMsg, addECToUserMsg: true, error: reason_theError as Error, additionalContent: { reason: reason_theError, rejectedPromise: p_passWhatIsThis_maybeThePromise, }, systemVersion: DyNTS_global_settings.systemVersion, level: DyFM_ErrorLevel.critical, }) ); } catch (error) { DyFM_Log.error('globalErrorHandler (MULTILEVEL) ERROR:', error); } } ); this.asyncConstruct(/* extended */).catch((error: any): void => { if (error instanceof DyFM_Error) { if (error.additionalContent?.constructErrors?.length) { error.additionalContent.constructErrors.forEach((errorItem: DyFM_Error): void => { DyFM_Error.logSimple('(constructor asyncConstruct.catch) error:', errorItem); /* if (errorItem instanceof DyFM_Error) { errorItem.logSimple(`(constructor asyncConstruct.catch) error:\n`); } else { DyFM_Log.H_warn( '(constructor asyncConstruct.catch) additional error content:\n', errorItem ); } */ }); } else { DyFM_Log.H_warn( '(constructor asyncConstruct.catch) additional error content:\n', error?.additionalContent ); } } DyFM_Error.logSimple('(constructor asyncConstruct.catch) error:', error); /* if ( !DyNTS_global_settings.log_settings.highDetailedLogs && (error instanceof DyFM_Error) ) { error.logSimple( `Application: "${this.params?.name}" start failed. (constructor asyncConstruct.catch)` + '\n all error messages (from this stack):\n\n"' + error._messages.join('"\n\n"') + '"\n\n' ); } else if (error instanceof DyFM_Error) { DyFM_Log.H_error( `Application: "${this.params?.name}" start failed. (constructor asyncConstruct.catch)`, `\n ERROR:`, error ); } */ const message: string = (error as DyFM_Error)?.additionalContent?.constructErrors?.flatMap( (errorItem: DyFM_Error): string[] => { return errorItem?._messages ?? []; } )?.join?.('\n') ?? (error as DyFM_Error)?.errors?.flatMap( (errorItem: DyFM_AnyError): string[] => (errorItem as DyFM_Error)?._messages ?? [ (errorItem as Error)?.message ] )?.join?.('\n') ?? error?.message ?? 'UNKNOWN'; DyFM_Log.testError('Application start failed:\n', message); process.exit(1); }); } protected async asyncConstruct(extended?: boolean): Promise { if (this.fnLogs && this.deepLog) DyFM_Log.log('\nfn:. asyncConstruct'); try { this.systemControls.app.init = true; this._params = this.getAppParams(); DyFM_Log.log( `\n\n\n\n\n\n\n\n\n\n` + `Starting ${this._params?.name}... ` + /* `v${version}` + */ `\n\n\n\n\n\n\n\n\n\n` ); if (!this._params) { throw new Error('getAppParams() must return a DyNTS_AppParams object!'); } if (this.params.systemShortCodeName) { DyNTS_global_settings.systemShortCodeName = this.params.systemShortCodeName; } if (this.params.systemName) { DyNTS_global_settings.systemName = this.params.systemName; DyFM_error_defaults.issuerSystem = this.params.systemName; } if (this.params.version) { DyNTS_global_settings.systemVersion = this.params.version; DyFM_error_defaults.systemVersion = this.params.version; } process.stdout.write( String.fromCharCode(27) + ']0;' + this._params?.name + String.fromCharCode(7) ); DyFM_error_defaults.issuerSystem = this._params.systemName; this.overrideDynamoNTSGlobalSettings?.(); if (DyNTS_global_settings.log_settings.setup) { DyFM_Log.S_info(`env settings;\n`, { systemName: DyNTS_global_settings.systemName, systemShortCodeName: DyNTS_global_settings.systemShortCodeName, systemVersion: DyNTS_global_settings.systemVersion, environment: DyNTS_global_settings.env_settings.environment, /* log_settings: DyNTS_global_settings.log_settings, env_settings: DyNTS_global_settings.env_settings, */ }); } this.globalService = DyNTS_GlobalService.getInstance(); await DyNTS_GlobalService.setServices(this.getGlobalServiceCollection()); DyNTS_GlobalService.setParams(this.params); if (this.getPortSettings) { this._portSettings = this.getPortSettings(); } if (this.getCertificationSettings) { this._cert = this.getCertificationSettings(); } if (this.getApiBasePath) { DyNTS_global_settings.baseUrl = this.getApiBasePath(); } if (this.getRoutingModules) { this._routingModules = this.getRoutingModules(); // ezt egyelőre csak tesztelem, nem kerül be /* if ( !DyNTS_global_settings.dontCreateDefaultRoute && !this._routingModules.some((routingModule: DyNTS_RoutingModule): boolean => routingModule.route === '/*') ) { this._routingModules.push( DyNTS_getStarRoute() ); } */ } if (this._params.dbUri) { await this.startDB(); // createEntries csak akkor fut, ha a DB tényleg fel-jött. // Ha a startDB jelezte a hibát, de továbbment, akkor a started flag false marad, // és nem próbálunk meg írni egy nem létező kapcsolatra. if (this.createEntries && this.systemControls.mongoose.started) { await this.createEntries(); } } else { DyFM_Log.log( `\nNo database connection created.`, ); } if (this._routingModules?.length) { if (this.logSetup) DyFM_Log.log('\nsetting up express routes...'); this.setSecurity(); await this.initExpresses(); await this.startExpresses(); if (this._security !== DyNTS_RouteSecurity.secure) { await this.mountOpenRoutes(); } if (this._security !== DyNTS_RouteSecurity.open && this._cert) { await this.mountSecureRoutes(); } // Generikus, auth-agnosztikus extension-point: custom middleware az API // route-ok UTÁN, de a SPA static catch-all (mountStaticClient) ELŐTT. await this.mountCustomMiddleware(); await this.mountStaticClient(); if (this.logSetup) { DyFM_Log.log(`\nRoutes mounted.... server using security: ${this._security}`); } } else { DyFM_Log.warn( `\nNo routes mounted!`, ); } if (this.getRootServices) { this._rootServices = await this.getRootServices(); } if (this.postProcess) { await this.postProcess().catch((error: any): void => { DyFM_Error.logSimple(`"${this._params.name}" postProcess failed:`, error); DyNTS_GlobalService.globalErrorHandler?.(error); }); } // FR-193 — bedrock OOM korai-figyelmeztetés: feltelepítjük a heap-watchdogot, ha // engedélyezve (DyNTS_global_settings.memoryGuard.enabled, default true). Biztonságos: // a guard egy könnyű setInterval, ami SOHA nem dob; csak near-OOM küszöböknél hagy // tartós nyomot az error-sinkbe, mielőtt a fatal heap-OOM megölné a process-t. try { if (DyNTS_global_settings.memoryGuard?.enabled) { DyNTS_MemoryGuard.getInstance().install(); } } catch (memoryGuardError: unknown) { DyFM_Log.warn('[DyNTS_MemoryGuard] auto-install skipped (non-fatal):', memoryGuardError); } // BFR-OVERSEER-007 — event-loop-lag diag a MemoryGuard MELLÉ: a „process él, HTTP néma" wedge // belső mérése (natív histogram, olcsó). Observation-only, sosem dob; a /server/diag olvas belőle. try { DyNTS_EventLoopDiag.getInstance().install(); } catch (eventLoopDiagError: unknown) { DyFM_Log.warn('[DyNTS_EventLoopDiag] auto-install skipped (non-fatal):', eventLoopDiagError); } // FR-258 / SR-4 — proactive collection-growth monitor (companion to the MemoryGuard). Warns // into the Errors-sink when a collection grows unbounded, BEFORE it can be loaded into heap and // OOM. Default-on, observation-only, never throws. try { if (DyNTS_global_settings.collectionGrowthMonitor?.enabled) { DyNTS_CollectionGrowthMonitor.getInstance().install(); } } catch (cgmError: unknown) { DyFM_Log.warn('[DyNTS_CollectionGrowthMonitor] auto-install skipped (non-fatal):', cgmError); } if (!extended) { await this.ready(); if (this.params.title) { DyFM_Log.success(this.params.title); } DyFM_Log.info(`Version: ${this.params.version}`); /* DyFM_Log.info(`NTS Version: ${this.ntsVersion}`); */ DyFM_Log.H_success(`${this.params.name} started successfully.`); } } catch (error) { this.constructErrors.push(error); if (this.deepLog) { if (DyNTS_global_settings.log_settings.highDetailedLogs) { DyFM_Log.H_error( `"${this._params.name}" start failed (in asyncConstruct (highDetailedLog)). `, `\n\n construct ERRORS:`, this.constructErrors, '\n\nlast error:', error ); } else { DyFM_Log.H_error( `"${this._params.name}" start failed (in asyncConstruct). `, `\n\n construct ERRORS:`, this.getSimplifiedConstructErrors(), '\n\nlast error:', error instanceof DyFM_Error ? error.getErrorSimplified() : error ); } } throw new DyFM_Error({ ...this._getDefaultErrorSettings('asyncConstruct', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-AS0-001`, additionalContent: { constructErrors: this.constructErrors, systemControls: this.systemControls, systemReadies: { app: this.systemControls.app.getIsReady(), mongoose: this.systemControls.mongoose.getIsReady(), httpServer: this.systemControls.httpServer.getIsReady(), httpsServer: this.systemControls.httpsServer.getIsReady(), }, }, }); } } async ready(timeout: number = this.defaultReadyTimeout): Promise { try { if (this.fnLogs) DyFM_Log.log('\nfn:. ready'); await DyFM_Async.delay(100); let ready: boolean = false; const start: number = +new Date(); if (this.constructErrors.length) { if (this.deepLog) { if (DyNTS_global_settings.log_settings.highDetailedLogs) { DyFM_Log.H_error( `"${this._params.name}" start failed. (ready; constructErrors check 1)`, `\n construct ERRORS:`, this.constructErrors ); } else { DyFM_Log.H_error( `"${this._params.name}" start failed. (ready; constructErrors check 1)`, `\n construct ERRORS:`, this.getSimplifiedConstructErrors(), ); } } throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'ready', new Error(`"${this._params.name}" start failed.`) ), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-AS0-R01`, additionalContent: this.constructErrors.length === 1 ? { error: this.constructErrors[0] } : { errors: this.constructErrors }, }); } while (!ready && +new Date() - start < timeout) { if (this.systemControls.app.init) { ready = ( this.systemControls.mongoose.getIsReady() && this.systemControls.httpServer.getIsReady() && this.systemControls.httpsServer.getIsReady() ); } else { DyFM_Log.error(`"${this._params.name}" APP NOT INITIALIZED while trying to get ready.`); } if (!ready) { await DyFM_Async.wait(100); } } if (timeout < +new Date() - start) { if (this.deepLog) { if (DyNTS_global_settings.log_settings.highDetailedLogs) { DyFM_Log.H_error( `"${this._params.name}" start failed. (ready; TIMEOUT check)`, `\n construct ERRORS:`, this.constructErrors ); } else { DyFM_Log.H_error( `"${this._params.name}" start failed. (ready; TIMEOUT check)`, `\n construct ERRORS:`, this.getSimplifiedConstructErrors(), ); } } throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'ready', new Error(`"${this._params.name}" start failed. TIMEOUT`) ), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-AS0-R02`, additionalContent: { constructErrors: this.constructErrors, systemControls: this.systemControls, systemReadies: { mongoose: this.systemControls.mongoose.getIsReady(), httpServer: this.systemControls.httpServer.getIsReady(), httpsServer: this.systemControls.httpsServer.getIsReady(), }, }, }); } if (this.constructErrors.length) { if (this.deepLog) { if (DyNTS_global_settings.log_settings.highDetailedLogs) { DyFM_Log.H_error( `"${this._params.name}" start failed. (ready; constructErrors check 2)`, `\n construct ERRORS:`, this.constructErrors ); } else { DyFM_Log.H_error( `"${this._params.name}" start failed. (ready; constructErrors check 2)`, `\n construct ERRORS:`, this.getSimplifiedConstructErrors(), ); } } throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'ready', new Error(`"${this._params.name}" start failed.`) ), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-AS0-R03`, additionalContent: this.constructErrors, }); } if (ready) { this.systemControls.app.started = true; if (this.fnLogs && this.deepLog) DyFM_Log.log('\nfn:. ready: return'); return; } this.systemControls.app.started = false; let msg: string = `"${this._params.name}" start failed. UNKNOWN`; if (this.systemControls.mongoose.init && !this.systemControls.mongoose.started) { msg += `\nMongoose start failed.`; } if (this.systemControls.httpServer.init && !this.systemControls.httpServer.started) { msg += `\nHTTP Server start failed.`; } if (this.systemControls.httpsServer.init && !this.systemControls.httpsServer.started) { msg += `\nHTTPS Server start failed.`; } DyFM_Log.error(msg, this.constructErrors); throw new DyFM_Error({ ...this._getDefaultErrorSettings('ready', new Error(msg)), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-AS0-R04`, additionalContent: { constructErrors: this.constructErrors, systemControls: this.systemControls, systemReadies: { app: this.systemControls.app.getIsReady(), mongoose: this.systemControls.mongoose.getIsReady(), httpServer: this.systemControls.httpServer.getIsReady(), httpsServer: this.systemControls.httpsServer.getIsReady(), }, }, error: this.constructErrors?.[0] ?? new Error(), }); } catch (error) { throw new DyFM_Error({ ...this._getDefaultErrorSettings('ready', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-AS0-READY0`, }); } } protected getSimplifiedConstructErrors(): string[] { return this.constructErrors.map((error: any): any => { if (error instanceof DyFM_Error) { return error.getErrorSimplified(); } else { return error; } }); } async stop(dontLog?: boolean): Promise { try { DyFM_Log.info('\nstopping server...\n'); await this.ready(); if (this.started) { if (this.systemControls.mongoose.init) { DyFM_Log.info(`\nstopping Mongoose....`); let tryCount: number = 0; while ( !this.systemControls.mongoose.started && !this.constructErrors.length && tryCount++ < 10 ) { DyFM_Log.warn(`Mongoose not even started yet....`); await DyFM_Async.wait(second); } this.systemControls.mongoose.started = false; if (this.mongoose) { await DyFM_Array.asyncForEach( Object.keys(this.mongoose.models), async (modelName): Promise => { this.mongoose.deleteModel(modelName); } ); const disconnect: Promise = new Promise((resolve): void => { this.mongoose.connection.on('disconnecting', (): void => { resolve(); }); }); await this.mongoose.disconnect(); await this.mongoose.connection.close(); await disconnect; this.mongoose.connection.removeAllListeners(); while ( this.mongoose.connection.readyState !== 0 && !this.constructErrors.length ) { DyFM_Log.warn(`\nMongoose still not disconnected....`); await DyFM_Async.wait(second); } } else { DyFM_Log.error(`\nMongoose not found.`); } this.systemControls.mongoose.init = false; } if (this.systemControls.httpServer.init) { this.systemControls.httpServer.started = false; if (this.httpServer) { await new Promise((resolve): void => { this.httpServer.close(resolve); }); } else { DyFM_Log.error(`\nHTTP Server not found.`); } this.systemControls.httpServer.init = false; } if (this.systemControls.httpsServer.init) { this.systemControls.httpsServer.started = false; if (this.httpsServer) { await new Promise((resolve): void => { this.httpsServer.close(resolve); }); } else { DyFM_Log.error(`\nHTTPS Server not found.`); } this.systemControls.httpsServer.init = false; } await DyFM_Async.wait(second); if (!dontLog) { DyFM_Log.H_log(`"${this._params.name}" stopped successfully.`); } } } catch (error) { throw new DyFM_Error({ ...this._getDefaultErrorSettings('stop', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-AS0-STOP0`, }); } } /** * */ private async startDB(): Promise { if (this.fnLogs && this.deepLog) DyFM_Log.log('\nfn:. startDB'); else if (this.logSetup) DyFM_Log.log('\nstarting DB connection...'); if (!this._params.dbUri) { throw new DyFM_Error({ ...this._getDefaultErrorSettings('startDB', new Error('DB URI is not set.')), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-AS0-SDB0`, }); } if (!this._params.dbOptions) { throw new DyFM_Error({ ...this._getDefaultErrorSettings('startDB', new Error('DB OPTIONS are not set.')), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-AS0-SDB0`, }); } if (this.systemControls.mongoose.init || this.systemControls.mongoose.started) { throw new DyFM_Error({ ...this._getDefaultErrorSettings('startDB', new Error('Mongoose is already initialized.')), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-AS0-SDB0`, }); } try { await new Promise( (resolve, reject): void => { this.systemControls.mongoose.init = true; this.mongoose.connection .once('open', (): void => { this.systemControls.mongoose.started = true; DyFM_Log.success(`\nConnected to MongoDB (${this._params.dbUri})\n`); resolve(); // FR-258 / SR-2 — install declared-retention TTL indexes. DEFERRED + fire-and-forget + // fully non-fatal: a create-if-missing index op (and the `indexes()` probes) must NOT // compete with boot-readiness. We delay it well past the deploy health-probe window so a // cold boot is never slowed by it, then run it detached. `.unref()` keeps it from holding // the event loop open. Any failure is logged but never crashes the app. (`'differs'` never // rebuilds a live index — see ensureRetentionTtlIndex.) const ttlInstallTimer: ReturnType = setTimeout((): void => { void this.installRetentionTtlIndexes(); }, 30000); if (typeof ttlInstallTimer.unref === 'function') { ttlInstallTimer.unref(); } }) .on('error', (error): void => { if (!this.systemControls.mongoose.started) { // Initial DB-csatlakozás sikertelen: // jelezzük a hibát (log + globalErrorHandler), de NEM szakítjuk meg az // App startup-ot (nincs constructErrors push, nincs reject). // A mongoose.init-et resetteljük, hogy a ready() ne várjon a DB-re. // Ha a mongoose később mégis tudna csatlakozni, az 'open' event // beállítja a started flag-et, így a runtime DB-műveletek elindulnak. this.systemControls.mongoose.init = false; const d_error: DyFM_Error = new DyFM_Error({ ...this._getDefaultErrorSettings('startDB', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-AS0-SDB1`, message: `Unable to connect to MongoDB server (${this._params.dbUri}), ` + `ERROR: ${error}`, level: DyFM_ErrorLevel.serious, }); DyFM_Log.H_error( `\nUnable to connect to MongoDB server (${this._params.dbUri}).` + `\nServer will continue WITHOUT DB connection.` + `\nDB-using endpoints will fail at runtime until connection recovers.` ); if (this.debugLog) DyFM_Log.S_error( `\nMongoDB connect ERROR: `, error ); DyNTS_GlobalService.globalErrorHandler?.(d_error); resolve(); } else { if (this.debugLog) DyFM_Log.error('\nMongoDB ERROR: ', error); const d_error: DyFM_Error = new DyFM_Error({ ...this._getDefaultErrorSettings('mongoose.connection.on(error)', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-AS0-SDB2`, message: `MongoDB ERROR: ${error}`, level: DyFM_ErrorLevel.critical, }); DyNTS_GlobalService.globalErrorHandler?.(d_error); } }); try { this.mongoose.connect( this._params.dbUri, this._params.dbOptions /* { directConnection: true, } */ ); } catch (error) { throw new DyFM_Error({ ...this._getDefaultErrorSettings('startDB', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-AS0-SDB3`, }); } } ); } catch (error) { throw new DyFM_Error({ ...this._getDefaultErrorSettings('startDB', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-AS0-SDB0`, }); } // 2026-06-20 — Mongo reconnect-guard. A MongoDB-driver a hostnevet connect-kor EGYSZER // resolválja + cache-eli az IP-t; ha a Mongo-konténert recreate-elik (új docker-network IP), // a driver a HALOTT IP-n ragad → ECONNREFUSED + buffering-timeout, és a service magától SOHA // nem áll vissza. A guard sustained-disconnect (readyState !== 1) esetén — a driver saját // grace-e UTÁN — TELJES disconnect()+connect()-et csinál → ÚJ MongoClient → ÚJ DNS-resolve → // új IP → reconnect. Best-effort, always-on, healthy connection-t (===1) SOHA nem bánt; csak // konténer-IP-frissítés, nem ír adatot. Lásd: mongo-reconnect-guard.util.ts. try { startMongoReconnectGuard({ getReadyState: (): number => this.mongoose.connection.readyState, reconnect: async (): Promise => { await this.mongoose.disconnect().catch((): void => undefined); await this.mongoose.connect(this._params.dbUri, this._params.dbOptions); }, log: (msg: string): void => DyFM_Log.warn(msg), }); } catch (guardErr) { DyFM_Log.warn(`[mongo-reconnect-guard] failed to start (non-fatal): ${guardErr instanceof Error ? guardErr.message : String(guardErr)}`); } } /** * FR-258 / SR-2 — install MongoDB TTL indexes for every data-model that declared `retention`. * * Runs ONCE right after the DB connection opens. For each registered DB service (including the * auto-created `_archived` siblings, which inherit the parent model's `retention`), it ensures a * TTL index on the `__created` Date field with the resolved `expireAfterSeconds` — so MongoDB * NATIVELY auto-deletes documents older than the configured age. This is the fleet-wide defense * against the unbounded-collection-growth → in-memory-load → GC-thrash/OOM class. * * Guarantees: * - **Non-fatal** — any error is logged but NEVER blocks startup (called fire-and-forget). * - **Idempotent** — an existing index with the same TTL is a no-op; a CHANGED retention (different * `expireAfterSeconds`, or an existing non-TTL `__created` index) is reconciled by drop+recreate * (the field index is restored immediately, so query coverage is preserved). */ private async installRetentionTtlIndexes(): Promise { try { const services: DyNTS_DBService[] = DyNTS_GlobalService.getAllDBServices(); let ensured: number = 0; for (const service of services) { const ttlSeconds: number | undefined = DyFM_resolveRetentionTtlSeconds(service?.dataParams?.retention); if (!ttlSeconds) { continue; } const dataName: string = service.dataParams.dataName; // The mongoose Model exposes the native driver collection (.indexes / .createIndex / .dropIndex). const collection: DyNTS_TtlIndexCollection | undefined = (service?.dataModel as unknown as { collection?: DyNTS_TtlIndexCollection })?.collection; if (!collection || typeof collection.createIndex !== 'function') { continue; } try { const action: DyNTS_TtlIndexAction = await DyNTS_App.ensureRetentionTtlIndex(collection, ttlSeconds); if (action === 'created') { ensured++; if (this.logSetup) { DyFM_Log.success( `[FR-258 retention] TTL index created on "${dataName}" — ` + `${Math.round(ttlSeconds / 86400)}d (${ttlSeconds}s)`, ); } } else if (action === 'differs') { // Live index has a different TTL — left untouched at boot (see ensureRetentionTtlIndex doc). DyFM_Log.warn( `[FR-258 retention] "${dataName}" already has a {__created} index with a DIFFERENT TTL than the ` + `declared ${Math.round(ttlSeconds / 86400)}d (${ttlSeconds}s) — left as-is (no boot-time rebuild). ` + `Apply the change via maintenance if intended.`, ); } } catch (idxErr: unknown) { DyFM_Log.warn( `[FR-258 retention] TTL index on "${dataName}" failed (non-fatal): ` + `${idxErr instanceof Error ? idxErr.message : String(idxErr)}`, ); } } if (ensured > 0) { DyFM_Log.success(`[FR-258 retention] ${ensured} TTL index(es) ensured (auto-retention active).`); } } catch (err: unknown) { DyFM_Log.warn( `[FR-258 retention] TTL-installer failed (non-fatal): ` + `${err instanceof Error ? err.message : String(err)}`, ); } } /** * FR-258 / SR-2 — ensure a single `{ __created: 1 }` TTL index with the given `expireAfterSeconds`. * Extracted as a pure-ish static so it is unit-testable with a mock collection. Idempotent + BOOT-SAFE: * - no existing `__created` index → create it → `'created'` * - existing with the SAME TTL → no-op → `'noop'` * - existing with a DIFFERENT TTL / non-TTL → leave it as-is → `'differs'` (warn only — NO drop+recreate) * * The `'differs'` case intentionally does NOT mutate the live index. Dropping + recreating a TTL index on a * large (multi-GB) production collection is a heavy server-side operation; doing it automatically inside the * startup path can starve the concurrent boot queries (including the deploy health-probe) and trigger a * fail-safe revert. Create-if-missing is the essential growth-prevention behaviour; a deliberate retention * CHANGE on an already-indexed collection is a maintenance action, not a silent every-boot rebuild. */ static async ensureRetentionTtlIndex( collection: DyNTS_TtlIndexCollection, ttlSeconds: number, ): Promise { const existingList: DyNTS_TtlIndexInfo[] = typeof collection.indexes === 'function' ? await collection.indexes().catch((): DyNTS_TtlIndexInfo[] => []) : []; const existing: DyNTS_TtlIndexInfo | undefined = existingList.find((ix): boolean => !!ix?.key && ix.key.__created === 1); if (!existing) { await collection.createIndex({ __created: 1 }, { expireAfterSeconds: ttlSeconds }); return 'created'; } if (existing.expireAfterSeconds === ttlSeconds) { return 'noop'; } // Retention differs from the live index (or it is a non-TTL `__created` index). Leave it UNTOUCHED at // boot — only report it. Avoids a heavy drop+recreate competing with boot-readiness on a large collection. return 'differs'; } /** * */ private async initExpresses(): Promise { if (this.fnLogs && this.deepLog) DyFM_Log.log('\nfn:. initExpresses'); try { if (this._security && this._security !== DyNTS_RouteSecurity.secure) { if (this._portSettings.httpPort === undefined || this._portSettings.httpPort === null) { let errorMsg: string = `\nYou have open routes, but httpPort is not set!` + `\nThere are ${this._routingModules.filter( m => m.security != DyNTS_RouteSecurity.secure ).length} open/both routes` + `\nroot security: ${this._security}` + `\nset httpPort in DynamoBEServer - setupRoutingModules() to enable secure routes.`; errorMsg += '\n\nThe routes setted to use open server:'; this._routingModules.forEach((module: DyNTS_RoutingModule): void => { if (module.security != DyNTS_RouteSecurity.secure) { errorMsg += `\n ${module.route} (security: ${module.security})`; errorMsg += `\n subroutes using open sever:`; module.endpoints.forEach((endpoint: DyNTS_Endpoint_Params): void => { if (endpoint.security != DyNTS_RouteSecurity.secure) { errorMsg += `\n ${endpoint.endpoint} (security: ${endpoint.security})`; } }); } }); const error = new Error(`Open routes cannot be established!\n${errorMsg}`); const errorStack: string[] = error.stack.split('\n'); errorStack.splice(1, 2); error.stack = errorStack.join('\n'); DyFM_Log.error(errorMsg); throw error; } await this.initOpenExpress(); } if (this._security && this._security !== DyNTS_RouteSecurity.open) { if (!this._cert || !this._portSettings.httpsPort) { let errorMsg: string = `\nYou have secure routes, but the certification paths or httpsPort are not set!` + `\nThere are ${this._routingModules.filter( m => m.security && m.security !== DyNTS_RouteSecurity.open ).length} secure routes` + `\nroot security: ${this._security}` + `\nset...` + `\n(missing exact howto...)` + /* `\n httpsPort and` + `\n cert: {` + `\n keyPath: FileSystem.PathLike,` + `\n certPath: FileSystem.PathLike,` + `\n }` + */ `\nin DynamoBEServer - getRoutingModules() to enable secure routes.`; errorMsg += '\n\nThe routes setted to use secure server:'; this._routingModules.forEach((module: DyNTS_RoutingModule): void => { if (module.security && module.security !== DyNTS_RouteSecurity.open) { errorMsg += `\n ${module.route} (security: ${module.security})`; errorMsg += `\n location: ${module.stackLocation}`; errorMsg += `\n subroutes using secure sever:`; module.endpoints.forEach((endpoint: DyNTS_Endpoint_Params): void => { if (endpoint.security && endpoint.security !== DyNTS_RouteSecurity.open) { errorMsg += `\n ${endpoint.endpoint} (security: ${endpoint.security})`; errorMsg += `\n location: ${endpoint.stackLocation}`; } }); } }); const error = new Error(`Secure routes cannot be established!\n${errorMsg}`); const errorStack: string[] = error.stack.split('\n'); errorStack.splice(1, 2); error.stack = errorStack.join('\n'); DyFM_Log.error(errorMsg); throw error; } await this.initSecureExpress(); } } catch (error) { throw new DyFM_Error({ ...this._getDefaultErrorSettings('initExpresses', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-AS0-IE0`, }); } } /** * */ protected async initOpenExpress(): Promise { if (this.fnLogs) DyFM_Log.log('\nfn:. initOpenExpress'); this.openExpress = Express(); this.openExpress.set('maxHeaderSize', 10 * megabyte); // 1024 * 1024 * 10, // 10MB this.openExpress.use(BodyParser.urlencoded(this._portSettings.httpUrlencoded)); this.openExpress.use(BodyParser.json(this._portSettings.httpJson)); // BFR-011 / SEC-R2-8 — security headerek a CORS ELŐTT (az OPTIONS preflight-válaszok is kapják). this.mountSecurityHeaders(this.openExpress); // FR-041 — CORS allowlist enforcement. No-op if getCorsSettings() is not overridden. this.mountCors(this.openExpress); // BFR-OVERSEER-009 — opt-in auto-load-shed 503 CRITICAL memória-nyomásnál (esszenciális utak kivételével). if (DyNTS_global_settings.memoryGuard?.loadShedEnabled) { this.openExpress.use(DyNTS_createLoadShedMiddleware()); } } /** * */ protected async initSecureExpress(): Promise { if (this.fnLogs) DyFM_Log.log('\nfn:. initSecureExpress'); this.secureExpress = Express(); this.secureExpress.use(BodyParser.urlencoded(this._portSettings.httpsUrlencoded)); this.secureExpress.use(BodyParser.json(this._portSettings.httpsJson)); // BFR-011 / SEC-R2-8 — security headerek a CORS ELŐTT (mint az open expressen). this.mountSecurityHeaders(this.secureExpress); // FR-041 — CORS allowlist enforcement (same as open express). this.mountCors(this.secureExpress); // BFR-OVERSEER-009 — opt-in auto-load-shed 503 (mint az open expressen). if (DyNTS_global_settings.memoryGuard?.loadShedEnabled) { this.secureExpress.use(DyNTS_createLoadShedMiddleware()); } const options = { key: FileSystem.readFileSync(this._cert.keyPath), cert: FileSystem.readFileSync(this._cert.certPath), maxHeaderSize: 10 * megabyte, // 1024 * 1024 * 10, // 10MB }; this.httpsServer = Https.createServer(options, this.secureExpress); } /** * */ private async startExpresses(): Promise { if (this.fnLogs && this.deepLog) DyFM_Log.log('\nfn:. startExpresses'); try { if (this._security && this._security !== DyNTS_RouteSecurity.open) { await new Promise((resolve, reject): void => { this.systemControls.httpsServer.init = true; this.httpsServer .listen( this._portSettings.httpsPort, this.params.secureHost, this.params.expressBacklog, (): void => { this.systemControls.httpsServer.started = true; DyFM_Log.success( `\nHTTPS (secure) server is listening on port: ` + `${this.params.secureHost}:${this._portSettings.httpsPort}` ); resolve(); }) .on('error', (error): void => { if (this.debugLog) DyFM_Log.error(`\nHTTPS (secure) server ERROR`, error); if (!this.systemControls.httpsServer.started) { const d_error: DyFM_Error = new DyFM_Error({ ...this._getDefaultErrorSettings('startExpresses', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-AS0-SE1`, message: `HTTPS (secure) start server ERROR`, }); this.constructErrors.push(d_error); DyNTS_GlobalService.globalErrorHandler?.(d_error); reject(d_error); } else { const d_error: DyFM_Error = new DyFM_Error({ ...this._getDefaultErrorSettings('httpsServer.on(error)', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-AS0-SE2`, message: `HTTPS (secure) server ERROR`, level: DyFM_ErrorLevel.serious, }); DyNTS_GlobalService.globalErrorHandler?.(d_error); } }) .on('uncaughtException', (exception): void => { const d_error: DyFM_Error = new DyFM_Error({ ...this._getDefaultErrorSettings('httpsServer.on(uncaughtException)', exception), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-AS0-SE3`, message: `HTTPS (secure) server uncaughtException`, level: DyFM_ErrorLevel.critical, }); if (this.debugLog) DyFM_Log.warn( `\nHTTPS (secure) server uncaughtException`, d_error ); DyNTS_GlobalService.globalErrorHandler?.(d_error); }); }); } if (this._security && this._security !== DyNTS_RouteSecurity.secure) { this.systemControls.httpServer.init = true; await new Promise((resolve, reject): void => { this.httpServer = this.openExpress .listen( this._portSettings.httpPort, this.params.openHost, this.params.expressBacklog, (): void => { this.systemControls.httpServer.started = true; const resolvedPort: number = this.httpServer?.address?.()?.['port'] ?? this._portSettings.httpPort; if (this._portSettings.httpPort === 0) { DyFM_Log.H_warn( `\nHTTP (open) server is using a randomly selected port by the OS: ${resolvedPort}` + `\n (httpPort was set to 0 — this is intended for testing only)` ); } DyFM_Log.success( `\nHTTP (open) server is listening on port: ` + `${this.params.openHost}:${resolvedPort}` ); resolve(); } ) .on('error', (error): void => { if (this.debugLog) DyFM_Log.error(`\nHTTP (open) server ERROR`, error); if (!this.systemControls.httpServer.started) { const d_error: DyFM_Error = new DyFM_Error({ ...this._getDefaultErrorSettings('startExpresses', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-AS0-SE3`, message: `HTTP (open) start server ERROR`, }); this.constructErrors.push(d_error); DyNTS_GlobalService.globalErrorHandler?.(d_error); reject(d_error); } else { const d_error: DyFM_Error = new DyFM_Error({ ...this._getDefaultErrorSettings('httpServer.on(error)', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-AS0-SE4`, message: `HTTP (open) server ERROR`, level: DyFM_ErrorLevel.serious, }); DyNTS_GlobalService.globalErrorHandler?.(d_error); } }) .on('uncaughtException', (exception): void => { const d_error: DyFM_Error = new DyFM_Error({ ...this._getDefaultErrorSettings('httpServer.on(uncaughtException)', exception), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-AS0-SE5`, message: `HTTP (open) server uncaughtException`, level: DyFM_ErrorLevel.critical, }); if (this.debugLog) { DyFM_Log.warn(`\nHTTP (open) server uncaughtException`, d_error); } DyNTS_GlobalService.globalErrorHandler?.(d_error); }); }); } } catch (error) { throw new DyFM_Error({ ...this._getDefaultErrorSettings('startExpresses', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-AS0-SE0`, }); } } /** * */ private async expressErrorHandling(error, req, res, next): Promise { try { if (error) { const d_error: DyFM_Error = new DyFM_Error({ ...this._getDefaultErrorSettings('expressErrorHandling', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-AS0-EEH1`, message: `Express ERROR`, additionalContent: { req, res, }, level: DyFM_ErrorLevel.error, }); await DyNTS_GlobalService.globalErrorHandler?.(d_error, req, res); // Kliens-safe strip (BFR-FDPAUTHSERVICE-002): az express-error út eddig NYERS error-t küldött // (0 strip) — DyFM-nél belső stack/verzió/issuer, axios-jellegű error-nál akár auth-header is // kimehetett. A strip-szabályok SSOT-ja a DyNTS_ClientSafeError_Util. res.send(DyNTS_ClientSafeError_Util.toClientSafe(error)) } else { DyFM_Log.H_error( 'WTF??? express error; without error?...' + '\nerr:', error, '\nreq:', req, '\nres:', res ); } } catch (error) { DyFM_Log.H_error( 'MULTILEVEL ERROR (expressErrorHandling)....' + '\n', error ); } } /** * */ private async mountSecureRoutes (): Promise { try { if (this.fnLogs && this.deepLog) DyFM_Log.log('\nfn:. mountSecureRoutes'); if (!this.secureExpress) { throw new Error( 'secureExpress was not initialized. ' + 'Secure routes require getCertificationSettings and httpsPort, and cert files must exist.' + '\n\nYou need to set security to secure or both in any route and set httpsPort in getPortSettings.' + '\n\nYou need to set cert files in getCertificationSettings.' ); } this.secureExpress.use( (error, req, res, next): Promise => this.expressErrorHandling(error, req, res, next) ); await DyFM_Array.asyncForEach( this._routingModules, async (module: DyNTS_RoutingModule): Promise => { if (module.security !== DyNTS_RouteSecurity.open) { if (this.logSetup) { DyFM_Log.log(`route mount (secure): ${module.route}`); } const existingRoutes: DyNTS_RoutingModule[] = this._routingModules.filter( (mod: DyNTS_RoutingModule): boolean => mod.route === module.route ); if (1 < existingRoutes.length) { const error: Error = new Error(`ROUTE DUPLICATION: ${module.route}`); /* const errorStack: string[] = error.stack.split('\n'); errorStack.splice(1, 4); error.stack = errorStack.join('\n'); */ error.stack = module.stackLocation; DyFM_Log.S_error(`ROUTE DUPLICATION: ${module.route}`, error); throw new DyFM_Error({ ...this._getDefaultErrorSettings('mountSecureRoutes', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-AS0-MSR1`, message: `ROUTE DUPLICATION: ${module.route}`, }); } this.secureExpress.use(module.route, module.secureRouter); } } ); } catch (error) { throw new DyFM_Error({ ...this._getDefaultErrorSettings('mountSecureRoutes', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-AS0-MSR0`, }); } } /** * */ private async mountOpenRoutes(): Promise { try { if (this.fnLogs && this.deepLog) DyFM_Log.log('\nfn:. mountOpenRoutes'); if (!this.openExpress) { throw new Error( 'openExpress was not initialized. Open routes require getOpenExpress and httpPort.' + '\n\nYou need to set security to open or both in any route and set httpPort in getPortSettings.' ); } this.openExpress.use( (error, req, res, next): Promise => this.expressErrorHandling(error, req, res, next) ); await DyFM_Array.asyncForEach( this._routingModules, async (module: DyNTS_RoutingModule): Promise => { if (module.security !== DyNTS_RouteSecurity.secure) { if (this.logSetup) { DyFM_Log.log(`route mount (open): ${module.route}`); } const existingRoutes: DyNTS_RoutingModule[] = this._routingModules.filter( (mod: DyNTS_RoutingModule): boolean => mod.route === module.route ); if (1 < existingRoutes.length) { const error: Error = new Error(`ROUTE DUPLICATION: ${module.route}`); /* const errorStack: string[] = error.stack.split('\n'); errorStack.splice(1, 4); error.stack = errorStack.join('\n'); */ error.stack = module.stackLocation; DyFM_Log.S_error(`ROUTE DUPLICATION: ${module.route}`, error); throw new DyFM_Error({ ...this._getDefaultErrorSettings('mountOpenRoutes', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-AS0-MOR1`, message: `ROUTE DUPLICATION: ${module.route}`, }); } this.openExpress.use(module.route, module.openRouter); } } ); } catch (error) { throw new DyFM_Error({ ...this._getDefaultErrorSettings('mountOpenRoutes', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-AS0-MOR0`, }); } } /** * FR-041 — CORS middleware. Echoes a matching `Access-Control-Allow-Origin` * for any request whose Origin header is in `getCorsSettings().allowedOrigins`, * adds the canonical `Access-Control-*` companion headers, and short-circuits * the OPTIONS preflight with a 204. * * No-op when the subclass does NOT override `getCorsSettings()` — preserves * back-compat for apps that have always been same-origin and never needed * CORS (the typical pre-FR-041 case). * * Why hand-rolled instead of the `cors` npm package: * - ~30 lines, zero new dependency. * - Full control over Vary/credentials/preflight semantics. * - Aligns with FDP "no unnecessary deps" philosophy. */ /** * BFR-011 / SEC-R2-8 — security response-headerek mountolása (a `mountCors` tükörképe). * A `DyNTS_global_settings.securityHeaders` alapján (default-ON) minden válaszra ráteszi a * biztonsági headereket + eltávolítja az `x-powered-by`-t. A CORS-mount ELŐTT hívandó, hogy az * OPTIONS preflight-válaszok (amiket a mountCors 204-gyel rövidre zár) is megkapják. */ protected mountSecurityHeaders(express: Express.Application): void { const settings: DyNTS_SecurityHeaders_Settings | undefined = DyNTS_global_settings.securityHeaders; if (!settings || !settings.enabled) { return; } if (settings.removePoweredBy !== false) { express.disable('x-powered-by'); } express.use(DyNTS_createSecurityHeadersMiddleware( settings, DyNTS_getEnvironment_settings().environment, )); } protected mountCors(express: Express.Application): void { const settings: DyNTS_Cors_Settings | undefined = this.getCorsSettings?.(); if (!settings) { return; } const allowCreds: boolean = settings.allowCredentials !== false; const methods: string[] = settings.allowedMethods ?? [ 'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', ]; const headers: string[] = settings.allowedHeaders ?? [ 'Authorization', 'Content-Type', 'X-Admin-Key', 'X-Requested-With', ]; const exposed: string[] = settings.exposedHeaders ?? [ 'Content-Length', 'Content-Type', ]; const maxAge: number = settings.maxAgeSeconds ?? 86400; const isAllowed: (origin: string) => boolean = (origin: string): boolean => { if (settings.allowedOrigins === '*') return true; if (typeof settings.allowedOrigins === 'function') { return settings.allowedOrigins(origin); } return settings.allowedOrigins.includes(origin); }; express.use((req: Express.Request, res: Express.Response, next: Express.NextFunction): void => { const origin: string | undefined = req.headers.origin; if (origin && isAllowed(origin)) { // Echo the matching origin (NOT wildcard) because credentials require it. res.setHeader('Access-Control-Allow-Origin', origin); if (allowCreds) { res.setHeader('Access-Control-Allow-Credentials', 'true'); } res.setHeader('Access-Control-Allow-Methods', methods.join(', ')); res.setHeader('Access-Control-Allow-Headers', headers.join(', ')); res.setHeader('Access-Control-Expose-Headers', exposed.join(', ')); res.setHeader('Access-Control-Max-Age', String(maxAge)); res.setHeader('Vary', 'Origin'); } if (req.method === 'OPTIONS') { res.status(204).end(); return; } next(); }); } /** * Generikus, auth-agnosztikus extension-point orchestrator. A `registerCustomMiddleware` * hookot hívja (ha a subclass override-olja) minden AKTÍV Express instance-ra * (open + secure), az API route-ok UTÁN, de a SPA static catch-all (`app.get('*')`) * ELŐTT — ez az EGYETLEN korrekt pont egy sub-path middleware-hez (pl. reverse-proxy), * mert a catch-all minden utána mountolt route-ot elnyel. No-op ha nincs override (back-compat). */ private async mountCustomMiddleware(): Promise { if (!this.registerCustomMiddleware) { return; } if (this.openExpress) { await this.registerCustomMiddleware(this.openExpress, DyNTS_RouteSecurity.open); } if (this.secureExpress) { await this.registerCustomMiddleware(this.secureExpress, DyNTS_RouteSecurity.secure); } if (this.logSetup) { DyFM_Log.log('\nCustom middleware registered (pre-static).'); } } /** * Ha getStaticClientSettings() visszaad beállításokat, a client static fájlok a '/' alatt * lesznek kiszolgálva (API route-ok után). SPA fallback opcionális (fallbackPath). * Asset cache (assetCacheMaxAge, assetCacheImmutable) és fallback cache (fallbackCacheMaxAge) * opcionálisak; ha nincs megadva fallbackCacheMaxAge, DyNTS_defaultFallbackCacheMaxAge (0) marad. */ private async mountStaticClient(): Promise { const settings: DyNTS_StaticClient_Settings | undefined = this.getStaticClientSettings?.(); if (!settings) { return; } const absoluteRoot: string = Path.isAbsolute(settings.root) ? settings.root : Path.resolve(process.cwd(), settings.root); const hasAssetCache: boolean = settings.assetCacheMaxAge !== undefined || settings.assetCacheImmutable === true; const staticOptions: { setHeaders: (res: Express.Response, filePath: string) => void } | undefined = hasAssetCache && settings.assetCacheMaxAge !== undefined ? { setHeaders: (res: Express.Response, filePath: string): void => { // A HTML entry-fájlok (index.html) no-cache-t kapnak, a hash-elt asset-ek // immutable long-cache-t — lásd DyNTS_resolveStaticCacheControlHeader (served-stale-index fix). res.setHeader( 'Cache-Control', DyNTS_resolveStaticCacheControlHeader({ filePath: filePath, assetCacheMaxAge: settings.assetCacheMaxAge!, assetCacheImmutable: settings.assetCacheImmutable === true, }), ); }, } : undefined; const staticMiddleware: Express.Handler = staticOptions ? Express.static(absoluteRoot, staticOptions) : Express.static(absoluteRoot); const fallbackMaxAge: number = settings.fallbackCacheMaxAge ?? DyNTS_defaultFallbackCacheMaxAge; // API-namespace guard (BFR-DYNAMOBUILDER-001): a catch-all fallback NE fedje az API-prefixeket — // egy ismeretlen `GET /api/...` 404 JSON-t kapjon, ne megtévesztő `200 + index.html`-t. // Konfigurálható (fallbackExcludePrefixes); default a bevett FDP namespace-pár. const excludePrefixes: string[] = settings.fallbackExcludePrefixes ?? ['/api', '/auth-api']; const isApiNamespacePath = (path: string): boolean => excludePrefixes.some((prefix: string): boolean => path === prefix || path.startsWith(`${prefix}/`)); if (this.openExpress) { this.openExpress.use('/', staticMiddleware); if (settings.fallbackPath) { this.openExpress.get('*', (req: Express.Request, res: Express.Response): void => { if (isApiNamespacePath(req.path)) { res.status(404).send({ message: `Unknown API endpoint: ${req.path}` }); return; } res.setHeader('Cache-Control', `max-age=${fallbackMaxAge}`); res.sendFile(settings.fallbackPath!, { root: absoluteRoot }); }); } else { this.openExpress.get('*', (req: Express.Request, res: Express.Response): void => { if (isApiNamespacePath(req.path)) { res.status(404).send({ message: `Unknown API endpoint: ${req.path}` }); return; } res.status(404).type('html').send(DyNTS_defaultNotFoundPageHtml); }); } } if (this.secureExpress) { this.secureExpress.use('/', staticMiddleware); if (settings.fallbackPath) { this.secureExpress.get('*', (req: Express.Request, res: Express.Response): void => { if (isApiNamespacePath(req.path)) { res.status(404).send({ message: `Unknown API endpoint: ${req.path}` }); return; } res.setHeader('Cache-Control', `max-age=${fallbackMaxAge}`); res.sendFile(settings.fallbackPath!, { root: absoluteRoot }); }); } else { this.secureExpress.get('*', (req: Express.Request, res: Express.Response): void => { if (isApiNamespacePath(req.path)) { res.status(404).send({ message: `Unknown API endpoint: ${req.path}` }); return; } res.status(404).type('html').send(DyNTS_defaultNotFoundPageHtml); }); } } if (this.logSetup) { DyFM_Log.log(`\nStatic client mounted at / (root: ${absoluteRoot})`); } } /** * */ private setSecurity(): void { if (this.fnLogs && this.deepLog) DyFM_Log.log('\nfn:. setSecurity'); this._routingModules.forEach((module: DyNTS_RoutingModule): void => { if (!module.security) { DyFM_Log.warn(`RoutingModule security is not set for ${module.route}\n`); } else if (!this._security) { this._security = module.security; } else if (this._security !== module.security) { this._security = DyNTS_RouteSecurity.both; } }); if (!this._security) { let msg = `Could not set security for the server! (${this.security})`; msg += '\n RoutingModules:'; this._routingModules.forEach((module: DyNTS_RoutingModule): void => { msg += `\n ${module.route} (security: ${module.security})`; }); if (this._routingModules.length === 0) { msg += '\n - no RoutingModule found -\n'; } DyFM_Log.warn(msg); } } private _getDefaultErrorSettings( fnName: string, error: DyFM_AnyError ): DyFM_Error_Settings { return { status: (error as DyFM_Error)?.___status ?? 500, message: (error as Error)?.message ?? (error as DyFM_Error)?._message ?? `${fnName} was UNSUCCESSFUL (NTS)`, userMessage: (error as DyFM_Error)?.__userMessage ?? this.defaultErrorUserMsg, addECToUserMsg: !(error as DyFM_Error)?.__userMessage, issuerService: `${this?.constructor?.name}-DyNTS_App`, level: DyFM_ErrorLevel.fatal, error: error, systemVersion: DyNTS_global_settings.systemVersion, }; } /** * MISSING Description (TODO) */ abstract getAppParams(): DyNTS_App_Params; /** * MISSING Description (TODO) */ abstract getGlobalServiceCollection(): DyNTS_GlobalService_Settings; /** * MISSING Description (TODO) */ abstract getPortSettings(): DyNTS_Http_Settings; /** * MISSING Description (TODO) */ overrideDynamoNTSGlobalSettings?(): void; /** * Ha megadva, a visszaadott értéket használjuk a route prefix-ként * (DyNTS_global_settings.baseUrl felülírása erre az app-ra). */ getApiBasePath?(): string; /** * MISSING Description (TODO) */ getRoutingModules?(): DyNTS_RoutingModule[]; /** * MISSING Description (TODO) */ getRootServices?(): Promise; /** * MISSING Description (TODO) */ getCertificationSettings?(): DyNTS_Certification_Settings; /** * Ha megadva, a client static fájlok a '/' alatt lesznek kiszolgálva (API route-ok után). * Opcionális fallbackPath pl. SPA index.html-hez. */ getStaticClientSettings?(): DyNTS_StaticClient_Settings | undefined; /** * FR-041 — Ha megadva, CORS middleware mount-olódik mindkét Express instance-ra * (open + secure). Az `allowedOrigins` listán szereplő Origin-eket echo-zza vissza, * `Access-Control-Allow-Credentials: true`-val (default) együtt a Bearer JWT * cross-domain auth flow miatt. OPTIONS preflight 204-gyel rövidre záródik. * Ha undefined → no CORS headers, no preflight handling — back-compat. */ getCorsSettings?(): DyNTS_Cors_Settings | undefined; /** * Opcionális generikus hook: custom Express middleware regisztrálása az API route-ok * UTÁN és a SPA static catch-all ELŐTT. Auth-AGNOSZTIKUS framework extension-point — * a subclass (pl. FDP base App) ezen keresztül mount-olhat reverse-proxyt egy sub-path-re * (pl. `/auth-api`), ami a `app.get('*')` catch-all elé kell kerüljön. A hívási időzítést * lásd: `mountCustomMiddleware`. Undefined → no-op (back-compat). */ registerCustomMiddleware?( express: Express.Application, security: DyNTS_RouteSecurity, ): void | Promise; /** * MISSING Description (TODO) */ createEntries?(): Promise; /** * MISSING Description (TODO) */ postProcess?(): Promise; }