Files
Obsidian/Notes/SQLHosting/Projektbeschreibung.md
T
Andrej Spielmann ea628b9a0a Initial commit
2026-04-21 10:53:28 +02:00

34 KiB
Raw Blame History

Hier ist die vollständige, finale Projektbeschreibung mit dem Applikationskonzept vollständig integriert:


SQL Server Hosting Platform Vollständige Technische Projektbeschreibung

Technologie-Stack

Schicht Technologie Version
Frontend Next.js (TypeScript) 16.x
Backend Django + Django REST Framework 5.2 LTS
Plattform-DB Microsoft SQL Server 2019+
Task Queue Celery + Redis 5.x
SQL Server Zugriff pyodbc (ODBC) 18.x
Auth JWT (SimpleJWT) 5.x
Container Docker + Docker Compose 24.x
Testing pytest + Vitest + Playwright -
CI/CD GitHub Actions -

Wichtig: Die Plattformdatenbank ist bewusst mssql keine SQL Server Eigenabhängigkeit. Alle Verbindungen zu Customer-SQL-Servern laufen ausschließlich über pyodbc.


📊 Projekt-Status (Aktualisiert: 2026-04-20)

Implementiert

Komponente Status Details
Backend Core Fertig Django 5.2, 17 Modelle, alle APIs
Frontend Foundation Fertig Next.js 16, 83+ TSX Dateien
Docker Setup Fertig 6 Services (mssql, redis, django, celery, celery-beat, nextjs)
Authentifizierung Fertig JWT mit simplejwt, Custom User Model
Admin Interface Fertig Server Pools, Server, Users, Query Logs
Customer Interface Fertig Dashboard, Applications, Booking Wizard
API Integration Fertig TanStack Query, BFF Pattern
Test Suite Fertig 99+ Backend Tests, Vitest, Playwright E2E
Dokumentation Fertig README, API-Docs, Deployment Guide

🔄 In Entwicklung

Komponente Status Blocker
OpenCode Frontend 🔄 Läuft Background Task, ~83 Dateien erstellt

📝 Noch Offen

  • Produktions-Deployment
  • SSL/TLS Konfiguration
  • Backup-Automatisierung
  • Monitoring & Alerting

📁 Repository Struktur

sqlserver-platform/
├── backend/
│   ├── apps/
│   │   ├── user_auth/
│   │   ├── serverpool/
│   │   ├── applications/
│   │   ├── provisioning/
│   │   └── auditlog/
│   ├── config/
│   ├── tests/
│   │   ├── unit/
│   │   └── integration/
│   └── requirements.txt
├── frontend/
│   ├── src/
│   │   ├── app/
│   │   │   ├── (admin)/
│   │   │   ├── (auth)/
│   │   │   ├── (customer)/
│   │   │   └── api/
│   │   ├── components/
│   │   ├── services/
│   │   └── lib/
│   ├── tests/e2e/
│   └── playwright.config.ts
├── docker-compose.dev.yml
├── docker-compose.yml
├── README.md
├── API_DOCUMENTATION.md
├── DEPLOYMENT.md
└── CONTRIBUTING.md

🐳 Docker Services

Service Port Beschreibung
mssql 1433 Microsoft SQL Server 2019+
redis 6379 Cache & Celery Broker
django 8000 Django REST API
celery-worker - Async Task Processing
celery-beat - Scheduled Tasks
nextjs-dev 3000 Next.js Frontend

🔑 Test Credentials


Systemarchitektur

┌──────────────────────────────────────────────────────────────────┐
│                         Next.js Frontend                          │
│           Admin Panel              │      Kunden Dashboard         │
└───────────────────────────────────┬──────────────────────────────┘
                                    │ REST API (JWT)
┌───────────────────────────────────▼──────────────────────────────┐
│                         Django REST API                           │
│  Auth │ ServerPool │ Application │ Booking │ QueryLog │ AuditLog  │
└──────────────┬────────────────────────────────┬──────────────────┘
               │                                │
    ┌──────────▼──────────┐        ┌────────────▼────────────┐
    │     MSSQL       │        │     Celery + Redis       │
    │   (Plattform-DB)     │        │  Discovery, Health,      │
    └─────────────────────┘        │  Provisioning, Quota     │
                                   └────────────┬────────────┘
                                                │ pyodbc
                            ┌───────────────────▼──────────────────┐
                            │          Customer SQL Server          │
                            │   Standalone    │   HA (Always On)    │
                            └───────────────────────────────────────┘

