package audit import ( "context" "database/sql" "encoding/json" "net/http" "time" "proxui/backend/internal/auth" "proxui/backend/internal/membership" "proxui/backend/internal/rbac" ) type Repository interface { ListTenantAudit(ctx context.Context, tenantID string, limit int, offset int) ([]Entry, error) } type SQLRepository struct { db *sql.DB } func NewSQLRepository(db *sql.DB) SQLRepository { return SQLRepository{db: db} } func (r SQLRepository) ListTenantAudit(ctx context.Context, tenantID string, limit int, offset int) ([]Entry, error) { if limit <= 0 || limit > 100 { limit = 50 } rows, err := r.db.QueryContext(ctx, ` select id::text, tenant_id::text, coalesce(profile_id::text, ''), action, target_type, coalesce(target_id::text, ''), metadata, created_at from public.audit_log where tenant_id = $1 order by created_at desc limit $2 offset $3 `, tenantID, limit, offset) if err != nil { return nil, err } defer rows.Close() var entries []Entry for rows.Next() { var e Entry var metaBytes []byte if err := rows.Scan(&e.ID, &e.TenantID, &e.ProfileID, &e.Action, &e.TargetType, &e.TargetID, &metaBytes, &e.CreatedAt); err != nil { return nil, err } if metaBytes != nil { e.Metadata = metaBytes } else { e.Metadata = json.RawMessage("{}") } entries = append(entries, e) } if err := rows.Err(); err != nil { return nil, err } return entries, nil } type Entry struct { ID string `json:"id"` TenantID string `json:"tenant_id"` ProfileID string `json:"profile_id"` Action string `json:"action"` TargetType string `json:"target_type"` TargetID string `json:"target_id"` Metadata json.RawMessage `json:"metadata"` CreatedAt time.Time `json:"created_at"` } type Handler struct { repository Repository } func NewHandler(repository Repository) Handler { return Handler{repository: repository} } func (h Handler) ListTenantAudit(w http.ResponseWriter, r *http.Request) { _, ok := auth.PrincipalFromRequest(r) if !ok { writeError(w, http.StatusUnauthorized, "unauthorized") return } tenantID := r.PathValue("tenantID") if tenantID == "" { writeError(w, http.StatusBadRequest, "tenant_id_required") return } membershipID, ok := membership.FromRequest(r) if !ok { writeError(w, http.StatusForbidden, "forbidden") return } if membershipID.TenantID != tenantID { writeError(w, http.StatusForbidden, "forbidden") return } if !rbac.Can(rbac.Role(membershipID.Role), rbac.ActionAuditRead) { writeError(w, http.StatusForbidden, "forbidden") return } limit := 50 offset := 0 entries, err := h.repository.ListTenantAudit(r.Context(), tenantID, limit, offset) if err != nil { writeError(w, http.StatusInternalServerError, "audit_list_failed") return } writeJSON(w, http.StatusOK, map[string]any{ "data": entries, "limit": limit, "offset": offset, }) } func writeJSON(w http.ResponseWriter, status int, body any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(body) } func writeError(w http.ResponseWriter, status int, message string) { writeJSON(w, status, map[string]string{"error": message}) }