diff --git a/package-lock.json b/package-lock.json index 5b8aa36..8c95914 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3793,6 +3793,7 @@ "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, "dependencies": { "@types/connect": "*", "@types/node": "*" @@ -3821,6 +3822,7 @@ "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, "dependencies": { "@types/node": "*" } @@ -3859,6 +3861,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.1.tgz", "integrity": "sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ==", + "dev": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", @@ -3878,6 +3881,7 @@ "version": "5.0.6", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", + "dev": true, "dependencies": { "@types/node": "*", "@types/qs": "*", @@ -3910,7 +3914,8 @@ "node_modules/@types/http-errors": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true }, "node_modules/@types/ioredis": { "version": "4.28.10", @@ -3993,7 +3998,8 @@ "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true }, "node_modules/@types/morgan": { "version": "1.9.9", @@ -4065,7 +4071,8 @@ "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true }, "node_modules/@types/rate-limit-redis": { "version": "1.7.4", @@ -4109,6 +4116,7 @@ "version": "0.17.4", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, "dependencies": { "@types/mime": "^1", "@types/node": "*" @@ -4118,6 +4126,7 @@ "version": "1.15.7", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, "dependencies": { "@types/http-errors": "*", "@types/node": "*", diff --git a/src/app.ts b/src/app.ts index bb0af9e..a595e0a 100644 --- a/src/app.ts +++ b/src/app.ts @@ -4,6 +4,7 @@ import express, { Response, RequestHandler, ErrorRequestHandler, + NextFunction, } from "express"; import morgan from "morgan"; import cors from "cors"; @@ -13,6 +14,7 @@ import sessionRouter from "./routes/session.routes"; import emailVerification from "./routes/emailVerification.routes"; import PaymentRoute from "./routes/PaymentLink.routes"; import authRoutes from "./routes/authRoutes"; +import auditLogRoute from "./routes/auditLogRoutes"; import userRoutes from "./routes/userRoutes"; import healthRouter from "./routes/health.routes"; import walletVerificationRoutes from "./routes/wallet-verification.routes"; @@ -30,6 +32,8 @@ import { requestLogger } from "./middlewares/requestLogger.middleware"; import RateLimitMonitoringService from "./services/rateLimitMonitoring.service"; import { startExpiredSessionCleanupCronJobs } from "./utils/schedular"; import logger from "./utils/logger"; +import { auth } from "express-openid-connect"; +import { auditMiddleware } from "./middlewares/audit.middleware"; // Initialize express app const app = express(); @@ -77,6 +81,21 @@ app.use((req, res, next) => { // Start scheduled jobs startExpiredSessionCleanupCronJobs(); +// Initialize the database connection +// AppDataSource.initialize() +// .then(() => { +// logger.info("Data Source has been initialized!"); +// }) +// .catch((err) => { +// logger.error("Error during Data Source initialization:", err); +// process.exit(1); // Exit if we can't connect to the database +// }); + +app.use(((req: Request, res: Response, next: NextFunction) => { + req.dataSource = AppDataSource; + next(); +}) as RequestHandler); + // Log application startup logger.info("Application started successfully"); @@ -91,7 +110,10 @@ app.use("/users", userRoutes); app.use("/merchants", merchantRoutes); app.use("/webhook-queue/merchant", merchantWebhookQueueRoutes); app.use("/reports/transactions", transactionReportsRoutes); -//app.use("/api/v1/stellar", stellarContractRoutes); +//app.use("/audit-log", auditLogRoute); +app.use("/api/v1/stellar", stellarContractRoutes); + + // Error handling middleware const customErrorHandler: ErrorRequestHandler = (err, req, res, _next) => { @@ -117,4 +139,4 @@ app.use(((req: Request, res: Response) => { }) as RequestHandler); // Export app -export default app; +export { app }; diff --git a/src/config/db.ts b/src/config/db.ts index b23a662..03246e7 100644 --- a/src/config/db.ts +++ b/src/config/db.ts @@ -9,11 +9,13 @@ import { WalletVerification } from "../entities/WalletVerification"; import { MerchantEntity } from "../entities/Merchant.entity"; import { MerchantWebhookEntity } from "../entities/MerchantWebhook.entity"; import { MerchantWebhookEventEntity } from "../entities/MerchantWebhookEvent.entity"; +import { AuditLog } from "../entities/AuditLog.entity"; import { PaymentLink } from "../entities/PaymentLink"; + dotenv.config(); -const AppDataSource = new DataSource({ +export const AppDataSource = new DataSource({ type: "postgres", host: process.env.POSTGRES_HOST, port: Number(process.env.POSTGRES_PORT), @@ -32,10 +34,9 @@ const AppDataSource = new DataSource({ MerchantEntity, MerchantWebhookEntity, MerchantWebhookEventEntity, + AuditLog, PaymentLink, ], migrations: ["src/migrations/*.ts"], migrationsTableName: "migrations", }); - -export default AppDataSource; diff --git a/src/controllers/AuditLogController.ts b/src/controllers/AuditLogController.ts new file mode 100644 index 0000000..5228b4b --- /dev/null +++ b/src/controllers/AuditLogController.ts @@ -0,0 +1,36 @@ +import { Request, Response } from "express"; + +import { AuditLogActionsEnum } from "../enums/AuditLogAction"; +import { AuditService } from "../services/Audit.service"; + +export class AuditLogController { + fetchAllAuditLogs = async (req: Request, res: Response) => { + const auditService = new AuditService(req.dataSource); + try { + const filters = { + entityType: req.query.entityType as string, + entityId: req.query.entityId as string, + userId: req.query.userId as string, + action: req.query.action as AuditLogActionsEnum, + startDate: req.query.startDate + ? new Date(req.query.startDate as string) + : undefined, + endDate: req.query.endDate + ? new Date(req.query.endDate as string) + : undefined, + page: req.query.page ? parseInt(req.query.page as string, 10) : 1, + limit: req.query.limit ? parseInt(req.query.limit as string, 10) : 10, + }; + + const result = await auditService.findAuditLogs(filters); + + res.status(200).json(result); + } catch (error) { + if (error instanceof Error) { + res.status(400).json({ message: error.message }); + } else { + res.status(500).json({ message: "Internal server error" }); + } + } + }; +} diff --git a/src/controllers/AuthController.ts b/src/controllers/AuthController.ts index 05694f9..1612c8a 100644 --- a/src/controllers/AuthController.ts +++ b/src/controllers/AuthController.ts @@ -2,11 +2,14 @@ import { Request, Response } from "express"; import { AuthService } from "../services/AuthService"; import { validateTwoFactorAuthentication } from "./validateTwoFactorAuthentication"; -import AppDataSource from "../config/db"; import { User } from "../entities/User"; import { compare } from "bcryptjs"; import { Auth0Profile } from "src/interfaces/auth.interfaces"; +import { AuditLogActionsEnum } from "../enums/AuditLogAction"; +import { AuditService } from "../services/Audit.service"; +import { AppDataSource } from "../config/db"; + export class AuthController { public authService: AuthService; @@ -17,6 +20,18 @@ export class AuthController { register = async (req: Request, res: Response): Promise => { try { const user = await this.authService.register(req.body); + console.log("Done with registration", { user }); + const auditService = new AuditService(req.dataSource); + await auditService.createAuditLogs({ + entityType: "User", + entityId: String(user.id), + action: AuditLogActionsEnum.CREATE, + newValues: req.body, + userId: String(user.id), + userEmail: user.email, + ipAddress: req.auditContext?.ipAddress || "0.0.0.0", + userAgent: req.auditContext?.userAgent || "Unknown", + }); res.status(201).json(user); } catch (error) { if (error instanceof Error) { diff --git a/src/controllers/PaymentLink.controller.ts b/src/controllers/PaymentLink.controller.ts index 84abdf7..97dd702 100644 --- a/src/controllers/PaymentLink.controller.ts +++ b/src/controllers/PaymentLink.controller.ts @@ -2,7 +2,7 @@ import { Request, Response } from "express"; import { PaymentLinkService } from "../services/PaymentLink.services"; import { Repository } from "typeorm"; import { PaymentLink } from "../entities/PaymentLink"; -import AppDataSource from "../config/db"; +import { AppDataSource } from "../config/db"; import { CreatePaymentLinkDto, UpdatePaymentLinkDto, diff --git a/src/controllers/twoFactorAuthController.ts b/src/controllers/twoFactorAuthController.ts index 836aa1a..4aa3056 100644 --- a/src/controllers/twoFactorAuthController.ts +++ b/src/controllers/twoFactorAuthController.ts @@ -1,7 +1,7 @@ import { User } from "../entities/User"; import { TwoFactorAuth } from "../entities/TwoFactorAuth"; import { generateTwoFactorSecret } from "../services/generateTwoFactorSecret"; -import AppDataSource from "../config/db"; +import { AppDataSource } from "../config/db"; import { validateTwoFactorAuthentication } from "./validateTwoFactorAuthentication"; export const enableTwoFactorAuthentication = async (userId: number) => { diff --git a/src/controllers/validateTwoFactorAuthentication.ts b/src/controllers/validateTwoFactorAuthentication.ts index 06a80ad..543b895 100644 --- a/src/controllers/validateTwoFactorAuthentication.ts +++ b/src/controllers/validateTwoFactorAuthentication.ts @@ -1,6 +1,6 @@ import { verifyTwoFactorCode } from "../services/verifyTwoFactorCode"; import { User } from "../entities/User"; -import AppDataSource from "../config/db"; +import { AppDataSource } from "../config/db"; export const validateTwoFactorAuthentication = async ( userId: number, diff --git a/src/entities/AuditLog.entity.ts b/src/entities/AuditLog.entity.ts new file mode 100644 index 0000000..1dcd73c --- /dev/null +++ b/src/entities/AuditLog.entity.ts @@ -0,0 +1,48 @@ +import { + Column, + CreateDateColumn, + Entity, + PrimaryGeneratedColumn, +} from "typeorm"; +import { AuditLogActionsEnum } from "../enums/AuditLogAction"; + +@Entity("audit_logs") +export class AuditLog { + @PrimaryGeneratedColumn("uuid") + id: string; + + @Column() + entityType: string; // 'User', 'PaymentLink', 'BusinessProfile', etc. + + @Column() + entityId: string; + + @Column({ + type: "enum", + enum: AuditLogActionsEnum, + default: AuditLogActionsEnum.CREATE, + }) + action: AuditLogActionsEnum; + + @Column("json", { nullable: true }) + oldValues?: Record; // used when there is an update, so we can see + // what value was there previously + + @Column("json", { nullable: true }) + newValues?: Record; + + @Column() + userId: string; + + @Column() + userEmail: string; + + @Column() + ipAddress: string; + + @Column() + userAgent: string; + + @CreateDateColumn() + createdAt: Date; +} diff --git a/src/entities/subscribers/entity.subscriber.ts b/src/entities/subscribers/entity.subscriber.ts new file mode 100644 index 0000000..0629d84 --- /dev/null +++ b/src/entities/subscribers/entity.subscriber.ts @@ -0,0 +1,125 @@ +import { + EntitySubscriberInterface, + InsertEvent, + UpdateEvent, + RemoveEvent, + EventSubscriber, + DataSource, +} from "typeorm"; +import { AuditService } from "../../services/Audit.service"; +import { AuditLogActionsEnum } from "../../interfaces/audit.interface"; + +/** + * This TypeORM subscriber will handle automatic audit logging for all entities + * It needs to be registered with the connection when the app starts + */ +@EventSubscriber() +export class EntityAuditSubscriber implements EntitySubscriberInterface { + private auditService: AuditService; + + constructor(dataSource: DataSource) { + // Initialize service with the dataSource + this.auditService = new AuditService(dataSource); + } + + /** + * Called after entity insertion + */ + async afterInsert(event: InsertEvent) { + // Skip auditing for AuditLog entity to prevent infinite recursion + if (event.metadata.tableName === "audit_logs") return; + + // Get entity name + const entityName = event.metadata.targetName; + const entityId = event.entity.id; + + // In Express context, we don't have direct access to request + // This is why we use middleware approach alongside subscribers + // We'll track basic entity changes here without user context + try { + // Here we have limited context - we can't know which user performed the action + await this.auditService.createAuditLogs({ + entityType: entityName, + entityId, + action: AuditLogActionsEnum.CREATE, + newValues: { ...event.entity }, + userId: "system", + userEmail: "system@example.com", + ipAddress: "0.0.0.0", + userAgent: "TypeORM Subscriber", + }); + } catch (error) { + console.error("Error creating audit log in subscriber:", error); + } + } + + /** + * Called after entity update + */ + async afterUpdate(event: UpdateEvent) { + // Skip auditing for AuditLog entity + if (event.metadata.tableName === "audit_logs") return; + + // Get entity name + const entityName = event.metadata.targetName; + const entityId = event.entity?.id || event.databaseEntity.id; + + // Extract changed columns and their values + const oldValues: Record = {}; + const newValues: Record = {}; + + if (event.updatedColumns.length > 0) { + for (const column of event.updatedColumns) { + const columnName = column.propertyName; + oldValues[columnName] = event.databaseEntity[columnName]; + newValues[columnName] = event.entity?.[columnName]; + } + } + + // Only create audit log if there are actual changes + if (Object.keys(newValues).length > 0) { + try { + await this.auditService.createAuditLogs({ + entityType: entityName, + entityId, + action: AuditLogActionsEnum.UPDATE, + oldValues, + newValues, + userId: "system", + userEmail: "system@example.com", + ipAddress: "0.0.0.0", + userAgent: "TypeORM Subscriber", + }); + } catch (error) { + console.error("Error creating audit log in subscriber:", error); + } + } + } + + /** + * Called after entity removal + */ + async afterRemove(event: RemoveEvent) { + // Skip auditing for AuditLog entity + if (event.metadata.tableName === "audit_logs") return; + + // Get entity name + const entityName = event.metadata.targetName; + const entityId = event.databaseEntity.id; + + try { + await this.auditService.createAuditLogs({ + entityType: entityName, + entityId, + action: AuditLogActionsEnum.DELETE, + oldValues: { ...event.databaseEntity }, + userId: "system", + userEmail: "system@example.com", + ipAddress: "0.0.0.0", + userAgent: "TypeORM Subscriber", + }); + } catch (error) { + console.error("Error creating audit log in subscriber:", error); + } + } +} diff --git a/src/enums/AuditLogAction.ts b/src/enums/AuditLogAction.ts new file mode 100644 index 0000000..230f9ad --- /dev/null +++ b/src/enums/AuditLogAction.ts @@ -0,0 +1,5 @@ +export enum AuditLogActionsEnum { + CREATE = "create", + UPDATE = "update", + DELETE = "delete", +} diff --git a/src/index.ts b/src/index.ts index a1a1fd0..4195d95 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ import "reflect-metadata"; -import app from "./app"; -import AppDataSource from "./config/db"; +import { AppDataSource } from "./config/db"; +import { app } from "./app"; +import { EntityAuditSubscriber } from "./entities/subscribers/entity.subscriber"; async function main() { try { @@ -8,6 +9,9 @@ async function main() { await AppDataSource.initialize(); console.log("✅ Database connected successfully"); + const subscriber = new EntityAuditSubscriber(AppDataSource); + AppDataSource.subscribers.push(subscriber); + // Start the server const PORT = process.env.PORT || 4000; app.listen(PORT, () => { diff --git a/src/interfaces/audit.interface.ts b/src/interfaces/audit.interface.ts new file mode 100644 index 0000000..e671933 --- /dev/null +++ b/src/interfaces/audit.interface.ts @@ -0,0 +1,25 @@ +import { AuditLogActionsEnum } from "../enums/AuditLogAction"; + +export interface CreateAditLogParams { + entityType: string; + entityId: string; + action: AuditLogActionsEnum; + oldValues?: Record; + newValues?: Record; + userId: string; + userEmail: string; + ipAddress: string; + userAgent: string; +} + +export interface FindAuditLogParams { + entityType?: string; + entityId?: string; + userId?: string; + action?: AuditLogActionsEnum; + startDate?: Date; + endDate?: Date; + page?: number; + limit?: number; +} +export { AuditLogActionsEnum }; diff --git a/src/middlewares/audit.middleware.ts b/src/middlewares/audit.middleware.ts new file mode 100644 index 0000000..0bb8071 --- /dev/null +++ b/src/middlewares/audit.middleware.ts @@ -0,0 +1,37 @@ +import { Request, Response, NextFunction } from "express"; +import { getConnection } from "typeorm"; + +/** + * This middleware captures the user context from the request + * and makes it available for the audit service + */ +export function auditMiddleware( + req: Request, + res: Response, + next: NextFunction, +) { + // Store the original methods that we'll be wrapping + const originalJson = res.json; + const originalSend = res.send; + + console.log("Entered here ", { originalJson, originalSend }); + + // Extract user context from the request + const user = req.user || { id: "system", email: "system@example.com" }; + const ipAddress = + req.ip || (req.headers["x-forwarded-for"] as string) || "0.0.0.0"; + const userAgent = req.headers["user-agent"] || "Unknown"; + + const userContext = { + userId: String(user.id), + userEmail: user.email, + ipAddress, + userAgent, + }; + + // Add user context to request for use in route handlers + + req.auditContext = userContext; + + next(); +} diff --git a/src/middlewares/authMiddleware.ts b/src/middlewares/authMiddleware.ts index e906d38..619b7ac 100644 --- a/src/middlewares/authMiddleware.ts +++ b/src/middlewares/authMiddleware.ts @@ -8,8 +8,9 @@ import { import { UserRole } from "../enums/UserRole"; import { UserService } from "../services/UserService"; import { redisClient } from "../config/redisConfig"; +import { DataSource } from "typeorm"; -declare module "express" { +declare module "express-serve-static-core" { interface Request { user?: { id: number; @@ -18,6 +19,18 @@ declare module "express" { jti?: string; role?: UserRole; }; + + // adding this for audit + auditContext?: { + userId: string; + userEmail: string; + ipAddress: string; + userAgent: string; + }; + preAuditEntity?: any; + entityType?: string; + + dataSource: DataSource; } } diff --git a/src/middlewares/entity-audit.middleware.ts b/src/middlewares/entity-audit.middleware.ts new file mode 100644 index 0000000..0cb6c59 --- /dev/null +++ b/src/middlewares/entity-audit.middleware.ts @@ -0,0 +1,106 @@ +import { Request, Response, NextFunction } from "express"; +import { AuditLogActionsEnum } from "../enums/AuditLogAction"; +import { AuditService } from "../services/Audit.service"; + +/** + * Higher-order function to create entity-specific audit middleware + * @param entityType The entity type to audit + * @param entityIdParam The parameter name containing the entity ID + */ +export function createEntityAuditMiddleware( + entityType: string, + entityIdParam: string = "id", +) { + return async (req: Request, res: Response, next: NextFunction) => { + try { + if (!req.auditContext) { + // If audit context middleware has not run + return next(); + } + + const entityId = req.params[entityIdParam]; + if (!entityId) { + return next(); + } + + // Get the DataSource from request + const dataSource = req.dataSource; + if (!dataSource) { + console.error("DataSource not available in request"); + return next(); + } + + // Create audit service instance + const auditService = new AuditService(dataSource); + + // Get the entity repository dynamically + const entityRepository = dataSource.getRepository(entityType); + + // Get the entity before any changes + const entity = await entityRepository.findOne({ + where: { id: entityId }, + }); + + if (!entity) { + return next(); + } + + // Store the entity in the request for comparison after operation + req.preAuditEntity = entity; + req.entityType = entityType; + + // Add event listeners for response completion + res.on("finish", async () => { + // This runs after the response has been sent + // Only audit successful operations (2xx status codes) + if (res.statusCode >= 200 && res.statusCode < 300) { + try { + // Check request method to determine operation type + let action: AuditLogActionsEnum; + let oldValues: Record | undefined; + let newValues: Record | undefined; + + switch (req.method) { + case "POST": + action = AuditLogActionsEnum.CREATE; + newValues = req.body; + break; + case "PUT": + case "PATCH": + action = AuditLogActionsEnum.UPDATE; + oldValues = { ...req.preAuditEntity }; + newValues = req.body; + break; + case "DELETE": + action = AuditLogActionsEnum.DELETE; + oldValues = { ...req.preAuditEntity }; + break; + default: + // Don't audit GET or other methods + return; + } + + // Create audit log + await auditService.createAuditLogs({ + action: action, + entityId: entityId, + entityType: entityType, + oldValues, + newValues, + ipAddress: req.auditContext!.ipAddress, + userAgent: req.auditContext!.userAgent, + userEmail: req.auditContext!.userEmail, + userId: req.auditContext!.userId, + }); + } catch (error) { + console.error("Error creating audit log:", error); + } + } + }); + + next(); + } catch (error) { + next(error); + } + }; +} diff --git a/src/routes/auditLogRoutes.ts b/src/routes/auditLogRoutes.ts new file mode 100644 index 0000000..4f81616 --- /dev/null +++ b/src/routes/auditLogRoutes.ts @@ -0,0 +1,62 @@ +import { + Router, + Request, + Response, + NextFunction, + RequestHandler, + ErrorRequestHandler, +} from "express"; +import { auth } from "express-openid-connect"; +import { oauthConfig } from "../config/auth0Config"; +import { AuditLogController } from "../controllers/AuditLogController"; + +const router = Router(); + +const auditLogController = new AuditLogController(); + +router.use((req, res, next) => { + console.log("Auth route middleware - Path:", req.path); + console.log("Auth route middleware - Method:", req.method); + console.log("Auth route middleware - Headers:", req.headers); + + // Skip Auth0 in development or for registration + if (process.env.NODE_ENV === "development") { + console.log("Skipping Auth0 middleware"); + next(); + } else { + console.log("Applying Auth0 middleware"); + auth(oauthConfig)(req, res, next); + } +}); + +// Error handling middleware +const errorHandler: ErrorRequestHandler = (err, req, res, _next) => { + console.error("Auth route error:", err); + res.status(500).json({ + error: "Internal Server Error", + message: + process.env.NODE_ENV === "development" + ? err.message + : "Something went wrong", + }); +}; + +router.use(errorHandler); + +// Helper function to wrap async route handlers +const asyncHandler = ( + fn: (req: Request, res: Response, next: NextFunction) => Promise, +): RequestHandler => { + return (req, res, next) => { + Promise.resolve(fn(req as Request, res, next)).catch(next); + }; +}; + +router.get( + "/", + asyncHandler(async (req, res) => { + await auditLogController.fetchAllAuditLogs(req, res); + }), +); + +export default router; diff --git a/src/routes/health.routes.ts b/src/routes/health.routes.ts index e35b962..24b638f 100644 --- a/src/routes/health.routes.ts +++ b/src/routes/health.routes.ts @@ -1,5 +1,5 @@ import { Router } from "express"; -import AppDataSource from "../config/db"; +import { AppDataSource } from "../config/db"; const router = Router(); diff --git a/src/routes/transactionReports.routes.ts b/src/routes/transactionReports.routes.ts index 3c01917..1b839c3 100644 --- a/src/routes/transactionReports.routes.ts +++ b/src/routes/transactionReports.routes.ts @@ -9,7 +9,7 @@ import { TransactionReportFilters, } from "../services/TransactionReportService"; import { AuthGuard } from "../guards/AuthGuard"; -import AppDataSource from "../config/db"; +import { AppDataSource } from "../config/db"; const router = express.Router(); diff --git a/src/services/Audit.service.ts b/src/services/Audit.service.ts new file mode 100644 index 0000000..1322ebb --- /dev/null +++ b/src/services/Audit.service.ts @@ -0,0 +1,101 @@ +import { AuditLog } from "../entities/AuditLog.entity"; +import { + CreateAditLogParams, + FindAuditLogParams, +} from "src/interfaces/audit.interface"; +import { auditConfigService } from "./AuditConfig.service"; +import { DataSource, Repository } from "typeorm"; + +export class AuditService { + private auditLogRepository: Repository; + + constructor(private dataSource: DataSource) { + // Initialize repository in constructor + this.auditLogRepository = dataSource.getRepository(AuditLog); + } + async createAuditLogs(payload: CreateAditLogParams) { + try { + const maskedOldValues = payload.oldValues + ? auditConfigService.maskSensitiveData( + payload.entityType, + payload.oldValues, + ) + : undefined; + + const maskedNewValues = payload.newValues + ? auditConfigService.maskSensitiveData( + payload.entityType, + payload.newValues, + ) + : undefined; + + // const auditLogRepository = this.dataSource.getRepository(AuditLog); + const auditLog = this.auditLogRepository.create({ + ...payload, + oldValues: maskedOldValues, + newValues: maskedNewValues, + }); + + return this.auditLogRepository.save(auditLog); + } catch (error) { + console.log({ error }); + } + } + + async findAuditLogs(filters: FindAuditLogParams) { + // const auditLogRepository = this.dataSource.getRepository(AuditLog); + const query = this.auditLogRepository.createQueryBuilder("auditLog"); + + if (filters.entityType) { + query.andWhere("auditLog.entityType = :entityType", { + entityType: filters.entityType, + }); + } + + if (filters.entityId) { + query.andWhere("auditLog.entityId = :entityId", { + entityId: filters.entityId, + }); + } + + if (filters.userId) { + query.andWhere("auditLog.userId = :userId", { userId: filters.userId }); + } + + if (filters.action) { + query.andWhere("auditLog.action = :action", { action: filters.action }); + } + + if (filters.startDate) { + query.andWhere("auditLog.createdAt >= :startDate", { + startDate: filters.startDate, + }); + } + + if (filters.endDate) { + query.andWhere("auditLog.createdAt <= :endDate", { + endDate: filters.endDate, + }); + } + + // Add pagination + const page = filters.page || 1; + const limit = filters.limit || 10; + const skip = (page - 1) * limit; + + query.skip(skip).take(limit); + query.orderBy("auditLog.createdAt", "DESC"); + + const [items, count] = await query.getManyAndCount(); + + return { + items, + meta: { + totalItems: count, + itemsPerPage: limit, + currentPage: page, + totalPages: Math.ceil(count / limit), + }, + }; + } +} diff --git a/src/services/AuditConfig.service.ts b/src/services/AuditConfig.service.ts new file mode 100644 index 0000000..2cb5c71 --- /dev/null +++ b/src/services/AuditConfig.service.ts @@ -0,0 +1,61 @@ +interface SensitiveFieldConfig { + fieldName: string; + maskFunction?: (value: any) => any; +} + +class AuditConfigService { + private readonly sensitiveEntities = new Map< + string, + SensitiveFieldConfig[] + >(); + + constructor() { + this.configuresSensitiveFields(); + } + + private configuresSensitiveFields() { + // Example: So every entity should set their sensitive fields + this.sensitiveEntities.set("users", [ + { fieldName: "password", maskFunction: () => "******" }, + { + fieldName: "walletAddress", + maskFunction: () => "***********************", + }, + ]); + + this.sensitiveEntities.set("merchants", [ + { fieldName: "secret", maskFunction: () => "********" }, + ]); + + this.sensitiveEntities.set("WalletVerification", [ + { fieldName: "walletAddress", maskFunction: () => "********" }, + ]); + } + + getSensitiveFieldsForEntity(entityName: string): SensitiveFieldConfig[] { + return this.sensitiveEntities.get(entityName) || []; + } + + maskSensitiveData( + entityName: string, + data: Record, + ): Record { + if (!data) return data; + + const sensitiveFieldConfigs = this.getSensitiveFieldsForEntity(entityName); + if (!sensitiveFieldConfigs.length) return data; + + const maskedData = { ...data }; + + for (const config of sensitiveFieldConfigs) { + if (maskedData[config.fieldName] !== undefined) { + maskedData[config.fieldName] = config.maskFunction + ? config.maskFunction(maskedData[config.fieldName]) + : "******"; + } + } + return maskedData; + } +} + +export const auditConfigService = new AuditConfigService(); diff --git a/src/services/AuthService.ts b/src/services/AuthService.ts index 6bba5ca..d8e292d 100644 --- a/src/services/AuthService.ts +++ b/src/services/AuthService.ts @@ -2,7 +2,7 @@ import { Repository } from "typeorm"; import { User } from "../entities/User"; import { compare } from "bcryptjs"; import { sign, verify, JwtPayload, SignOptions } from "jsonwebtoken"; -import AppDataSource from "../config/db"; +import { AppDataSource } from "../config/db"; import { randomBytes, createHash } from "crypto"; import { v4 as uuidv4 } from "uuid"; import { redisClient } from "../config/redisConfig"; diff --git a/src/services/UserService.ts b/src/services/UserService.ts index 3070639..7386a37 100644 --- a/src/services/UserService.ts +++ b/src/services/UserService.ts @@ -1,5 +1,5 @@ import { Repository } from "typeorm"; -import AppDataSource from "../config/db"; +import { AppDataSource } from "../config/db"; import { User } from "../entities/User"; import { validate } from "class-validator"; import { CreateUserDTO } from "../dtos/CreateUserDTO"; diff --git a/src/services/emailVerification.service.ts b/src/services/emailVerification.service.ts index 5fea98f..846647b 100644 --- a/src/services/emailVerification.service.ts +++ b/src/services/emailVerification.service.ts @@ -1,10 +1,11 @@ import { Repository } from "typeorm"; -import DataSource from "../config/db"; + import { EmailVerification } from "../entities/emailVerification"; import { User } from "../entities/User"; import { sendVerificationEmail } from "../utils/sendVerificationEmail"; import { verifyToken, generateVerificationToken } from "../utils/token"; import { sendWalletVerificationEmail } from "../utils/sendWalletVerificationEmail"; +import { AppDataSource } from "../config/db"; class EmailVerificationService { private emailVerificationRepository: Repository; @@ -12,8 +13,8 @@ class EmailVerificationService { constructor() { this.emailVerificationRepository = - DataSource.getRepository(EmailVerification); - this.userRepository = DataSource.getRepository(User); + AppDataSource.getRepository(EmailVerification); + this.userRepository = AppDataSource.getRepository(User); } async sendWalletVerificationEmail( email: string, diff --git a/src/services/inAppNotificationService.ts b/src/services/inAppNotificationService.ts index e921459..30f379f 100644 --- a/src/services/inAppNotificationService.ts +++ b/src/services/inAppNotificationService.ts @@ -1,5 +1,5 @@ import { Repository } from "typeorm"; -import AppDataSource from "../config/db"; +import { AppDataSource } from "../config/db"; import { InAppNotificationEntity, NotificationType, diff --git a/src/services/merchant.service.ts b/src/services/merchant.service.ts index 0f3efe7..945e4f9 100644 --- a/src/services/merchant.service.ts +++ b/src/services/merchant.service.ts @@ -2,7 +2,7 @@ import { Merchant } from "../interfaces/webhook.interfaces"; import { Repository, DataSource } from "typeorm"; import { validate } from "class-validator"; import { MerchantEntity } from "../entities/Merchant.entity"; -import AppDataSource from "../config/db"; +import { AppDataSource } from "../config/db"; import { CreateMerchantDTO } from "../dtos/CreateMerchantDTO"; import { UpdateMerchantProfileDTO } from "../dtos/UpdateMerchantProfileDTO"; import { CreateMerchantProfileDTO } from "../dtos/CreateMerchantProfileDTO"; diff --git a/src/services/merchantWebhookQueue.service.ts b/src/services/merchantWebhookQueue.service.ts index e1ddbea..7387a76 100644 --- a/src/services/merchantWebhookQueue.service.ts +++ b/src/services/merchantWebhookQueue.service.ts @@ -5,7 +5,7 @@ import { MerchantWebhook, } from "../interfaces/webhook.interfaces"; import { MerchantWebhookEventEntity } from "../entities/MerchantWebhookEvent.entity"; -import AppDataSource from "../config/db"; +import { AppDataSource } from "../config/db"; import { MerchantWebhookEventEntityStatus } from "../enums/MerchantWebhookEventStatus"; import { NotificationService } from "./inAppNotificationService"; import { diff --git a/src/services/rateLimitMonitoring.service.ts b/src/services/rateLimitMonitoring.service.ts index a9cfdd8..cf38015 100644 --- a/src/services/rateLimitMonitoring.service.ts +++ b/src/services/rateLimitMonitoring.service.ts @@ -87,8 +87,8 @@ class RateLimitMonitoringService { endpoint: req.originalUrl, userAgent: req.headers["user-agent"], timestamp: new Date(), - userId: req.user?.id, - email: req.user?.email, + userId: req.user!.id, + email: req.user!.email, }; this.logRateLimitEvent(event).catch((err) => { diff --git a/src/services/session.service.ts b/src/services/session.service.ts index 85d55f1..c23ce28 100644 --- a/src/services/session.service.ts +++ b/src/services/session.service.ts @@ -1,5 +1,5 @@ import { Repository } from "typeorm"; -import AppDataSource from "../config/db"; // ✅ Import the configured DataSource +import { AppDataSource } from "../config/db"; // ✅ Import the configured DataSource import { Session } from "../entities/Session"; import { User } from "../entities/User"; import { v4 as uuidv4 } from "uuid"; diff --git a/src/services/wallet-verification.service.ts b/src/services/wallet-verification.service.ts index eaa45c1..7cf0237 100644 --- a/src/services/wallet-verification.service.ts +++ b/src/services/wallet-verification.service.ts @@ -1,5 +1,5 @@ import { Repository, MoreThan } from "typeorm"; -import DataSource from "../config/db"; +import { AppDataSource } from "../config/db"; import { WalletVerification } from "../entities/WalletVerification"; import { User } from "../entities/User"; import * as crypto from "crypto"; @@ -15,8 +15,9 @@ class WalletVerificationService { private emailService: EmailService; constructor() { - this.walletVerificationRepo = DataSource.getRepository(WalletVerification); - this.userRepo = DataSource.getRepository(User); + this.walletVerificationRepo = + AppDataSource.getRepository(WalletVerification); + this.userRepo = AppDataSource.getRepository(User); this.emailService = new EmailService(); } diff --git a/src/services/webhook.service.ts b/src/services/webhook.service.ts index 2d2e4d9..5137740 100644 --- a/src/services/webhook.service.ts +++ b/src/services/webhook.service.ts @@ -1,6 +1,6 @@ import { MerchantWebhook } from "../interfaces/webhook.interfaces"; import { Repository } from "typeorm"; -import AppDataSource from "../config/db"; +import { AppDataSource } from "../config/db"; import { MerchantWebhookEntity } from "./../entities/MerchantWebhook.entity"; import { MerchantWebhookQueueService } from "./merchantWebhookQueue.service"; diff --git a/src/types/express.d.ts b/src/types/express.d.ts index c9c9402..0addbbc 100644 --- a/src/types/express.d.ts +++ b/src/types/express.d.ts @@ -1,3 +1,4 @@ +import { DataSource } from "typeorm"; import { User } from "../entities/User"; declare module "express-serve-static-core" { @@ -5,5 +6,17 @@ declare module "express-serve-static-core" { user?: Partial; validatedIp?: string; tokenExp?: number; + + // adding this for audit + auditContext?: { + userId: string; + userEmail: string; + ipAddress: string; + userAgent: string; + }; + preAuditEntity?: any; + entityType?: string; + + dataSource: DataSource; } }