Kernkonzept: Applikation als Container

Das zentrale Objekt aus Kundensicht ist die Applikation nicht die einzelne Datenbank. Eine Applikation ist ein logischer Container, der dem Kunden gehört, einen Namen trägt und einem HA-Typ (Standalone oder High Availability) zugeordnet ist.

  • Bei Standalone ist die Applikation eine logische Gruppe von Datenbanken auf demselben Server
  • Bei High Availability entspricht die Applikation genau einer Availability Group die AG wird nach der Applikation benannt (AG_<AppName>)
  • Der Kunde erstellt zuerst eine Applikation (inkl. erster Datenbank), kann danach weitere Datenbanken zur selben Applikation hinzufügen
  • Neue Datenbanken einer HA-Applikation werden zur bestehenden AG hinzugefügt (ALTER AVAILABILITY GROUP ... ADD DATABASE) es wird keine neue AG erstellt

Modul 1 Benutzerverwaltung & Authentifizierung

Rollen & Aktivierung

Das System kennt zwei Rollen: ADMIN und CUSTOMER. Ein neuer Kunde registriert sich und landet im Status is_approved = False ohne Zugangsberechtigung, bis der Admin ihn manuell freischaltet. Der Admin sieht im Panel alle ausstehenden Aktivierungen mit Registrierungsdatum und kann per Klick freischalten oder ablehnen.

Django-Modelle:

class User(AbstractUser):
    role         = CharField(choices=["ADMIN", "CUSTOMER"])
    is_approved  = BooleanField(default=False)
    approved_by  = FK(User, null=True)
    approved_at  = DateTimeField(null=True)

class CustomerProfile(Model):
    user          = OneToOneField(User)
    company_name  = CharField()
    contact_phone = CharField()
    created_at    = DateTimeField()

API-Endpunkte:

POST   /api/auth/register/
POST   /api/auth/login/
POST   /api/auth/token/refresh/
GET    /api/admin/users/pending/
PATCH  /api/admin/users/{id}/approve/
PATCH  /api/admin/users/{id}/reject/

Modul 2 Server Pool Management

Pool-Typen

Der Admin verwaltet Server in Pools. Es gibt zwei Typen:

Typ 1 Standalone: Ein einzelner SQL Server. Datenbanken laufen ohne Redundanz.

Typ 2 High Availability: Zwei SQL Server werden als Pärchen hinzugefügt (Primary/Secondary in einer Always On Availability Group). Primär und Sekundär können nach Failover wechseln.

Django-Modelle:

class ServerPool(Model):
    name         = CharField()
    pool_type    = CharField(choices=["STANDALONE", "HA"])
    description  = TextField()
    is_active    = BooleanField()
    created_at   = DateTimeField()

class Server(Model):
    pool               = FK(ServerPool)
    hostname           = CharField()
    ip_address         = FK(IPAddress)
    sql_instance       = CharField()        # z.B. HOSTNAME\INSTANCE
    # Discovery  Ressourcen
    total_ram_gb       = DecimalField()
    total_cpu_cores    = IntegerField()
    total_disk_gb      = DecimalField()
    free_ram_gb        = DecimalField()
    free_disk_gb       = DecimalField()
    # Discovery  SQL Server Version
    sql_version_full   = CharField()        # z.B. 16.0.4135.4
    sql_version_major  = IntegerField()     # z.B. 16
    sql_version_level  = CharField()        # RTM / CU14 / SP1
    sql_edition        = CharField()        # Enterprise / Standard
    # Discovery  HA-relevante Felder
    collation          = CharField()
    endpoint_name      = CharField()
    endpoint_port      = IntegerField()
    server_fqdn        = CharField()
    # Status
    last_checked       = DateTimeField()
    is_active          = BooleanField()
    discovery_status   = CharField()        # PENDING / OK / ERROR

class HAServerPair(Model):
    pool              = FK(ServerPool)
    primary_server    = FK(Server)
    secondary_server  = FK(Server)
    listener          = FK(AGListener)      # muss Admin vorher zuweisen
    is_ready          = BooleanField()      # True wenn Listener zugewiesen

API-Endpunkte:

GET    /api/admin/server-pools/
POST   /api/admin/server-pools/
POST   /api/admin/server-pools/{id}/add-server/
POST   /api/admin/server-pools/{id}/add-ha-pair/
POST   /api/admin/servers/{id}/discover/
GET    /api/admin/servers/{id}/status/

