// Package auditlog provides a system for logging events that require permissions to the audit log package auditlog import ( "context" "encoding/json" "fmt" "net/http" "time" "git.haelnorr.com/h/oslstats/internal/db" "github.com/pkg/errors" "github.com/uptrace/bun" ) type Logger struct { conn *bun.DB } func NewLogger(conn *bun.DB) *Logger { return &Logger{conn: conn} } // LogSuccess logs a successful permission-protected action func (l *Logger) LogSuccess( ctx context.Context, tx bun.Tx, user *db.User, action string, resourceType string, resourceID any, // Can be int, string, or nil details map[string]any, r *http.Request, ) error { return l.log(ctx, tx, user, action, resourceType, resourceID, details, "success", nil, r) } // LogError logs a failed action due to an error func (l *Logger) LogError( ctx context.Context, tx bun.Tx, user *db.User, action string, resourceType string, resourceID any, err error, r *http.Request, ) error { errMsg := err.Error() return l.log(ctx, tx, user, action, resourceType, resourceID, nil, "error", &errMsg, r) } func (l *Logger) log( ctx context.Context, tx bun.Tx, user *db.User, action string, resourceType string, resourceID any, details map[string]any, result string, errorMessage *string, r *http.Request, ) error { if user == nil { return errors.New("user cannot be nil for audit logging") } // Convert resourceID to string var resourceIDStr *string if resourceID != nil { idStr := fmt.Sprintf("%v", resourceID) resourceIDStr = &idStr } // Marshal details to JSON var detailsJSON json.RawMessage if details != nil { jsonBytes, err := json.Marshal(details) if err != nil { return errors.Wrap(err, "json.Marshal details") } detailsJSON = jsonBytes } // Extract IP and User-Agent from request ipAddress := r.RemoteAddr userAgent := r.UserAgent() log := &db.AuditLog{ UserID: user.ID, Action: action, ResourceType: resourceType, ResourceID: resourceIDStr, Details: detailsJSON, IPAddress: ipAddress, UserAgent: userAgent, Result: result, ErrorMessage: errorMessage, CreatedAt: time.Now().Unix(), } return db.CreateAuditLog(ctx, tx, log) } // GetRecentLogs retrieves recent audit logs with pagination func (l *Logger) GetRecentLogs(ctx context.Context, pageOpts *db.PageOpts) (*db.List[db.AuditLog], error) { var logs *db.List[db.AuditLog] if err := db.WithTxFailSilently(ctx, l.conn, func(ctx context.Context, tx bun.Tx) error { var err error logs, err = db.GetAuditLogs(ctx, tx, pageOpts, nil) if err != nil { return errors.Wrap(err, "db.GetAuditLogs") } return nil }); err != nil { return nil, errors.Wrap(err, "db.WithTxFailSilently") } return logs, nil } // GetLogsByUser retrieves audit logs for a specific user func (l *Logger) GetLogsByUser(ctx context.Context, userID int, pageOpts *db.PageOpts) (*db.List[db.AuditLog], error) { var logs *db.List[db.AuditLog] if err := db.WithTxFailSilently(ctx, l.conn, func(ctx context.Context, tx bun.Tx) error { var err error logs, err = db.GetAuditLogsByUser(ctx, tx, userID, pageOpts) if err != nil { return errors.Wrap(err, "db.GetAuditLogsByUser") } return nil }); err != nil { return nil, errors.Wrap(err, "db.WithTxFailSilently") } return logs, nil } // CleanupOldLogs deletes audit logs older than the specified number of days func (l *Logger) CleanupOldLogs(ctx context.Context, daysToKeep int) (int, error) { if daysToKeep <= 0 { return 0, errors.New("daysToKeep must be positive") } cutoffTime := time.Now().AddDate(0, 0, -daysToKeep).Unix() var count int if err := db.WithTxFailSilently(ctx, l.conn, func(ctx context.Context, tx bun.Tx) error { var err error count, err = db.CleanupOldAuditLogs(ctx, tx, cutoffTime) if err != nil { return errors.Wrap(err, "db.CleanupOldAuditLogs") } return nil }); err != nil { return 0, errors.Wrap(err, "db.WithTxFailSilently") } return count, nil }