package training import ( "context" "encoding/json" "fmt" "time" "github.com/google/uuid" ) // LogAction creates an audit log entry func (s *Store) LogAction(ctx context.Context, entry *AuditLogEntry) error { entry.ID = uuid.New() entry.CreatedAt = time.Now().UTC() details, _ := json.Marshal(entry.Details) _, err := s.pool.Exec(ctx, ` INSERT INTO training_audit_log ( id, tenant_id, user_id, action, entity_type, entity_id, details, ip_address, created_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) `, entry.ID, entry.TenantID, entry.UserID, string(entry.Action), string(entry.EntityType), entry.EntityID, details, entry.IPAddress, entry.CreatedAt, ) return err } // ListAuditLog lists audit log entries for a tenant func (s *Store) ListAuditLog(ctx context.Context, tenantID uuid.UUID, filters *AuditLogFilters) ([]AuditLogEntry, int, error) { countQuery := "SELECT COUNT(*) FROM training_audit_log WHERE tenant_id = $1" countArgs := []interface{}{tenantID} countArgIdx := 2 query := ` SELECT id, tenant_id, user_id, action, entity_type, entity_id, details, ip_address, created_at FROM training_audit_log WHERE tenant_id = $1` args := []interface{}{tenantID} argIdx := 2 if filters != nil { if filters.UserID != nil { query += fmt.Sprintf(" AND user_id = $%d", argIdx) args = append(args, *filters.UserID) argIdx++ countQuery += fmt.Sprintf(" AND user_id = $%d", countArgIdx) countArgs = append(countArgs, *filters.UserID) countArgIdx++ } if filters.Action != "" { query += fmt.Sprintf(" AND action = $%d", argIdx) args = append(args, string(filters.Action)) argIdx++ countQuery += fmt.Sprintf(" AND action = $%d", countArgIdx) countArgs = append(countArgs, string(filters.Action)) countArgIdx++ } if filters.EntityType != "" { query += fmt.Sprintf(" AND entity_type = $%d", argIdx) args = append(args, string(filters.EntityType)) argIdx++ countQuery += fmt.Sprintf(" AND entity_type = $%d", countArgIdx) countArgs = append(countArgs, string(filters.EntityType)) countArgIdx++ } } var total int err := s.pool.QueryRow(ctx, countQuery, countArgs...).Scan(&total) if err != nil { return nil, 0, err } query += " ORDER BY created_at DESC" if filters != nil && filters.Limit > 0 { query += fmt.Sprintf(" LIMIT $%d", argIdx) args = append(args, filters.Limit) argIdx++ if filters.Offset > 0 { query += fmt.Sprintf(" OFFSET $%d", argIdx) args = append(args, filters.Offset) argIdx++ } } rows, err := s.pool.Query(ctx, query, args...) if err != nil { return nil, 0, err } defer rows.Close() var entries []AuditLogEntry for rows.Next() { var entry AuditLogEntry var action, entityType string var details []byte err := rows.Scan( &entry.ID, &entry.TenantID, &entry.UserID, &action, &entityType, &entry.EntityID, &details, &entry.IPAddress, &entry.CreatedAt, ) if err != nil { return nil, 0, err } entry.Action = AuditAction(action) entry.EntityType = AuditEntityType(entityType) json.Unmarshal(details, &entry.Details) if entry.Details == nil { entry.Details = map[string]interface{}{} } entries = append(entries, entry) } if entries == nil { entries = []AuditLogEntry{} } return entries, total, nil }