Modul 3 Server Discovery

Beim Hinzufügen eines Servers wird sofort ein Celery-Task ausgelöst. Alle dabei ausgeführten SQL-Queries werden automatisch im Query Log gespeichert.

Discovery-Queries

-- 1. RAM & CPU
SELECT physical_memory_kb / 1024 / 1024 AS total_ram_gb,
       cpu_count
FROM sys.dm_os_sys_info;

-- 2. Disk Space
SELECT volume_mount_point,
       total_bytes / 1073741824.0     AS total_gb,
       available_bytes / 1073741824.0 AS free_gb
FROM sys.dm_os_volume_stats(DB_ID('master'), 1);

-- 3. SQL Server Version
SELECT SERVERPROPERTY('ProductVersion')       AS version_full,
       SERVERPROPERTY('ProductMajorVersion')  AS version_major,
       SERVERPROPERTY('ProductLevel')         AS version_level,
       SERVERPROPERTY('Edition')              AS edition;

-- 4. Collation
SELECT SERVERPROPERTY('Collation') AS server_collation;

-- 5. HA Endpoint (nur für HA-Server)
SELECT name, port
FROM sys.tcp_endpoints
WHERE type_desc = 'DATABASE_MIRRORING';

-- 6. Server FQDN
SELECT SERVERPROPERTY('MachineName') AS machine_name,
       DEFAULT_DOMAIN()              AS domain;

-- 7. IP-Adresse (zur Zuordnung in IP-Pool)
SELECT local_net_address
FROM sys.dm_exec_connections
WHERE session_id = @@SPID;

SQL Server Version → Anzeigename & AG-Template

version_major Anzeigename AG-Template
13 SQL Server 2016 BASIC Availability Groups
14 SQL Server 2017 Standard AG
15 SQL Server 2019 Standard AG
16 SQL Server 2022 Standard AG / CONTAINED AG möglich

Die Major-Version steuert welches Query-Template bei der AG-Erstellung verwendet wird. Templates werden pro query_type + sql_version_major in der Datenbank hinterlegt.


Modul 4 Scheduled Health Checks

Ein Celery-Beat-Task prüft regelmäßig alle aktiven Server auf freie Ressourcen. Intervall ist im Admin konfigurierbar.

Admin-Konfiguration: ServerSelectionThreshold

class ServerSelectionThreshold(Model):
    server_pool              = FK(ServerPool)
    min_free_disk_gb         = DecimalField()   # z.B. 10 GB müssen frei sein
    min_free_ram_gb          = DecimalField()
    min_free_cpu_percent     = IntegerField()   # z.B. 20 % CPU muss frei sein
    health_check_interval    = IntegerField()   # Minuten

Beim Buchungsprozess wählt das System automatisch den passenden Server/Pair anhand dieser Schwellwerte. Ein Server, der die Thresholds nicht erfüllt, wird automatisch aus der Auswahl ausgeschlossen.


Modul 5 IP-Adresspool

Der Admin legt Netzwerke manuell an. IPs werden bei der Server-Discovery automatisch dem passenden Netz zugeordnet. Für AG-Listener wird die nächste freie IP aus dem passenden Netz automatisch vergeben.

class IPNetwork(Model):
    cidr         = CIDRField()          # z.B. 10.10.1.0/24
    description  = CharField()
    vlan_id      = IntegerField(null=True)
    created_at   = DateTimeField()

class IPAddress(Model):
    address           = GenericIPAddressField()
    network           = FK(IPNetwork)
    purpose           = CharField(choices=["SERVER", "LISTENER", "FREE"])
    assigned_to_type  = CharField(null=True)    # GenericFK
    assigned_to_id    = IntegerField(null=True)
    reserved_at       = DateTimeField(null=True)

API-Endpunkte:

GET    /api/admin/ip-networks/
POST   /api/admin/ip-networks/
POST   /api/admin/ip-networks/{id}/add-ip-range/
GET    /api/admin/ip-addresses/
PATCH  /api/admin/ip-addresses/{id}/assign/

Modul 6 AG-Listener Verwaltung

Bevor ein HA-ServerPair für Kundenbuchungen verfügbar ist, muss der Admin einen Listener zuweisen. Erst dann wird HAServerPair.is_ready = True. Der Listener besteht aus Name, IP (aus dem IP-Pool) und Port.

