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_`) - 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 | ***