Initial commit
This commit is contained in:
@@ -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 |
|
||||
|
||||
***
|
||||
Reference in New Issue
Block a user