class AGListener(Model):
    name          = CharField()          # z.B. AG-LISTENER-01
    ip_address    = FK(IPAddress)
    port          = IntegerField()       # Standard: 1433
    subnet_mask   = CharField()
    assigned_to   = OneToOneField(HAServerPair, null=True)
    created_at    = DateTimeField()

API-Endpunkte:

GET    /api/admin/ag-listeners/
POST   /api/admin/ag-listeners/
PATCH  /api/admin/ha-pairs/{id}/assign-listener/

Modul 7 Service Account Isolation

Pro ServerPool wird ein dedizierter Service Account konfiguriert. Ein kompromittierter Account hat damit nur Zugriff auf seinen eigenen Pool. Das Passwort wird mit Fernet-Verschlüsselung gespeichert (Key in .env).

class ElevatedAccount(Model):
    server_pool              = OneToOneField(ServerPool)
    username                 = CharField()
    password_encrypted       = BinaryField()    # Fernet encrypted
    last_verified            = DateTimeField()
    permissions_ok           = BooleanField()   # dbcreator + securityadmin
    last_permission_check    = DateTimeField()

Ein Celery-Task prüft regelmäßig ob der Account noch dbcreator und securityadmin besitzt. Fehlt die Berechtigung, wird der Pool für neue Buchungen gesperrt und ein Audit-Log-Eintrag erstellt.


Modul 8 Kundenbuchungsprozess

Konzept: Applikation → Datenbank

Applikation (Container)
├── Name: "MeinShop"           ← vom Kunden vergeben
├── Typ: HA                    ← bestimmt AG-Name: AG_MeinShop
├── Server/Pair: automatisch   ← anhand Thresholds gewählt
├── Collation: SQL_Latin1_...  ← aus Discovery des gewählten Pools
│
├── Datenbank: "MeinShop_Prod" ← erste DB bei Erstellung
├── Datenbank: "MeinShop_Dev"  ← später hinzugefügt
└── Datenbank: "MeinShop_Log"  ← später hinzugefügt

Buchungs-Wizard Neue Applikation (5 Schritte)

Schritt 1: Applikationsname
           ┌─────────────────────────────────────────────┐
           │  Applikationsname: [ MeinShop             ] │
           │  (Nur Buchstaben, Zahlen, Unterstriche;     │
           │   wird Teil des AG-Namens bei HA)           │
           └─────────────────────────────────────────────┘

Schritt 2: HA-Typ wählen
           ┌───────────────────┐  ┌──────────────────────────────┐
           │    Standalone     │  │     High Availability        │
           │ SQL Server 2022   │  │ SQL Server 2022 Enterprise   │
           │ Standard          │  │ Always On  2 Nodes          │
           └───────────────────┘  └──────────────────────────────┘

Schritt 3: SQL Server Collation wählen
           Dropdown  befüllt aus discovered Collations der
           verfügbaren Server im gewählten Pool-Typ

Schritt 4: Name der ersten Datenbank + Größe (GB)
           ┌─────────────────────────────────────────────┐
           │  Datenbankname:  [ MeinShop_Prod          ] │
           │  Größe (GB):     [ 10                     ] │
           └─────────────────────────────────────────────┘

Schritt 5: Zusammenfassung + "Jetzt buchen"
           Anzeige: Applikation, Typ, Collation, Version,
                    DB-Name, Größe, ggf. AG-Name

Weitere Datenbank zu bestehender Applikation hinzufügen

Kunde → "Applikation MeinShop" → "Neue Datenbank hinzufügen"

Schritt 1: Datenbankname + Größe
Schritt 2: Zusammenfassung + "Erstellen"

→ Bei HA: ALTER AVAILABILITY GROUP [AG_MeinShop] ADD DATABASE [neuer_name]
→ Bei Standalone: CREATE DATABASE auf demselben Server wie die Applikation

Django-Modelle

class CustomerApplication(Model):
    customer          = FK(CustomerProfile)
    app_name          = CharField()          # z.B. "MeinShop"
    ha_type           = CharField(choices=["STANDALONE", "HA"])
    # Zugewiesener Server/Pair (bei Erstellung automatisch gewählt)
    server            = FK(Server, null=True)       # Standalone
    ha_pair           = FK(HAServerPair, null=True) # HA
    collation         = CharField()
    # HA-spezifisch
    ag_name           = CharField(null=True)   # = "AG_" + app_name
    ag_created        = BooleanField(default=False)
    # Status
    status            = CharField(choices=[
                            "PROVISIONING", "ACTIVE",
                            "ERROR", "DELETED"
                        ])
    created_at        = DateTimeField()
    deleted_at        = DateTimeField(null=True)

