Files
2026-06-12 14:25:34 +02:00

148 lines
3.3 KiB
Go

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
}
if entries == nil {
entries = []Entry{}
}
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})
}