187 lines
4.6 KiB
Go
187 lines
4.6 KiB
Go
package db
|
|
|
|
import (
|
|
"net/http"
|
|
"reflect"
|
|
"strings"
|
|
)
|
|
|
|
type AuditMeta struct {
|
|
ipAddress string
|
|
userAgent string
|
|
u *User
|
|
}
|
|
|
|
func NewAudit(ipAdd, agent string, user *User) *AuditMeta {
|
|
return &AuditMeta{ipAdd, agent, user}
|
|
}
|
|
|
|
func NewAuditFromRequest(r *http.Request) *AuditMeta {
|
|
u := CurrentUser(r.Context())
|
|
return &AuditMeta{r.RemoteAddr, r.UserAgent(), 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, match := strings.CutPrefix(part, "table:")
|
|
if match {
|
|
return part
|
|
}
|
|
return part
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback: use struct name in lowercase + "s"
|
|
return strings.ToLower(t.Name()) + "s"
|
|
}
|
|
|
|
// extractTableName gets the bun table alias from a model type using reflection
|
|
// Example: Season with `bun:"table:seasons,alias:s"` returns "s"
|
|
func extractTableAlias[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, match := strings.CutPrefix(part, "alias:")
|
|
if match {
|
|
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
|
|
}
|