Files
oslstats/internal/auditlog/logger.go
2026-02-03 21:37:06 +11:00

167 lines
3.8 KiB
Go

// 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
// TODO: change this to user db.PageOpts
func (l *Logger) GetRecentLogs(ctx context.Context, limit, offset int) ([]*db.AuditLog, int, error) {
tx, err := l.conn.BeginTx(ctx, nil)
if err != nil {
return nil, 0, errors.Wrap(err, "conn.BeginTx")
}
defer func() { _ = tx.Rollback() }()
logs, total, err := db.GetAuditLogs(ctx, tx, limit, offset, nil)
if err != nil {
return nil, 0, err
}
_ = tx.Commit() // read only transaction
return logs, total, nil
}
// GetLogsByUser retrieves audit logs for a specific user
// TODO: change this to user db.PageOpts
func (l *Logger) GetLogsByUser(ctx context.Context, userID int, limit, offset int) ([]*db.AuditLog, int, error) {
tx, err := l.conn.BeginTx(ctx, nil)
if err != nil {
return nil, 0, errors.Wrap(err, "conn.BeginTx")
}
defer func() { _ = tx.Rollback() }()
logs, total, err := db.GetAuditLogsByUser(ctx, tx, userID, limit, offset)
if err != nil {
return nil, 0, err
}
_ = tx.Commit() // read only transaction
return logs, total, 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()
tx, err := l.conn.BeginTx(ctx, nil)
if err != nil {
return 0, errors.Wrap(err, "conn.BeginTx")
}
defer func() { _ = tx.Rollback() }()
count, err := db.CleanupOldAuditLogs(ctx, tx, cutoffTime)
if err != nil {
return 0, err
}
err = tx.Commit()
if err != nil {
return 0, errors.Wrap(err, "tx.Commit")
}
return count, nil
}