class CustomerDatabase(Model):
    application       = FK(CustomerApplication)     # ← Neu: gehört zur App
    customer          = FK(CustomerProfile)
    db_name           = CharField()
    size_gb           = IntegerField()
    connection_string = CharField()
    # Soft Delete
    status            = CharField(choices=[
                            "ACTIVE", "DELETION_REQUESTED",
                            "DELETION_CONFIRMED", "DELETED"
                        ])
    created_at        = DateTimeField()
    deleted_at        = DateTimeField(null=True)
    deletion_confirmed_at  = DateTimeField(null=True)
    drop_executed_at  = DateTimeField(null=True)
    deletion_token    = UUIDField(null=True)

Automatische Serverauswahl (Backend-Logik)

def select_server(pool_type, required_size_gb, collation):
    threshold = ServerSelectionThreshold.objects.get(
        server_pool__pool_type=pool_type
    )
    candidates = Server.objects.filter(
        pool__pool_type=pool_type,
        collation=collation,
        free_disk_gb__gte=required_size_gb + threshold.min_free_disk_gb,
        free_ram_gb__gte=threshold.min_free_ram_gb,
        is_active=True
    ).order_by('free_disk_gb')  # kleinsten passenden nehmen
    return candidates.first()

API-Endpunkte:

GET    /api/customer/applications/
POST   /api/customer/applications/                    # neue App + erste DB
GET    /api/customer/applications/{id}/
POST   /api/customer/applications/{id}/databases/     # weitere DB hinzufügen
GET    /api/customer/applications/{id}/databases/
DELETE /api/customer/applications/{id}/delete-request/
DELETE /api/customer/applications/{id}/delete-confirm/

Modul 9 Datenbankbereitstellung (Celery-Task)

Nach Buchung läuft ein Celery-Task, der via pyodbc alle nötigen SQL-Schritte ausführt. Alle Queries werden im Query Log gespeichert.

Fall A: Neue Standalone-Applikation (erste DB)

CREATE DATABASE [{{db_name}}] COLLATE {{collation}};
ALTER DATABASE [{{db_name}}]
    MODIFY FILE (NAME = '{{db_name}}', SIZE = {{size_gb}}GB);

Fall B: Weitere DB zu bestehender Standalone-Applikation

-- Auf demselben Server wie die Applikation
CREATE DATABASE [{{db_name}}] COLLATE {{collation}};
ALTER DATABASE [{{db_name}}]
    MODIFY FILE (NAME = '{{db_name}}', SIZE = {{size_gb}}GB);

Fall C: Neue HA-Applikation (AG + erste DB)

-- 1. DB auf Primary erstellen
CREATE DATABASE [{{db_name}}] COLLATE {{collation}};

-- 2. Recovery Model setzen (AG-Voraussetzung)
ALTER DATABASE [{{db_name}}] SET RECOVERY FULL;

-- 3. Full Backup (AG-Voraussetzung)
BACKUP DATABASE [{{db_name}}]
    TO DISK = '\\{{backup_share}}\{{db_name}}.bak'
    WITH FORMAT, INIT;

-- 4. Availability Group erstellen (SQL 2022 Template)
CREATE AVAILABILITY GROUP [AG_{{app_name}}]
WITH (
    DB_FAILOVER = ON,
    REQUIRED_SYNCHRONIZED_SECONDARIES_TO_COMMIT = 0
)
FOR DATABASE [{{db_name}}]
REPLICA ON
    '{{primary_fqdn}}' WITH (
        ENDPOINT_URL = 'TCP://{{ep1_host}}:{{ep1_port}}',
        AVAILABILITY_MODE = SYNCHRONOUS_COMMIT,
        FAILOVER_MODE = AUTOMATIC,
        SEEDING_MODE = AUTOMATIC,
        SECONDARY_ROLE (ALLOW_CONNECTIONS = ALL)
    ),
    '{{secondary_fqdn}}' WITH (
        ENDPOINT_URL = 'TCP://{{ep2_host}}:{{ep2_port}}',
        AVAILABILITY_MODE = SYNCHRONOUS_COMMIT,
        FAILOVER_MODE = AUTOMATIC,
        SEEDING_MODE = AUTOMATIC,
        SECONDARY_ROLE (ALLOW_CONNECTIONS = ALL)
    );

