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

144 lines
3.7 KiB
Go

package db
import (
"context"
"database/sql"
"encoding/json"
"github.com/pkg/errors"
"github.com/uptrace/bun"
)
type AuditLog struct {
bun.BaseModel `bun:"table:audit_log,alias:al"`
ID int `bun:"id,pk,autoincrement"`
UserID int `bun:"user_id,notnull"`
Action string `bun:"action,notnull"`
ResourceType string `bun:"resource_type,notnull"`
ResourceID *string `bun:"resource_id"`
Details json.RawMessage `bun:"details,type:jsonb"`
IPAddress string `bun:"ip_address"`
UserAgent string `bun:"user_agent"`
Result string `bun:"result,notnull"` // success, denied, error
ErrorMessage *string `bun:"error_message"`
CreatedAt int64 `bun:"created_at,notnull"`
// Relations
User *User `bun:"rel:belongs-to,join:user_id=id"`
}
// TODO: add AuditLogs to match list style with PageOpts
// CreateAuditLog creates a new audit log entry
func CreateAuditLog(ctx context.Context, tx bun.Tx, log *AuditLog) error {
if log == nil {
return errors.New("log cannot be nil")
}
_, err := tx.NewInsert().
Model(log).
Exec(ctx)
if err != nil {
return errors.Wrap(err, "tx.NewInsert")
}
return nil
}
type AuditLogFilters struct {
UserID *int
Action *string
ResourceType *string
Result *string
}
// GetAuditLogs retrieves audit logs with optional filters and pagination
// TODO: change this to use db.PageOpts
func GetAuditLogs(ctx context.Context, tx bun.Tx, limit, offset int, filters *AuditLogFilters) ([]*AuditLog, int, error) {
query := tx.NewSelect().
Model((*AuditLog)(nil)).
Relation("User").
Order("created_at DESC")
// Apply filters if provided
if filters != nil {
if filters.UserID != nil {
query = query.Where("al.user_id = ?", *filters.UserID)
}
if filters.Action != nil {
query = query.Where("al.action = ?", *filters.Action)
}
if filters.ResourceType != nil {
query = query.Where("al.resource_type = ?", *filters.ResourceType)
}
if filters.Result != nil {
query = query.Where("al.result = ?", *filters.Result)
}
}
// Get total count
total, err := query.Count(ctx)
if err != nil {
return nil, 0, errors.Wrap(err, "query.Count")
}
// Get paginated results
var logs []*AuditLog
err = query.
Limit(limit).
Offset(offset).
Scan(ctx, &logs)
if err != nil && err != sql.ErrNoRows {
return nil, 0, errors.Wrap(err, "query.Scan")
}
return logs, total, nil
}
// GetAuditLogsByUser retrieves audit logs for a specific user
// TODO: change this to use db.PageOpts
func GetAuditLogsByUser(ctx context.Context, tx bun.Tx, userID int, limit, offset int) ([]*AuditLog, int, error) {
if userID <= 0 {
return nil, 0, errors.New("userID must be positive")
}
filters := &AuditLogFilters{
UserID: &userID,
}
return GetAuditLogs(ctx, tx, limit, offset, filters)
}
// GetAuditLogsByAction retrieves audit logs for a specific action
// TODO: change this to use db.PageOpts
func GetAuditLogsByAction(ctx context.Context, tx bun.Tx, action string, limit, offset int) ([]*AuditLog, int, error) {
if action == "" {
return nil, 0, errors.New("action cannot be empty")
}
filters := &AuditLogFilters{
Action: &action,
}
return GetAuditLogs(ctx, tx, limit, offset, filters)
}
// CleanupOldAuditLogs deletes audit logs older than the specified timestamp
func CleanupOldAuditLogs(ctx context.Context, tx bun.Tx, olderThan int64) (int, error) {
result, err := tx.NewDelete().
Model((*AuditLog)(nil)).
Where("created_at < ?", olderThan).
Exec(ctx)
if err != nil {
return 0, errors.Wrap(err, "tx.NewDelete")
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return 0, errors.Wrap(err, "result.RowsAffected")
}
return int(rowsAffected), nil
}