Files
paliad/internal/services/system_audit_log_service.go
mAi a4e2f3526d feat(checklists): t-paliad-225 Slice A backend — user-authored templates
m/paliad#61 Slice A. Introduces paliad.checklists (mig 114) as the
DB-backed companion to the static Go catalog. ChecklistCatalogService
unifies both sources at read time; ChecklistTemplateService handles
authoring CRUD + visibility toggle (private↔firm; Slice B opens
'shared' and 'global').

Schema (mig 114, idempotent):
- paliad.checklists (uuid, slug UNIQUE, owner_id FK, title/description
  /regime/court/reference/deadline/lang, body jsonb, visibility CHECK
  ('private','shared','firm','global'), promoted_at/_by, timestamps)
- paliad.can_see_checklist(uuid, uuid) STABLE SECURITY DEFINER —
  owner OR firm/global. Slice B extends with the explicit-share branch.
- RLS: select via can_see_checklist; insert owner=self; update/delete
  owner OR global_admin
- ALTER paliad.checklist_instances ADD COLUMN template_snapshot jsonb
  (snapshot semantics so per-Akte instances stay decoupled from
  subsequent template edits)

Services:
- ChecklistCatalogService — ListVisible, Find, SnapshotBody, IsStaticSlug.
  Reapplies visibility application-side (service-role bypasses RLS, per
  visibility.go pattern). Static-slug map computed once at boot for
  collision detection.
- ChecklistTemplateService — Create (auto-generates u-<slug>-<hex> with
  retry), Update (changed_fields[] in audit), SetVisibility, Delete,
  ListOwnedBy, GetBySlug. Owner-or-global_admin gate.
- SystemAuditLogService.WriteChecklistEvent — thin helper writing into
  paliad.system_audit_log with scope='org'.
- ChecklistInstanceService.Create now captures template_snapshot via
  the catalog; GetByID returns it inline so the frontend can render
  the captured body even after the upstream template is mutated.

Endpoints (all owner-gated where mutating):
- GET    /api/checklists                 — merged catalog (static + DB visible)
- GET    /api/checklists/{slug}          — single template; static-first lookup
- GET    /api/checklists/templates/mine  — caller's authored templates
- POST   /api/checklists/templates       — create
- PATCH  /api/checklists/templates/{slug}            — edit
- PATCH  /api/checklists/templates/{slug}/visibility — private↔firm
- DELETE /api/checklists/templates/{slug}            — delete
- GET    /checklists/new, /checklists/{slug}/edit    — author wizard pages

Tests: pure-helper unit tests cover slugifyTitle (umlaut → ae/oe/ue/ss
normalisation + clamp), regime/lang/visibility validation, body-shape
enforcement, static-slug detection, predicate shape, clamp.
2026-05-20 15:24:06 +02:00

69 lines
2.1 KiB
Go

package services
import (
"context"
"encoding/json"
"fmt"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
)
// SystemAuditLogService is a thin write helper for paliad.system_audit_log
// (mig 102). Each domain emits its own event_type prefix
// (checklist.* / data_export* / …) so dashboards can group by feature.
//
// The audit row is best-effort INSIDE the caller's transaction — the
// caller passes its in-flight *sqlx.Tx so the audit write rolls back
// with the data change if anything else fails.
type SystemAuditLogService struct {
db *sqlx.DB
}
func NewSystemAuditLogService(db *sqlx.DB) *SystemAuditLogService {
return &SystemAuditLogService{db: db}
}
// ChecklistAuditEvent is the input shape for the WriteChecklistEvent
// helper. Scope defaults to 'org' since template-level events are firm-
// wide; instance-level events stay on paliad.project_events via the
// existing helpers.
type ChecklistAuditEvent struct {
EventType string // e.g. "checklist.authored", "checklist.edited"
ActorID uuid.UUID
ActorEmail string // captured at write time; survives user deletion
Metadata map[string]any
}
// WriteChecklistEvent inserts a row into paliad.system_audit_log with
// scope='org' and scope_root=NULL. Metadata is JSON-encoded.
func (s *SystemAuditLogService) WriteChecklistEvent(ctx context.Context, tx *sqlx.Tx, evt ChecklistAuditEvent) error {
if evt.EventType == "" {
return fmt.Errorf("system_audit_log: event_type required")
}
if evt.Metadata == nil {
evt.Metadata = map[string]any{}
}
mb, err := json.Marshal(evt.Metadata)
if err != nil {
return fmt.Errorf("system_audit_log marshal: %w", err)
}
exec := func(q string, args ...any) error {
if tx != nil {
_, err := tx.ExecContext(ctx, q, args...)
return err
}
_, err := s.db.ExecContext(ctx, q, args...)
return err
}
if err := exec(
`INSERT INTO paliad.system_audit_log
(event_type, actor_id, actor_email, scope, scope_root, metadata)
VALUES ($1, $2, $3, 'org', NULL, $4::jsonb)`,
evt.EventType, evt.ActorID, evt.ActorEmail, string(mb),
); err != nil {
return fmt.Errorf("system_audit_log insert: %w", err)
}
return nil
}