// Package audit provides audit logging for user actions. package audit import ( "context" "database/sql" "log/slog" "net/http" "go.uber.org/fx" "sneak.berlin/go/upaas/internal/database" "sneak.berlin/go/upaas/internal/logger" "sneak.berlin/go/upaas/internal/metrics" "sneak.berlin/go/upaas/internal/middleware" "sneak.berlin/go/upaas/internal/models" ) // ServiceParams contains dependencies for Service. type ServiceParams struct { fx.In Logger *logger.Logger Database *database.Database Metrics *metrics.Metrics } // Service provides audit logging functionality. type Service struct { log *slog.Logger db *database.Database metrics *metrics.Metrics } // New creates a new audit Service. func New(_ fx.Lifecycle, params ServiceParams) (*Service, error) { return &Service{ log: params.Logger.Get(), db: params.Database, metrics: params.Metrics, }, nil } // LogEntry records an audit event. type LogEntry struct { UserID int64 Username string Action models.AuditAction ResourceType models.AuditResourceType ResourceID string Detail string RemoteIP string } // Log records an audit log entry and increments the audit metrics counter. func (svc *Service) Log(ctx context.Context, entry LogEntry) { auditEntry := models.NewAuditEntry(svc.db) auditEntry.Username = entry.Username auditEntry.Action = entry.Action auditEntry.ResourceType = entry.ResourceType if entry.UserID != 0 { auditEntry.UserID = sql.NullInt64{Int64: entry.UserID, Valid: true} } if entry.ResourceID != "" { auditEntry.ResourceID = sql.NullString{String: entry.ResourceID, Valid: true} } if entry.Detail != "" { auditEntry.Detail = sql.NullString{String: entry.Detail, Valid: true} } if entry.RemoteIP != "" { auditEntry.RemoteIP = sql.NullString{String: entry.RemoteIP, Valid: true} } err := auditEntry.Save(ctx) if err != nil { svc.log.Error("failed to save audit entry", "error", err, "action", entry.Action, "username", entry.Username, ) return } svc.metrics.AuditEventsTotal.WithLabelValues(string(entry.Action)).Inc() svc.log.Info("audit", "action", entry.Action, "username", entry.Username, "resource_type", entry.ResourceType, "resource_id", entry.ResourceID, ) } // LogFromRequest records an audit log entry, extracting the remote IP from // the HTTP request using the middleware's trusted-proxy-aware IP resolution. func (svc *Service) LogFromRequest( ctx context.Context, request *http.Request, entry LogEntry, ) { entry.RemoteIP = middleware.RealIP(request) svc.Log(ctx, entry) } // Recent returns the most recent audit log entries. func (svc *Service) Recent( ctx context.Context, limit int, ) ([]*models.AuditEntry, error) { return models.FindAuditEntries(ctx, svc.db, limit) } // ForResource returns audit log entries for a specific resource. func (svc *Service) ForResource( ctx context.Context, resourceType models.AuditResourceType, resourceID string, limit int, ) ([]*models.AuditEntry, error) { return models.FindAuditEntriesByResource(ctx, svc.db, resourceType, resourceID, limit) }