diff --git a/packages/data-model/docs/schema.dbml b/packages/data-model/docs/schema.dbml index c08e6c33..cfd1dc6f 100644 --- a/packages/data-model/docs/schema.dbml +++ b/packages/data-model/docs/schema.dbml @@ -92,7 +92,6 @@ table submissions { dictionary_id integer [not null] errors jsonb organization varchar [not null] - published_at timestamp status submission_status [not null] created_at timestamp [default: `now()`] created_by varchar diff --git a/packages/data-provider/index.ts b/packages/data-provider/index.ts index c4a11df5..9f44103e 100644 --- a/packages/data-provider/index.ts +++ b/packages/data-provider/index.ts @@ -20,6 +20,9 @@ export { } from './src/middleware/auth.js'; export { errorHandler } from './src/middleware/errorHandler.js'; +// middleware +export { actionLoggerMiddleware } from './src/middleware/actionLogger.js'; + // routes export { default as dictionaryRouters } from './src/routers/dictionaryRouter.js'; export { default as migrationRouter } from './src/routers/migrationRouter.js'; diff --git a/packages/data-provider/src/config/config.ts b/packages/data-provider/src/config/config.ts index 46caf505..623e32eb 100644 --- a/packages/data-provider/src/config/config.ts +++ b/packages/data-provider/src/config/config.ts @@ -32,6 +32,7 @@ export type SubmissionServiceConfig = { export type LoggerConfig = { level?: string; file?: boolean; + routerLoggerEnabled?: boolean; }; export type IdServiceConfig = { @@ -72,6 +73,7 @@ export interface BaseDependencies { features?: FeaturesConfig; idService: IdServiceConfig; logger: Logger; + routerLoggerEnabled: boolean; onFinishCommit?: (resultOnCommit: ResultOnCommit) => Promise; schemaService: SchemaServiceConfig; submissionService: SubmissionServiceConfig; diff --git a/packages/data-provider/src/controllers/auditController.ts b/packages/data-provider/src/controllers/auditController.ts index 6b74b92b..aad4e898 100644 --- a/packages/data-provider/src/controllers/auditController.ts +++ b/packages/data-provider/src/controllers/auditController.ts @@ -8,8 +8,7 @@ import { AuditPaginatedResponse } from '../utils/types.js'; const controller = (dependencies: BaseDependencies) => { const auditService = auditSvc(dependencies); - const { logger } = dependencies; - const LOG_MODULE = 'AUDIT_CONTROLLER'; + return { byCategoryIdAndOrganization: validateRequest(auditByCatAndOrgRequestSchema, async (req, res, next) => { try { @@ -23,8 +22,6 @@ const controller = (dependencies: BaseDependencies) => { // optional query parameters const { entityName, eventType, startDate, endDate, systemId } = req.query; - logger.info(LOG_MODULE, 'Request Audit', `categoryId '${categoryId}' organization '${organization}'`); - const auditRecords = await auditService.byCategoryIdAndOrganization(categoryId, { entityName, eventType, diff --git a/packages/data-provider/src/controllers/categoryController.ts b/packages/data-provider/src/controllers/categoryController.ts index 84a3c41e..02d063ac 100644 --- a/packages/data-provider/src/controllers/categoryController.ts +++ b/packages/data-provider/src/controllers/categoryController.ts @@ -15,8 +15,6 @@ const controller = (dependencies: BaseDependencies) => { try { const categoryId = Number(req.params.categoryId); - logger.info(LOG_MODULE, 'Request Get Category Details', `categoryId '${categoryId}'`); - const details = await categoryService.getDetails(categoryId); if (!details) { diff --git a/packages/data-provider/src/controllers/dictionaryController.ts b/packages/data-provider/src/controllers/dictionaryController.ts index 5435fab4..c49f196d 100644 --- a/packages/data-provider/src/controllers/dictionaryController.ts +++ b/packages/data-provider/src/controllers/dictionaryController.ts @@ -7,8 +7,7 @@ import { BaseDependencies } from '../config/config.js'; import dictionarySvc from '../services/dictionaryService.js'; import { NotFound } from '../utils/errors.js'; import { validateRequest } from '../utils/requestValidation.js'; -import { dictionaryRegisterRequestSchema } from '../utils/schemas.js'; -import { downloadDataFileTemplatesSchema } from '../utils/schemas.js'; +import { dictionaryRegisterRequestSchema, downloadDataFileTemplatesSchema } from '../utils/schemas.js'; import { RegisterDictionaryResult } from '../utils/types.js'; const controller = (dependencies: BaseDependencies) => { @@ -25,11 +24,6 @@ const controller = (dependencies: BaseDependencies) => { const forceRegistration = req.query.force?.toLowerCase() === 'true'; const user = req.user; - logger.info( - LOG_MODULE, - `Register Dictionary Request categoryName '${categoryName}' name '${dictionaryName}' version '${dictionaryVersion}'`, - ); - const { dictionary, category, migrationId } = await dictionaryService.register({ categoryName, dictionaryName, diff --git a/packages/data-provider/src/controllers/submissionController.ts b/packages/data-provider/src/controllers/submissionController.ts index f202dce5..a3d7373e 100644 --- a/packages/data-provider/src/controllers/submissionController.ts +++ b/packages/data-provider/src/controllers/submissionController.ts @@ -42,8 +42,6 @@ const controller = ({ }) => { const submissionService = createSubmissionService(baseDependencies); const dataService = createSubmittedDataService(baseDependencies); - const { logger } = baseDependencies; - const LOG_MODULE = 'SUBMISSION_CONTROLLER'; return { commit: validateRequest(submissionCommitRequestSchema, async (req, res, next) => { try { @@ -51,8 +49,6 @@ const controller = ({ const submissionId = Number(req.params.submissionId); const user = req.user; - logger.info(LOG_MODULE, `Request Commit Active Submission '${submissionId}' on category '${categoryId}'`); - const submission = await submissionService.getSubmissionById(submissionId); if (!submission) { throw new BadRequest(`Submission '${submissionId}' not found`); @@ -77,8 +73,6 @@ const controller = ({ const user = req.user; const force = req.query.force?.toLowerCase() === 'true'; - logger.info(LOG_MODULE, `Request Delete Active Submission '${submissionId}'`); - const submission = await submissionService.getSubmissionById(submissionId); if (!submission) { throw new BadRequest(`Submission '${submissionId}' not found`); @@ -116,11 +110,6 @@ const controller = ({ const index = req.query.index ? parseInt(req.query.index) : null; const user = req.user; - logger.info( - LOG_MODULE, - `Request Delete '${entityName ? entityName : 'all'}' records on '{${actionType}}' Active Submission '${submissionId}'`, - ); - const submission = await submissionService.getSubmissionById(submissionId); if (!submission) { throw new BadRequest(`Submission '${submissionId}' not found`); @@ -157,8 +146,6 @@ const controller = ({ const systemId = req.params.systemId; const user = req.user; - logger.info(LOG_MODULE, `Request Delete Submitted Data systemId '${systemId}' on categoryId '${categoryId}'`); - // get SubmittedData by SystemId const foundRecordToDelete = await dataService.getSubmittedDataBySystemId(categoryId, systemId, { view: 'flat', @@ -192,8 +179,6 @@ const controller = ({ const payload = req.body; const user = req.user; - logger.info(LOG_MODULE, `Request Edit Submitted Data`); - if (!payload || payload.length == 0) { throw new BadRequest( 'The "payload" parameter is missing or empty. Please include the records in the request for processing.', @@ -231,14 +216,6 @@ const controller = ({ const pageSize = parseInt(String(req.query.pageSize)) || DEFAULT_PAGE_SIZE; const username = req.query.username; - logger.info( - LOG_MODULE, - `Request Submission categoryId '${categoryId}'`, - `pagination params: page '${page}' pageSize '${pageSize}'`, - `onlyActive '${onlyActive}'`, - `organization '${organization}'`, - ); - const submissionsResult = await submissionService.getSubmissionsByCategory( categoryId, { page, pageSize }, @@ -265,8 +242,6 @@ const controller = ({ try { const submissionId = Number(req.params.submissionId); - logger.info(LOG_MODULE, `Request Active Submission submissionId '${submissionId}'`); - const submission = await submissionService.getSubmissionById(submissionId); if (isEmpty(submission)) { @@ -289,8 +264,6 @@ const controller = ({ const page = parseInt(String(req.query.page)) || DEFAULT_PAGE; const pageSize = parseInt(String(req.query.pageSize)) || DEFAULT_PAGE_SIZE; - logger.info(LOG_MODULE, `Request Submission Details by ID '${submissionId}'`); - const submission = await submissionService.getSubmissionDetailsById({ submissionId, paginationOptions: { page, pageSize }, @@ -311,11 +284,6 @@ const controller = ({ const categoryId = Number(req.params.categoryId); const organization = req.params.organization; - logger.info( - LOG_MODULE, - `Request Active Submission categoryId '${categoryId}' and organization '${organization}'`, - ); - // Get username from auth const username = req.user?.username || ''; @@ -344,13 +312,6 @@ const controller = ({ // TODO: Validate file-entity map in body: no duplicate filenames, and that entities exist in schemaNames - logger.info( - LOG_MODULE, - `Submission Request: categoryId '${categoryId}'`, - ` organization '${organization}'`, - ` entityName '${entityName}'`, - ); - // TODO: parse body payload if (!payload || !Array.isArray(payload) || payload.length == 0) { @@ -392,14 +353,6 @@ const controller = ({ // Get username from auth const username = req.user?.username || ''; - logger.info( - LOG_MODULE, - `Upload Submission Request: categoryId '${categoryId}'`, - ` organization '${organization}'`, - ` files '${files?.map((f) => f.originalname)}'`, - ` fileEntityMap ${JSON.stringify(fileEntityMap)}`, - ); - if (!shouldBypassAuth(req, authConfig) && !hasUserWriteAccess(organization, req.user)) { throw new Forbidden(`User is not authorized to submit data to '${organization}'`); } diff --git a/packages/data-provider/src/controllers/submittedDataController.ts b/packages/data-provider/src/controllers/submittedDataController.ts index b4c43d4c..5cd99669 100644 --- a/packages/data-provider/src/controllers/submittedDataController.ts +++ b/packages/data-provider/src/controllers/submittedDataController.ts @@ -42,13 +42,6 @@ const controller = ({ const view = convertToViewType(req.query.view) || defaultView; const user = req.user; - logger.info( - LOG_MODULE, - `Request Submitted Data on categoryId '${categoryId}'`, - `pagination params: page '${page}' pageSize '${pageSize}'`, - `view '${view}'`, - ); - const organizations = getUserReadableOrganizations(user); const submittedDataResult = await service.getSubmittedDataByCategory( @@ -89,13 +82,6 @@ const controller = ({ const view = convertToViewType(String(req.query.view)) || defaultView; const user = req.user; - logger.info( - LOG_MODULE, - `Request Submitted Data on categoryId '${categoryId}' and organization '${organization}'`, - `pagination params: page '${page}' pageSize '${pageSize}'`, - `view '${view}'`, - ); - if (!shouldBypassAuth(req, authConfig) && !hasUserReadAccess(organization, user)) { throw new Forbidden(`User is not authorized to read submitted data for organization '${organization}'`); } @@ -142,15 +128,6 @@ const controller = ({ const pageSize = parseInt(String(req.query.pageSize)) || DEFAULT_PAGE_SIZE; const user = req.user; - logger.info( - LOG_MODULE, - 'Request Query Submitted Data', - `categoryId '${categoryId}'`, - `organization '${organization}'`, - `sqon '${JSON.stringify(sqon)}'`, - `pagination params: page '${page}' pageSize '${pageSize}'`, - ); - if (!shouldBypassAuth(req, authConfig) && !hasUserReadAccess(organization, user)) { throw new Forbidden(`User is not authorized to read submitted data for organization '${organization}'`); } @@ -191,14 +168,6 @@ const controller = ({ const view = convertToViewType(String(req.query.view)) || defaultView; const user = req.user; - logger.info( - LOG_MODULE, - 'Request Submitted Data', - `categoryId '${categoryId}'`, - `systemId '${systemId}'`, - `params: view '${view}'`, - ); - const submittedDataResult = await service.getSubmittedDataBySystemId(categoryId, systemId, { view, }); diff --git a/packages/data-provider/src/controllers/validationController.ts b/packages/data-provider/src/controllers/validationController.ts index 7de9a697..9f4465b2 100644 --- a/packages/data-provider/src/controllers/validationController.ts +++ b/packages/data-provider/src/controllers/validationController.ts @@ -12,8 +12,6 @@ const controller = ({ baseDependencies: BaseDependencies; validatorConfig: ValidatorConfig; }) => { - const { logger } = baseDependencies; - const LOG_MODULE = 'VALIDATION_CONTROLLER'; const validationSvc = validationService(baseDependencies); return { @@ -22,15 +20,6 @@ const controller = ({ const { categoryId, entityName } = req.params; const { organization, value } = req.query; - logger.info( - LOG_MODULE, - 'Validation Request', - `categoryId '${categoryId}'`, - `entityName '${entityName}'`, - `organization '${organization}'`, - `value '${value}'`, - ); - // check if validator is enabled for this category, and entity name const validatorEntry = findValidatorEntry({ validatorConfig, categoryId, entityName }); diff --git a/packages/data-provider/src/core/provider.ts b/packages/data-provider/src/core/provider.ts index 37bd5c70..21fd2b30 100644 --- a/packages/data-provider/src/core/provider.ts +++ b/packages/data-provider/src/core/provider.ts @@ -55,6 +55,7 @@ const provider = (configData: AppConfig, options?: ProviderOptions) => { features: configData.features, idService: configData.idService, logger: getLogger(configData.logger), + routerLoggerEnabled: configData.logger.routerLoggerEnabled ?? true, schemaService: configData.schemaService, submissionService: configData.submissionService, onFinishCommit: configData.onFinishCommit, diff --git a/packages/data-provider/src/middleware/actionLogger.ts b/packages/data-provider/src/middleware/actionLogger.ts new file mode 100644 index 00000000..b5632f0a --- /dev/null +++ b/packages/data-provider/src/middleware/actionLogger.ts @@ -0,0 +1,40 @@ +import type { NextFunction, Response } from 'express'; + +import type { Logger } from '../config/logger.js'; +import { ActionResult, extractActionMetadata, formatActionLog } from '../utils/actionLoggerUtils.js'; +import type { RequestWithUser } from './auth.js'; + +export type ActionLoggerConfig = { + enabled: boolean; +}; + +/** + * Action Logger Middleware + */ +export const actionLoggerMiddleware = (config: ActionLoggerConfig, logger: Logger) => { + return (req: RequestWithUser, res: Response, next: NextFunction) => { + // Skip logging if disabled + if (!config.enabled) { + return next(); + } + + /** + * Log the action after response is sent + */ + res.on('finish', () => { + const metadata = extractActionMetadata(req); + // Extract metadata now that route has been matched and params are populated + const statusResult = res.statusCode >= 200 && res.statusCode < 400 ? ActionResult.ALLOWED : ActionResult.DENIED; + + const logMessage = formatActionLog(metadata, statusResult, res.statusCode, res.locals.user); + + if (statusResult === 'DENIED') { + logger.warn(logMessage); + } else { + logger.info(logMessage); + } + }); + + next(); + }; +}; diff --git a/packages/data-provider/src/middleware/auth.ts b/packages/data-provider/src/middleware/auth.ts index bb0505e0..bc86cd61 100644 --- a/packages/data-provider/src/middleware/auth.ts +++ b/packages/data-provider/src/middleware/auth.ts @@ -65,6 +65,8 @@ export const authMiddleware = (authConfig: AuthConfig) => { const authResult = typeof authConfig.customAuthHandler === 'function' ? await authConfig.customAuthHandler(req) : {}; + // Define locals to access the user result in lyric actionLoggerMiddleware + res.locals.user = authResult.user; if (authResult.errorCode) { return res.status(authResult.errorCode).json({ message: authResult.errorMessage }); } diff --git a/packages/data-provider/src/middleware/errorHandler.ts b/packages/data-provider/src/middleware/errorHandler.ts index e1948cc0..78a984fd 100644 --- a/packages/data-provider/src/middleware/errorHandler.ts +++ b/packages/data-provider/src/middleware/errorHandler.ts @@ -52,5 +52,17 @@ export const errorHandler = (err: Error, req: Request, res: Response, _next: Nex status = 500; } - return res.status(status).send({ error: err.name, message: customizableMsg, details: details }); + // Get request context from res.locals (set by validateRequest middleware) + const requestContext = res.locals.requestContext; + if (requestContext) { + req.params = requestContext.params || req.params; + req.query = requestContext.query || req.query; + req.body = requestContext.body || req.body; + } + + return res.status(status).send({ + error: err.name, + message: customizableMsg, + details: details, + }); }; diff --git a/packages/data-provider/src/routers/auditRouter.ts b/packages/data-provider/src/routers/auditRouter.ts index d3e78542..fb110141 100644 --- a/packages/data-provider/src/routers/auditRouter.ts +++ b/packages/data-provider/src/routers/auditRouter.ts @@ -2,6 +2,7 @@ import { json, Router, urlencoded } from 'express'; import { BaseDependencies } from '../config/config.js'; import auditController from '../controllers/auditController.js'; +import { actionLoggerMiddleware } from '../middleware/actionLogger.js'; import { type AuthConfig, authMiddleware } from '../middleware/auth.js'; const router = ({ @@ -15,6 +16,7 @@ const router = ({ router.use(urlencoded({ extended: false })); router.use(json()); + router.use(actionLoggerMiddleware({ enabled: baseDependencies.routerLoggerEnabled }, baseDependencies.logger)); router.use(authMiddleware(authConfig)); router.get( diff --git a/packages/data-provider/src/routers/categoryRouter.ts b/packages/data-provider/src/routers/categoryRouter.ts index 3ab9cb85..3ad84e12 100644 --- a/packages/data-provider/src/routers/categoryRouter.ts +++ b/packages/data-provider/src/routers/categoryRouter.ts @@ -2,6 +2,7 @@ import { json, Router, urlencoded } from 'express'; import { BaseDependencies } from '../config/config.js'; import categoryController from '../controllers/categoryController.js'; +import { actionLoggerMiddleware } from '../middleware/actionLogger.js'; import { type AuthConfig, authMiddleware } from '../middleware/auth.js'; const router = ({ @@ -15,6 +16,7 @@ const router = ({ router.use(urlencoded({ extended: false })); router.use(json()); + router.use(actionLoggerMiddleware({ enabled: baseDependencies.routerLoggerEnabled }, baseDependencies.logger)); router.use(authMiddleware(authConfig)); router.get('/', categoryController(baseDependencies).listAll); diff --git a/packages/data-provider/src/routers/dictionaryRouter.ts b/packages/data-provider/src/routers/dictionaryRouter.ts index 8af65a1a..1db15a34 100644 --- a/packages/data-provider/src/routers/dictionaryRouter.ts +++ b/packages/data-provider/src/routers/dictionaryRouter.ts @@ -2,6 +2,7 @@ import { json, Router, urlencoded } from 'express'; import { BaseDependencies } from '../config/config.js'; import dictionaryController from '../controllers/dictionaryController.js'; +import { actionLoggerMiddleware } from '../middleware/actionLogger.js'; import { type AuthConfig, authMiddleware } from '../middleware/auth.js'; const router = ({ @@ -15,6 +16,7 @@ const router = ({ router.use(urlencoded({ extended: false })); router.use(json()); + router.use(actionLoggerMiddleware({ enabled: baseDependencies.routerLoggerEnabled }, baseDependencies.logger)); router.use(authMiddleware(authConfig)); router.post('/register', dictionaryController(baseDependencies).registerDictionary); diff --git a/packages/data-provider/src/routers/submissionRouter.ts b/packages/data-provider/src/routers/submissionRouter.ts index 02ddcb86..ac882385 100644 --- a/packages/data-provider/src/routers/submissionRouter.ts +++ b/packages/data-provider/src/routers/submissionRouter.ts @@ -6,6 +6,7 @@ import multer from 'multer'; import { BaseDependencies } from '../config/config.js'; import createSubmissionController from '../controllers/submissionController.js'; +import { actionLoggerMiddleware } from '../middleware/actionLogger.js'; import { type AuthConfig, authMiddleware } from '../middleware/auth.js'; const router = ({ @@ -67,6 +68,7 @@ const router = ({ }), ); + router.use(actionLoggerMiddleware({ enabled: baseDependencies.routerLoggerEnabled }, baseDependencies.logger)); router.use(authMiddleware(authConfig)); router.get('/:submissionId', submissionController.getSubmissionById); diff --git a/packages/data-provider/src/routers/submittedDataRouter.ts b/packages/data-provider/src/routers/submittedDataRouter.ts index 5f4ec9a3..8be253c8 100644 --- a/packages/data-provider/src/routers/submittedDataRouter.ts +++ b/packages/data-provider/src/routers/submittedDataRouter.ts @@ -2,6 +2,7 @@ import { json, Router, urlencoded } from 'express'; import { BaseDependencies } from '../config/config.js'; import submittedDataController from '../controllers/submittedDataController.js'; +import { actionLoggerMiddleware } from '../middleware/actionLogger.js'; import { type AuthConfig, authMiddleware } from '../middleware/auth.js'; const router = ({ @@ -15,6 +16,7 @@ const router = ({ router.use(urlencoded({ extended: false })); router.use(json()); + router.use(actionLoggerMiddleware({ enabled: baseDependencies.routerLoggerEnabled }, baseDependencies.logger)); router.use(authMiddleware(authConfig)); router.get( diff --git a/packages/data-provider/src/routers/validationRouter.ts b/packages/data-provider/src/routers/validationRouter.ts index 3c1857f2..389d7a41 100644 --- a/packages/data-provider/src/routers/validationRouter.ts +++ b/packages/data-provider/src/routers/validationRouter.ts @@ -2,6 +2,7 @@ import { json, Router, urlencoded } from 'express'; import { BaseDependencies, type ValidatorConfig } from '../config/config.js'; import validationController from '../controllers/validationController.js'; +import { actionLoggerMiddleware } from '../middleware/actionLogger.js'; import { type AuthConfig, authMiddleware } from '../middleware/auth.js'; const router = ({ @@ -17,6 +18,7 @@ const router = ({ router.use(urlencoded({ extended: false })); router.use(json()); + router.use(actionLoggerMiddleware({ enabled: baseDependencies.routerLoggerEnabled }, baseDependencies.logger)); router.use(authMiddleware(authConfig)); router.get( diff --git a/packages/data-provider/src/utils/actionLoggerUtils.ts b/packages/data-provider/src/utils/actionLoggerUtils.ts new file mode 100644 index 00000000..05889324 --- /dev/null +++ b/packages/data-provider/src/utils/actionLoggerUtils.ts @@ -0,0 +1,152 @@ +import type { Request } from 'express'; + +import type { RequestWithUser, UserSession } from '../middleware/auth.js'; + +export const ActionType = { + READ: 'READ', + WRITE: 'WRITE', +} as const; + +export type ActionTypeValues = (typeof ActionType)[keyof typeof ActionType]; + +export const ActionResult = { + ALLOWED: 'ALLOWED', + DENIED: 'DENIED', +} as const; + +export type ActionResultValues = (typeof ActionResult)[keyof typeof ActionResult]; + +export interface ActionLogMetadata { + action: ActionTypeValues; + method: string; + path: string; + categoryId?: number; + organization?: string; + userId?: string; + entityName?: string; + systemId?: string; + submissionId?: number; +} + +/** + * Determines if an HTTP method represents a read or write operation + */ +export const getActionType = (method: string): ActionTypeValues => { + return method === 'GET' || method === 'HEAD' ? 'READ' : 'WRITE'; +}; + +/** + * Extracts category ID from request parameters + */ +export const extractCategoryId = (req: Request) => { + const categoryId = Number(req.params.categoryId); + + if (categoryId) { + const parsed = Number(categoryId); + return isNaN(parsed) ? undefined : parsed; + } + return; +}; + +/** + * Extracts organization from request parameters or query + */ +export const extractOrganization = (req: Request) => { + if (req.params.organization) { + return req.params.organization; + } + if (req.query.organization && typeof req.query.organization === 'string') { + return req.query.organization; + } + return; +}; + +/** + * Extracts entity name from request parameters or query + */ +export const extractEntityName = (req: Request) => { + if (req.params.entityName && typeof req.params.entityName === 'string') { + return req.params.entityName; + } + if (req.query.entityName && typeof req.query.entityName === 'string') { + return req.query.entityName; + } + return; +}; + +/** + * Extracts system ID from request parameters + */ +export const extractSystemId = (req: Request) => { + return req.params.systemId; +}; + +/** + * Extracts submission ID from request parameters + */ +export const extractSubmissionId = (req: Request) => { + const submissionId = req.params.submissionId; + if (submissionId) { + const parsed = Number(submissionId); + return isNaN(parsed) ? undefined : parsed; + } + return; +}; + +/** + * Extracts all relevant metadata from a request for action logging + */ +export const extractActionMetadata = (req: RequestWithUser): ActionLogMetadata => { + return { + action: getActionType(req.method), + method: req.method, + path: req.originalUrl || req.path, + categoryId: extractCategoryId(req), + organization: extractOrganization(req), + entityName: extractEntityName(req), + systemId: extractSystemId(req), + submissionId: extractSubmissionId(req), + }; +}; + +/** + * Formats action log metadata into a readable string for logging + */ +export const formatActionLog = ( + metadata: ActionLogMetadata, + result: ActionResultValues, + statusCode: number, + user?: UserSession, +): string => { + const parts = ['[ACTION_LOG]', metadata.action, metadata.method, metadata.path]; + + if (metadata.categoryId) { + parts.push(`categoryId: ${metadata.categoryId}`); + } + + if (metadata.organization) { + parts.push(`organization: ${metadata.organization}`); + } + + if (metadata.entityName) { + parts.push(`entityName: ${metadata.entityName}`); + } + + if (metadata.systemId) { + parts.push(`systemId: ${metadata.systemId}`); + } + + if (metadata.submissionId) { + parts.push(`submissionId: ${metadata.submissionId}`); + } + + parts.push(`result: ${result}`); + + if (user) { + parts.push(`user: ${user.username}`); + } + + parts.push(`status: ${statusCode}`); + + return parts.join(' - '); +}; diff --git a/packages/data-provider/src/utils/requestValidation.ts b/packages/data-provider/src/utils/requestValidation.ts index d5273274..71b675ed 100644 --- a/packages/data-provider/src/utils/requestValidation.ts +++ b/packages/data-provider/src/utils/requestValidation.ts @@ -47,6 +47,16 @@ export function validateRequest< schema.pathParams.parse(req.params); } + // Capture request context after validation (when params/body/query are populated) + // Needed for logging and for error middleware to access request context + res.locals.requestContext = { + method: req.method, + path: req.originalUrl || req.path, + params: req.params, + query: req.query, + ...(req.method !== 'GET' && req.body ? { body: req.body } : {}), + }; + return handler(req, res, next); } catch (error) { if (error instanceof ZodError) { diff --git a/packages/data-provider/src/workers/workerContext.ts b/packages/data-provider/src/workers/workerContext.ts index 5062baa3..72265d56 100644 --- a/packages/data-provider/src/workers/workerContext.ts +++ b/packages/data-provider/src/workers/workerContext.ts @@ -27,6 +27,7 @@ export const initializeWorkerContext = async (configData: AppConfig): Promise