import { Request, Response } from 'express'; import { DyFM_AnyError, DyFM_Array, DyFM_Endpoint_SettingsBase, DyFM_EnvironmentFlag, DyFM_Error, DyFM_getConstructionStackLocation, DyFM_HttpCallType, DyFM_Log, DyFM_Object } from '@futdevpro/fsm-dynamo'; import { DyNTS_global_settings } from '../../_collections/global-settings.const'; import { DyNTS_ClientSafeError_Util } from '../../_collections/client-safe-error.util'; import { DyNTS_RouteSecurity } from '../../_enums/route-security.enum'; import { DyNTS_GlobalService } from '../../_services/core/global.service'; /** * High level endpoint for API * used for specific events such as saving or getting data, * triggering events, logging in or out, and much more * * (You'll need to add them to a {@link DyNTS_Controller} through the {@link DyNTS_Controller.addEndpoint} method) * * @example * ```typescript * // Example from note-book controller implementation * new DyNTS_Endpoint_Params({ * name: 'getMyNoteBooks', * type: DyFM_HttpCallType.get, * endpoint: '/:userId/get-my', * preProcesses: [ authService.authenticate_tokenSelf ], * tasks: [ * async (req: Request, res: Response, issuer: string): Promise => { * const notes_DS = new NoteBook_DataService({ * issuer: issuer, * }); * * const notes = await notes_DS.getMyNoteBooks(req.params.userId); * res.send(notes); * }, * ], * }) * ``` */ export class DyNTS_Endpoint_Params< T_Result = unknown, I_Inputs extends Array = [], T_Body = undefined > extends DyFM_Endpoint_SettingsBase { /* name: string; */ security: DyNTS_RouteSecurity; /* type: DyFM_HttpCallType; endpoint: string; */ /* TODO: ENCRYPTION !!EZTET useEncryptionKey?: string; */ private readonly pathParams: string[]; private readonly preProcesses: ((req: Request, res: Response) => Promise)[] = []; private readonly tasks: ((req: Request, res: Response, issuer?: string) => Promise)[]; private readonly logRequest: boolean; private readonly logRequestsParams: boolean; private readonly logRequestsContent: boolean; private readonly logResponseContent: boolean; readonly stackLocation: string; private readonly autoResolveCirculation: boolean; constructor( set: { /** * naming the endpoint will help to follow events on service */ name: string, /** * security settings for API, different security paths will need different handlers * * open; http, secure; https or both */ security?: DyNTS_RouteSecurity, /** * define basic Http Call Type such as; get, post, put, patch, delete */ type: DyFM_HttpCallType, /** * set endpoint here, without baseUrl and route module path * * NOTE: the endpoint will already include the routing module's endpoint. * like: '/get-user/:userId' will be like: '/api/user/get-user/:userId' */ endpoint: string, /** * preProcesses are the functions you need to run before the actual function, * such as authentications */ preProcesses?: ((req: Request, res: Response) => Promise)[], /** * the actual tasks to run, * the last one should contain the res.send(); execution to send response on API requests */ tasks: ((req: Request, res: Response, issuer: string) => Promise)[], /** * log settings for request and response */ logRequest?: boolean, /** * log settings for request params */ logRequestsParams?: boolean, /** * log settings for request content */ logRequestsContent?: boolean, /** * log settings for response content */ logResponseContent?: boolean, /** * auto resolve circulation errors */ autoResolveCirculation?: boolean, } ) { try { super(set); this.name = set.name ?? set.endpoint; this.security = set.security ?? DyNTS_global_settings.defaultRouteSecurity; this.type = set.type; this.endpoint = set.endpoint; this.logRequest = set.logRequest ?? DyNTS_global_settings.log_settings.request; this.logRequestsParams = set.logRequestsParams ?? DyNTS_global_settings.log_settings.requestsParams; this.logRequestsContent = set.logRequestsContent ?? DyNTS_global_settings.log_settings.requestsContent; this.logResponseContent = set.logResponseContent ?? DyNTS_global_settings.log_settings.responseContent; this.autoResolveCirculation = set.autoResolveCirculation ?? DyNTS_global_settings.autoResolveEndpointCirculationErrors; if (!this.endpoint) { throw new DyFM_Error({ status: 406, errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-EP0-C00`, addECToUserMsg: true, message: 'trying to create DyNTS_Endpoint_Params without endpoint value', userMessage: 'We encountered an unhandled Server Error, ' + 'please contact the responsible development team.', additionalContent: set, }); } let pathParts: string[] = this.endpoint.split('/'); pathParts = pathParts.filter((part: string): boolean => part.startsWith(':')); pathParts = pathParts.map((part: string): string => part.replace(':', '')); this.pathParams = pathParts; this.preProcesses = set.preProcesses ?? []; this.tasks = set.tasks ?? []; this.stackLocation = DyFM_getConstructionStackLocation(); } catch (error) { DyFM_Log.error( `\nEndpoint params setup failed: name: '${set.name}' ` + `(security: ${set.security}) endpoint: ${set.endpoint}` + `\nERROR:` + `\n`, error ); throw error; } } /** * * @returns */ private async preLog(req: Request, res: Response, issuer: string): Promise { try { if (this.logRequest) { let msg: string = `\n===>>> incoming '${this.name}' request... (issuer: ${issuer})`; if (this.logRequestsParams) { const params = this.getPathParamsLogContent(req); msg += `\npathParams: ${params}`; } if ( this.logRequestsContent && req.body && 0 < Object.keys(req.body).length ) { DyFM_Log.info(msg + `\nbody:`, req.body); } else { DyFM_Log.info(msg); } } } catch (error) { this.error(req, res, error, issuer); } } /** * * @returns */ getFullExecution(): (req: Request, res: Response) => Promise { let issuer: string; return async (req: Request, res: Response): Promise => { try { let issuer: string; try { issuer = DyNTS_GlobalService?.getAuthService()?.getIssuerFromRequest(req); } catch (error) { DyFM_Log.warn(DyFM_Error.getAnyMessage(error)); issuer = 'unknown-issuer'; } if (this.logRequest) { await this.preLog(req, res, issuer); } if (DyNTS_global_settings.log_settings.requestStackLocation) { DyFM_Log.info(` endpoint location: "${this.stackLocation}"`); } if (!this.preProcesses) { throw new DyFM_Error({ status: 500, errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-EP1-C00`, addECToUserMsg: true, message: 'trying to execute DyNTS_Endpoint_Params without preProcesses value, ' + 'make sure you are using DyNTS_Endpoint_Params constructor correctly', userMessage: 'We encountered an unhandled Server Error, ' + 'please contact the responsible development team.', }); } await DyFM_Array.asyncForEach(this.preProcesses, async (preProcess: (req: Request, res: Response) => Promise): Promise => { await preProcess(req, res); } ); // FR-263 defense-in-depth (security): ha egy preProcess (pl. egy auth-guard) MÁR választ küldött // (`res.headersSent` — pl. egy 401-et vagy redirect-et), NE futtassuk a task-okat. Enélkül egy // response-t küldő guard után is lefutna az endpoint-logika, és a task 200-as válasza felül- // írhatná/megelőzhetné a guard elutasítását → info-disclosure (l. FR-263 auth error-endpoint leak). // Standard Express-minta; a normál (nem-válaszoló preProcess-ű) endpointokra nincs hatással. if (res.headersSent) { return; } await DyFM_Array.asyncForEach(this.tasks, async ( task: (req: Request, res: Response, issuer?: string) => Promise ): Promise => { await task(req, res, issuer); } ); if (this.logRequest) { if (this.logResponseContent) { DyFM_Log.success(` <<<===== '${this.name}' result sent.`); DyFM_Log.warn('sorry, the logResponseContent is not implemented yet.'); } else { DyFM_Log.success(` <<<===== '${this.name}' result sent.`); } } } catch (error) { this.error(req, res, error, issuer); } }; } /** * * @param res * @param error */ private async error( req: Request, res: Response, error: DyFM_AnyError, issuer: string, autoSecondTry?: boolean ): Promise { try { let msg: string = `Endpoint caught an error. '${this.name}' (${this.endpoint})`; msg += this.getPathParamsLogContent(req); if (error instanceof DyFM_Error) { msg += `\n ErrorCode: ${error._errorCode}`; } msg += `\n error: "${(error as DyFM_Error)?._message ?? (error as Error)?.message}"`; msg += '\n at ' + this.stackLocation; DyFM_Log.testError(msg); if ( DyNTS_global_settings.log_settings.highDetailedLogs || !(error instanceof DyFM_Error) ) { DyFM_Log.H_error( `Endpoint "${this.endpoint}" caught an error. (${this.name})`, '\n ERROR:', error ); } else if (DyNTS_global_settings.log_settings.api_errors) { error.logSimple(`Endpoint "${this.endpoint}" caught an error. (${this.name})`); } if ((error as DyFM_Error)?.flag?.includes('DYNAMO')) { if (!(error as DyFM_Error).additionalContent) { (error as DyFM_Error).additionalContent = {}; } (error as DyFM_Error).additionalContent.endpointInfo = msg; } // FR-071 (2026-06-06) — rich debug-descriptive request-context. // Per memory [[feedback_rich_error_handling]] minden errornak debug-level // descriptive-nek kell lennie + persisted to errors table. Az endpoint- // params catch-blokkjaban minden errorhoz csatoljuk a request snapshot-ot // (URL, method, headers KULCSAI ertek nelkul, query, params, origin, // user-agent), igy a TEST environment-en a kliens-oldali fejleszto a // response-bol latja MI hianyzott a request-bol, NEM csak az error-code-ot. // Header-VALUEK nem szivarognak ki — csak a kulcs-listat es az auth-header // alakjat (length + first 7 chars hogy lassek "Bearer "-rel kezd-e). try { const headerKeys: string[] = Object.keys(req?.headers ?? {}); const authHeader: unknown = req?.headers?.['authorization']; const authHeaderShape: string = typeof authHeader === 'string' ? `present (len=${authHeader.length}, startsWith="${authHeader.substring(0, 7)}")` : 'MISSING'; const requestSnapshot: Record = { url: req?.originalUrl ?? req?.url, method: req?.method, origin: req?.headers?.origin ?? null, referer: req?.headers?.referer ?? null, userAgent: req?.headers?.['user-agent'] ?? null, hostHeader: req?.headers?.host ?? null, headerKeyCount: headerKeys.length, headerKeys: headerKeys.sort(), hasAuthorizationKey: headerKeys.includes('authorization'), authHeaderShape: authHeaderShape, hasSecretKeyKey: headerKeys.includes('secret-key'), hasAdminKeyKey: headerKeys.includes('x-admin-key'), hasContentTypeKey: headerKeys.includes('content-type'), params: req?.params, query: req?.query, remoteAddress: req?.ip ?? null, }; if (error instanceof DyFM_Error) { if (!(error as DyFM_Error).additionalContent) { (error as DyFM_Error).additionalContent = {}; } (error as DyFM_Error).additionalContent.requestDebug = requestSnapshot; } // Plusz: 1-soros debug log a server-side log-buffer-ben. DyFM_Log.warn( `[FR-071 req-debug] ${req?.method ?? '?'} ${req?.originalUrl ?? req?.url ?? '?'} ` + `(endpoint=${this.name}, origin=${req?.headers?.origin ?? 'none'}, ` + `auth=${headerKeys.includes('authorization') ? 'yes' : 'NO'}, ` + `secretKey=${headerKeys.includes('secret-key') ? 'yes' : 'no'}, ` + `adminKey=${headerKeys.includes('x-admin-key') ? 'yes' : 'no'}, ` + `errorCode=${(error as DyFM_Error)?._errorCode ?? '(raw)'})` ); } catch /* (snapshotErr) */ { // ha a snapshot kepzese maga is bukik, ne maszek bele az error-flow-ba. } await DyNTS_GlobalService.globalErrorHandler?.( error, req, res, issuer ).catch((err): void => { DyFM_Log.warn( 'DyNTS_GlobalService.globalErrorHandler failed to handle error: ', err ); DyFM_Log.warn('It will proceed as normal.'); }); // FR-067 (2026-06-06): non-DyFM_Error wrap. // Korabban a plain Error / axios HTTP error nyersen ment kifele // -> kliens "[object Object]"-et latott, nem volt _errorCode, nem volt // userMessage, a kliens-oldali DyFM_Error pipeline ures error-t kapott. // Most MINDEN non-DyFM_Error errort egy DyFM_Error-ba csomagolunk // az endpoint-nev-vel es a system short-code-jal egy felismerheto // `_errorCode`-szal: `|DyNTS-EPP-`. // Plusz a downstream HTTP-status-t (axios `status` / Express `statusCode`) // betesszuk a DyFM_Error `___status` mezojebe, igy a kovetkezo blokk // mar konzekvensen olvasni tudja. if (!(error instanceof DyFM_Error)) { const errAny: { statusCode?: unknown; status?: unknown; message?: string } = error as { statusCode?: unknown; status?: unknown; message?: string }; const downstreamRaw: unknown = errAny?.statusCode ?? errAny?.status; const downstreamStatus: number | undefined = typeof downstreamRaw === 'number' ? downstreamRaw : (typeof downstreamRaw === 'string' && downstreamRaw !== '' && !isNaN(Number(downstreamRaw))) ? Number(downstreamRaw) : undefined; const wrapStatus: number = ( downstreamStatus !== undefined && downstreamStatus >= 100 && downstreamStatus <= 599 ) ? downstreamStatus : 500; error = new DyFM_Error({ error: error as Error, message: errAny?.message ?? `Unhandled error in endpoint "${this.name}"`, errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-EPP-${this.name ?? 'UNKNOWN'}`, userMessage: 'An internal error occurred.\nPlease contact the responsible development team.', status: wrapStatus, }); } if (error instanceof DyFM_Error) { if (DyNTS_global_settings.compactErrorResponse) { error = (error as DyFM_Error).getErrorsFlat(); delete (error as DyFM_Error).errors; /* delete (error as DyFM_Error).error; */ delete (error as DyFM_Error).stack; } } if ( (error as DyFM_Error)?.flag?.includes('DYNAMO') && (error as DyFM_Error).confidentialContent ) { delete (error as DyFM_Error).confidentialContent; } try { JSON.stringify(error); } catch /* (stringifyError) */ { if (this.autoResolveCirculation) { try { error = DyFM_Object.resolveCirculation(error); JSON.stringify(error); } catch /* (resolveError) */ { DyFM_Log.H_error( 'DyNTS_Endpoint_Params: error object is not serializable, and not resolvable!' ); error = new DyFM_Error({ error: new Error('UNRESOLVABLE UNSERIALIZABLE ERROR'), }); } } else { DyFM_Log.H_error('DyNTS_Endpoint_Params: error object is not serializable!'); error = new DyFM_Error({ error: new Error('UNSERIALIZABLE ERROR'), }); } } // FR-066 (2026-06-05): a default-status fallback 501 helyett 500-ra // valtott. A 501 ('Not Implemented') felrevezeto klienseknek (az endpoint // LETEZIK, csak a handler ___status-nelkuli error-t dobott). A 500 // ('Internal Server Error') a standard fallback erre. Plusz: // tovabbi konvenciókat is olvasunk a status-resolver-ben — DyFM_Error // `___status`-en kívül a `statusCode` (Express convention) es `status` // (axios / general HTTP) mezőket is. Igy a downstream HTTP-hivasok // (pl. axios-bol jovo error) helyes HTTP-status-szal továbbítódnak. const errorAny: { statusCode?: unknown; status?: unknown } = error as { statusCode?: unknown; status?: unknown }; const resolvedStatusRaw: unknown = (error as DyFM_Error)?.___status ?? errorAny?.statusCode ?? errorAny?.status; const resolvedStatus: number | null = typeof resolvedStatusRaw === 'number' ? resolvedStatusRaw : (typeof resolvedStatusRaw === 'string' && !isNaN(Number(resolvedStatusRaw))) ? Number(resolvedStatusRaw) : null; if (resolvedStatus !== null && resolvedStatus >= 100 && resolvedStatus <= 599) { res.status(resolvedStatus); } else { res.status(500); } // Kliens-safe strip ENV-FÜGGETLENÜL (BFR-FDPAUTHSERVICE-002): a korábbi prod-only blokk a // test/dev környezetben TELJES DyFM-leaket hagyott (__localStack + ___systemVersion + issuer). // A szabályok (mindig-strip / prod-only additionalContent / kliens-mezők megtartása) SSOT-ja // a DyNTS_ClientSafeError_Util. res.send(DyNTS_ClientSafeError_Util.toClientSafe(error)); if (this.logRequest) { if (this.logResponseContent) { DyFM_Log.error( ` <<<===== '${this.name}' error sent: ${(error as DyFM_Error)?._message ?? ''}` ); DyFM_Log.error( 'sorry, the logResponseContent is not implemented yet.' ); } else { DyFM_Log.error( ` <<<===== '${this.name}' error sent: ${(error as DyFM_Error)?._message ?? ''}` ); } } DyFM_Log.error(''); } catch (errorLvl2) { this.multiLevelError( errorLvl2, { req: req, res: res, error: error, issuer: issuer, autoSecondTry: autoSecondTry, } ); } } private multiLevelError( errorLvl2: any, errorInputs: { req: Request, res: Response, error: Error | DyFM_Error, issuer: string, autoSecondTry: boolean } ): void { if (DyNTS_global_settings.log_settings.highDetailedLogs) { const msg = `\n\nDYNAMO MULTILEVEL ERROR:DyNTS_Endpoint_Params: error: ` + `(${this.name}, ${this.endpoint})` + `\n(DYNAMO MULTILEVEL ERROR means, that the ERROR HANDLING is ALSO FAILED, ` + `and the error message was not sent.)` + `\nthisEndpointStackLocation: ${this.stackLocation}`; if (errorLvl2 instanceof DyFM_Error) { errorLvl2.logSimple(msg); } else { DyFM_Log.H_error(msg, '\nERROR:', errorLvl2, '\n'); } } else { const msg = `\n\nDYNAMO MULTILEVEL ERROR:DyNTS_Endpoint_Params: error: ` + `(${this.name}, ${this.endpoint})` + `\nthisEndpointStackLocation: ${this.stackLocation}` + `\n(DYNAMO MULTILEVEL ERROR means, that the ERROR HANDLING is ALSO FAILED, ` + `and the error message was not sent.)`; if (errorLvl2 instanceof DyFM_Error) { errorLvl2.logSimple(msg); } else { DyFM_Log.H_error(msg, '\nERROR:', errorLvl2, '\n'); } } try { JSON.stringify(errorInputs.error); } catch /* (errorLvl2Replication) */ { DyFM_Log.error(' ...response object is not serializable!'); let resolvedError: any; try { resolvedError = DyFM_Object.resolveCirculation(errorInputs.error); } catch (errorLvl3) { DyFM_Log.error(' ...response object is not resolvable!'); DyFM_Log.error(' ...', errorLvl3); } if (resolvedError) { if (this.autoResolveCirculation && !errorInputs.autoSecondTry) { this.error( errorInputs.req, errorInputs.res, resolvedError, errorInputs.issuer, true ); DyFM_Log.warn(' ...automatic circulation error resolution was successful!'); return; } else { DyFM_Log.error( ' ...response object is resolvable! (use DyFM_Object.resolveCirculation)' ); } } } if ( DyNTS_global_settings.log_settings.highDetailedLogs || !(errorLvl2 instanceof DyFM_Error) ) { DyFM_Log.H_error( `Endpoint "${this.endpoint}" caught an error and FAILED TO RESOLVE. (${this.name})`, '\n ERROR:', errorLvl2 ); } else { errorLvl2.logSimple( `Endpoint "${this.endpoint}" caught an error and FAILED TO RESOLVE. (${this.name})` ); } } private getPathParamsLogContent(req: Request): string { let params: string = ''; this.pathParams.forEach((param: string): void => { params += `\n${param}: ${req.params[param]}`; }); return params; } }