Files
oslstats/internal/db/auditlog.go
2026-02-11 19:10:39 +11:00

172 lines
4.6 KiB
Go

package db
import (
"context"
"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"`
}
// 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 := Insert(tx, log).Exec(ctx)
if err != nil {
return errors.Wrap(err, "db.Insert")
}
return nil
}
type AuditLogFilter struct {
*ListFilter
}
func NewAuditLogFilter() *AuditLogFilter {
return &AuditLogFilter{
ListFilter: NewListFilter(),
}
}
func (a *AuditLogFilter) UserID(id int) *AuditLogFilter {
a.Add("al.user_id", "=", id)
return a
}
func (a *AuditLogFilter) Action(action string) *AuditLogFilter {
a.Add("al.action", "=", action)
return a
}
func (a *AuditLogFilter) ResourceType(resourceType string) *AuditLogFilter {
a.Add("al.resource_type", "=", resourceType)
return a
}
func (a *AuditLogFilter) Result(result string) *AuditLogFilter {
a.Add("al.result", "=", result)
return a
}
func (a *AuditLogFilter) DateRange(start, end int64) *AuditLogFilter {
if start > 0 {
a.Add("al.created_at", ">=", start)
}
if end > 0 {
a.Add("al.created_at", "<=", end)
}
return a
}
// GetAuditLogs retrieves audit logs with optional filters and pagination
func GetAuditLogs(ctx context.Context, tx bun.Tx, pageOpts *PageOpts, filters *AuditLogFilter) (*List[AuditLog], error) {
defaultPageOpts := &PageOpts{
Page: 1,
PerPage: 50,
Order: bun.OrderDesc,
OrderBy: "created_at",
}
return GetList[AuditLog](tx).
Relation("User").
Filter(filters.filters...).
GetPaged(ctx, pageOpts, defaultPageOpts)
}
// GetAuditLogsByUser retrieves audit logs for a specific user
func GetAuditLogsByUser(ctx context.Context, tx bun.Tx, userID int, pageOpts *PageOpts) (*List[AuditLog], error) {
if userID <= 0 {
return nil, errors.New("userID must be positive")
}
filters := NewAuditLogFilter().UserID(userID)
return GetAuditLogs(ctx, tx, pageOpts, filters)
}
// GetAuditLogsByAction retrieves audit logs for a specific action
func GetAuditLogsByAction(ctx context.Context, tx bun.Tx, action string, pageOpts *PageOpts) (*List[AuditLog], error) {
if action == "" {
return nil, errors.New("action cannot be empty")
}
filters := NewAuditLogFilter().Action(action)
return GetAuditLogs(ctx, tx, pageOpts, filters)
}
// GetAuditLogByID retrieves a single audit log by ID
func GetAuditLogByID(ctx context.Context, tx bun.Tx, id int) (*AuditLog, error) {
if id <= 0 {
return nil, errors.New("id must be positive")
}
return GetByID[AuditLog](tx, id).Relation("User").Get(ctx)
}
// GetUniqueActions retrieves a list of all unique actions in the audit log
func GetUniqueActions(ctx context.Context, tx bun.Tx) ([]string, error) {
var actions []string
err := tx.NewSelect().
Model((*AuditLog)(nil)).
Column("action").
Distinct().
Order("action ASC").
Scan(ctx, &actions)
if err != nil {
return nil, errors.Wrap(err, "tx.NewSelect")
}
return actions, nil
}
// GetUniqueResourceTypes retrieves a list of all unique resource types in the audit log
func GetUniqueResourceTypes(ctx context.Context, tx bun.Tx) ([]string, error) {
var resourceTypes []string
err := tx.NewSelect().
Model((*AuditLog)(nil)).
Column("resource_type").
Distinct().
Order("resource_type ASC").
Scan(ctx, &resourceTypes)
if err != nil {
return nil, errors.Wrap(err, "tx.NewSelect")
}
return resourceTypes, nil
}
// 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
}