package audit import ( "context" "crypto/rand" "encoding/json" "errors" "fmt" "time" "github.com/jackc/pgx/v5/pgxpool" ) type Event struct { ID string Actor string Action string ResourceType string ResourceID string Result string OccurredAt time.Time CorrelationID string Before map[string]any After map[string]any } type Store interface { Append(context.Context, Event) error } type MemoryStore struct { Events []Event } func (store *MemoryStore) Append(_ context.Context, event Event) error { if event.ID == "" { event.ID = newID() } if event.OccurredAt.IsZero() { event.OccurredAt = time.Now().UTC() } store.Events = append(store.Events, event) return nil } type PostgresStore struct{ Pool *pgxpool.Pool } func (store PostgresStore) Append(ctx context.Context, event Event) error { if store.Pool == nil { return errors.New("audit database pool is nil") } if event.ID == "" { event.ID = newID() } if event.OccurredAt.IsZero() { event.OccurredAt = time.Now().UTC() } before, err := json.Marshal(event.Before) if err != nil { return errors.New("marshal audit before diff") } after, err := json.Marshal(event.After) if err != nil { return errors.New("marshal audit after diff") } _, err = store.Pool.Exec(ctx, `INSERT INTO audit_events (id, actor, action, resource_type, resource_id, result, occurred_at, correlation_id, before_diff, after_diff) VALUES ($1::uuid, $2, $3, $4, NULLIF($5, '')::uuid, $6, $7, $8, $9::jsonb, $10::jsonb)`, event.ID, event.Actor, event.Action, event.ResourceType, event.ResourceID, event.Result, event.OccurredAt, event.CorrelationID, before, after) if err != nil { return errors.New("write audit event") } return nil } func newID() string { bytes := make([]byte, 16) if _, err := rand.Read(bytes); err != nil { return "00000000-0000-4000-8000-000000000000" } bytes[6] = (bytes[6] & 0x0f) | 0x40 bytes[8] = (bytes[8] & 0x3f) | 0x80 return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x", bytes[0:4], bytes[4:6], bytes[6:8], bytes[8:10], bytes[10:16]) } func RecordSecurityAction(ctx context.Context, store Store, actor, action, result, correlationID string) error { return store.Append(ctx, Event{Actor: actor, Action: action, ResourceType: "security", Result: result, CorrelationID: correlationID}) }