-- 5. Listener hinzufügen
ALTER AVAILABILITY GROUP [AG_{{app_name}}]
ADD LISTENER '{{listener_name}}' (
    WITH IP (('{{listener_ip}}', '{{subnet_mask}}')),
    PORT = {{listener_port}}
);

-- 6. Secondary joinen (läuft auf Secondary-Server)
ALTER AVAILABILITY GROUP [AG_{{app_name}}] JOIN;
ALTER AVAILABILITY GROUP [AG_{{app_name}}]
    GRANT CREATE ANY DATABASE;

Fall D: Weitere DB zu bestehender HA-Applikation

-- 1. DB auf Primary erstellen (AG bereits vorhanden)
CREATE DATABASE [{{db_name}}] COLLATE {{collation}};
ALTER DATABASE [{{db_name}}] SET RECOVERY FULL;

-- 2. Backup
BACKUP DATABASE [{{db_name}}]
    TO DISK = '\\{{backup_share}}\{{db_name}}.bak'
    WITH FORMAT, INIT;

-- 3. Zur bestehenden AG hinzufügen
ALTER AVAILABILITY GROUP [AG_{{app_name}}]
    ADD DATABASE [{{db_name}}];
-- Kein neuer Listener nötig  AG_{{app_name}} existiert bereits

Modul 10 Soft Delete (Doppelte Bestätigung)

Das Löschen kann auf zwei Ebenen erfolgen: Einzelne Datenbank oder gesamte Applikation. Beide Ebenen durchlaufen den gleichen 3-stufigen Prozess.

STUFE 1  Löschanfrage
  Kunde klickt "Löschen" (DB oder App)
  → Modal: "Bitte Namen eingeben zur Bestätigung"
  → POST /api/customer/applications/{id}/delete-request/
     oder POST /api/customer/databases/{id}/delete-request/
  → Status: DELETION_REQUESTED
  → Karenzzeit: deleted_at = jetzt + 48h
  → Audit Log: DELETE_REQUEST
  → E-Mail (wenn aktiv): Bestätigungs-Link

STUFE 2  Finale Bestätigung
  Dashboard-Banner: "Löschung ausstehend"
  Kunde klickt "Endgültig löschen" + gibt Namen erneut ein
  → POST .../delete-confirm/
  → Status: DELETION_CONFIRMED
  → Audit Log: DELETE_CONFIRMED

STUFE 3  Celery-Task Ausführung
  → Einzelne DB Standalone:
     DROP DATABASE [{{db_name}}]

  → Einzelne DB HA:
     ALTER AVAILABILITY GROUP [AG_{{app_name}}]
         REMOVE DATABASE [{{db_name}}];
     DROP DATABASE [{{db_name}}];

  → Gesamte Applikation HA:
     DROP AVAILABILITY GROUP [AG_{{app_name}}];
     DROP DATABASE [{{db_name}}];  -- für jede DB der App

  → Status: DELETED, drop_executed_at = timestamp
  → Audit Log: DELETE_EXECUTED
  → E-Mail (wenn aktiv): Bestätigung der Löschung

Modul 11 Login-Verwaltung (Kundenseite)

Der Kunde kann für seine Datenbanken SQL Server Logins erstellen und verwalten. Alle Aktionen laufen über den ElevatedAccount des zugehörigen Pools. Der Kunde sieht ausschließlich Datenbanken und Logins, die seiner CustomerProfile-ID gehören.

-- Login erstellen
CREATE LOGIN [{{login_name}}]
    WITH PASSWORD = '{{password}}',
    DEFAULT_DATABASE = [{{db_name}}];

USE [{{db_name}}];
CREATE USER [{{login_name}}] FOR LOGIN [{{login_name}}];
ALTER ROLE db_datareader ADD MEMBER [{{login_name}}];
ALTER ROLE db_datawriter ADD MEMBER [{{login_name}}];

-- Login löschen
USE [{{db_name}}];
DROP USER [{{login_name}}];
USE master;
DROP LOGIN [{{login_name}}];

Django-Modell DatabaseLogin:

