Files
patrickandClaude Sonnet 5 2dc9b44e97
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (push) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Canceled after 0s
CI / Web (dms-browser) (push) Canceled after 0s
CI / Web (pdf-viewer) (push) Canceled after 0s
CI / Web (shl) (push) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s
INT-20: trigger-/automatisierungssystem nach upload
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 12:53:59 +02:00

115 lines
4.0 KiB
Go

package trigger
import (
"context"
"fmt"
"log"
"regexp"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/dms/internal/audittrail"
"gitea.perlbach24.de/scripte/nexarch/dms/internal/documenttype"
"gitea.perlbach24.de/scripte/nexarch/dms/internal/tag"
)
// Fire wertet alle aktivierten Trigger fuer event gegen documentID aus und
// fuehrt die passenden Aktionen aus. Ein Fehler in einer Aktion oder beim
// Werten eines einzelnen Triggers wird in trigger_runs und dem Audit-Log
// festgehalten, bricht aber die Auswertung der uebrigen Trigger nicht ab und
// wird NIE an den Aufrufer zurueckgegeben (siehe Package-Doc) — Fire selbst
// gibt nur einen Fehler zurueck, wenn das Dokument nicht lesbar ist.
func Fire(ctx context.Context, pool *pgxpool.Pool, v Vault, tenantSlug, event, documentID string) error {
var title, typeID string
err := pool.QueryRow(ctx, `
SELECT title, COALESCE(document_type_id::text, '') FROM documents
WHERE id = $1 AND trashed_at IS NULL AND deleted_at IS NULL
`, documentID).Scan(&title, &typeID)
if err == pgx.ErrNoRows {
return nil // Dokument geloescht/im Papierkorb: kein Trigger feuert.
}
if err != nil {
return fmt.Errorf("trigger: dokument lesen: %w", err)
}
rows, err := pool.Query(ctx, `SELECT `+columns+`, webhook_secret_wrapped_dek, webhook_secret_enc FROM triggers WHERE event = $1 AND enabled`, event)
if err != nil {
return fmt.Errorf("trigger: liste lesen: %w", err)
}
type row struct {
Trigger
wrappedDEK, secretEnc []byte
}
var list []row
for rows.Next() {
var r row
if err := rows.Scan(&r.ID, &r.Name, &r.Event, &r.FilenamePattern, &r.DocumentTypeID, &r.ActionType, &r.ActionTagID, &r.ActionDocumentTypeID, &r.WebhookURL, &r.Enabled, &r.CreatedBy, &r.wrappedDEK, &r.secretEnc); err != nil {
rows.Close()
return fmt.Errorf("trigger: liste lesen: %w", err)
}
list = append(list, r)
}
rows.Close()
if err := rows.Err(); err != nil {
return err
}
for _, r := range list {
if r.FilenamePattern != nil && !regexp.MustCompile(*r.FilenamePattern).MatchString(title) {
continue
}
if r.DocumentTypeID != nil && *r.DocumentTypeID != typeID {
continue
}
runErr := runAction(ctx, pool, v, tenantSlug, r.Trigger, r.wrappedDEK, r.secretEnc, documentID)
status := "ok"
errText := ""
if runErr != nil {
status = "failed"
errText = runErr.Error()
log.Printf("trigger: aktion fuer %q an dokument %q fehlgeschlagen: %v", r.Name, documentID, runErr)
}
if _, logErr := pool.Exec(ctx, `
INSERT INTO trigger_runs (trigger_id, document_id, event, status, error, fired_at)
VALUES ($1, $2, $3, $4, NULLIF($5, ''), $6)
`, r.ID, documentID, event, status, errText, time.Now()); logErr != nil {
log.Printf("trigger: lauf konnte nicht protokolliert werden: %v", logErr)
}
if runErr != nil {
_ = audittrail.Record(ctx, pool, audittrail.SystemActor, "trigger_failed", "trigger", r.ID, map[string]any{"document_id": documentID, "error": errText})
} else {
_ = audittrail.Record(ctx, pool, audittrail.SystemActor, "trigger_fired", "trigger", r.ID, map[string]any{"document_id": documentID, "action": r.ActionType})
}
}
return nil
}
func runAction(ctx context.Context, pool *pgxpool.Pool, v Vault, tenantSlug string, t Trigger, wrappedDEK, secretEnc []byte, documentID string) error {
switch t.ActionType {
case ActionSetTag:
return tag.Assign(ctx, pool, documentID, *t.ActionTagID)
case ActionSetType:
_, err := documenttype.ChangeDocumentType(ctx, pool, documentID, *t.ActionDocumentTypeID, false)
return err
case ActionWebhook:
secret := ""
if len(secretEnc) > 0 {
var err error
secret, err = v.Open(ctx, tenantSlug, wrappedDEK, secretEnc)
if err != nil {
return fmt.Errorf("geheimnis entschluesseln: %w", err)
}
}
return sendWebhook(ctx, *t.WebhookURL, secret, webhookPayload{
Event: t.Event,
DocumentID: documentID,
TriggerID: t.ID,
OccurredAt: time.Now().UTC().Format(time.RFC3339),
})
default:
return fmt.Errorf("unbekannte aktion %q", t.ActionType)
}
}