Files
oslstats/internal/db/audit.go

149 lines
3.6 KiB
Go

package db
import (
"net/http"
"reflect"
"strings"
)
type AuditMeta struct {
r *http.Request
u *User
}
func NewAudit(r *http.Request, u *User) *AuditMeta {
if u == nil {
u = CurrentUser(r.Context())
}
return &AuditMeta{r, u}
}
// AuditInfo contains metadata for audit logging
type AuditInfo struct {
Action string // e.g., "seasons.create", "users.update"
ResourceType string // e.g., "season", "user"
ResourceID any // Primary key value (int, string, etc.)
Details any // Changed fields or additional metadata
}
// extractTableName gets the bun table name from a model type using reflection
// Example: Season with `bun:"table:seasons,alias:s"` returns "seasons"
func extractTableName[T any]() string {
var model T
t := reflect.TypeOf(model)
// Handle pointer types
if t.Kind() == reflect.Pointer {
t = t.Elem()
}
// Look for bun.BaseModel field with table tag
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
if field.Type.Name() == "BaseModel" {
bunTag := field.Tag.Get("bun")
if bunTag != "" {
// Parse tag: "table:seasons,alias:s" -> "seasons"
for part := range strings.SplitSeq(bunTag, ",") {
part, _ := strings.CutPrefix(part, "table:")
return part
}
}
}
}
// Fallback: use struct name in lowercase + "s"
return strings.ToLower(t.Name()) + "s"
}
// extractResourceType converts a table name to singular resource type
// Example: "seasons" -> "season", "users" -> "user"
func extractResourceType(tableName string) string {
// Simple singularization: remove trailing 's'
if strings.HasSuffix(tableName, "s") && len(tableName) > 1 {
return tableName[:len(tableName)-1]
}
return tableName
}
// buildAction creates a permission-style action string
// Example: ("season", "create") -> "seasons.create"
func buildAction(resourceType, operation string) string {
// Pluralize resource type (simple: add 's')
plural := resourceType
if !strings.HasSuffix(plural, "s") {
plural = plural + "s"
}
return plural + "." + operation
}
// extractPrimaryKey uses reflection to find and return the primary key value from a model
// Returns nil if no primary key is found
func extractPrimaryKey[T any](model *T) any {
if model == nil {
return nil
}
v := reflect.ValueOf(model)
if v.Kind() == reflect.Pointer {
v = v.Elem()
}
t := v.Type()
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
bunTag := field.Tag.Get("bun")
if bunTag != "" && strings.Contains(bunTag, "pk") {
// Found primary key field
fieldValue := v.Field(i)
if fieldValue.IsValid() && fieldValue.CanInterface() {
return fieldValue.Interface()
}
}
}
return nil
}
// extractChangedFields builds a map of field names to their new values
// Only includes fields specified in the columns list
func extractChangedFields[T any](model *T, columns []string) map[string]any {
if model == nil || len(columns) == 0 {
return nil
}
result := make(map[string]any)
v := reflect.ValueOf(model)
if v.Kind() == reflect.Pointer {
v = v.Elem()
}
t := v.Type()
// Build map of bun column names to field names
columnToField := make(map[string]int)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
bunTag := field.Tag.Get("bun")
if bunTag != "" {
// Parse bun tag to get column name (first part before comma)
parts := strings.Split(bunTag, ",")
if len(parts) > 0 && parts[0] != "" {
columnToField[parts[0]] = i
}
}
}
// Extract values for requested columns
for _, col := range columns {
if fieldIdx, ok := columnToField[col]; ok {
fieldValue := v.Field(fieldIdx)
if fieldValue.IsValid() && fieldValue.CanInterface() {
result[col] = fieldValue.Interface()
}
}
}
return result
}