class DatabaseLogin(Model):
    database    = FK(CustomerDatabase)
    login_name  = CharField()
    created_at  = DateTimeField()
    created_by  = FK(User)
    is_active   = BooleanField()

API-Endpunkte:

GET    /api/customer/databases/{id}/logins/
POST   /api/customer/databases/{id}/logins/
DELETE /api/customer/databases/{id}/logins/{login_id}/

Modul 12 Query Log & Admin Panel

Jede T-SQL-Abfrage wird geloggt. Der Admin kann fehlerhafte Queries per Override korrigieren ohne Code-Deployment.

class QueryLog(Model):
    server          = FK(Server)
    executed_by     = CharField()      # "system" / "admin" / username
    query_type      = CharField(choices=[
                          "DISCOVERY", "HEALTH_CHECK",
                          "DB_CREATE", "DB_DROP",
                          "LOGIN_CREATE", "LOGIN_DROP",
                          "AG_CREATE", "AG_DROP", "AG_ADD_DB", "AG_REMOVE_DB",
                          "QUOTA_CHECK", "PERMISSION_CHECK"
                      ])
    query_text      = TextField()      # tatsächlich ausgeführte Query
    executed_at     = DateTimeField()
    duration_ms     = IntegerField()
    success         = BooleanField()
    error_message   = TextField(null=True)
    # Override-System
    is_overridden   = BooleanField(default=False)
    override_query  = TextField(null=True)
    override_by     = FK(User, null=True)
    override_at     = DateTimeField(null=True)

Override-Logik: Das System prüft vor jeder Ausführung ob eine Override-Query existiert und nutzt diese bevorzugt. Damit kann der Admin z. B. eine fehlerhafte AG-Query reparieren ohne Deployment.

API-Endpunkte:

GET    /api/admin/query-logs/
GET    /api/admin/query-logs/?type=AG_CREATE&success=false
PATCH  /api/admin/query-logs/{id}/override/
DELETE /api/admin/query-logs/{id}/override/

Modul 13 Quota-Monitoring

Ein Celery-Beat-Task prüft regelmäßig die tatsächliche Datenbankgröße und vergleicht sie mit dem gebuchten Wert. Warnungen werden geloggt, E-Mails nur wenn QUOTA_WARNINGS_ENABLED = True.

SELECT
    d.name,
    SUM(mf.size) * 8 / 1024.0 AS current_size_mb
FROM sys.databases d
JOIN sys.master_files mf ON d.database_id = mf.database_id
WHERE d.name = '{{db_name}}'
GROUP BY d.name;
class DatabaseQuota(Model):
    database                    = OneToOneField(CustomerDatabase)
    booked_size_gb              = DecimalField()
    current_size_gb             = DecimalField()
    last_checked                = DateTimeField()
    warning_threshold_percent   = IntegerField(default=80)
    critical_threshold_percent  = IntegerField(default=95)
    last_warning_sent           = DateTimeField(null=True)

Modul 14 E-Mail-System (vorbereitet, deaktiviert)

# settings.py
EMAIL_NOTIFICATIONS_ENABLED = False
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
# Aktivierung: EMAIL_BACKEND = "anymail.backends.mailgun.EmailBackend"

def send_notification(template, recipient, context):
    if not settings.EMAIL_NOTIFICATIONS_ENABLED:
        return  # Nur loggen, nicht senden

Vorbereitete E-Mail-Events:

  • Kundenkonto freigeschaltet / abgelehnt
  • Applikation / Datenbank erfolgreich erstellt
  • Erstellungsfehler (an Admin + Kunde)
  • Löschanfrage Stufe 1 (Bestätigungs-Link)
  • Löschung ausgeführt
  • Quota-Warnung 80% / 95%
  • Service Account Berechtigung fehlt (an Admin)
  • Passwort-Reset

Modul 15 Audit Log

Das Audit Log protokolliert Benutzeraktionen getrennt vom Query Log. Es ist read-only und unveränderbar.

