34 KiB
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
- Admin: admin@sqlhosting.com / Admin123!
- Backend Health: http://localhost:8000/api/v1/health/
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
auditlogapp vsdjango-auditlogPackage - Jinja2 Dependency - Wird von Django für Templates benötigt, aber oft vergessen
- Build Cache - Bei Dependency-Änderungen
--no-cacheverwenden: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 |