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 c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": "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 err := h.repo.Create(&req); err != nil { // Log internal error but don't expose details return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": "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") cursor, err := parseAuditCursor(cursorRaw) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": "Invalid cursor", }) } logs, err := h.repo.FindPage(c.Context(), limit+1, cursor) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": "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)) }