Problem: Frontend JS bundle used credentials:'same-origin' which dropped cookies on cross-domain requests. Login worked (200) but the subsequent /auth/me check returned 401, leaving the user stuck on the login screen. Fix: - AuthController now sets a dtp_jwt HttpOnly cookie on login/refresh - Cookie uses Domain=.fahrschultermin.de (shared between frontend and api subdomain), Secure, SameSite=Lax, Max-Age=8h - JwtMiddleware reads JWT from Authorization header OR cookie - Added AuthController::me() endpoint (was missing, caused 500) - Logout endpoint clears the cookie - Frontend index.php patches fetch() to use credentials:'include' for all /api/v1/* calls
370 lines
16 KiB
PL/PgSQL
Executable File
370 lines
16 KiB
PL/PgSQL
Executable File
-- Migration: 001_initial_schema.sql
|
|
-- Created: 2024
|
|
|
|
-- Enable UUID extension
|
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
|
|
|
-- ============================================
|
|
-- PLATFORM ADMINS (internal team)
|
|
-- ============================================
|
|
CREATE TABLE platform_admins (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
email VARCHAR(255) NOT NULL UNIQUE,
|
|
password_hash VARCHAR(255) NOT NULL,
|
|
name VARCHAR(255) NOT NULL,
|
|
role VARCHAR(50) NOT NULL DEFAULT 'support' CHECK (role IN ('super_admin', 'support', 'sales')),
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
last_login_at TIMESTAMP WITH TIME ZONE
|
|
);
|
|
|
|
CREATE INDEX idx_platform_admins_email ON platform_admins(email);
|
|
|
|
-- ============================================
|
|
-- MODULES (global module definitions)
|
|
-- ============================================
|
|
CREATE TABLE modules (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
key VARCHAR(100) NOT NULL UNIQUE,
|
|
name VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
monthly_price INTEGER NOT NULL DEFAULT 0, -- cent
|
|
is_active BOOLEAN DEFAULT TRUE,
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
-- Insert default modules
|
|
INSERT INTO modules (key, name, description, monthly_price) VALUES
|
|
('office', 'Office', 'Kundenverwaltung, Fahrzeugverwaltung, Berichte', 0),
|
|
('calendar', 'Kalender', 'Terminplanung (Integration fahrschultermin.de)', 0),
|
|
('messenger', 'Messenger', 'WhatsApp-ähnlicher Messenger', 0),
|
|
('learning_blank', 'Lernsoftware (Blanko)', 'Eigene Inhalte erstellen', 0),
|
|
('learning_premium', 'Lernsoftware (Premium)', 'Inkl. vorgefertigter Lerninhalte', 0);
|
|
|
|
-- ============================================
|
|
-- PACKAGES (pricing bundles)
|
|
-- ============================================
|
|
CREATE TABLE packages (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
key VARCHAR(100) NOT NULL UNIQUE,
|
|
name VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
monthly_price INTEGER NOT NULL DEFAULT 0, -- cent
|
|
modules_included JSONB DEFAULT '[]',
|
|
max_users INTEGER DEFAULT 5,
|
|
max_branches INTEGER DEFAULT 1,
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
-- Insert default packages
|
|
INSERT INTO packages (key, name, description, monthly_price, modules_included, max_users, max_branches) VALUES
|
|
('starter', 'Starter', 'Basic-Paket für kleine Fahrschulen', 4900, '["office", "calendar", "messenger"]', 5, 1),
|
|
('professional', 'Professional', 'Für mittlere Fahrschulen', 9900, '["office", "calendar", "messenger", "learning_blank"]', 20, 3),
|
|
('premium', 'Premium', 'Alle Funktionen inkl. Premium-Lerninhalte', 14900, '["office", "calendar", "messenger", "learning_premium"]', 100, 10);
|
|
|
|
-- ============================================
|
|
-- TENANTS (fahrschulen / customers)
|
|
-- ============================================
|
|
CREATE TABLE tenants (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
name VARCHAR(255) NOT NULL,
|
|
slug VARCHAR(255) NOT NULL UNIQUE,
|
|
email VARCHAR(255) NOT NULL,
|
|
phone VARCHAR(100),
|
|
address TEXT,
|
|
status VARCHAR(50) NOT NULL DEFAULT 'pending_approval' CHECK (status IN ('pending_approval', 'active', 'suspended', 'trial')),
|
|
trial_ends_at TIMESTAMP WITH TIME ZONE,
|
|
is_self_hosted BOOLEAN DEFAULT FALSE,
|
|
self_hosted_license_key VARCHAR(255),
|
|
-- Branding
|
|
logo_url VARCHAR(500),
|
|
primary_color VARCHAR(7) DEFAULT '#2563eb',
|
|
secondary_color VARCHAR(7) DEFAULT '#64748b',
|
|
custom_css TEXT,
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE INDEX idx_tenants_slug ON tenants(slug);
|
|
CREATE INDEX idx_tenants_status ON tenants(status);
|
|
CREATE INDEX idx_tenants_email ON tenants(email);
|
|
|
|
-- ============================================
|
|
-- BRANCHES (filialen)
|
|
-- ============================================
|
|
CREATE TABLE branches (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
|
name VARCHAR(255) NOT NULL,
|
|
address TEXT,
|
|
phone VARCHAR(100),
|
|
email VARCHAR(255),
|
|
opening_hours JSONB DEFAULT '{}',
|
|
is_headquarters BOOLEAN DEFAULT FALSE,
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE INDEX idx_branches_tenant ON branches(tenant_id);
|
|
|
|
-- ============================================
|
|
-- USERS (tenant users)
|
|
-- ============================================
|
|
CREATE TABLE users (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
|
branch_id UUID REFERENCES branches(id) ON DELETE SET NULL,
|
|
email VARCHAR(255) NOT NULL,
|
|
password_hash VARCHAR(255) NOT NULL,
|
|
first_name VARCHAR(255) NOT NULL,
|
|
last_name VARCHAR(255) NOT NULL,
|
|
user_type VARCHAR(50) NOT NULL DEFAULT 'student' CHECK (user_type IN ('admin', 'office', 'instructor', 'lecturer', 'student')),
|
|
status VARCHAR(50) NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'inactive', 'pending')),
|
|
avatar_url VARCHAR(500),
|
|
phone VARCHAR(100),
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
last_login_at TIMESTAMP WITH TIME ZONE,
|
|
UNIQUE(tenant_id, email)
|
|
);
|
|
|
|
CREATE INDEX idx_users_tenant ON users(tenant_id);
|
|
CREATE INDEX idx_users_branch ON users(branch_id);
|
|
CREATE INDEX idx_users_email ON users(email);
|
|
|
|
-- ============================================
|
|
-- TENANT MODULES (module activation per tenant)
|
|
-- ============================================
|
|
CREATE TABLE tenant_modules (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
|
module_id UUID NOT NULL REFERENCES modules(id) ON DELETE RESTRICT,
|
|
status VARCHAR(50) NOT NULL DEFAULT 'enabled' CHECK (status IN ('enabled', 'disabled')),
|
|
enabled_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
expires_at TIMESTAMP WITH TIME ZONE,
|
|
trial_ends_at TIMESTAMP WITH TIME ZONE,
|
|
UNIQUE(tenant_id, module_id)
|
|
);
|
|
|
|
CREATE INDEX idx_tenant_modules_tenant ON tenant_modules(tenant_id);
|
|
|
|
-- ============================================
|
|
-- TENANT PACKAGES (active package per tenant)
|
|
-- ============================================
|
|
CREATE TABLE tenant_packages (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
|
package_id UUID NOT NULL REFERENCES packages(id) ON DELETE RESTRICT,
|
|
started_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
expires_at TIMESTAMP WITH TIME ZONE,
|
|
UNIQUE(tenant_id)
|
|
);
|
|
|
|
CREATE INDEX idx_tenant_packages_tenant ON tenant_packages(tenant_id);
|
|
|
|
-- ============================================
|
|
-- LEARNING CONTENTS
|
|
-- ============================================
|
|
CREATE TABLE learning_contents (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE, -- NULL = platform global
|
|
category VARCHAR(255) NOT NULL,
|
|
title VARCHAR(500) NOT NULL,
|
|
description TEXT,
|
|
content_type VARCHAR(50) NOT NULL CHECK (content_type IN ('video', 'document', 'quiz', 'exercise')),
|
|
content_url VARCHAR(500),
|
|
thumbnail_url VARCHAR(500),
|
|
duration_minutes INTEGER,
|
|
is_premium BOOLEAN DEFAULT FALSE,
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE INDEX idx_learning_contents_tenant ON learning_contents(tenant_id);
|
|
CREATE INDEX idx_learning_contents_category ON learning_contents(category);
|
|
|
|
-- ============================================
|
|
-- STUDENT PROGRESS
|
|
-- ============================================
|
|
CREATE TABLE student_progress (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
content_id UUID NOT NULL REFERENCES learning_contents(id) ON DELETE CASCADE,
|
|
status VARCHAR(50) NOT NULL DEFAULT 'not_started' CHECK (status IN ('not_started', 'in_progress', 'completed')),
|
|
completed_at TIMESTAMP WITH TIME ZONE,
|
|
notes TEXT,
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
UNIQUE(user_id, content_id)
|
|
);
|
|
|
|
CREATE INDEX idx_student_progress_user ON student_progress(user_id);
|
|
CREATE INDEX idx_student_progress_content ON student_progress(content_id);
|
|
|
|
-- ============================================
|
|
-- AUDIT LOGS
|
|
-- ============================================
|
|
CREATE TABLE audit_logs (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE, -- NULL = platform admin action
|
|
user_id UUID, -- can be NULL if system action
|
|
action VARCHAR(100) NOT NULL,
|
|
entity_type VARCHAR(100) NOT NULL,
|
|
entity_id UUID,
|
|
old_values JSONB,
|
|
new_values JSONB,
|
|
ip_address VARCHAR(45),
|
|
user_agent TEXT,
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE INDEX idx_audit_logs_tenant ON audit_logs(tenant_id);
|
|
CREATE INDEX idx_audit_logs_created ON audit_logs(created_at);
|
|
CREATE INDEX idx_audit_logs_entity ON audit_logs(entity_type, entity_id);
|
|
|
|
-- ============================================
|
|
-- INVOICES
|
|
-- ============================================
|
|
CREATE TABLE invoices (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
|
invoice_number VARCHAR(100) NOT NULL UNIQUE,
|
|
amount INTEGER NOT NULL, -- cent
|
|
vat_amount INTEGER NOT NULL DEFAULT 0,
|
|
total_amount INTEGER NOT NULL,
|
|
currency VARCHAR(3) DEFAULT 'EUR',
|
|
status VARCHAR(50) NOT NULL DEFAULT 'pending' CHECK (status IN ('draft', 'pending', 'paid', 'overdue', 'cancelled')),
|
|
billing_period_start DATE,
|
|
billing_period_end DATE,
|
|
due_date DATE,
|
|
paid_at TIMESTAMP WITH TIME ZONE,
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE INDEX idx_invoices_tenant ON invoices(tenant_id);
|
|
CREATE INDEX idx_invoices_status ON invoices(status);
|
|
CREATE INDEX idx_invoices_due_date ON invoices(due_date);
|
|
|
|
CREATE TABLE invoice_items (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
invoice_id UUID NOT NULL REFERENCES invoices(id) ON DELETE CASCADE,
|
|
description TEXT NOT NULL,
|
|
quantity INTEGER NOT NULL DEFAULT 1,
|
|
unit_price INTEGER NOT NULL, -- cent
|
|
total_price INTEGER NOT NULL
|
|
);
|
|
|
|
CREATE INDEX idx_invoice_items_invoice ON invoice_items(invoice_id);
|
|
|
|
-- ============================================
|
|
-- NOTIFICATIONS
|
|
-- ============================================
|
|
CREATE TABLE notification_templates (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
key VARCHAR(100) NOT NULL UNIQUE,
|
|
subject VARCHAR(500) NOT NULL,
|
|
body_html TEXT,
|
|
body_text TEXT,
|
|
variables JSONB DEFAULT '[]',
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE TABLE notifications (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
|
|
template_key VARCHAR(100),
|
|
subject VARCHAR(500) NOT NULL,
|
|
body TEXT,
|
|
status VARCHAR(50) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'sent', 'failed')),
|
|
sent_at TIMESTAMP WITH TIME ZONE,
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE INDEX idx_notifications_user ON notifications(user_id);
|
|
CREATE INDEX idx_notifications_tenant ON notifications(tenant_id);
|
|
|
|
-- ============================================
|
|
-- FILES (DKS)
|
|
-- ============================================
|
|
CREATE TABLE files (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
|
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
entity_type VARCHAR(100) NOT NULL,
|
|
entity_id UUID,
|
|
file_type VARCHAR(100) NOT NULL,
|
|
original_filename VARCHAR(500) NOT NULL,
|
|
storage_path VARCHAR(1000) NOT NULL,
|
|
mime_type VARCHAR(255),
|
|
size_bytes BIGINT,
|
|
expires_at TIMESTAMP WITH TIME ZONE, -- for temp files
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE INDEX idx_files_tenant ON files(tenant_id);
|
|
CREATE INDEX idx_files_entity ON files(entity_type, entity_id);
|
|
CREATE INDEX idx_files_user ON files(user_id);
|
|
|
|
-- ============================================
|
|
-- MESSENGER
|
|
-- ============================================
|
|
CREATE TABLE conversations (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
|
type VARCHAR(50) NOT NULL DEFAULT 'direct' CHECK (type IN ('direct', 'group')),
|
|
name VARCHAR(255), -- only for groups
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE INDEX idx_conversations_tenant ON conversations(tenant_id);
|
|
|
|
CREATE TABLE conversation_participants (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
joined_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
last_read_at TIMESTAMP WITH TIME ZONE,
|
|
UNIQUE(conversation_id, user_id)
|
|
);
|
|
|
|
CREATE INDEX idx_conversation_participants_conversation ON conversation_participants(conversation_id);
|
|
CREATE INDEX idx_conversation_participants_user ON conversation_participants(user_id);
|
|
|
|
CREATE TABLE messages (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
body TEXT NOT NULL,
|
|
is_edited BOOLEAN DEFAULT FALSE,
|
|
is_deleted BOOLEAN DEFAULT FALSE,
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE INDEX idx_messages_conversation ON messages(conversation_id);
|
|
CREATE INDEX idx_messages_created ON messages(created_at);
|
|
|
|
-- ============================================
|
|
-- UPDATE TIMESTAMP TRIGGER
|
|
-- ============================================
|
|
CREATE OR REPLACE FUNCTION update_updated_at()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
NEW.updated_at = CURRENT_TIMESTAMP;
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
CREATE TRIGGER tr_platform_admins_updated_at BEFORE UPDATE ON platform_admins FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
|
CREATE TRIGGER tr_tenants_updated_at BEFORE UPDATE ON tenants FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
|
CREATE TRIGGER tr_branches_updated_at BEFORE UPDATE ON branches FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
|
CREATE TRIGGER tr_users_updated_at BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
|
CREATE TRIGGER tr_learning_contents_updated_at BEFORE UPDATE ON learning_contents FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
|
CREATE TRIGGER tr_student_progress_updated_at BEFORE UPDATE ON student_progress FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
|
CREATE TRIGGER tr_invoices_updated_at BEFORE UPDATE ON invoices FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
|
CREATE TRIGGER tr_conversations_updated_at BEFORE UPDATE ON conversations FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
|
CREATE TRIGGER tr_messages_updated_at BEFORE UPDATE ON messages FOR EACH ROW EXECUTE FUNCTION update_updated_at(); |