package api import ( "strconv" "strings" "time" "github.com/gofiber/fiber/v2" "wucher/internal/service" sharedconst "wucher/internal/shared/const" ) func AuditMiddleware(auditSvc *service.AuditLogService) fiber.Handler { return func(c *fiber.Ctx) error { if auditSvc == nil { return c.Next() } start := time.Now() err := c.Next() status := c.Response().StatusCode() if status == 0 { status = fiber.StatusOK } requestID := c.GetRespHeader(fiber.HeaderXRequestID) if requestID == "" { requestID = localString(c.Locals("requestid")) } meta := map[string]any{ "latency_ms": time.Since(start).Milliseconds(), } if err != nil { meta["error"] = err.Error() } module, action := resolveAuditModuleAction(c) if !shouldPersistAuditByMethod(c.Method()) { return err } auditSvc.Log(c.UserContext(), service.AuditLogInput{ RequestID: requestID, ActorUserID: localUserID(c.Locals("user_id")), Layer: "api", Module: module, Action: action, Method: c.Method(), Path: c.Path(), StatusCode: status, Success: status >= 200 && status < 400 && err == nil, IP: c.IP(), UserAgent: c.Get(fiber.HeaderUserAgent), Message: "HTTP " + strconv.Itoa(status), Metadata: meta, }) return err } } func shouldPersistAuditByMethod(method string) bool { switch strings.ToUpper(strings.TrimSpace(method)) { case fiber.MethodPost, fiber.MethodPut, fiber.MethodPatch, fiber.MethodDelete: return true default: return false } } func resolveAuditModuleAction(c *fiber.Ctx) (string, string) { method := c.Method() path := c.Path() normalized := strings.ToLower(strings.TrimSpace(path)) parts := strings.Split(strings.Trim(normalized, "/"), "/") if len(parts) < 3 || parts[0] != "api" || parts[1] != "v1" { return sharedconst.AuditModuleUnknown, actionFromMethod(method) } moduleSegment := strings.TrimSpace(parts[2]) module := moduleFromPathSegment(moduleSegment) if moduleSegment == "bases" { module = resolveBaseModuleVariant(c, module) } suffix := "" if len(parts) > 3 { suffix = strings.TrimSpace(parts[len(parts)-1]) } switch suffix { case "get-all", "dt": return module, sharedconst.AuditActionList case "get": return module, sharedconst.AuditActionGet case "create": return module, sharedconst.AuditActionCreate case "update": return module, sharedconst.AuditActionUpdate case "delete": return module, sharedconst.AuditActionDelete case "export": return module, sharedconst.AuditActionExport } return module, actionFromMethod(method) } func resolveBaseModuleVariant(c *fiber.Ctx, fallback string) string { raw := strings.TrimSpace(c.Query("category_type")) if raw == "" { raw = strings.TrimSpace(c.Query("base_category")) } if raw == "" { raw = strings.TrimSpace(c.Query("category")) } switch strings.ToLower(raw) { case "regular": return sharedconst.AuditModuleBaseRegular case "hems": return sharedconst.AuditModuleBaseHEMS default: return fallback } } func actionFromMethod(method string) string { switch strings.ToUpper(strings.TrimSpace(method)) { case fiber.MethodGet: return sharedconst.AuditActionGet case fiber.MethodPost: return sharedconst.AuditActionCreate case fiber.MethodPut, fiber.MethodPatch: return sharedconst.AuditActionUpdate case fiber.MethodDelete: return sharedconst.AuditActionDelete default: return sharedconst.AuditActionCustom } } func moduleFromPathSegment(segment string) string { switch strings.ToLower(strings.TrimSpace(segment)) { case "air-rescuer-checklist": return sharedconst.AuditModuleAirRescuerChecklist case "audit-logs": return sharedconst.AuditModuleAuditLog case "auth": return sharedconst.AuditModuleAuth case "bases": return sharedconst.AuditModuleBase case "branding", "public": return sharedconst.AuditModuleBranding case "contacts": return sharedconst.AuditModuleContact case "duty-roster": return sharedconst.AuditModuleDutyRoster case "dul": return sharedconst.AuditModuleDUL case "facilities": return sharedconst.AuditModuleFacility case "federal-state": return sharedconst.AuditModuleFederalState case "file-manager": return sharedconst.AuditModuleFileManager case "flight-data": return sharedconst.AuditModuleFlightData case "flights": return sharedconst.AuditModuleFlight case "forces-present": return sharedconst.AuditModuleForcesPresent case "health-insurance-companies": return sharedconst.AuditModuleHealthInsuranceCompany case "helicopter-files": return sharedconst.AuditModuleHelicopterFile case "helicopters": return sharedconst.AuditModuleHelicopter case "hems-operation", "hems-operation-category", "hems-operational-data": return sharedconst.AuditModuleUnknown case "hospital": return sharedconst.AuditModuleHospital case "icao": return sharedconst.AuditModuleICAO case "insurance-patient-data": return sharedconst.AuditModuleInsurancePatientData case "land": return sharedconst.AuditModuleLand case "master-settings": return sharedconst.AuditModuleMasterSetting case "medicine": return sharedconst.AuditModuleMedicine case "mission": return sharedconst.AuditModuleMission case "opc": return sharedconst.AuditModuleOPC case "patient-data": return sharedconst.AuditModulePatientData case "reserve-acs": return sharedconst.AuditModuleReserveAC case "roles": return sharedconst.AuditModuleRole case "users": return sharedconst.AuditModuleUser case "vocation": return sharedconst.AuditModuleVocation default: return sharedconst.AuditModuleUnknown } } func localString(v any) string { s, _ := v.(string) return s } func localUserID(v any) []byte { id, _ := v.([]byte) if len(id) != 16 { return nil } return id }