ITSM/schema.sql

297 lines
16 KiB
SQL
Executable File

-- ITSM-Plattform: PostgreSQL-Schema
-- Mandantenfaehig (mehrere Organisationen auf einer Instanz, Daten per tenant_id getrennt).
-- Enthaelt audit_log fuer Nachvollziehbarkeit gemaess ISO 27001 A.12.4 / DSGVO Art. 30.
CREATE TABLE IF NOT EXISTS tenants (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
dsb_name TEXT, -- Datenschutzbeauftragter/Kontakt
dsb_email TEXT,
retention_tickets_days INTEGER NOT NULL DEFAULT 1095, -- DSGVO Speicherbegrenzung (Default ~3 Jahre)
retention_audit_days INTEGER NOT NULL DEFAULT 1825, -- Audit-Log-Aufbewahrung (Default ~5 Jahre)
sla_antwort_minuten INTEGER NOT NULL DEFAULT 480,
sla_loesung_minuten INTEGER NOT NULL DEFAULT 2880
);
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
tenant_id INTEGER NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
email TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'agent', -- 'admin' | 'agent'
active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
last_login_at TIMESTAMPTZ
);
CREATE TABLE IF NOT EXISTS services (
id SERIAL PRIMARY KEY,
tenant_id INTEGER NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
name TEXT NOT NULL,
beschreibung TEXT,
kategorie TEXT,
gebucht BOOLEAN NOT NULL DEFAULT TRUE,
endpoint TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS tickets (
id SERIAL PRIMARY KEY,
tenant_id INTEGER NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
ticket_nr TEXT NOT NULL,
titel TEXT NOT NULL,
beschreibung TEXT,
service_id INTEGER REFERENCES services(id) ON DELETE SET NULL,
status TEXT NOT NULL DEFAULT 'Offen',
prioritaet TEXT NOT NULL DEFAULT 'Mittel',
kategorie TEXT NOT NULL DEFAULT 'Task',
zugewiesen_an TEXT,
ersteller_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
fortschritt INTEGER NOT NULL DEFAULT 0,
sla_antwort_minuten INTEGER,
sla_loesung_minuten INTEGER,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (tenant_id, ticket_nr)
);
CREATE TABLE IF NOT EXISTS ticket_timeline (
id SERIAL PRIMARY KEY,
ticket_id INTEGER NOT NULL REFERENCES tickets(id) ON DELETE CASCADE,
tenant_id INTEGER NOT NULL,
zeit TIMESTAMPTZ NOT NULL DEFAULT now(),
akteur TEXT,
text TEXT
);
CREATE TABLE IF NOT EXISTS audit_log (
id SERIAL PRIMARY KEY,
tenant_id INTEGER REFERENCES tenants(id) ON DELETE CASCADE,
user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
zeit TIMESTAMPTZ NOT NULL DEFAULT now(),
aktion TEXT NOT NULL,
entity_typ TEXT,
entity_id TEXT,
details JSONB,
ip TEXT
);
CREATE INDEX IF NOT EXISTS idx_tickets_tenant_status ON tickets (tenant_id, status);
CREATE INDEX IF NOT EXISTS idx_tickets_tenant_kategorie ON tickets (tenant_id, kategorie);
CREATE INDEX IF NOT EXISTS idx_timeline_ticket ON ticket_timeline (ticket_id);
CREATE INDEX IF NOT EXISTS idx_audit_tenant_zeit ON audit_log (tenant_id, zeit DESC);
CREATE INDEX IF NOT EXISTS idx_services_tenant ON services (tenant_id);
CREATE INDEX IF NOT EXISTS idx_users_tenant ON users (tenant_id);
-- Vorbereitung fuer spaetere AD/LDAP-Anbindung (Nutzer-Vorgabe 2026-07-13):
-- 'local' = im ITSM selbst verwaltetes Konto (heutiger Stand), 'ldap' reserviert
-- fuer die kommende Ausbaustufe. Kein Verhaltensunterschied heute, nur Vorbereitung.
ALTER TABLE users ADD COLUMN IF NOT EXISTS auth_source TEXT NOT NULL DEFAULT 'local';
-- Zusaetzliche Nutzer-Details (Nutzer-Vorgabe 2026-07-13: "Mehr Details fuer
-- die User (Telefon, Adresse, etc.)"). Alle optional, da nicht jeder Mandant
-- diese Angaben pflegen moechte.
ALTER TABLE users ADD COLUMN IF NOT EXISTS vorname TEXT;
ALTER TABLE users ADD COLUMN IF NOT EXISTS nachname TEXT;
ALTER TABLE users ADD COLUMN IF NOT EXISTS telefon TEXT;
ALTER TABLE users ADD COLUMN IF NOT EXISTS abteilung TEXT;
ALTER TABLE users ADD COLUMN IF NOT EXISTS adresse TEXT;
-- ── Wissensdatenbank ──────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS kb_articles (
id SERIAL PRIMARY KEY,
tenant_id INTEGER NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
titel TEXT NOT NULL,
kategorie TEXT NOT NULL DEFAULT 'Allgemein',
inhalt TEXT NOT NULL,
tags TEXT,
autor_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_kb_tenant_kategorie ON kb_articles (tenant_id, kategorie);
-- ── CMDB (Configuration Management Database) ──────────────────────────────────
CREATE TABLE IF NOT EXISTS configuration_items (
id SERIAL PRIMARY KEY,
tenant_id INTEGER NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
name TEXT NOT NULL,
ci_typ TEXT NOT NULL DEFAULT 'Sonstiges',
status TEXT NOT NULL DEFAULT 'Aktiv',
beschreibung TEXT,
attribute JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_ci_tenant_typ ON configuration_items (tenant_id, ci_typ);
CREATE INDEX IF NOT EXISTS idx_ci_tenant_status ON configuration_items (tenant_id, status);
CREATE TABLE IF NOT EXISTS ci_relationships (
id SERIAL PRIMARY KEY,
tenant_id INTEGER NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
from_ci_id INTEGER NOT NULL REFERENCES configuration_items(id) ON DELETE CASCADE,
to_ci_id INTEGER NOT NULL REFERENCES configuration_items(id) ON DELETE CASCADE,
beziehungs_typ TEXT NOT NULL DEFAULT 'haengt ab von',
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_cirel_tenant ON ci_relationships (tenant_id);
CREATE INDEX IF NOT EXISTS idx_cirel_from ON ci_relationships (from_ci_id);
CREATE INDEX IF NOT EXISTS idx_cirel_to ON ci_relationships (to_ci_id);
-- ═══════════════════════════════════════════════════════════════════════════════
-- Migration 2026-07-15: ITIL-v3/v4-Ausrichtung + Sicherheits-Haertung (Rust-Rewrite)
-- Alle Aenderungen idempotent, damit init beim App-Start gefahrlos wiederholt laeuft.
-- ═══════════════════════════════════════════════════════════════════════════════
-- ── Rollen (ITIL-orientiert) ──────────────────────────────────────────────────
-- 'admin' (Service Owner), 'change_manager' (Change Enablement/CAB),
-- 'agent' (Service Desk), 'user' (Requester/Self-Service).
-- users.role existiert bereits als TEXT; erlaubte Werte prueft die App.
-- ── Tickets: Impact/Urgency-Prioritaetsmatrix (ITIL v3, SO 4.2.5.4) ──────────
ALTER TABLE tickets ADD COLUMN IF NOT EXISTS impact TEXT NOT NULL DEFAULT 'Mittel';
ALTER TABLE tickets ADD COLUMN IF NOT EXISTS urgency TEXT NOT NULL DEFAULT 'Mittel';
-- ── Tickets: Change Enablement (v4) / Change Management (v3) ──────────────────
ALTER TABLE tickets ADD COLUMN IF NOT EXISTS change_typ TEXT;
ALTER TABLE tickets ADD COLUMN IF NOT EXISTS approval_status TEXT;
ALTER TABLE tickets ADD COLUMN IF NOT EXISTS approved_by INTEGER REFERENCES users(id) ON DELETE SET NULL;
ALTER TABLE tickets ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ;
-- ── Tickets: Problem Management (v3 SO 4.4 / v4 Practice) ─────────────────────
ALTER TABLE tickets ADD COLUMN IF NOT EXISTS problem_id INTEGER REFERENCES tickets(id) ON DELETE SET NULL;
ALTER TABLE tickets ADD COLUMN IF NOT EXISTS known_error BOOLEAN NOT NULL DEFAULT FALSE;
CREATE INDEX IF NOT EXISTS idx_tickets_problem ON tickets (problem_id);
-- ── Tickets: SLA-Zeitstempel (Service Level Management) ───────────────────────
ALTER TABLE tickets ADD COLUMN IF NOT EXISTS first_response_at TIMESTAMPTZ;
ALTER TABLE tickets ADD COLUMN IF NOT EXISTS resolved_at TIMESTAMPTZ;
ALTER TABLE tickets ADD COLUMN IF NOT EXISTS closed_at TIMESTAMPTZ;
-- ── Ticket-CI-Verknuepfung (SACM v3 / Service Configuration Management v4) ────
CREATE TABLE IF NOT EXISTS ticket_ci_links (
id SERIAL PRIMARY KEY,
tenant_id INTEGER NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
ticket_id INTEGER NOT NULL REFERENCES tickets(id) ON DELETE CASCADE,
ci_id INTEGER NOT NULL REFERENCES configuration_items(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (ticket_id, ci_id)
);
CREATE INDEX IF NOT EXISTS idx_tcl_ticket ON ticket_ci_links (ticket_id);
CREATE INDEX IF NOT EXISTS idx_tcl_ci ON ticket_ci_links (ci_id);
-- ── Wissensdatenbank: Freigabe-Workflow (Knowledge Management) ────────────────
-- Bestehende Artikel werden einmalig 'Freigegeben'; neue starten als 'Entwurf'.
ALTER TABLE kb_articles ADD COLUMN IF NOT EXISTS status TEXT;
UPDATE kb_articles SET status='Freigegeben' WHERE status IS NULL;
ALTER TABLE kb_articles ALTER COLUMN status SET DEFAULT 'Entwurf';
ALTER TABLE kb_articles ALTER COLUMN status SET NOT NULL;
-- ── Login-Rate-Limiting (Brute-Force-Schutz, DB-basiert => multi-worker-fest) ──
CREATE TABLE IF NOT EXISTS login_attempts (
id SERIAL PRIMARY KEY,
email TEXT NOT NULL,
ip TEXT,
zeit TIMESTAMPTZ NOT NULL DEFAULT now(),
success BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE INDEX IF NOT EXISTS idx_login_attempts_email_zeit ON login_attempts (email, zeit DESC);
CREATE INDEX IF NOT EXISTS idx_login_attempts_ip_zeit ON login_attempts (ip, zeit DESC);
-- ── Server-seitige Sessions (Rust-Rewrite 2026-07-15) ─────────────────────────
-- In der DB liegt nur der SHA-256-Hash des Cookie-Tokens: ein DB-Leak
-- kompromittiert keine laufenden Sessions; Sessions sind widerrufbar.
CREATE TABLE IF NOT EXISTS sessions (
token_hash TEXT PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
csrf_token TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
expires_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions (user_id);
CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions (expires_at);
-- ═══════════════════════════════════════════════════════════════════════════════
-- Migration 2026-07-15b: Konzernplattform phase-001 — Module HR + CRM (ADR-009)
-- HR-Daten: besonders schuetzenswert; Zugriff nur admin + hr_manager (App-RBAC).
-- ═══════════════════════════════════════════════════════════════════════════════
-- ── Modul HR ──────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS hr_employees (
id SERIAL PRIMARY KEY,
tenant_id INTEGER NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
personalnr TEXT,
vorname TEXT NOT NULL,
nachname TEXT NOT NULL,
email TEXT,
abteilung TEXT,
position TEXT,
eintritt DATE,
austritt DATE,
status TEXT NOT NULL DEFAULT 'Aktiv', -- Aktiv | Onboarding | Ausgeschieden
notizen TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_hr_emp_tenant_status ON hr_employees (tenant_id, status);
CREATE TABLE IF NOT EXISTS hr_absences (
id SERIAL PRIMARY KEY,
tenant_id INTEGER NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
employee_id INTEGER NOT NULL REFERENCES hr_employees(id) ON DELETE CASCADE,
typ TEXT NOT NULL DEFAULT 'Urlaub', -- Urlaub | Krankheit | Sonderurlaub | Fortbildung
von DATE NOT NULL,
bis DATE NOT NULL,
status TEXT NOT NULL DEFAULT 'Beantragt', -- Beantragt | Genehmigt | Abgelehnt
kommentar TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_hr_abs_tenant ON hr_absences (tenant_id, status);
CREATE INDEX IF NOT EXISTS idx_hr_abs_emp ON hr_absences (employee_id);
-- ── Modul CRM ─────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS crm_companies (
id SERIAL PRIMARY KEY,
tenant_id INTEGER NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
name TEXT NOT NULL,
branche TEXT,
website TEXT,
telefon TEXT,
adresse TEXT,
status TEXT NOT NULL DEFAULT 'Lead', -- Lead | Kunde | Partner | Inaktiv
notizen TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_crm_comp_tenant_status ON crm_companies (tenant_id, status);
CREATE TABLE IF NOT EXISTS crm_contacts (
id SERIAL PRIMARY KEY,
tenant_id INTEGER NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
company_id INTEGER NOT NULL REFERENCES crm_companies(id) ON DELETE CASCADE,
vorname TEXT,
nachname TEXT NOT NULL,
email TEXT,
telefon TEXT,
position TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_crm_cont_company ON crm_contacts (company_id);
CREATE TABLE IF NOT EXISTS crm_deals (
id SERIAL PRIMARY KEY,
tenant_id INTEGER NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
company_id INTEGER NOT NULL REFERENCES crm_companies(id) ON DELETE CASCADE,
titel TEXT NOT NULL,
wert_cent BIGINT NOT NULL DEFAULT 0,
stufe TEXT NOT NULL DEFAULT 'Lead', -- Lead | Qualifiziert | Angebot | Verhandlung | Gewonnen | Verloren
faellig DATE,
notizen TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_crm_deals_tenant_stufe ON crm_deals (tenant_id, stufe);
CREATE INDEX IF NOT EXISTS idx_crm_deals_company ON crm_deals (company_id);