class AuditLog(Model):
    actor           = FK(User)
    action          = CharField(choices=[
                          "APP_CREATE", "APP_DELETE_REQUEST",
                          "APP_DELETE_CONFIRMED", "APP_DELETE_EXECUTED",
                          "DB_CREATE", "DB_DELETE_REQUEST",
                          "DB_DELETE_CONFIRMED", "DB_DELETE_EXECUTED",
                          "LOGIN_CREATE", "LOGIN_DELETE",
                          "CUSTOMER_APPROVED", "CUSTOMER_REJECTED",
                          "SERVER_ADDED", "SERVER_REMOVED",
                          "POOL_CREATED", "AG_CREATED",
                          "LISTENER_ASSIGNED", "IP_ASSIGNED",
                          "QUOTA_WARNING", "PERMISSION_FAIL",
                          "QUERY_OVERRIDE", "EMAIL_SENT"
                      ])
    target_type     = CharField()
    target_id       = IntegerField()
    target_repr     = CharField()       # Snapshot: "App: MeinShop / customer@example.com"
    ip_address      = GenericIPAddressField()
    timestamp       = DateTimeField(auto_now_add=True)
    extra_data      = JSONField(null=True)

Benötigte Pakete Vollständige Liste

Backend (Python/Django)

Paket Zweck
django Web Framework
djangorestframework REST API
mssql-django Optionaler SQL Server ORM Support
pyodbc Verbindung zu Customer SQL Servern
celery Async Task Queue
celery[redis] Redis als Broker
django-celery-beat Scheduled/Periodic Tasks
django-celery-results Task-Ergebnisse in DB
djangorestframework-simplejwt JWT Authentication
django-cors-headers CORS für Next.js
django-filter Filterbare API-Endpunkte
drf-spectacular OpenAPI / Swagger Dokumentation
cryptography Fernet-Verschlüsselung für Service Accounts
netaddr IP-Adress- und Netzwerkberechnung
python-decouple .env Feature-Flags & Config
psycopg2-binary mssql Plattform-DB
django-anymail E-Mail (vorbereitet, deaktiviert)
django-safedelete Soft-Delete Mixin
django-auditlog Audit Logging via Signals
Jinja2 Dynamische Query-Templates ({{app_name}} etc.)
redis Redis-Client für Cache & Celery

Frontend (Next.js / TypeScript)

Paket Zweck
next-auth Authentifizierung + Session
@tanstack/react-query Server State & Data Fetching
axios HTTP Client
zustand State Management
react-hook-form Formulare (Buchungs-Wizard, Bestätigungs-Modals)
zod Schema-Validierung Frontend + API
shadcn/ui UI-Komponentenbibliothek
tailwindcss Styling
@tanstack/react-table Admin-Tabellen (Server Pool, Query Log, Kunden)
lucide-react Icons
sonner Toast-Notifications
@codemirror/react + @uiw/react-codemirror SQL Query-Editor im Admin (Query Override)
date-fns Datums-Formatierung
recharts Ressourcen-Charts im Admin (CPU/RAM/Disk)

Ergänzende technische Hinweise

Docker Learnings

  • ODBC Treiber muss auf dem Django-Server installiert sein: msodbcsql18 (Linux/Docker)
  • Apt-Key deprecated in Debian 12+ - moderner GPG-Keyring Approach verwenden
  • Django App Name Conflicts - Custom auditlog app vs django-auditlog Package
  • Jinja2 Dependency - Wird von Django für Templates benötigt, aber oft vergessen
  • Build Cache - Bei Dependency-Änderungen --no-cache verwenden: docker compose up -d --build --no-cache

Frontend Architektur

  • Next.js 16 App Router mit React 19
  • TanStack Query für Server State Management
  • Zustand für Client State
  • BFF Pattern - API Routes als Proxy zu Django (vermeidet CORS)
  • shadcn/ui + Tailwind für UI-Komponenten
  • Zod für Schema-Validierung

Testing Strategy

  • Backend: pytest mit 99+ Tests (Models, Serializers, Services)
  • Frontend: Vitest für Unit Tests, Playwright für E2E
  • Coverage Target: 80%+ für Core Functionality
  • Test Database: SQLite für schnelle Unit Tests

Security

  • JWT Tokens: Access 15min, Refresh 7 Tage
  • Fernet Encryption für Service Account Passwörter
  • Soft Delete mit django-safedelete (3-stufige Bestätigung)
  • Audit Logging für alle Benutzeraktionen

Bekannte Hürden

Problem Lösung
apt-key deprecated GPG Keyring statt apt-key verwenden
App Name Konflikt Nur custom app in INSTALLED_APPS
Django Admin 500 Jinja2 Backend entfernen, nur DjangoTemplates
Migration Konflikte token_blacklist aus INSTALLED_APPS entfernen
CORS Issues BFF Pattern mit API Routes