Install LOC guardrails (check-loc.sh, architecture.md, pre-commit hook) and split all 44 files exceeding 500 LOC into domain-focused modules: - consent-service (Go): models, handlers, services, database splits - backend-core (Python): security_api, rbac_api, pdf_service, auth splits - admin-core (TypeScript): 5 page.tsx + sidebar extractions - pitch-deck (TypeScript): 6 slides, 3 UI components, engine.ts splits - voice-service (Python): enhanced_task_orchestrator split Result: 0 violations, 36 exempted (pipeline, tests, pure-data files). Go build verified clean. No behavior changes — pure structural splits. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
268 lines
12 KiB
Go
268 lines
12 KiB
Go
package database
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
)
|
|
|
|
// migrateDSR creates DSGVO Data Subject Request tables (Phase 10)
|
|
// and EduSearch seed management tables (Phase 11).
|
|
func migrateDSR(db *DB) error {
|
|
ctx := context.Background()
|
|
|
|
migrations := []string{
|
|
// =============================================
|
|
// Phase 10: DSGVO Betroffenenanfragen (DSR)
|
|
// Data Subject Request Management
|
|
// =============================================
|
|
|
|
// Sequence for request numbers
|
|
`CREATE SEQUENCE IF NOT EXISTS dsr_request_number_seq START 1`,
|
|
|
|
// Main table: Data Subject Requests
|
|
`CREATE TABLE IF NOT EXISTS data_subject_requests (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
request_number VARCHAR(50) UNIQUE NOT NULL,
|
|
request_type VARCHAR(30) NOT NULL,
|
|
status VARCHAR(30) NOT NULL DEFAULT 'intake',
|
|
priority VARCHAR(20) DEFAULT 'normal',
|
|
source VARCHAR(30) NOT NULL DEFAULT 'api',
|
|
requester_email VARCHAR(255) NOT NULL,
|
|
requester_name VARCHAR(255),
|
|
requester_phone VARCHAR(50),
|
|
identity_verified BOOLEAN DEFAULT FALSE,
|
|
identity_verified_at TIMESTAMPTZ,
|
|
identity_verified_by UUID REFERENCES users(id),
|
|
identity_verification_method VARCHAR(50),
|
|
request_details JSONB DEFAULT '{}',
|
|
deadline_at TIMESTAMPTZ NOT NULL,
|
|
legal_deadline_days INT NOT NULL,
|
|
extended_deadline_at TIMESTAMPTZ,
|
|
extension_reason TEXT,
|
|
assigned_to UUID REFERENCES users(id),
|
|
processing_notes TEXT,
|
|
completed_at TIMESTAMPTZ,
|
|
completed_by UUID REFERENCES users(id),
|
|
result_summary TEXT,
|
|
result_data JSONB,
|
|
rejected_at TIMESTAMPTZ,
|
|
rejected_by UUID REFERENCES users(id),
|
|
rejection_reason TEXT,
|
|
rejection_legal_basis TEXT,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
created_by UUID REFERENCES users(id)
|
|
)`,
|
|
|
|
// DSR Status History for audit trail
|
|
`CREATE TABLE IF NOT EXISTS dsr_status_history (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
request_id UUID NOT NULL REFERENCES data_subject_requests(id) ON DELETE CASCADE,
|
|
from_status VARCHAR(30),
|
|
to_status VARCHAR(30) NOT NULL,
|
|
changed_by UUID REFERENCES users(id),
|
|
comment TEXT,
|
|
metadata JSONB,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
)`,
|
|
|
|
// DSR Communications log
|
|
`CREATE TABLE IF NOT EXISTS dsr_communications (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
request_id UUID NOT NULL REFERENCES data_subject_requests(id) ON DELETE CASCADE,
|
|
direction VARCHAR(10) NOT NULL,
|
|
channel VARCHAR(20) NOT NULL,
|
|
communication_type VARCHAR(50) NOT NULL,
|
|
template_version_id UUID,
|
|
subject VARCHAR(500),
|
|
body_html TEXT,
|
|
body_text TEXT,
|
|
recipient_email VARCHAR(255),
|
|
sent_at TIMESTAMPTZ,
|
|
error_message TEXT,
|
|
attachments JSONB DEFAULT '[]',
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
created_by UUID REFERENCES users(id)
|
|
)`,
|
|
|
|
// DSR Templates
|
|
`CREATE TABLE IF NOT EXISTS dsr_templates (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
template_type VARCHAR(50) UNIQUE NOT NULL,
|
|
name VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
request_types JSONB DEFAULT '["access","rectification","erasure","restriction","portability"]',
|
|
is_active BOOLEAN DEFAULT TRUE,
|
|
sort_order INT DEFAULT 0,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
)`,
|
|
|
|
// DSR Template Versions
|
|
`CREATE TABLE IF NOT EXISTS dsr_template_versions (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
template_id UUID REFERENCES dsr_templates(id) ON DELETE CASCADE,
|
|
version VARCHAR(20) NOT NULL,
|
|
language VARCHAR(5) DEFAULT 'de',
|
|
subject VARCHAR(500) NOT NULL,
|
|
body_html TEXT NOT NULL,
|
|
body_text TEXT NOT NULL,
|
|
status VARCHAR(20) DEFAULT 'draft',
|
|
published_at TIMESTAMPTZ,
|
|
created_by UUID REFERENCES users(id),
|
|
approved_by UUID REFERENCES users(id),
|
|
approved_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
UNIQUE(template_id, version, language)
|
|
)`,
|
|
|
|
// DSR Exception Checks (for Art. 17(3) erasure exceptions)
|
|
`CREATE TABLE IF NOT EXISTS dsr_exception_checks (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
request_id UUID NOT NULL REFERENCES data_subject_requests(id) ON DELETE CASCADE,
|
|
exception_type VARCHAR(50) NOT NULL,
|
|
description TEXT NOT NULL,
|
|
applies BOOLEAN,
|
|
checked_by UUID REFERENCES users(id),
|
|
checked_at TIMESTAMPTZ,
|
|
notes TEXT,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
)`,
|
|
|
|
// Phase 10 Indexes
|
|
`CREATE INDEX IF NOT EXISTS idx_dsr_user ON data_subject_requests(user_id)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_dsr_status ON data_subject_requests(status)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_dsr_type ON data_subject_requests(request_type)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_dsr_deadline ON data_subject_requests(deadline_at)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_dsr_assigned ON data_subject_requests(assigned_to)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_dsr_request_number ON data_subject_requests(request_number)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_dsr_created ON data_subject_requests(created_at)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_dsr_status_history_request ON dsr_status_history(request_id)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_dsr_communications_request ON dsr_communications(request_id)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_dsr_exception_checks_request ON dsr_exception_checks(request_id)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_dsr_templates_type ON dsr_templates(template_type)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_dsr_template_versions_template ON dsr_template_versions(template_id)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_dsr_template_versions_status ON dsr_template_versions(status)`,
|
|
|
|
// Insert default DSR templates
|
|
`INSERT INTO dsr_templates (template_type, name, description, request_types, sort_order)
|
|
VALUES
|
|
('dsr_receipt_access', 'Eingangsbestätigung Auskunft', 'Bestätigung des Eingangs einer Auskunftsanfrage nach Art. 15 DSGVO', '["access"]', 1),
|
|
('dsr_receipt_rectification', 'Eingangsbestätigung Berichtigung', 'Bestätigung des Eingangs einer Berichtigungsanfrage nach Art. 16 DSGVO', '["rectification"]', 2),
|
|
('dsr_receipt_erasure', 'Eingangsbestätigung Löschung', 'Bestätigung des Eingangs einer Löschanfrage nach Art. 17 DSGVO', '["erasure"]', 3),
|
|
('dsr_receipt_restriction', 'Eingangsbestätigung Einschränkung', 'Bestätigung des Eingangs einer Einschränkungsanfrage nach Art. 18 DSGVO', '["restriction"]', 4),
|
|
('dsr_receipt_portability', 'Eingangsbestätigung Datenübertragung', 'Bestätigung des Eingangs einer Datenübertragungsanfrage nach Art. 20 DSGVO', '["portability"]', 5),
|
|
('dsr_identity_request', 'Anfrage Identitätsnachweis', 'Aufforderung zur Identitätsverifizierung', '["access","rectification","erasure","restriction","portability"]', 6),
|
|
('dsr_processing_started', 'Bearbeitungsbestätigung', 'Bestätigung, dass die Bearbeitung begonnen hat', '["access","rectification","erasure","restriction","portability"]', 7),
|
|
('dsr_processing_update', 'Zwischenbericht', 'Zwischenstand zur Bearbeitung', '["access","rectification","erasure","restriction","portability"]', 8),
|
|
('dsr_clarification_request', 'Rückfragen', 'Anfrage zur Klärung des Begehrens', '["access","rectification","erasure","restriction","portability"]', 9),
|
|
('dsr_completed_access', 'Auskunft erteilt', 'Abschließende Mitteilung mit Datenauskunft', '["access"]', 10),
|
|
('dsr_completed_access_negative', 'Negativauskunft', 'Mitteilung dass keine Daten vorhanden sind', '["access"]', 11),
|
|
('dsr_completed_rectification', 'Berichtigung durchgeführt', 'Bestätigung der Datenberichtigung', '["rectification"]', 12),
|
|
('dsr_completed_erasure', 'Löschung durchgeführt', 'Bestätigung der Datenlöschung', '["erasure"]', 13),
|
|
('dsr_completed_restriction', 'Einschränkung aktiviert', 'Bestätigung der Verarbeitungseinschränkung', '["restriction"]', 14),
|
|
('dsr_completed_portability', 'Daten bereitgestellt', 'Mitteilung zur Datenübermittlung', '["portability"]', 15),
|
|
('dsr_restriction_lifted', 'Einschränkung aufgehoben', 'Vorabbenachrichtigung vor Aufhebung der Einschränkung', '["restriction"]', 16),
|
|
('dsr_rejected_identity', 'Ablehnung - Identität nicht verifizierbar', 'Ablehnung mangels Identitätsnachweis', '["access","rectification","erasure","restriction","portability"]', 17),
|
|
('dsr_rejected_exception', 'Ablehnung - Ausnahme', 'Ablehnung aufgrund gesetzlicher Ausnahmen (z.B. Art. 17 Abs. 3)', '["erasure","restriction"]', 18),
|
|
('dsr_rejected_unfounded', 'Ablehnung - Offensichtlich unbegründet', 'Ablehnung nach Art. 12 Abs. 5 DSGVO', '["access","rectification","erasure","restriction","portability"]', 19)
|
|
ON CONFLICT (template_type) DO NOTHING`,
|
|
|
|
// =============================================
|
|
// Phase 11: EduSearch Seeds Management
|
|
// Seed URLs for the education search crawler
|
|
// =============================================
|
|
|
|
// EduSearch Seed Categories
|
|
`CREATE TABLE IF NOT EXISTS edu_search_categories (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
name VARCHAR(50) UNIQUE NOT NULL,
|
|
display_name VARCHAR(100) NOT NULL,
|
|
description TEXT,
|
|
icon VARCHAR(10),
|
|
sort_order INT DEFAULT 0,
|
|
is_active BOOLEAN DEFAULT TRUE,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
)`,
|
|
|
|
// EduSearch Seeds (crawler seed URLs)
|
|
`CREATE TABLE IF NOT EXISTS edu_search_seeds (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
url VARCHAR(500) UNIQUE NOT NULL,
|
|
name VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
category_id UUID REFERENCES edu_search_categories(id) ON DELETE SET NULL,
|
|
source_type VARCHAR(20) DEFAULT 'GOV',
|
|
scope VARCHAR(20) DEFAULT 'FEDERAL',
|
|
state VARCHAR(5),
|
|
trust_boost DECIMAL(3,2) DEFAULT 0.50,
|
|
enabled BOOLEAN DEFAULT TRUE,
|
|
crawl_depth INT DEFAULT 2,
|
|
crawl_frequency VARCHAR(20) DEFAULT 'weekly',
|
|
last_crawled_at TIMESTAMPTZ,
|
|
last_crawl_status VARCHAR(20),
|
|
last_crawl_docs INT DEFAULT 0,
|
|
total_documents INT DEFAULT 0,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
created_by UUID REFERENCES users(id)
|
|
)`,
|
|
|
|
// EduSearch Crawl Runs (history of crawl executions)
|
|
`CREATE TABLE IF NOT EXISTS edu_search_crawl_runs (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
seed_id UUID REFERENCES edu_search_seeds(id) ON DELETE CASCADE,
|
|
status VARCHAR(20) DEFAULT 'running',
|
|
started_at TIMESTAMPTZ DEFAULT NOW(),
|
|
completed_at TIMESTAMPTZ,
|
|
pages_crawled INT DEFAULT 0,
|
|
documents_indexed INT DEFAULT 0,
|
|
errors_count INT DEFAULT 0,
|
|
error_details JSONB,
|
|
triggered_by UUID REFERENCES users(id)
|
|
)`,
|
|
|
|
// EduSearch Denylist (URLs/domains to never crawl)
|
|
`CREATE TABLE IF NOT EXISTS edu_search_denylist (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
pattern VARCHAR(500) UNIQUE NOT NULL,
|
|
pattern_type VARCHAR(20) DEFAULT 'domain',
|
|
reason TEXT,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
created_by UUID REFERENCES users(id)
|
|
)`,
|
|
|
|
// Phase 11 Indexes
|
|
`CREATE INDEX IF NOT EXISTS idx_edu_search_seeds_category ON edu_search_seeds(category_id)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_edu_search_seeds_enabled ON edu_search_seeds(enabled)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_edu_search_seeds_state ON edu_search_seeds(state)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_edu_search_seeds_scope ON edu_search_seeds(scope)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_edu_search_crawl_runs_seed ON edu_search_crawl_runs(seed_id)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_edu_search_crawl_runs_status ON edu_search_crawl_runs(status)`,
|
|
|
|
// Insert default EduSearch categories
|
|
`INSERT INTO edu_search_categories (name, display_name, description, icon, sort_order)
|
|
VALUES
|
|
('federal', 'Bundesebene', 'KMK, BMBF, Bildungsserver', '🏛️', 1),
|
|
('states', 'Bundesländer', 'Ministerien, Landesbildungsserver', '🗺️', 2),
|
|
('science', 'Wissenschaft', 'Bertelsmann, PISA, IGLU, TIMSS', '🔬', 3),
|
|
('universities', 'Universitäten', 'Deutsche Hochschulen', '🎓', 4),
|
|
('schools', 'Schulen', 'Schulwebsites', '🏫', 5),
|
|
('portals', 'Bildungsportale', 'Lehrer-Online, 4teachers, ZUM', '📚', 6),
|
|
('eu', 'EU/International', 'Europäische Bildungsberichte', '🇪🇺', 7),
|
|
('authorities', 'Schulbehörden', 'Regierungspräsidien, Schulämter', '📋', 8)
|
|
ON CONFLICT (name) DO NOTHING`,
|
|
}
|
|
|
|
for _, migration := range migrations {
|
|
if _, err := db.Pool.Exec(ctx, migration); err != nil {
|
|
return fmt.Errorf("migrateDSR: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|