package handler import ( "baron-sso-backend/internal/domain" "encoding/base64" "errors" "strings" "time" "github.com/gofiber/fiber/v2" "github.com/google/uuid" ) type AuditHandler struct { repo domain.AuditRepository } func NewAuditHandler(repo domain.AuditRepository) *AuditHandler { return &AuditHandler{repo: repo} } // CreateLog handles POST /api/v1/audit func (h *AuditHandler) CreateLog(c *fiber.Ctx) error { var req domain.AuditLog if err := c.BodyParser(&req); err != nil { return errorJSON(c, fiber.StatusBadRequest, "Cannot parse JSON") } // Auto-fill metadata if missing if req.IPAddress == "" { req.IPAddress = c.IP() } if req.UserAgent == "" { req.UserAgent = c.Get("User-Agent") } if req.Timestamp.IsZero() { req.Timestamp = time.Now() } if req.EventID == "" { req.EventID = ensureRequestID(c) } if h.repo == nil { return errorJSON(c, fiber.StatusServiceUnavailable, "Audit service unavailable") } if err := h.repo.Create(&req); err != nil { // Log internal error but don't expose details return errorJSON(c, fiber.StatusInternalServerError, "Failed to save audit log") } return c.Status(fiber.StatusCreated).JSON(fiber.Map{ "message": "Audit log saved", }) } // ListLogs handles GET /api/v1/audit func (h *AuditHandler) ListLogs(c *fiber.Ctx) error { limit := c.QueryInt("limit", 50) cursorRaw := c.Query("cursor") requestedTenantID := c.Query("tenantId") cursor, err := parseAuditCursor(cursorRaw) if err != nil { return errorJSON(c, fiber.StatusBadRequest, "Invalid cursor") } if h.repo == nil { return errorJSON(c, fiber.StatusServiceUnavailable, "Audit service unavailable") } // [New] Role-based Filtering profile, _ := c.Locals("user_profile").(*domain.UserProfileResponse) var filterTenantID string if profile != nil { if profile.Role == domain.RoleSuperAdmin { // Super Admin can see everything or filter by a specific tenant if requested filterTenantID = requestedTenantID } else { return errorJSON(c, fiber.StatusForbidden, "forbidden") } } logs, err := h.repo.FindPage(c.Context(), limit+1, cursor, filterTenantID) if err != nil { return errorJSON(c, fiber.StatusInternalServerError, "Failed to retrieve audit logs") } nextCursor := "" if len(logs) > limit { last := logs[limit-1] nextCursor = encodeAuditCursor(last) logs = logs[:limit] } return c.JSON(fiber.Map{ "items": logs, "limit": limit, "cursor": cursorRaw, "next_cursor": nextCursor, }) } func ensureRequestID(c *fiber.Ctx) string { reqID := c.Get("X-Request-Id") if reqID == "" { reqID = uuid.New().String() c.Set("X-Request-Id", reqID) } return reqID } func parseAuditCursor(raw string) (*domain.AuditCursor, error) { if raw == "" { return nil, nil } decoded, err := base64.RawURLEncoding.DecodeString(raw) if err != nil { return nil, err } parts := strings.SplitN(string(decoded), "|", 2) if len(parts) != 2 { return nil, errors.New("invalid cursor") } ts, err := time.Parse(time.RFC3339Nano, parts[0]) if err != nil { return nil, err } return &domain.AuditCursor{ Timestamp: ts, EventID: parts[1], }, nil } func encodeAuditCursor(log domain.AuditLog) string { payload := log.Timestamp.UTC().Format(time.RFC3339Nano) + "|" + log.EventID return base64.RawURLEncoding.EncodeToString([]byte(payload)) }