Initial commit

This commit is contained in:
Andrej Spielmann
2026-04-21 10:53:28 +02:00
commit ea628b9a0a
30 changed files with 2758 additions and 0 deletions
+905
View File
@@ -0,0 +1,905 @@
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:**
```python
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:**
```python
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
```sql
-- 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`
```python
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.
```python
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.
```python
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`).
```python
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
```python
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)
```python
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)
```sql
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
```sql
-- 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)
```sql
-- 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
```sql
-- 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.
```sql
-- 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`:**
```python
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.
```python
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`.
```sql
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;
```
```python
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)
```python
# 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.
```python
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 |
***