import { Request, Response } from 'express'; import { DyFM_Array, DyFM_Error, DyFM_HttpCallType, DyFM_Errors, DyFM_RelativeDate } from '@futdevpro/fsm-dynamo'; import { DyNTS_Controller } from '../../../_services/route/controller.service'; import { DyNTS_Endpoint_Params } from '../../../_models/control-models/endpoint-params.control-model'; import { DyNTS_Errors_ControlService } from './errors.control-service'; import { DyNTS_global_settings } from '../../../_collections/global-settings.const'; /** * Auth retrofit config a `DyNTS_Errors_Controller`-hez. Opt-in — ha az * `authPreProcess` nincs megadva, a controller a regi (auth nelkuli) * viselkedest tartja. * * **Subclass-szintu konfiguracio:** az `authConfig` static, igy az abstract * osztaly minden subclass-a kozott OSZTOTT. Ha tobb subclass-od van kulonbozo * policy-val, hivd a `configure()`-t a megfelelo subclasson — TypeScript a * static field-eket per-class hatarozza meg. */ export interface DyNTS_ErrorsController_AuthConfig { /** * Pre-process fuggveny ami minden vedett endpoint elott fut. Tipikusan: * `DyNTS_AdminApiKey_AuthService.getInstance().verify`. * * Ha hianyzik vagy `undefined`, a controller NEM ad hozza auth-ot semelyik * endpoint-hoz (regi viselkedes, opt-in safety). */ authPreProcess?: (req: Request, res: Response) => Promise; /** * Vedett endpoint nevek listaja. Ha hianyzik, az osszes 8 endpoint vedett * (`authPreProcess` jelenleteben). * * Endpoint nevek: lasd `ALL_ENDPOINT_NAMES` (logError, newError, markErrorDone, * markAllErrorDone, recordFixAttempt, resolveError, getErrors, getErrorsPaged, * getErrorsByCategoryPaged, getErrorsByStatusPaged, getLastErrors, searchErrors). */ protectedEndpoints?: string[]; } /** * Az osszes built-in endpoint neve — a `configure({ protectedEndpoints })` * default-jat es a config validacio-t segiti. */ const ALL_ENDPOINT_NAMES: string[] = [ 'logError', 'newError', 'markErrorDone', 'markAllErrorDone', 'recordFixAttempt', 'resolveError', 'getErrors', 'getErrorsPaged', 'getErrorsByCategoryPaged', 'getErrorsByStatusPaged', 'getLastErrors', 'searchErrors', ]; /** * Endpoints: * * logError * POST /error/log * * markErrorDone * GET /error/mark-done/:errorId * * markAllErrorDone * GET /error/mark-all-done * * getErrors * GET /error/get-range/:range * * getErrorsPaged * GET /error/get-paged/:range/:pageSize/:pageIndex * * getLastErrors * GET /error/get-last-paged/:range/:pageSize/:pageIndex * * */ // Documentation: // Local: [MD](./errors.md) // export abstract class DyNTS_Errors_Controller< T_Error extends DyFM_Error, T_Errors extends DyFM_Errors, T_Error_ControlService extends DyNTS_Errors_ControlService > extends DyNTS_Controller { /* static getInstance(): DyNTS_Error_Controller { return DyNTS_Error_Controller.getSingletonInstance(); } */ /* protected abstract errorDataService: DyNTS_Error_DataService; */ protected abstract getError_ControlService( set: { data?: T_Error, issuer: string } ): T_Error_ControlService; protected readonly additionalEndpoints: DyNTS_Endpoint_Params[] = []; /** * Static auth config — opt-in retrofit. Default ures objektum → NO auth * (a meglevo integraciok valtoznatlan viselkedessel folytatodnak). */ protected static authConfig: DyNTS_ErrorsController_AuthConfig = {}; /** * Static config setter. Hivhato barmikor a szerver startup-jan az endpoint * registration ELOTT. * * Use case (host app): * ```ts * const adminAuth = DyNTS_AdminApiKey_AuthService.getInstance(); * DyNTS_Errors_Controller.configure({ authPreProcess: adminAuth.verify }); * // vagy fine-grained: * DyNTS_Errors_Controller.configure({ * authPreProcess: adminAuth.verify, * protectedEndpoints: ['markAllErrorDone', 'logError'], * }); * ``` */ static configure(config: DyNTS_ErrorsController_AuthConfig): void { DyNTS_Errors_Controller.authConfig = config; } /** * Aktualis auth config olvasasa (test/diagnosztika celokra). */ static getAuthConfig(): DyNTS_ErrorsController_AuthConfig { return DyNTS_Errors_Controller.authConfig; } /** * Test-only: visszaallitja az auth config-ot ures objektumra. Production code NE hivja. */ static _resetAuthConfigForTesting(): void { DyNTS_Errors_Controller.authConfig = {}; } /** * Vissza-adja az adott endpoint-hoz tartozo `preProcesses` array-t. * Ha az auth NINCS configurolva (default) vagy az endpoint nincs a * `protectedEndpoints` listan, ures array-t ad vissza. */ protected getPreProcessesFor(endpointName: string): ((req: Request, res: Response) => Promise)[] { const cfg: DyNTS_ErrorsController_AuthConfig = DyNTS_Errors_Controller.authConfig; if (!cfg.authPreProcess) { return []; } const protectedNames: string[] = cfg.protectedEndpoints ?? ALL_ENDPOINT_NAMES; return protectedNames.includes(endpointName) ? [cfg.authPreProcess] : []; } /** * FR-263 (2026-06-26) — LAZY, REQUEST-IDŐBEN kiértékelt guard-wrapper. * * GYÖKÉR-OK: a `setupEndpoints()` a controller ELSŐ konstrukciójakor fut, ami a built-in * error-controllernél a host-app `configure()`-ja ELŐTT történhet (a framework korán példányosítja * a singletont). Ilyenkor a `getPreProcessesFor()` még az ÜRES configot olvasta → a guard SOHA nem * kötődött → az admin error-read endpoint-ok (`get-range`, `get-paged`, …) token nélkül **200**-at * adtak vissza, kiszivárogtatva a belső error-logot (info-disclosure). A 39 másik (controller-szinten * HARDCODE-olt) guard azért működött, mert nem függ a `configure()` időzítésétől. * * FIX: a `preProcesses` egy wrapper, ami a configot KÉRÉS-IDŐBEN olvassa (`getPreProcessesFor`), így * a `configure()` SORREND-FÜGGETLEN — bármikor hívható az első kérés előtt. A 401-et a VALÓDI guard * küldi; `res.headersSent`-re leállunk (nincs dupla-send), és nem-védett endpointnál no-op (átenged). */ protected lazyPreProcessesFor(endpointName: string): ((req: Request, res: Response) => Promise)[] { return [ async (req: Request, res: Response): Promise => { const guards: ((req: Request, res: Response) => Promise)[] = this.getPreProcessesFor(endpointName); for (const guard of guards) { if (res.headersSent) { return; } await guard(req, res); } }, ]; } setupEndpoints(): void { /* if (!this.getErrorDataService) { throw new DyFM_Error({ message: 'getUserDataService() method not implemented in Error_Controller.', errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-ERC-001`, }); } */ if (!this.additionalEndpoints) { throw new DyFM_Error({ message: 'additionalEndpoints must be defined in Error_Controller.', errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-ERC-002`, }); } this.endpoints = [ new DyNTS_Endpoint_Params({ name: 'logError', type: DyFM_HttpCallType.post, endpoint: '/error/log', preProcesses: this.lazyPreProcessesFor('logError'), tasks: [ async (req: Request, res: Response, issuer: string = 'unknown-issuer'): Promise => { const error_CS: T_Error_ControlService = this.getError_ControlService({ issuer, }); await error_CS.recordError(req.body, issuer); res.send({ result: 'error logged', }); }, ], }), new DyNTS_Endpoint_Params({ name: 'newError', type: DyFM_HttpCallType.post, endpoint: '/error/new', preProcesses: this.lazyPreProcessesFor('newError'), tasks: [ async (req: Request, res: Response, issuer: string = 'unknown-issuer'): Promise => { const error_CS: T_Error_ControlService = this.getError_ControlService({ issuer, }); await error_CS.recordError(req.body, issuer); res.send({ result: 'error logged', }); }, ], }), new DyNTS_Endpoint_Params({ name: 'markErrorDone', type: DyFM_HttpCallType.get, endpoint: '/error/mark-done/:errorId', preProcesses: this.lazyPreProcessesFor('markErrorDone'), tasks: [ async (req: Request, res: Response, issuer: string = 'unknown-issuer'): Promise => { const error_CS: T_Error_ControlService = this.getError_ControlService({ issuer, }); await error_CS.deleteError(req.params.errorId, issuer, true); res.send({ result: 'error deleted', }); }, ], }), new DyNTS_Endpoint_Params({ name: 'markAllErrorDone', type: DyFM_HttpCallType.get, endpoint: '/error/mark-all-done', preProcesses: this.lazyPreProcessesFor('markAllErrorDone'), tasks: [ async (req: Request, res: Response, issuer: string = 'unknown-issuer'): Promise => { const error_CS: T_Error_ControlService = this.getError_ControlService({ issuer, }); await error_CS.deleteAllErrors(issuer, true); res.send({ result: 'all error deleted', }); }, ], }), new DyNTS_Endpoint_Params({ name: 'recordFixAttempt', type: DyFM_HttpCallType.post, endpoint: '/error/:errorId/fix-attempt', preProcesses: this.lazyPreProcessesFor('recordFixAttempt'), tasks: [ async (req: Request, res: Response, issuer: string = 'unknown-issuer'): Promise => { const errorId: string = req.params.errorId; const version: string = req.body?.version; const hypothesis: string = req.body?.hypothesis; const notes: string = req.body?.notes; if (!errorId || !version || !hypothesis) { res.status(400).send({ error: 'missing required field(s): errorId / version / hypothesis', }); return; } const error_CS: T_Error_ControlService = this.getError_ControlService({ issuer }); const attempt = await error_CS.recordFixAttempt(errorId, version, hypothesis, issuer, notes); res.send({ result: 'fix attempt recorded', attempt, }); }, ], }), new DyNTS_Endpoint_Params({ name: 'resolveError', type: DyFM_HttpCallType.post, endpoint: '/error/:errorId/resolve', preProcesses: this.lazyPreProcessesFor('resolveError'), tasks: [ async (req: Request, res: Response, issuer: string = 'unknown-issuer'): Promise => { const errorId: string = req.params.errorId; const notes: string = req.body?.notes; if (!errorId) { res.status(400).send({ error: 'missing required field: errorId', }); return; } const error_CS: T_Error_ControlService = this.getError_ControlService({ issuer }); await error_CS.resolveError(errorId, issuer, notes); res.send({ result: 'error resolved', }); }, ], }), /* new DyNTS_Endpoint_Params({ name: 'markErrorDone', type: DyFM_HttpCallType.get, endpoint: '/mark-error-done/:errorId', tasks: [ async (req: Request, res: Response, issuer?: string): Promise => { const errorDataService: DyNTS_Error_DataService = this.getErrorDataService({ data: { _id: req.params.errorId }, issuer: issuer, }); await errorDataService.deleteData(); res.send({ result: 'error deleted', }); }, ], }), */ new DyNTS_Endpoint_Params({ name: 'getErrors', type: DyFM_HttpCallType.get, endpoint: '/error/get-range/:range', preProcesses: this.lazyPreProcessesFor('getErrors'), tasks: [ async (req: Request, res: Response, issuer: string = 'unknown-issuer') : Promise => { const error_CS: T_Error_ControlService = this.getError_ControlService({ issuer, }); res.send( await error_CS.getErrorsInRange( req.params.range as DyFM_RelativeDate, issuer ) ); }, ], }), new DyNTS_Endpoint_Params({ name: 'getErrorsPaged', type: DyFM_HttpCallType.get, endpoint: '/error/get-paged/:range/:pageSize/:pageIndex', preProcesses: this.lazyPreProcessesFor('getErrorsPaged'), tasks: [ async (req: Request, res: Response, issuer: string = 'unknown-issuer') : Promise => { const error_CS: T_Error_ControlService = this.getError_ControlService({ issuer, }); let pageSize: number = req.params.pageSize ? +req.params.pageSize : 30; let pageIndex: number = req.params.pageIndex ? +req.params.pageIndex : 0; // TODO: temporary fix for too big error list loads if (30 < pageSize) { pageSize = 30; pageIndex = 0; } res.send( await error_CS.getErrorsPaged( req.params.range as DyFM_RelativeDate, pageSize, pageIndex, issuer ) ); }, ], }), new DyNTS_Endpoint_Params({ name: 'getErrorsByCategoryPaged', type: DyFM_HttpCallType.get, endpoint: '/error/get-by-category/:category/:range/:pageSize/:pageIndex', preProcesses: this.lazyPreProcessesFor('getErrorsByCategoryPaged'), tasks: [ async (req: Request, res: Response, issuer: string = 'unknown-issuer'): Promise => { const error_CS: T_Error_ControlService = this.getError_ControlService({ issuer }); let pageSize: number = req.params.pageSize ? +req.params.pageSize : 30; let pageIndex: number = req.params.pageIndex ? +req.params.pageIndex : 0; if (30 < pageSize) { pageSize = 30; pageIndex = 0; } res.send( await error_CS.getErrorsByCategoryPaged( req.params.category, req.params.range as DyFM_RelativeDate, pageSize, pageIndex, issuer ) ); }, ], }), new DyNTS_Endpoint_Params({ name: 'getErrorsByStatusPaged', type: DyFM_HttpCallType.get, endpoint: '/error/get-by-status/:status/:range/:pageSize/:pageIndex', preProcesses: this.lazyPreProcessesFor('getErrorsByStatusPaged'), tasks: [ async (req: Request, res: Response, issuer: string = 'unknown-issuer'): Promise => { const error_CS: T_Error_ControlService = this.getError_ControlService({ issuer }); let pageSize: number = req.params.pageSize ? +req.params.pageSize : 30; let pageIndex: number = req.params.pageIndex ? +req.params.pageIndex : 0; if (30 < pageSize) { pageSize = 30; pageIndex = 0; } res.send( await error_CS.getErrorsByStatusPaged( req.params.status, req.params.range as DyFM_RelativeDate, pageSize, pageIndex, issuer ) ); }, ], }), new DyNTS_Endpoint_Params({ name: 'getLastErrors', type: DyFM_HttpCallType.get, endpoint: '/error/get-last-paged/:range/:pageSize/:pageIndex', preProcesses: this.lazyPreProcessesFor('getLastErrors'), tasks: [ async (req: Request, res: Response, issuer: string = 'unknown-issuer') : Promise => { const error_CS: T_Error_ControlService = this.getError_ControlService({ issuer, }); let pageSize: number = req.params.pageSize ? +req.params.pageSize : 30; let pageIndex: number = req.params.pageIndex ? +req.params.pageIndex : 0; // TODO: temporary fix for too big error list loads if (30 < pageSize) { pageSize = 30; pageIndex = 0; } res.send( await error_CS.getLastErrors( req.params.range as DyFM_RelativeDate, pageSize, pageIndex, issuer ) ); }, ], }), new DyNTS_Endpoint_Params({ name: 'searchErrors', type: DyFM_HttpCallType.get, endpoint: '/error/search', preProcesses: this.lazyPreProcessesFor('searchErrors'), tasks: [ async (req: Request, res: Response, issuer: string = 'unknown-issuer') : Promise => { const error_CS: T_Error_ControlService = this.getError_ControlService({ issuer, }); res.send( await error_CS.searchErrors(req.body, issuer) ); }, ], }), ...this.additionalEndpoints, ]; } }