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

906 lines
34 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 |
***