aktiv-inaktiv der Lizenzen ist gefixt
Dieser Commit ist enthalten in:
@@ -1,42 +0,0 @@
|
||||
# Blueprint Migration Complete
|
||||
|
||||
## Summary
|
||||
|
||||
The blueprint migration has been successfully completed. All 60 routes that were previously in `app.py` have been moved to their respective blueprint files and the originals have been commented out in `app.py`.
|
||||
|
||||
## Changes Made
|
||||
|
||||
1. **Commented out all duplicate routes in app.py**
|
||||
- 60 routes total were commented out
|
||||
- Routes are preserved as comments for reference
|
||||
|
||||
2. **Registered all blueprints**
|
||||
- auth_bp (auth_routes.py) - 9 routes
|
||||
- admin_bp (admin_routes.py) - 10 routes
|
||||
- api_bp (api_routes.py) - 14 routes (with /api prefix)
|
||||
- batch_bp (batch_routes.py) - 4 routes
|
||||
- customer_bp (customer_routes.py) - 7 routes
|
||||
- export_bp (export_routes.py) - 5 routes (with /export prefix)
|
||||
- license_bp (license_routes.py) - 4 routes
|
||||
- resource_bp (resource_routes.py) - 7 routes
|
||||
- session_bp (session_routes.py) - 6 routes
|
||||
|
||||
3. **Fixed route inconsistencies**
|
||||
- Updated `/session/terminate/<int:session_id>` to `/session/end/<int:session_id>` in session_routes.py to match the original
|
||||
|
||||
## Application Structure
|
||||
|
||||
The application now follows a proper blueprint structure:
|
||||
- `app.py` - Contains only Flask app initialization, configuration, and scheduler setup
|
||||
- `routes/` - Contains all route blueprints organized by functionality
|
||||
- All routes are properly organized and no duplicates exist
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Test the application to ensure all routes work correctly
|
||||
2. Remove commented route code from app.py once verified working
|
||||
3. Consider further refactoring of large blueprint files if needed
|
||||
|
||||
## Backup
|
||||
|
||||
A backup of the original app.py was created with timestamp before making changes.
|
||||
@@ -1,318 +0,0 @@
|
||||
# Fehlersuche - v2_adminpanel Refactoring
|
||||
|
||||
## Aktueller Stand (18.06.2025 - 02:30 Uhr)
|
||||
✅ **ALLE KRITISCHEN PROBLEME GELÖST**
|
||||
- Resources Route funktioniert jetzt korrekt
|
||||
- Customers-Licenses Route funktioniert jetzt korrekt
|
||||
- Container startet ohne Fehler
|
||||
|
||||
### Finale Fixes (18.06.2025 - 02:45 Uhr)
|
||||
|
||||
#### Customers-Licenses Testdaten-Filter
|
||||
- Problem: `/customers-licenses?show_test=false` zeigte trotzdem alle Kunden (auch Testdaten)
|
||||
- Ursache: Die SQL-Query in `customers_licenses()` berücksichtigte den `show_test` Parameter nicht
|
||||
- Lösung:
|
||||
- `show_test` Parameter aus der URL auslesen
|
||||
- WHERE-Klausel hinzugefügt: `WHERE (%s OR c.is_test = false)`
|
||||
- `c.is_test` in SELECT und GROUP BY hinzugefügt
|
||||
- `show_test` Parameter an Template weitergeben
|
||||
- Standardverhalten: Nur Produktivdaten werden angezeigt (wenn show_test=false oder nicht gesetzt)
|
||||
|
||||
### Bereits gelöste Probleme (18.06.2025 - 02:35 Uhr)
|
||||
|
||||
#### Backups Route Fix
|
||||
- Problem 1: 500 Error bei `/backups` - `url_for('admin.create_backup')` existiert nicht
|
||||
- Lösung 1:
|
||||
- `url_for('admin.create_backup')` → `url_for('admin.create_backup_route')`
|
||||
- `url_for('admin.restore_backup', backup_id='')` → `/backup/restore/${backupId}`
|
||||
- `url_for('admin.delete_backup', backup_id='')` → `/backup/delete/${backupId}`
|
||||
|
||||
- Problem 2: "SyntaxError: Unexpected token '<'" beim Backup erstellen
|
||||
- Ursache: Routes gaben HTML (redirect) statt JSON zurück
|
||||
- Lösung 2:
|
||||
- `create_backup_route()` und `restore_backup_route()` geben jetzt JSON zurück
|
||||
- Entfernt: `return redirect(url_for('admin.backups'))`
|
||||
- Hinzugefügt: `return jsonify({'success': True/False, 'message': '...'})`
|
||||
|
||||
### Bereits gelöste Probleme (18.06.2025 - 02:30 Uhr)
|
||||
1. **Customers-Licenses Template Fix**:
|
||||
- Problem: `url_for('api.toggle_license', license_id='')` mit leerem String
|
||||
- Lösung: Hardcodierte URL verwendet: `/api/license/${licenseId}/toggle`
|
||||
|
||||
2. **Resources Route Fix**:
|
||||
- Problem 1: `invalid literal for int() with base 10: ''` bei page Parameter
|
||||
- Lösung 1: Try-except Block für sichere Konvertierung des page Parameters
|
||||
- Problem 2: `url_for('resources.quarantine', resource_id='')` mit leerem String im Template
|
||||
- Lösung 2: Hardcodierte URL verwendet: `/resources/quarantine/${resourceId}`
|
||||
- Zusätzlich: Debug-Logging hinzugefügt für bessere Fehlerdiagnose
|
||||
|
||||
### Wichtige Erkenntnisse:
|
||||
- Flask's `url_for()` kann nicht mit leeren Parametern für Integer-Routen umgehen
|
||||
- Bei JavaScript-generierten URLs ist es oft besser, hardcodierte URLs zu verwenden
|
||||
- Container muss nach Template-Änderungen neu gestartet werden
|
||||
|
||||
## Stand vom 17.06.2025 - 11:00 Uhr
|
||||
|
||||
### Erfolgreiches Refactoring
|
||||
- Die ursprüngliche 5000+ Zeilen große app.py wurde erfolgreich in Module aufgeteilt:
|
||||
- 9 Blueprint-Module in `routes/`
|
||||
- Separate Module für auth/, utils/, config.py, db.py, models.py
|
||||
- Hauptdatei app.py nur noch 178 Zeilen
|
||||
|
||||
### Funktionierende Teile
|
||||
- ✅ Routing-System funktioniert (alle Routen sind registriert)
|
||||
- ✅ Login-System funktioniert
|
||||
- ✅ Einfache Test-Routen funktionieren (/simple-test)
|
||||
- ✅ Blueprint-Registrierung funktioniert korrekt
|
||||
- ✅ /test-db Route funktioniert nach Docker-Rebuild
|
||||
- ✅ Kunden-Anzeige funktioniert mit Test-Daten-Filter
|
||||
- ✅ Lizenzen-Anzeige funktioniert mit erweiterten Filtern
|
||||
- ✅ Batch-Lizenzerstellung funktioniert
|
||||
- ✅ Ressourcen-Pool funktioniert vollständig
|
||||
- ✅ Ressourcen hinzufügen funktioniert
|
||||
|
||||
### Gelöste Probleme
|
||||
|
||||
#### 1. **Dict/Tuple Inkonsistenzen** ✅ GELÖST
|
||||
**Problem**: Templates erwarteten Tuple-Zugriff (row[0], row[1]), aber models.py lieferte Dictionaries
|
||||
**Lösung**: Alle betroffenen Templates wurden auf Dictionary-Zugriff umgestellt:
|
||||
- customers.html: `customer[0]` → `customer.id`, `customer[1]` → `customer.name`, etc.
|
||||
- customers_licenses.html: Komplett auf Dictionary-Zugriff umgestellt
|
||||
- licenses.html, edit_license.html, sessions.html, audit_log.html, resources.html, backups.html: Alle konvertiert
|
||||
|
||||
#### 2. **Fehlende /api/customers Route** ✅ GELÖST
|
||||
**Problem**: Batch-Lizenzerstellung konnte keine Kunden laden (Select2 AJAX-Fehler)
|
||||
**Lösung**: api_customers() Funktion zu api_routes.py hinzugefügt
|
||||
|
||||
#### 3. **Doppelte api_customers Funktion** ✅ GELÖST
|
||||
**Problem**: AssertionError beim Start - View function mapping is overwriting existing endpoint
|
||||
**Lösung**: Doppelte Definition in api_routes.py entfernt (Zeilen 833-943)
|
||||
|
||||
#### 4. **502 Bad Gateway Error** ✅ GELÖST
|
||||
**Problem**: Admin-Panel war nicht erreichbar, nginx gab 502 zurück
|
||||
**Ursache**: Container startete nicht wegen doppelter Route-Definition
|
||||
**Lösung**: Doppelte api_customers Funktion entfernt, Container neu gebaut
|
||||
|
||||
#### 5. **Test-Daten Filter** ✅ GELÖST
|
||||
**Problem**: Test-Daten wurden immer angezeigt, Checkbox funktionierte nicht
|
||||
**Lösung**: get_customers() in models.py unterstützt jetzt show_test Parameter
|
||||
|
||||
## Debugging-Schritte
|
||||
|
||||
### 1. Container komplett neu bauen
|
||||
```bash
|
||||
cd C:\Users\Administrator\Documents\GitHub\v2-Docker\v2
|
||||
docker-compose down
|
||||
docker-compose build --no-cache
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### 2. Logs überprüfen
|
||||
```bash
|
||||
docker logs admin-panel --tail 100
|
||||
```
|
||||
|
||||
### 3. Test-Routen
|
||||
- `/simple-test` - Sollte "Simple test works!" zeigen
|
||||
- `/debug-routes` - Zeigt alle registrierten Routen
|
||||
- `/test-db` - Testet Datenbankverbindung
|
||||
|
||||
### 4. Login-Test
|
||||
1. Gehe zu https://admin-panel-undso.z5m7q9dk3ah2v1plx6ju.com/
|
||||
2. Logge dich mit den Admin-Credentials ein
|
||||
3. Versuche dann /customers-licenses aufzurufen
|
||||
|
||||
## Code-Fixes bereits implementiert
|
||||
|
||||
### 1. Datenbankverbindungen
|
||||
- Alle kritischen Funktionen verwenden jetzt `conn = get_connection()` mit normalem Cursor
|
||||
- Verhindert Dictionary/Tuple Konflikte
|
||||
|
||||
### 2. Spaltennamen korrigiert
|
||||
- `is_active` statt `active` für licenses Tabelle
|
||||
- `is_active` statt `active` für sessions Tabelle
|
||||
- `is_test` statt `is_test_license`
|
||||
- Entfernt: phone, address, notes aus customers (existieren nicht)
|
||||
|
||||
### 3. Blueprint-Referenzen
|
||||
- Alle url_for() Aufrufe haben korrekte Blueprint-Präfixe
|
||||
- z.B. `url_for('auth.login')` statt `url_for('login')`
|
||||
|
||||
## Nächste Schritte
|
||||
|
||||
1. **Container neu bauen** (siehe oben)
|
||||
2. **Einloggen** und testen ob /customers-licenses funktioniert
|
||||
3. **Falls weiterhin Fehler**: Docker Logs nach "CUSTOMERS-LICENSES ROUTE CALLED" durchsuchen
|
||||
4. **Alternative**: Temporär auf die große app.py.backup zurückwechseln:
|
||||
```bash
|
||||
cp app.py.backup app.py
|
||||
docker-compose restart admin-panel
|
||||
```
|
||||
|
||||
## Bekannte funktionierende Routen (nach Login)
|
||||
- `/` - Dashboard
|
||||
- `/customers` - Kundenliste
|
||||
- `/licenses` - Lizenzliste
|
||||
- `/resources` - Ressourcen
|
||||
- `/audit` - Audit Log
|
||||
- `/sessions` - Sessions
|
||||
|
||||
## Debug-Informationen in customer_routes.py
|
||||
Die customers_licenses Funktion hat erweiterte Logging-Ausgaben:
|
||||
- "=== CUSTOMERS-LICENSES ROUTE CALLED ==="
|
||||
- "=== QUERY RETURNED X ROWS ==="
|
||||
- Details über Datentypen der Ergebnisse
|
||||
|
||||
Diese erscheinen in den Docker Logs und helfen bei der Fehlersuche.
|
||||
|
||||
## Zusammenfassung der Fixes
|
||||
|
||||
### Template-Konvertierungen (Dict statt Tuple)
|
||||
Folgende Templates wurden von Tuple-Zugriff auf Dictionary-Zugriff umgestellt:
|
||||
1. **customers.html**: customer[0] → customer.id, etc.
|
||||
2. **customers_licenses.html**: Komplett umgestellt
|
||||
3. **edit_customer.html**: customer[0] → customer.id, etc.
|
||||
4. **licenses.html**: license[0] → license.id, etc.
|
||||
5. **edit_license.html**: license[0] → license.id, etc.
|
||||
6. **sessions.html**: session[0] → session.id, etc.
|
||||
7. **audit_log.html**: log[0] → log.id, etc.
|
||||
8. **resources.html**: resource[0] → resource.id, etc.
|
||||
9. **backups.html**: backup[0] → backup.id, etc.
|
||||
|
||||
### API-Fixes
|
||||
1. **api_routes.py**: Fehlende /api/customers Route hinzugefügt
|
||||
2. **api_routes.py**: Doppelte api_customers Funktion entfernt
|
||||
|
||||
### Model-Fixes
|
||||
1. **models.py**: get_customers() unterstützt jetzt show_test und search Parameter
|
||||
2. **customer_routes.py**: customers() nutzt die neuen Parameter
|
||||
|
||||
### Status
|
||||
✅ **Alle bekannten Probleme wurden behoben**
|
||||
✅ **Admin-Panel ist vollständig funktionsfähig**
|
||||
✅ **Docker Container läuft stabil**
|
||||
|
||||
## Weitere gelöste Probleme (17.06.2025 - 11:00 Uhr)
|
||||
|
||||
### 1. **Test-Daten Checkbox funktioniert nicht** ✅ GELÖST
|
||||
**Problem**: Die Checkbox zum Anzeigen von Test-Daten in Kunden- und Lizenzansicht funktionierte nicht
|
||||
**Ursache**: Fehlende Blueprint-Präfixe in Template-URLs
|
||||
**Lösung**:
|
||||
- `customers.html`: Alle `url_for('customers')` → `url_for('customer.customers')`
|
||||
- `licenses.html`: Alle `url_for('licenses')` → `url_for('license.licenses')`
|
||||
- Formulare senden jetzt korrekt mit `show_test` Parameter
|
||||
|
||||
### 2. **Lizenz-Filter erweitert** ✅ GELÖST
|
||||
**Problem**: Filter für Test-/Live-Daten fehlte in Lizenzansicht
|
||||
**Lösung**: `license_routes.py` erweitert mit:
|
||||
- Typ-Filter: `full`, `test`, `test_data`, `live_data`
|
||||
- Status-Filter: `active`, `expiring`, `expired`, `inactive`
|
||||
- Suche über Lizenzschlüssel, Kundenname und E-Mail
|
||||
|
||||
### 3. **Resource Pool Anzeige** ✅ GELÖST
|
||||
**Problem**: Ressourcen-Pool Seite hatte fehlerhafte Links und Filter funktionierten nicht
|
||||
**Lösung**:
|
||||
- `resources.html`: Form-Action korrigiert zu `url_for('resources.resources')`
|
||||
- JavaScript `toggleTestResources()` arbeitet jetzt mit URL-Parametern
|
||||
- Alle Sortier- und Paginierungs-Links korrigiert
|
||||
|
||||
### 4. **Ressourcen hinzufügen fehlte** ✅ GELÖST
|
||||
**Problem**: Route `/resources/add` existierte nicht
|
||||
**Lösung**: Komplette `add_resources()` Funktion in `resource_routes.py` implementiert:
|
||||
- Validierung für Domains, IPv4-Adressen und Telefonnummern
|
||||
- Duplikat-Prüfung
|
||||
- Bulk-Import mit detailliertem Feedback
|
||||
- Test/Produktion Unterscheidung
|
||||
|
||||
### 5. **Navigation-Links** ✅ GELÖST
|
||||
**Problem**: Sidebar-Links für Ressourcen verwendeten hardcodierte URLs
|
||||
**Lösung**: `base.html` aktualisiert:
|
||||
- Resource Pool Link: `href="{{ url_for('resources.resources') }}"`
|
||||
- Add Resources Link: `href="{{ url_for('resources.add_resources') }}"`
|
||||
- Active-Status Prüfung korrigiert für Blueprint-Endpunkte
|
||||
|
||||
## Routing-Analyse (17.06.2025 - 11:30 Uhr)
|
||||
|
||||
### Identifizierte Routing-Probleme
|
||||
|
||||
Nach systematischer Analyse wurden folgende Routing-Probleme gefunden:
|
||||
|
||||
#### 1. **Fehlende Blueprint-Präfixe** ⚠️ OFFEN
|
||||
Viele `url_for()` Aufrufe fehlen Blueprint-Präfixe. Dies verursacht 500-Fehler:
|
||||
|
||||
**Betroffene Templates:**
|
||||
- `profile.html`: 3 fehlerhafte Aufrufe (`change_password`, `disable_2fa`, `setup_2fa`)
|
||||
- `setup_2fa.html`: 2 fehlerhafte Aufrufe (`profile`, `enable_2fa`)
|
||||
- `backup_codes.html`: 1 fehlerhafter Aufruf (`profile`)
|
||||
- `resource_history.html`: 2 fehlerhafte Aufrufe (`resources`, `edit_license`)
|
||||
- `resource_metrics.html`: 2 fehlerhafte Aufrufe (`resources`, `resources_report`)
|
||||
- `resource_report.html`: 2 fehlerhafte Aufrufe
|
||||
- `sessions.html`: Mehrere fehlerhafte Aufrufe
|
||||
- `audit_log.html`: Mehrere fehlerhafte Aufrufe
|
||||
|
||||
#### 2. **Hardcodierte URLs** ⚠️ OFFEN
|
||||
Über 50 hardcodierte URLs gefunden, die mit `url_for()` ersetzt werden sollten:
|
||||
|
||||
**Hauptprobleme in `base.html`:**
|
||||
- `href="/"` → `href="{{ url_for('admin.dashboard') }}"`
|
||||
- `href="/profile"` → `href="{{ url_for('auth.profile') }}"`
|
||||
- `href="/logout"` → `href="{{ url_for('auth.logout') }}"`
|
||||
- `href="/customers-licenses"` → `href="{{ url_for('customer.customers_licenses') }}"`
|
||||
- `href="/customer/create"` → `href="{{ url_for('customer.create_customer') }}"`
|
||||
- `href="/create"` → `href="{{ url_for('license.create_license') }}"`
|
||||
- `href="/batch"` → `href="{{ url_for('batch.batch_create') }}"`
|
||||
- `href="/audit"` → `href="{{ url_for('admin.audit_log') }}"`
|
||||
- `href="/sessions"` → `href="{{ url_for('session.sessions') }}"`
|
||||
- `href="/backups"` → `href="{{ url_for('admin.backups') }}"`
|
||||
|
||||
#### 3. **Doppelte Route-Definitionen** ✅ GELÖST
|
||||
- Entfernt: Doppelte `add_resource` Funktion in `resource_routes.py`
|
||||
|
||||
#### 4. **Route-Namenskonsistenz** ⚠️ OFFEN
|
||||
- `resource_report` vs `resources_report` - inkonsistente Benennung
|
||||
|
||||
### Prioritäten für Fixes
|
||||
|
||||
1. **KRITISCH**: Fehlende Blueprint-Präfixe (verursachen 500-Fehler)
|
||||
2. **HOCH**: Hardcodierte URLs in Navigation (`base.html`)
|
||||
3. **MITTEL**: Andere hardcodierte URLs
|
||||
4. **NIEDRIG**: Namenskonsistenz
|
||||
|
||||
### Vollständiger Report
|
||||
Ein detaillierter Report wurde erstellt: `ROUTING_ISSUES_REPORT.md`
|
||||
|
||||
## Aktuelle Probleme (18.06.2025 - 01:30 Uhr)
|
||||
|
||||
### 1. **Resources Route funktioniert nicht** ✅ GELÖST (18.06.2025 - 02:00 Uhr)
|
||||
**Problem**: `/resources` Route leitete auf Dashboard um mit Fehlermeldung "Fehler beim Laden der Ressourcen!"
|
||||
**Fehlermeldungen im Log**:
|
||||
1. Ursprünglich: `FEHLER: Spalte l.customer_name existiert nicht`
|
||||
2. Nach Fix: `'dict object' has no attribute 'total'`
|
||||
|
||||
**Gelöst durch**:
|
||||
1. Stats Dictionary korrekt initialisiert mit allen erforderlichen Feldern inkl. `available_percent`
|
||||
2. Fehlende Template-Variablen hinzugefügt: `total`, `page`, `total_pages`, `sort_by`, `sort_order`, `recent_activities`, `datetime`
|
||||
3. Template-Variable `search_query` → `search` korrigiert
|
||||
4. Route-Namen korrigiert: `quarantine_resource` → `quarantine`, `release_resources` → `release`
|
||||
5. Export-Route korrigiert: `resource_report` → `resources_report`
|
||||
|
||||
### 2. **URL-Generierungsfehler** ✅ GELÖST
|
||||
**Problem**: Mehrere `url_for()` Aufrufe mit falschen Endpunkt-Namen
|
||||
**Gelöste Fehler**:
|
||||
- `api.generate_license_key` → `api.api_generate_key`
|
||||
- `api.customers` → `api.api_customers`
|
||||
- `export.customers` → `export.export_customers`
|
||||
- `export.licenses` → `export.export_licenses`
|
||||
- `url_for()` mit leeren Parametern durch hardcodierte URLs ersetzt
|
||||
|
||||
### 3. **Customers-Licenses Route** ✅ GELÖST (18.06.2025 - 02:00 Uhr)
|
||||
**Problem**: `/customers-licenses` Route leitete auf Dashboard um
|
||||
**Fehlermeldung im Log**: `ValueError: invalid literal for int() with base 10: ''`
|
||||
**Ursache**: Template versuchte Server-seitiges Rendering von Daten, die per AJAX geladen werden sollten
|
||||
|
||||
**Gelöst durch**:
|
||||
1. Entfernt: Server-seitiges Rendering von `selected_customer` und `licenses` im Template
|
||||
2. Template zeigt jetzt nur "Wählen Sie einen Kunden aus" bis AJAX-Daten geladen sind
|
||||
3. Korrigiert: `selected_customer_id` Variable entfernt
|
||||
4. Export-Links funktionieren jetzt ohne `customer_id` Parameter
|
||||
5. API-Endpunkt korrekt referenziert mit `url_for('customers.api_customer_licenses')`
|
||||
@@ -1,43 +0,0 @@
|
||||
# Migration zu Passwort-Änderung und 2FA
|
||||
|
||||
## Übersicht
|
||||
Das Admin Panel unterstützt jetzt Passwort-Änderungen und Zwei-Faktor-Authentifizierung (2FA). Um diese Features zu nutzen, müssen bestehende Benutzer migriert werden.
|
||||
|
||||
## Migration durchführen
|
||||
|
||||
1. **Container neu bauen** (für neue Dependencies):
|
||||
```bash
|
||||
docker-compose down
|
||||
docker-compose build adminpanel
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
2. **Migration ausführen**:
|
||||
```bash
|
||||
docker exec -it v2_adminpanel python migrate_users.py
|
||||
```
|
||||
|
||||
Dies erstellt Datenbankeinträge für die in der .env konfigurierten Admin-Benutzer.
|
||||
|
||||
## Nach der Migration
|
||||
|
||||
### Passwort ändern
|
||||
1. Einloggen mit bisherigem Passwort
|
||||
2. Klick auf "👤 Profil" in der Navigation
|
||||
3. Neues Passwort eingeben (min. 8 Zeichen)
|
||||
|
||||
### 2FA aktivieren
|
||||
1. Im Profil auf "2FA einrichten" klicken
|
||||
2. QR-Code mit Google Authenticator oder Authy scannen
|
||||
3. 6-stelligen Code eingeben
|
||||
4. Backup-Codes sicher aufbewahren!
|
||||
|
||||
## Wichtige Hinweise
|
||||
- Backup-Codes unbedingt speichern (Drucker, USB-Stick, etc.)
|
||||
- Jeder Backup-Code kann nur einmal verwendet werden
|
||||
- Bei Verlust des 2FA-Geräts können nur Backup-Codes helfen
|
||||
|
||||
## Rückwärtskompatibilität
|
||||
- Benutzer aus .env funktionieren weiterhin
|
||||
- Diese haben aber keinen Zugriff auf Profil-Features
|
||||
- Migration ist erforderlich für neue Features
|
||||
@@ -1,156 +0,0 @@
|
||||
# Migration Discrepancies - Backup vs Current Blueprint Structure
|
||||
|
||||
## 1. Missing Routes
|
||||
|
||||
### Authentication/Profile Routes (Not in any blueprint)
|
||||
- `/profile` - User profile page
|
||||
- `/profile/change-password` - Change password endpoint
|
||||
- `/profile/setup-2fa` - Setup 2FA page
|
||||
- `/profile/enable-2fa` - Enable 2FA endpoint
|
||||
- `/profile/disable-2fa` - Disable 2FA endpoint
|
||||
- `/heartbeat` - Session heartbeat endpoint
|
||||
|
||||
### Customer API Routes (Missing from api_routes.py)
|
||||
- `/api/customer/<int:customer_id>/licenses` - Get licenses for a customer
|
||||
- `/api/customer/<int:customer_id>/quick-stats` - Get quick stats for a customer
|
||||
|
||||
### Resource Routes (Missing from resource_routes.py)
|
||||
- `/resources` - Main resources page
|
||||
- `/resources/add` - Add new resources page
|
||||
- `/resources/quarantine/<int:resource_id>` - Quarantine a resource
|
||||
- `/resources/release` - Release resources from quarantine
|
||||
- `/resources/history/<int:resource_id>` - View resource history
|
||||
- `/resources/metrics` - Resource metrics page
|
||||
- `/resources/report` - Resource report page
|
||||
|
||||
### Main Dashboard Route (Missing)
|
||||
- `/` - Main dashboard (currently in backup shows dashboard with stats)
|
||||
|
||||
## 2. Database Column Discrepancies
|
||||
|
||||
### Column Name Differences
|
||||
- **created_by** - Used in backup_history table but not consistently referenced
|
||||
- **is_test_license** vs **is_test** - The database uses `is_test` but some code might reference `is_test_license`
|
||||
|
||||
### Session Table Aliases
|
||||
The sessions table has multiple column aliases that need to be handled:
|
||||
- `login_time` (alias for `started_at`)
|
||||
- `last_activity` (alias for `last_heartbeat`)
|
||||
- `logout_time` (alias for `ended_at`)
|
||||
- `active` (alias for `is_active`)
|
||||
|
||||
## 3. Template Name Mismatches
|
||||
|
||||
### Templates Referenced in Backup
|
||||
- `login.html` - Login page
|
||||
- `verify_2fa.html` - 2FA verification
|
||||
- `profile.html` - User profile
|
||||
- `setup_2fa.html` - 2FA setup
|
||||
- `backup_codes.html` - 2FA backup codes
|
||||
- `dashboard.html` - Main dashboard
|
||||
- `index.html` - Create license form
|
||||
- `batch_result.html` - Batch operation results
|
||||
- `batch_form.html` - Batch form
|
||||
- `edit_license.html` - Edit license
|
||||
- `edit_customer.html` - Edit customer
|
||||
- `create_customer.html` - Create customer
|
||||
- `customers_licenses.html` - Customer-license overview
|
||||
- `sessions.html` - Sessions list
|
||||
- `audit_log.html` - Audit log
|
||||
- `backups.html` - Backup management
|
||||
- `blocked_ips.html` - Blocked IPs
|
||||
- `resources.html` - Resources list
|
||||
- `add_resources.html` - Add resources form
|
||||
- `resource_history.html` - Resource history
|
||||
- `resource_metrics.html` - Resource metrics
|
||||
- `resource_report.html` - Resource report
|
||||
|
||||
## 4. URL_FOR References That Need Blueprint Prefixes
|
||||
|
||||
### In Templates and Redirects
|
||||
- `url_for('login')` → `url_for('auth.login')`
|
||||
- `url_for('logout')` → `url_for('auth.logout')`
|
||||
- `url_for('verify_2fa')` → `url_for('auth.verify_2fa')`
|
||||
- `url_for('profile')` → `url_for('auth.profile')` (needs implementation)
|
||||
- `url_for('index')` → `url_for('main.index')` or appropriate blueprint
|
||||
- `url_for('blocked_ips')` → `url_for('admin.blocked_ips')`
|
||||
- `url_for('audit_log')` → `url_for('admin.audit_log')`
|
||||
- `url_for('backups')` → `url_for('admin.backups')`
|
||||
|
||||
## 5. Missing Functions/Middleware
|
||||
|
||||
### Authentication Decorators
|
||||
- `@login_required` decorator implementation needs to be verified
|
||||
- `@require_2fa` decorator (if used)
|
||||
|
||||
### Helper Functions
|
||||
- `get_connection()` - Database connection helper
|
||||
- `log_audit()` - Audit logging function
|
||||
- `create_backup()` - Backup creation function
|
||||
- Rate limiting functions for login attempts
|
||||
|
||||
### Session Management
|
||||
- Session timeout handling
|
||||
- Heartbeat mechanism for active sessions
|
||||
|
||||
## 6. API Endpoint Inconsistencies
|
||||
|
||||
### URL Prefix Issues
|
||||
- API routes in backup don't use `/api` prefix consistently
|
||||
- Some use `/api/...` while others are at root level
|
||||
|
||||
### Missing API Endpoints
|
||||
- `/api/generate-license-key` - Generate license key
|
||||
- `/api/global-search` - Global search functionality
|
||||
|
||||
## 7. Export Routes Organization
|
||||
|
||||
### Current vs Expected
|
||||
- Export routes might need different URL structure
|
||||
- Check if all export types are covered:
|
||||
- `/export/licenses`
|
||||
- `/export/audit`
|
||||
- `/export/customers`
|
||||
- `/export/sessions`
|
||||
- `/export/resources`
|
||||
|
||||
## 8. Special Configurations
|
||||
|
||||
### Missing Configurations
|
||||
- TOTP/2FA configuration
|
||||
- Backup encryption settings
|
||||
- Rate limiting configuration
|
||||
- Session timeout settings
|
||||
|
||||
### Environment Variables
|
||||
- Check if all required environment variables are properly loaded
|
||||
- Database connection parameters
|
||||
- Secret keys and encryption keys
|
||||
|
||||
## 9. JavaScript/AJAX Endpoints
|
||||
|
||||
### API calls that might be broken
|
||||
- Device management endpoints
|
||||
- Quick edit functionality
|
||||
- Bulk operations
|
||||
- Resource allocation checks
|
||||
|
||||
## 10. Permission/Access Control
|
||||
|
||||
### Missing or Incorrect Access Control
|
||||
- All routes need `@login_required` decorator
|
||||
- Some routes might need additional permission checks
|
||||
- API routes need proper authentication
|
||||
|
||||
## Action Items
|
||||
|
||||
1. **Implement missing profile/auth routes** in auth_routes.py
|
||||
2. **Add missing customer API routes** to api_routes.py
|
||||
3. **Create complete resource management blueprint** with all routes
|
||||
4. **Fix main dashboard route** - decide which blueprint should handle "/"
|
||||
5. **Update all url_for() calls** in templates to use blueprint prefixes
|
||||
6. **Verify database column names** are consistent throughout
|
||||
7. **Check template names** match between routes and actual files
|
||||
8. **Implement heartbeat mechanism** for session management
|
||||
9. **Add missing helper functions** to appropriate modules
|
||||
10. **Test all export routes** work correctly with new structure
|
||||
@@ -1,118 +0,0 @@
|
||||
# V2 Admin Panel - Routing Issues Report
|
||||
|
||||
Generated: 2025-06-17
|
||||
|
||||
## Summary of Findings
|
||||
|
||||
After systematically analyzing the v2_adminpanel application, I've identified several routing issues that need to be addressed:
|
||||
|
||||
### 1. Missing Blueprint Prefixes in url_for() Calls
|
||||
|
||||
The following templates have `url_for()` calls that are missing the required blueprint prefix:
|
||||
|
||||
#### In `profile.html`:
|
||||
- `url_for('change_password')` → Should be `url_for('auth.change_password')`
|
||||
- `url_for('disable_2fa')` → Should be `url_for('auth.disable_2fa')`
|
||||
- `url_for('setup_2fa')` → Should be `url_for('auth.setup_2fa')`
|
||||
|
||||
#### In `setup_2fa.html`:
|
||||
- `url_for('profile')` → Should be `url_for('auth.profile')`
|
||||
- `url_for('enable_2fa')` → Should be `url_for('auth.enable_2fa')`
|
||||
|
||||
#### In `backup_codes.html`:
|
||||
- `url_for('profile')` → Should be `url_for('auth.profile')`
|
||||
|
||||
#### In `resource_history.html`:
|
||||
- `url_for('resources')` → Should be `url_for('resources.resources')`
|
||||
- `url_for('edit_license', license_id=...)` → Should be `url_for('licenses.edit_license', license_id=...)`
|
||||
|
||||
#### In `resource_metrics.html`:
|
||||
- `url_for('resources')` → Should be `url_for('resources.resources')`
|
||||
- `url_for('resources_report')` → Should be `url_for('resources.resource_report')`
|
||||
|
||||
#### In `resource_report.html`:
|
||||
- `url_for('resources')` → Should be `url_for('resources.resources')`
|
||||
- `url_for('resources_report')` → Should be `url_for('resources.resource_report')`
|
||||
|
||||
#### In `sessions.html`:
|
||||
- `url_for('sessions', ...)` → Should be `url_for('sessions.sessions', ...)`
|
||||
|
||||
#### In `audit_log.html`:
|
||||
- `url_for('audit_log', ...)` → Should be `url_for('admin.audit_log', ...)`
|
||||
|
||||
#### In `licenses.html`:
|
||||
- `url_for('licenses', ...)` → Should be `url_for('licenses.licenses', ...)`
|
||||
|
||||
#### In `customers.html`:
|
||||
- `url_for('customers', ...)` → Should be `url_for('customers.customers', ...)`
|
||||
|
||||
#### In `resources.html`:
|
||||
- Several instances of incorrect references:
|
||||
- `url_for('customers.customers_licenses', ...)` → Should be `url_for('customers.customers_licenses', ...)`
|
||||
- `url_for('licenses.edit_license', ...)` → Correct
|
||||
- `url_for('resource_history', ...)` → Should be `url_for('resources.resource_history', ...)`
|
||||
- `url_for('edit_license', ...)` → Should be `url_for('licenses.edit_license', ...)`
|
||||
- `url_for('customers_licenses', ...)` → Should be `url_for('customers.customers_licenses', ...)`
|
||||
|
||||
### 2. Hardcoded URLs That Need Replacement
|
||||
|
||||
Many templates contain hardcoded URLs that should be replaced with `url_for()` calls:
|
||||
|
||||
#### In `base.html`:
|
||||
- `href="/"` → Should be `href="{{ url_for('admin.index') }}"`
|
||||
- `href="/profile"` → Should be `href="{{ url_for('auth.profile') }}"`
|
||||
- `href="/logout"` → Should be `href="{{ url_for('auth.logout') }}"`
|
||||
- `href="/customers-licenses"` → Should be `href="{{ url_for('customers.customers_licenses') }}"`
|
||||
- `href="/customer/create"` → Should be `href="{{ url_for('customers.create_customer') }}"`
|
||||
- `href="/create"` → Should be `href="{{ url_for('licenses.create_license') }}"`
|
||||
- `href="/batch"` → Should be `href="{{ url_for('batch.batch_licenses') }}"`
|
||||
- `href="/audit"` → Should be `href="{{ url_for('admin.audit_log') }}"`
|
||||
- `href="/sessions"` → Should be `href="{{ url_for('sessions.sessions') }}"`
|
||||
- `href="/backups"` → Should be `href="{{ url_for('admin.backups') }}"`
|
||||
- `href="/security/blocked-ips"` → Should be `href="{{ url_for('admin.blocked_ips') }}"`
|
||||
|
||||
#### In `customers_licenses.html` and `customers_licenses_old.html`:
|
||||
- Multiple hardcoded URLs for editing, creating, and exporting that need to be replaced with proper `url_for()` calls
|
||||
|
||||
#### In `edit_license.html`, `create_customer.html`, `index.html`:
|
||||
- `href="/customers-licenses"` → Should use `url_for()`
|
||||
|
||||
#### In `dashboard.html`:
|
||||
- Multiple hardcoded URLs that should use `url_for()`
|
||||
|
||||
#### In error pages (`404.html`, `500.html`):
|
||||
- `href="/"` → Should be `href="{{ url_for('admin.index') }}"`
|
||||
|
||||
### 3. Blueprint Configuration
|
||||
|
||||
Current blueprint configuration:
|
||||
- `export_bp` has `url_prefix='/export'`
|
||||
- `api_bp` has `url_prefix='/api'`
|
||||
- All other blueprints have no url_prefix
|
||||
|
||||
### 4. Route Naming Inconsistencies
|
||||
|
||||
Some routes have inconsistent naming between the route definition and the function name:
|
||||
- Route `/resources/report` has function name `resource_report` (note the singular vs plural)
|
||||
- This causes confusion with `url_for()` calls
|
||||
|
||||
### 5. Duplicate Route Risk Areas
|
||||
|
||||
While no exact duplicates were found, there are potential conflicts:
|
||||
- Both `admin_bp` and `customer_bp` might handle customer-related routes
|
||||
- API routes in `api_bp` overlap with functionality in other blueprints
|
||||
|
||||
## Recommendations
|
||||
|
||||
1. **Fix all `url_for()` calls** to include the correct blueprint prefix
|
||||
2. **Replace all hardcoded URLs** with `url_for()` calls
|
||||
3. **Standardize route naming** to match function names
|
||||
4. **Add url_prefix to blueprints** where appropriate to avoid conflicts
|
||||
5. **Create a route mapping document** for developers to reference
|
||||
|
||||
## Priority Actions
|
||||
|
||||
1. **High Priority**: Fix missing blueprint prefixes in `url_for()` calls - these will cause runtime errors
|
||||
2. **High Priority**: Replace hardcoded URLs in navigation (base.html) - affects site-wide navigation
|
||||
3. **Medium Priority**: Fix other hardcoded URLs in individual templates
|
||||
4. **Low Priority**: Refactor route naming for consistency
|
||||
@@ -1,73 +0,0 @@
|
||||
# Template Fixes Needed - Tuple to Dictionary Migration
|
||||
|
||||
## Problem
|
||||
Die models.py Funktionen geben Dictionaries zurück, aber viele Templates erwarten noch Tupel mit numerischen Indizes.
|
||||
|
||||
## Betroffene Templates und Routes:
|
||||
|
||||
### 1. ✅ FIXED: customers.html
|
||||
- Route: `/customers`
|
||||
- Funktion: `get_customers()`
|
||||
- Status: Bereits gefixt
|
||||
|
||||
### 2. ✅ FIXED: customers_licenses.html
|
||||
- Route: `/customers-licenses`
|
||||
- Status: Teilweise gefixt (customers list)
|
||||
- TODO: selected_customer wird per JavaScript geladen
|
||||
|
||||
### 3. ✅ FIXED: edit_customer.html
|
||||
- Route: `/customer/edit/<id>`
|
||||
- Funktion: `get_customer_by_id()`
|
||||
- Status: Bereits gefixt
|
||||
|
||||
### 4. ❌ licenses.html
|
||||
- Route: `/licenses`
|
||||
- Funktion: `get_licenses()`
|
||||
- Problem: Nutzt license[0], license[1], etc.
|
||||
- Lösung: Ändern zu license.id, license.license_key, etc.
|
||||
|
||||
### 5. ❌ edit_license.html
|
||||
- Route: `/license/edit/<id>`
|
||||
- Funktion: `get_license_by_id()`
|
||||
- Problem: Nutzt license[x] Syntax
|
||||
|
||||
### 6. ❌ sessions.html
|
||||
- Route: `/sessions`
|
||||
- Funktion: `get_active_sessions()`
|
||||
- Problem: Nutzt session[x] Syntax
|
||||
|
||||
### 7. ❌ audit_log.html
|
||||
- Route: `/audit`
|
||||
- Problem: Nutzt entry[x] Syntax
|
||||
|
||||
### 8. ❌ resources.html
|
||||
- Route: `/resources`
|
||||
- Problem: Nutzt resource[x] Syntax
|
||||
|
||||
### 9. ❌ backups.html
|
||||
- Route: `/backups`
|
||||
- Problem: Nutzt backup[x] Syntax
|
||||
|
||||
### 10. ✅ FIXED: batch_form.html
|
||||
- Route: `/batch`
|
||||
- Problem: Fehlende /api/customers Route
|
||||
- Status: API Route hinzugefügt
|
||||
|
||||
### 11. ❌ dashboard.html (index.html)
|
||||
- Route: `/`
|
||||
- Problem: Möglicherweise nutzt auch numerische Indizes
|
||||
|
||||
## Batch License Problem
|
||||
- batch_create.html existiert nicht, stattdessen batch_form.html
|
||||
- Template mismatch in batch_routes.py Zeile 118
|
||||
|
||||
## Empfohlene Lösung
|
||||
1. Alle Templates systematisch durchgehen und von Tupel auf Dictionary-Zugriff umstellen
|
||||
2. Alternativ: Models.py ändern um Tupel statt Dictionaries zurückzugeben (nicht empfohlen)
|
||||
3. Batch template name fix: batch_create.html → batch_form.html
|
||||
|
||||
## Quick Fix für Batch
|
||||
Zeile 118 in batch_routes.py ändern:
|
||||
```python
|
||||
return render_template("batch_form.html", customers=customers)
|
||||
```
|
||||
Binäre Datei nicht angezeigt.
Binäre Datei nicht angezeigt.
Binäre Datei nicht angezeigt.
Binäre Datei nicht angezeigt.
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
@@ -1,124 +0,0 @@
|
||||
import os
|
||||
import time
|
||||
import json
|
||||
import logging
|
||||
import requests
|
||||
from io import BytesIO
|
||||
from datetime import datetime, timedelta
|
||||
from zoneinfo import ZoneInfo
|
||||
from pathlib import Path
|
||||
|
||||
from flask import Flask, render_template, request, redirect, session, url_for, send_file, jsonify, flash
|
||||
from flask_session import Session
|
||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
import pandas as pd
|
||||
from psycopg2.extras import Json
|
||||
|
||||
# Import our new modules
|
||||
import config
|
||||
from db import get_connection, get_db_connection, get_db_cursor, execute_query
|
||||
from auth.decorators import login_required
|
||||
from auth.password import hash_password, verify_password
|
||||
from auth.two_factor import (
|
||||
generate_totp_secret, generate_qr_code, verify_totp,
|
||||
generate_backup_codes, hash_backup_code, verify_backup_code
|
||||
)
|
||||
from auth.rate_limiting import (
|
||||
get_client_ip, check_ip_blocked, record_failed_attempt,
|
||||
reset_login_attempts, get_login_attempts
|
||||
)
|
||||
from utils.audit import log_audit
|
||||
from utils.license import generate_license_key, validate_license_key
|
||||
from utils.backup import create_backup, restore_backup, get_or_create_encryption_key
|
||||
from utils.export import (
|
||||
create_excel_export, format_datetime_for_export,
|
||||
prepare_license_export_data, prepare_customer_export_data,
|
||||
prepare_session_export_data, prepare_audit_export_data
|
||||
)
|
||||
from models import get_user_by_username
|
||||
|
||||
app = Flask(__name__)
|
||||
# Load configuration from config module
|
||||
app.config['SECRET_KEY'] = config.SECRET_KEY
|
||||
app.config['SESSION_TYPE'] = config.SESSION_TYPE
|
||||
app.config['JSON_AS_ASCII'] = config.JSON_AS_ASCII
|
||||
app.config['JSONIFY_MIMETYPE'] = config.JSONIFY_MIMETYPE
|
||||
app.config['PERMANENT_SESSION_LIFETIME'] = config.PERMANENT_SESSION_LIFETIME
|
||||
app.config['SESSION_COOKIE_HTTPONLY'] = config.SESSION_COOKIE_HTTPONLY
|
||||
app.config['SESSION_COOKIE_SECURE'] = config.SESSION_COOKIE_SECURE
|
||||
app.config['SESSION_COOKIE_SAMESITE'] = config.SESSION_COOKIE_SAMESITE
|
||||
app.config['SESSION_COOKIE_NAME'] = config.SESSION_COOKIE_NAME
|
||||
app.config['SESSION_REFRESH_EACH_REQUEST'] = config.SESSION_REFRESH_EACH_REQUEST
|
||||
Session(app)
|
||||
|
||||
# ProxyFix für korrekte IP-Adressen hinter Nginx
|
||||
app.wsgi_app = ProxyFix(
|
||||
app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1
|
||||
)
|
||||
|
||||
# Configuration is now loaded from config module
|
||||
|
||||
# Scheduler für automatische Backups
|
||||
scheduler = BackgroundScheduler()
|
||||
scheduler.start()
|
||||
|
||||
# Logging konfigurieren
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
|
||||
# Scheduled Backup Job
|
||||
def scheduled_backup():
|
||||
"""Führt ein geplantes Backup aus"""
|
||||
logging.info("Starte geplantes Backup...")
|
||||
create_backup(backup_type="scheduled", created_by="scheduler")
|
||||
|
||||
# Scheduler konfigurieren - täglich um 3:00 Uhr
|
||||
scheduler.add_job(
|
||||
scheduled_backup,
|
||||
'cron',
|
||||
hour=config.SCHEDULER_CONFIG['backup_hour'],
|
||||
minute=config.SCHEDULER_CONFIG['backup_minute'],
|
||||
id='daily_backup',
|
||||
replace_existing=True
|
||||
)
|
||||
|
||||
|
||||
def verify_recaptcha(response):
|
||||
"""Verifiziert die reCAPTCHA v2 Response mit Google"""
|
||||
secret_key = config.RECAPTCHA_SECRET_KEY
|
||||
|
||||
# Wenn kein Secret Key konfiguriert ist, CAPTCHA als bestanden werten (für PoC)
|
||||
if not secret_key:
|
||||
logging.warning("RECAPTCHA_SECRET_KEY nicht konfiguriert - CAPTCHA wird übersprungen")
|
||||
return True
|
||||
|
||||
# Verifizierung bei Google
|
||||
try:
|
||||
verify_url = 'https://www.google.com/recaptcha/api/siteverify'
|
||||
data = {
|
||||
'secret': secret_key,
|
||||
'response': response
|
||||
}
|
||||
|
||||
# Timeout für Request setzen
|
||||
r = requests.post(verify_url, data=data, timeout=5)
|
||||
result = r.json()
|
||||
|
||||
# Log für Debugging
|
||||
if not result.get('success'):
|
||||
logging.warning(f"reCAPTCHA Validierung fehlgeschlagen: {result.get('error-codes', [])}")
|
||||
|
||||
return result.get('success', False)
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
logging.error(f"reCAPTCHA Verifizierung fehlgeschlagen: {str(e)}")
|
||||
# Bei Netzwerkfehlern CAPTCHA als bestanden werten
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.error(f"Unerwarteter Fehler bei reCAPTCHA: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
# Now copy all the route handlers from the original file
|
||||
# Starting from line 693...
|
||||
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
@@ -1,156 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Cleanup-Skript zum sicheren Entfernen auskommentierter Routes aus app.py
|
||||
Entfernt alle auskommentierten @app.route Blöcke nach der Blueprint-Migration
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
def cleanup_commented_routes(file_path):
|
||||
"""Entfernt auskommentierte Route-Blöcke aus app.py"""
|
||||
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
print(f"📄 Datei geladen: {len(lines)} Zeilen")
|
||||
|
||||
cleaned_lines = []
|
||||
in_commented_block = False
|
||||
block_start_line = -1
|
||||
removed_blocks = []
|
||||
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
line = lines[i]
|
||||
|
||||
# Prüfe ob eine auskommentierte Route beginnt
|
||||
if re.match(r'^# @app\.route\(', line.strip()):
|
||||
in_commented_block = True
|
||||
block_start_line = i + 1 # 1-basiert für Anzeige
|
||||
block_lines = [line]
|
||||
i += 1
|
||||
|
||||
# Sammle den ganzen auskommentierten Block
|
||||
while i < len(lines):
|
||||
current_line = lines[i]
|
||||
|
||||
# Block endet wenn:
|
||||
# 1. Eine neue Funktion/Route beginnt (nicht auskommentiert)
|
||||
# 2. Eine Leerzeile nach mehreren kommentierten Zeilen
|
||||
# 3. Eine neue auskommentierte Route beginnt
|
||||
|
||||
if re.match(r'^@', current_line.strip()) or \
|
||||
re.match(r'^def\s+\w+', current_line.strip()) or \
|
||||
re.match(r'^class\s+\w+', current_line.strip()):
|
||||
# Nicht-kommentierter Code gefunden, Block endet
|
||||
break
|
||||
|
||||
if re.match(r'^# @app\.route\(', current_line.strip()) and len(block_lines) > 1:
|
||||
# Neue auskommentierte Route, vorheriger Block endet
|
||||
break
|
||||
|
||||
# Prüfe ob die Zeile zum kommentierten Block gehört
|
||||
if current_line.strip() == '' and i + 1 < len(lines):
|
||||
# Schaue voraus
|
||||
next_line = lines[i + 1]
|
||||
if not (next_line.strip().startswith('#') or next_line.strip() == ''):
|
||||
# Leerzeile gefolgt von nicht-kommentiertem Code
|
||||
block_lines.append(current_line)
|
||||
i += 1
|
||||
break
|
||||
|
||||
if current_line.strip().startswith('#') or current_line.strip() == '':
|
||||
block_lines.append(current_line)
|
||||
i += 1
|
||||
else:
|
||||
# Nicht-kommentierte Zeile gefunden, Block endet
|
||||
break
|
||||
|
||||
# Entferne trailing Leerzeilen vom Block
|
||||
while block_lines and block_lines[-1].strip() == '':
|
||||
block_lines.pop()
|
||||
i -= 1
|
||||
|
||||
removed_blocks.append({
|
||||
'start_line': block_start_line,
|
||||
'end_line': block_start_line + len(block_lines) - 1,
|
||||
'lines': len(block_lines),
|
||||
'route': extract_route_info(block_lines)
|
||||
})
|
||||
|
||||
in_commented_block = False
|
||||
|
||||
# Füge eine Leerzeile ein wenn nötig (um Abstand zu wahren)
|
||||
if cleaned_lines and cleaned_lines[-1].strip() != '' and \
|
||||
i < len(lines) and lines[i].strip() != '':
|
||||
cleaned_lines.append('\n')
|
||||
|
||||
else:
|
||||
cleaned_lines.append(line)
|
||||
i += 1
|
||||
|
||||
return cleaned_lines, removed_blocks
|
||||
|
||||
def extract_route_info(block_lines):
|
||||
"""Extrahiert Route-Information aus dem kommentierten Block"""
|
||||
for line in block_lines:
|
||||
match = re.search(r'# @app\.route\("([^"]+)"', line)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return "Unknown"
|
||||
|
||||
def main():
|
||||
file_path = 'app.py'
|
||||
backup_path = f'app.py.backup_before_cleanup_{datetime.now().strftime("%Y%m%d_%H%M%S")}'
|
||||
|
||||
print("🧹 Starte Cleanup der auskommentierten Routes...\n")
|
||||
|
||||
# Backup erstellen
|
||||
print(f"💾 Erstelle Backup: {backup_path}")
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
with open(backup_path, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
# Cleanup durchführen
|
||||
cleaned_lines, removed_blocks = cleanup_commented_routes(file_path)
|
||||
|
||||
# Statistiken anzeigen
|
||||
print(f"\n📊 Cleanup-Statistiken:")
|
||||
print(f" Entfernte Blöcke: {len(removed_blocks)}")
|
||||
print(f" Entfernte Zeilen: {sum(block['lines'] for block in removed_blocks)}")
|
||||
print(f" Neue Dateigröße: {len(cleaned_lines)} Zeilen")
|
||||
|
||||
print(f"\n🗑️ Entfernte Routes:")
|
||||
for block in removed_blocks:
|
||||
print(f" Zeilen {block['start_line']}-{block['end_line']}: {block['route']} ({block['lines']} Zeilen)")
|
||||
|
||||
# Bestätigung abfragen
|
||||
print(f"\n⚠️ Diese Operation wird {sum(block['lines'] for block in removed_blocks)} Zeilen entfernen!")
|
||||
response = input("Fortfahren? (ja/nein): ")
|
||||
|
||||
if response.lower() in ['ja', 'j', 'yes', 'y']:
|
||||
# Bereinigte Datei schreiben
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
f.writelines(cleaned_lines)
|
||||
print(f"\n✅ Cleanup abgeschlossen! Backup gespeichert als: {backup_path}")
|
||||
|
||||
# Größenvergleich
|
||||
import os
|
||||
old_size = os.path.getsize(backup_path)
|
||||
new_size = os.path.getsize(file_path)
|
||||
reduction = old_size - new_size
|
||||
reduction_percent = (reduction / old_size) * 100
|
||||
|
||||
print(f"\n📉 Dateigrößen-Reduktion:")
|
||||
print(f" Vorher: {old_size:,} Bytes")
|
||||
print(f" Nachher: {new_size:,} Bytes")
|
||||
print(f" Reduziert um: {reduction:,} Bytes ({reduction_percent:.1f}%)")
|
||||
|
||||
else:
|
||||
print("\n❌ Cleanup abgebrochen. Keine Änderungen vorgenommen.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,153 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Automatisches Cleanup-Skript zum Entfernen auskommentierter Routes aus app.py
|
||||
Führt das Cleanup ohne Benutzerinteraktion durch
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from datetime import datetime
|
||||
import os
|
||||
|
||||
def cleanup_commented_routes(file_path):
|
||||
"""Entfernt auskommentierte Route-Blöcke aus app.py"""
|
||||
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
print(f"📄 Datei geladen: {len(lines)} Zeilen")
|
||||
|
||||
cleaned_lines = []
|
||||
removed_blocks = []
|
||||
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
line = lines[i]
|
||||
|
||||
# Prüfe ob eine auskommentierte Route beginnt
|
||||
if re.match(r'^# @app\.route\(', line.strip()):
|
||||
block_start_line = i + 1 # 1-basiert für Anzeige
|
||||
block_lines = [line]
|
||||
i += 1
|
||||
|
||||
# Sammle den ganzen auskommentierten Block
|
||||
while i < len(lines):
|
||||
current_line = lines[i]
|
||||
|
||||
# Block endet wenn:
|
||||
# 1. Eine neue Funktion/Route beginnt (nicht auskommentiert)
|
||||
# 2. Eine neue auskommentierte Route beginnt
|
||||
# 3. Nicht-kommentierter Code gefunden wird
|
||||
|
||||
if re.match(r'^@', current_line.strip()) or \
|
||||
re.match(r'^def\s+\w+', current_line.strip()) or \
|
||||
re.match(r'^class\s+\w+', current_line.strip()):
|
||||
# Nicht-kommentierter Code gefunden, Block endet
|
||||
break
|
||||
|
||||
if re.match(r'^# @app\.route\(', current_line.strip()) and len(block_lines) > 1:
|
||||
# Neue auskommentierte Route, vorheriger Block endet
|
||||
break
|
||||
|
||||
# Prüfe ob die Zeile zum kommentierten Block gehört
|
||||
if current_line.strip() == '' and i + 1 < len(lines):
|
||||
# Schaue voraus
|
||||
next_line = lines[i + 1]
|
||||
if not (next_line.strip().startswith('#') or next_line.strip() == ''):
|
||||
# Leerzeile gefolgt von nicht-kommentiertem Code
|
||||
block_lines.append(current_line)
|
||||
i += 1
|
||||
break
|
||||
|
||||
if current_line.strip().startswith('#') or current_line.strip() == '':
|
||||
block_lines.append(current_line)
|
||||
i += 1
|
||||
else:
|
||||
# Nicht-kommentierte Zeile gefunden, Block endet
|
||||
break
|
||||
|
||||
# Entferne trailing Leerzeilen vom Block
|
||||
while block_lines and block_lines[-1].strip() == '':
|
||||
block_lines.pop()
|
||||
i -= 1
|
||||
|
||||
removed_blocks.append({
|
||||
'start_line': block_start_line,
|
||||
'end_line': block_start_line + len(block_lines) - 1,
|
||||
'lines': len(block_lines),
|
||||
'route': extract_route_info(block_lines)
|
||||
})
|
||||
|
||||
# Füge eine Leerzeile ein wenn nötig (um Abstand zu wahren)
|
||||
if cleaned_lines and cleaned_lines[-1].strip() != '' and \
|
||||
i < len(lines) and lines[i].strip() != '':
|
||||
cleaned_lines.append('\n')
|
||||
|
||||
else:
|
||||
cleaned_lines.append(line)
|
||||
i += 1
|
||||
|
||||
return cleaned_lines, removed_blocks
|
||||
|
||||
def extract_route_info(block_lines):
|
||||
"""Extrahiert Route-Information aus dem kommentierten Block"""
|
||||
for line in block_lines:
|
||||
match = re.search(r'# @app\.route\("([^"]+)"', line)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return "Unknown"
|
||||
|
||||
def main():
|
||||
file_path = 'app.py'
|
||||
backup_path = f'app.py.backup_before_cleanup_{datetime.now().strftime("%Y%m%d_%H%M%S")}'
|
||||
|
||||
print("🧹 Starte automatisches Cleanup der auskommentierten Routes...\n")
|
||||
|
||||
# Backup erstellen
|
||||
print(f"💾 Erstelle Backup: {backup_path}")
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
with open(backup_path, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
# Cleanup durchführen
|
||||
cleaned_lines, removed_blocks = cleanup_commented_routes(file_path)
|
||||
|
||||
# Statistiken anzeigen
|
||||
print(f"\n📊 Cleanup-Statistiken:")
|
||||
print(f" Entfernte Blöcke: {len(removed_blocks)}")
|
||||
print(f" Entfernte Zeilen: {sum(block['lines'] for block in removed_blocks)}")
|
||||
print(f" Neue Dateigröße: {len(cleaned_lines)} Zeilen")
|
||||
|
||||
if len(removed_blocks) > 10:
|
||||
print(f"\n🗑️ Erste 10 entfernte Routes:")
|
||||
for block in removed_blocks[:10]:
|
||||
print(f" Zeilen {block['start_line']}-{block['end_line']}: {block['route']} ({block['lines']} Zeilen)")
|
||||
print(f" ... und {len(removed_blocks) - 10} weitere Routes")
|
||||
else:
|
||||
print(f"\n🗑️ Entfernte Routes:")
|
||||
for block in removed_blocks:
|
||||
print(f" Zeilen {block['start_line']}-{block['end_line']}: {block['route']} ({block['lines']} Zeilen)")
|
||||
|
||||
# Bereinigte Datei schreiben
|
||||
print(f"\n✍️ Schreibe bereinigte Datei...")
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
f.writelines(cleaned_lines)
|
||||
|
||||
# Größenvergleich
|
||||
old_size = os.path.getsize(backup_path)
|
||||
new_size = os.path.getsize(file_path)
|
||||
reduction = old_size - new_size
|
||||
reduction_percent = (reduction / old_size) * 100
|
||||
|
||||
print(f"\n📉 Dateigrößen-Reduktion:")
|
||||
print(f" Vorher: {old_size:,} Bytes")
|
||||
print(f" Nachher: {new_size:,} Bytes")
|
||||
print(f" Reduziert um: {reduction:,} Bytes ({reduction_percent:.1f}%)")
|
||||
|
||||
print(f"\n✅ Cleanup erfolgreich abgeschlossen!")
|
||||
print(f" Backup: {backup_path}")
|
||||
print(f" Rollback möglich mit: cp {backup_path} app.py")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,5 +0,0 @@
|
||||
# Netscape HTTP Cookie File
|
||||
# https://curl.se/docs/http-cookies.html
|
||||
# This file was generated by libcurl! Edit at your own risk.
|
||||
|
||||
#HttpOnly_localhost FALSE / FALSE 1749329847 admin_session aojqyq4GcSt5oT7NJPeg7UHPoEZUVkn-s1Kr-EAnJWM
|
||||
@@ -1,20 +0,0 @@
|
||||
-- Create users table if it doesn't exist
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
username VARCHAR(50) UNIQUE NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(100),
|
||||
totp_secret VARCHAR(32),
|
||||
totp_enabled BOOLEAN DEFAULT FALSE,
|
||||
backup_codes TEXT, -- JSON array of hashed backup codes
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
last_password_change TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
password_reset_token VARCHAR(64),
|
||||
password_reset_expires TIMESTAMP WITH TIME ZONE,
|
||||
failed_2fa_attempts INTEGER DEFAULT 0,
|
||||
last_failed_2fa TIMESTAMP WITH TIME ZONE
|
||||
);
|
||||
|
||||
-- Index for faster login lookups
|
||||
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_reset_token ON users(password_reset_token) WHERE password_reset_token IS NOT NULL;
|
||||
@@ -1,13 +0,0 @@
|
||||
-- Fix für die fehlerhafte Migration - entfernt doppelte Bindestriche
|
||||
UPDATE licenses
|
||||
SET license_key = REPLACE(license_key, 'AF--', 'AF-')
|
||||
WHERE license_key LIKE 'AF--%';
|
||||
|
||||
UPDATE licenses
|
||||
SET license_key = REPLACE(license_key, '6--', '6-')
|
||||
WHERE license_key LIKE '%6--%';
|
||||
|
||||
-- Zeige die korrigierten Keys
|
||||
SELECT id, license_key, license_type
|
||||
FROM licenses
|
||||
ORDER BY id;
|
||||
@@ -1,5 +0,0 @@
|
||||
-- Markiere alle existierenden Ressourcen als Testdaten
|
||||
UPDATE resource_pools SET is_test = TRUE WHERE is_test = FALSE OR is_test IS NULL;
|
||||
|
||||
-- Zeige Anzahl der aktualisierten Ressourcen
|
||||
SELECT COUNT(*) as updated_resources FROM resource_pools WHERE is_test = TRUE;
|
||||
@@ -1,13 +0,0 @@
|
||||
-- Migration: Setze device_limit für bestehende Test-Lizenzen auf 3
|
||||
-- Dieses Script wird nur einmal ausgeführt, um bestehende Lizenzen zu aktualisieren
|
||||
|
||||
-- Setze device_limit = 3 für alle bestehenden Lizenzen, die noch keinen Wert haben
|
||||
UPDATE licenses
|
||||
SET device_limit = 3
|
||||
WHERE device_limit IS NULL;
|
||||
|
||||
-- Bestätige die Änderung
|
||||
SELECT COUNT(*) as updated_licenses,
|
||||
COUNT(CASE WHEN is_test = TRUE THEN 1 END) as test_licenses_updated
|
||||
FROM licenses
|
||||
WHERE device_limit = 3;
|
||||
@@ -1,54 +0,0 @@
|
||||
-- Migration der Lizenzschlüssel vom alten Format zum neuen Format
|
||||
-- Alt: AF-YYYYMMFT-XXXX-YYYY-ZZZZ
|
||||
-- Neu: AF-F-YYYYMM-XXXX-YYYY-ZZZZ
|
||||
|
||||
-- Backup der aktuellen Schlüssel erstellen (für Sicherheit)
|
||||
CREATE TEMP TABLE license_backup AS
|
||||
SELECT id, license_key FROM licenses;
|
||||
|
||||
-- Update für Fullversion Keys (F)
|
||||
UPDATE licenses
|
||||
SET license_key =
|
||||
CONCAT(
|
||||
SUBSTRING(license_key, 1, 3), -- 'AF-'
|
||||
'-F-',
|
||||
SUBSTRING(license_key, 4, 6), -- 'YYYYMM'
|
||||
'-',
|
||||
SUBSTRING(license_key, 11) -- Rest des Keys
|
||||
)
|
||||
WHERE license_key LIKE 'AF-%F-%'
|
||||
AND license_type = 'full'
|
||||
AND license_key NOT LIKE 'AF-F-%'; -- Nicht bereits migriert
|
||||
|
||||
-- Update für Testversion Keys (T)
|
||||
UPDATE licenses
|
||||
SET license_key =
|
||||
CONCAT(
|
||||
SUBSTRING(license_key, 1, 3), -- 'AF-'
|
||||
'-T-',
|
||||
SUBSTRING(license_key, 4, 6), -- 'YYYYMM'
|
||||
'-',
|
||||
SUBSTRING(license_key, 11) -- Rest des Keys
|
||||
)
|
||||
WHERE license_key LIKE 'AF-%T-%'
|
||||
AND license_type = 'test'
|
||||
AND license_key NOT LIKE 'AF-T-%'; -- Nicht bereits migriert
|
||||
|
||||
-- Zeige die Änderungen
|
||||
SELECT
|
||||
b.license_key as old_key,
|
||||
l.license_key as new_key,
|
||||
l.license_type
|
||||
FROM licenses l
|
||||
JOIN license_backup b ON l.id = b.id
|
||||
WHERE b.license_key != l.license_key
|
||||
ORDER BY l.id;
|
||||
|
||||
-- Anzahl der migrierten Keys
|
||||
SELECT
|
||||
COUNT(*) as total_migrated,
|
||||
SUM(CASE WHEN license_type = 'full' THEN 1 ELSE 0 END) as full_licenses,
|
||||
SUM(CASE WHEN license_type = 'test' THEN 1 ELSE 0 END) as test_licenses
|
||||
FROM licenses l
|
||||
JOIN license_backup b ON l.id = b.id
|
||||
WHERE b.license_key != l.license_key;
|
||||
@@ -1,78 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Migration script to create initial users in the database from environment variables
|
||||
Run this once after creating the users table
|
||||
"""
|
||||
|
||||
import os
|
||||
import psycopg2
|
||||
import bcrypt
|
||||
from dotenv import load_dotenv
|
||||
from datetime import datetime
|
||||
|
||||
load_dotenv()
|
||||
|
||||
def get_connection():
|
||||
return psycopg2.connect(
|
||||
host=os.getenv("POSTGRES_HOST", "postgres"),
|
||||
port=os.getenv("POSTGRES_PORT", "5432"),
|
||||
dbname=os.getenv("POSTGRES_DB"),
|
||||
user=os.getenv("POSTGRES_USER"),
|
||||
password=os.getenv("POSTGRES_PASSWORD"),
|
||||
options='-c client_encoding=UTF8'
|
||||
)
|
||||
|
||||
def hash_password(password):
|
||||
return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
|
||||
|
||||
def migrate_users():
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Check if users already exist
|
||||
cur.execute("SELECT COUNT(*) FROM users")
|
||||
user_count = cur.fetchone()[0]
|
||||
|
||||
if user_count > 0:
|
||||
print(f"Users table already contains {user_count} users. Skipping migration.")
|
||||
return
|
||||
|
||||
# Get admin users from environment
|
||||
admin1_user = os.getenv("ADMIN1_USERNAME")
|
||||
admin1_pass = os.getenv("ADMIN1_PASSWORD")
|
||||
admin2_user = os.getenv("ADMIN2_USERNAME")
|
||||
admin2_pass = os.getenv("ADMIN2_PASSWORD")
|
||||
|
||||
if not all([admin1_user, admin1_pass, admin2_user, admin2_pass]):
|
||||
print("ERROR: Admin credentials not found in environment variables!")
|
||||
return
|
||||
|
||||
# Insert admin users
|
||||
users = [
|
||||
(admin1_user, hash_password(admin1_pass), f"{admin1_user}@v2-admin.local"),
|
||||
(admin2_user, hash_password(admin2_pass), f"{admin2_user}@v2-admin.local")
|
||||
]
|
||||
|
||||
for username, password_hash, email in users:
|
||||
cur.execute("""
|
||||
INSERT INTO users (username, password_hash, email, totp_enabled, created_at)
|
||||
VALUES (%s, %s, %s, %s, %s)
|
||||
""", (username, password_hash, email, False, datetime.now()))
|
||||
print(f"Created user: {username}")
|
||||
|
||||
conn.commit()
|
||||
print("\nMigration completed successfully!")
|
||||
print("Users can now log in with their existing credentials.")
|
||||
print("They can enable 2FA from their profile page.")
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
print(f"ERROR during migration: {e}")
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Starting user migration...")
|
||||
migrate_users()
|
||||
@@ -1,52 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Remove duplicate routes that have been moved to blueprints
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
# Read the current app.py
|
||||
with open('app.py', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# List of function names that have been moved to blueprints
|
||||
moved_functions = [
|
||||
# Auth routes
|
||||
'login',
|
||||
'logout',
|
||||
'verify_2fa',
|
||||
'profile',
|
||||
'change_password',
|
||||
'setup_2fa',
|
||||
'enable_2fa',
|
||||
'disable_2fa',
|
||||
'heartbeat',
|
||||
# Admin routes
|
||||
'dashboard',
|
||||
'audit_log',
|
||||
'backups',
|
||||
'create_backup_route',
|
||||
'restore_backup_route',
|
||||
'download_backup',
|
||||
'delete_backup',
|
||||
'blocked_ips',
|
||||
'unblock_ip',
|
||||
'clear_attempts'
|
||||
]
|
||||
|
||||
# Create a pattern to match route decorators and their functions
|
||||
for func_name in moved_functions:
|
||||
# Pattern to match from @app.route to the end of the function
|
||||
pattern = rf'@app\.route\([^)]+\)\s*(?:@login_required\s*)?def {func_name}\([^)]*\):.*?(?=\n@app\.route|\n@[a-zA-Z]|\nif __name__|$)'
|
||||
|
||||
# Replace with a comment
|
||||
replacement = f'# Function {func_name} moved to blueprint'
|
||||
|
||||
content = re.sub(pattern, replacement, content, flags=re.DOTALL)
|
||||
|
||||
# Write the modified content
|
||||
with open('app_no_duplicates.py', 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
print("Created app_no_duplicates.py with duplicate routes removed")
|
||||
print("Please review the file before using it")
|
||||
Binäre Datei nicht angezeigt.
BIN
v2_adminpanel/routes/__pycache__/admin_routes.cpython-312.pyc
Normale Datei
BIN
v2_adminpanel/routes/__pycache__/admin_routes.cpython-312.pyc
Normale Datei
Binäre Datei nicht angezeigt.
BIN
v2_adminpanel/routes/__pycache__/api_routes.cpython-312.pyc
Normale Datei
BIN
v2_adminpanel/routes/__pycache__/api_routes.cpython-312.pyc
Normale Datei
Binäre Datei nicht angezeigt.
BIN
v2_adminpanel/routes/__pycache__/auth_routes.cpython-312.pyc
Normale Datei
BIN
v2_adminpanel/routes/__pycache__/auth_routes.cpython-312.pyc
Normale Datei
Binäre Datei nicht angezeigt.
BIN
v2_adminpanel/routes/__pycache__/batch_routes.cpython-312.pyc
Normale Datei
BIN
v2_adminpanel/routes/__pycache__/batch_routes.cpython-312.pyc
Normale Datei
Binäre Datei nicht angezeigt.
Binäre Datei nicht angezeigt.
BIN
v2_adminpanel/routes/__pycache__/export_routes.cpython-312.pyc
Normale Datei
BIN
v2_adminpanel/routes/__pycache__/export_routes.cpython-312.pyc
Normale Datei
Binäre Datei nicht angezeigt.
BIN
v2_adminpanel/routes/__pycache__/license_routes.cpython-312.pyc
Normale Datei
BIN
v2_adminpanel/routes/__pycache__/license_routes.cpython-312.pyc
Normale Datei
Binäre Datei nicht angezeigt.
BIN
v2_adminpanel/routes/__pycache__/resource_routes.cpython-312.pyc
Normale Datei
BIN
v2_adminpanel/routes/__pycache__/resource_routes.cpython-312.pyc
Normale Datei
Binäre Datei nicht angezeigt.
BIN
v2_adminpanel/routes/__pycache__/session_routes.cpython-312.pyc
Normale Datei
BIN
v2_adminpanel/routes/__pycache__/session_routes.cpython-312.pyc
Normale Datei
Binäre Datei nicht angezeigt.
@@ -65,22 +65,22 @@ def toggle_license(license_id):
|
||||
if not license_data:
|
||||
return jsonify({'error': 'Lizenz nicht gefunden'}), 404
|
||||
|
||||
new_status = not license_data['active']
|
||||
new_status = not license_data['is_active']
|
||||
|
||||
# Update status
|
||||
cur.execute("UPDATE licenses SET active = %s WHERE id = %s", (new_status, license_id))
|
||||
cur.execute("UPDATE licenses SET is_active = %s WHERE id = %s", (new_status, license_id))
|
||||
conn.commit()
|
||||
|
||||
# Log change
|
||||
log_audit('TOGGLE', 'license', license_id,
|
||||
old_values={'active': license_data['active']},
|
||||
new_values={'active': new_status})
|
||||
old_values={'is_active': license_data['is_active']},
|
||||
new_values={'is_active': new_status})
|
||||
|
||||
return jsonify({'success': True, 'active': new_status})
|
||||
return jsonify({'success': True, 'is_active': new_status})
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
logging.error(f"Fehler beim Umschalten der Lizenz: {str(e)}")
|
||||
logging.error(f"Fehler beim Umschalten der Lizenz: {str(e)}", exc_info=True)
|
||||
return jsonify({'error': 'Fehler beim Umschalten der Lizenz'}), 500
|
||||
finally:
|
||||
cur.close()
|
||||
@@ -104,8 +104,8 @@ def bulk_activate_licenses():
|
||||
# Update all selected licenses
|
||||
cur.execute("""
|
||||
UPDATE licenses
|
||||
SET active = true
|
||||
WHERE id = ANY(%s) AND active = false
|
||||
SET is_active = true
|
||||
WHERE id = ANY(%s) AND is_active = false
|
||||
RETURNING id
|
||||
""", (license_ids,))
|
||||
|
||||
@@ -115,7 +115,7 @@ def bulk_activate_licenses():
|
||||
# Log changes
|
||||
for license_id in updated_ids:
|
||||
log_audit('BULK_ACTIVATE', 'license', license_id,
|
||||
new_values={'active': True})
|
||||
new_values={'is_active': True})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
@@ -148,8 +148,8 @@ def bulk_deactivate_licenses():
|
||||
# Update all selected licenses
|
||||
cur.execute("""
|
||||
UPDATE licenses
|
||||
SET active = false
|
||||
WHERE id = ANY(%s) AND active = true
|
||||
SET is_active = false
|
||||
WHERE id = ANY(%s) AND is_active = true
|
||||
RETURNING id
|
||||
""", (license_ids,))
|
||||
|
||||
@@ -159,7 +159,7 @@ def bulk_deactivate_licenses():
|
||||
# Log changes
|
||||
for license_id in updated_ids:
|
||||
log_audit('BULK_DEACTIVATE', 'license', license_id,
|
||||
new_values={'active': False})
|
||||
new_values={'is_active': False})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
@@ -451,10 +451,10 @@ def quick_edit_license(license_id):
|
||||
new_values['valid_until'] = data['valid_until']
|
||||
|
||||
if 'active' in data:
|
||||
updates.append("active = %s")
|
||||
updates.append("is_active = %s")
|
||||
params.append(bool(data['active']))
|
||||
old_values['active'] = current_license['active']
|
||||
new_values['active'] = bool(data['active'])
|
||||
old_values['is_active'] = current_license['is_active']
|
||||
new_values['is_active'] = bool(data['active'])
|
||||
|
||||
if not updates:
|
||||
return jsonify({'error': 'Keine Änderungen angegeben'}), 400
|
||||
@@ -797,7 +797,7 @@ def global_search():
|
||||
try:
|
||||
# Suche in Lizenzen
|
||||
cur.execute("""
|
||||
SELECT id, license_key, customer_name, active
|
||||
SELECT id, license_key, customer_name, is_active
|
||||
FROM licenses
|
||||
WHERE license_key ILIKE %s
|
||||
OR customer_name ILIKE %s
|
||||
@@ -810,7 +810,7 @@ def global_search():
|
||||
'id': row[0],
|
||||
'license_key': row[1],
|
||||
'customer_name': row[2],
|
||||
'active': row[3]
|
||||
'is_active': row[3]
|
||||
})
|
||||
|
||||
# Suche in Kunden
|
||||
|
||||
@@ -1,943 +0,0 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from zoneinfo import ZoneInfo
|
||||
from flask import Blueprint, request, jsonify, session
|
||||
|
||||
import config
|
||||
from auth.decorators import login_required
|
||||
from utils.audit import log_audit
|
||||
from utils.network import get_client_ip
|
||||
from utils.license import generate_license_key
|
||||
from db import get_connection, get_db_connection, get_db_cursor
|
||||
from models import get_license_by_id, get_customers
|
||||
|
||||
# Create Blueprint
|
||||
api_bp = Blueprint('api', __name__, url_prefix='/api')
|
||||
|
||||
|
||||
@api_bp.route("/customers", methods=["GET"])
|
||||
@login_required
|
||||
def api_customers():
|
||||
"""API endpoint for customer search (used by Select2)"""
|
||||
search = request.args.get('q', '').strip()
|
||||
page = int(request.args.get('page', 1))
|
||||
per_page = 20
|
||||
|
||||
try:
|
||||
# Get all customers (with optional search)
|
||||
customers = get_customers(show_test=True, search=search)
|
||||
|
||||
# Pagination
|
||||
start = (page - 1) * per_page
|
||||
end = start + per_page
|
||||
paginated_customers = customers[start:end]
|
||||
|
||||
# Format for Select2
|
||||
results = []
|
||||
for customer in paginated_customers:
|
||||
results.append({
|
||||
'id': customer['id'],
|
||||
'text': f"{customer['name']} ({customer['email'] or 'keine E-Mail'})"
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'results': results,
|
||||
'pagination': {
|
||||
'more': len(customers) > end
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error in api_customers: {str(e)}")
|
||||
return jsonify({'error': 'Fehler beim Laden der Kunden'}), 500
|
||||
|
||||
|
||||
@api_bp.route("/license/<int:license_id>/toggle", methods=["POST"])
|
||||
@login_required
|
||||
def toggle_license(license_id):
|
||||
"""Toggle license active status"""
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Get current status
|
||||
license_data = get_license_by_id(license_id)
|
||||
if not license_data:
|
||||
return jsonify({'error': 'Lizenz nicht gefunden'}), 404
|
||||
|
||||
new_status = not license_data['active']
|
||||
|
||||
# Update status
|
||||
cur.execute("UPDATE licenses SET active = %s WHERE id = %s", (new_status, license_id))
|
||||
conn.commit()
|
||||
|
||||
# Log change
|
||||
log_audit('TOGGLE', 'license', license_id,
|
||||
old_values={'active': license_data['active']},
|
||||
new_values={'active': new_status})
|
||||
|
||||
return jsonify({'success': True, 'active': new_status})
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
logging.error(f"Fehler beim Umschalten der Lizenz: {str(e)}")
|
||||
return jsonify({'error': 'Fehler beim Umschalten der Lizenz'}), 500
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@api_bp.route("/licenses/bulk-activate", methods=["POST"])
|
||||
@login_required
|
||||
def bulk_activate_licenses():
|
||||
"""Aktiviere mehrere Lizenzen gleichzeitig"""
|
||||
data = request.get_json()
|
||||
license_ids = data.get('license_ids', [])
|
||||
|
||||
if not license_ids:
|
||||
return jsonify({'error': 'Keine Lizenzen ausgewählt'}), 400
|
||||
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Update all selected licenses
|
||||
cur.execute("""
|
||||
UPDATE licenses
|
||||
SET active = true
|
||||
WHERE id = ANY(%s) AND active = false
|
||||
RETURNING id
|
||||
""", (license_ids,))
|
||||
|
||||
updated_ids = [row[0] for row in cur.fetchall()]
|
||||
conn.commit()
|
||||
|
||||
# Log changes
|
||||
for license_id in updated_ids:
|
||||
log_audit('BULK_ACTIVATE', 'license', license_id,
|
||||
new_values={'active': True})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'updated_count': len(updated_ids)
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
logging.error(f"Fehler beim Bulk-Aktivieren: {str(e)}")
|
||||
return jsonify({'error': 'Fehler beim Aktivieren der Lizenzen'}), 500
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@api_bp.route("/licenses/bulk-deactivate", methods=["POST"])
|
||||
@login_required
|
||||
def bulk_deactivate_licenses():
|
||||
"""Deaktiviere mehrere Lizenzen gleichzeitig"""
|
||||
data = request.get_json()
|
||||
license_ids = data.get('license_ids', [])
|
||||
|
||||
if not license_ids:
|
||||
return jsonify({'error': 'Keine Lizenzen ausgewählt'}), 400
|
||||
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Update all selected licenses
|
||||
cur.execute("""
|
||||
UPDATE licenses
|
||||
SET active = false
|
||||
WHERE id = ANY(%s) AND active = true
|
||||
RETURNING id
|
||||
""", (license_ids,))
|
||||
|
||||
updated_ids = [row[0] for row in cur.fetchall()]
|
||||
conn.commit()
|
||||
|
||||
# Log changes
|
||||
for license_id in updated_ids:
|
||||
log_audit('BULK_DEACTIVATE', 'license', license_id,
|
||||
new_values={'active': False})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'updated_count': len(updated_ids)
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
logging.error(f"Fehler beim Bulk-Deaktivieren: {str(e)}")
|
||||
return jsonify({'error': 'Fehler beim Deaktivieren der Lizenzen'}), 500
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@api_bp.route("/license/<int:license_id>/devices")
|
||||
@login_required
|
||||
def get_license_devices(license_id):
|
||||
"""Hole alle Geräte einer Lizenz"""
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Hole Lizenz-Info
|
||||
license_data = get_license_by_id(license_id)
|
||||
if not license_data:
|
||||
return jsonify({'error': 'Lizenz nicht gefunden'}), 404
|
||||
|
||||
# Hole registrierte Geräte
|
||||
cur.execute("""
|
||||
SELECT
|
||||
dr.id,
|
||||
dr.device_id,
|
||||
dr.device_name,
|
||||
dr.device_type,
|
||||
dr.registration_date,
|
||||
dr.last_seen,
|
||||
dr.is_active,
|
||||
(SELECT COUNT(*) FROM sessions s
|
||||
WHERE s.license_key = dr.license_key
|
||||
AND s.device_id = dr.device_id
|
||||
AND s.active = true) as active_sessions
|
||||
FROM device_registrations dr
|
||||
WHERE dr.license_key = %s
|
||||
ORDER BY dr.registration_date DESC
|
||||
""", (license_data['license_key'],))
|
||||
|
||||
devices = []
|
||||
for row in cur.fetchall():
|
||||
devices.append({
|
||||
'id': row[0],
|
||||
'device_id': row[1],
|
||||
'device_name': row[2],
|
||||
'device_type': row[3],
|
||||
'registration_date': row[4].isoformat() if row[4] else None,
|
||||
'last_seen': row[5].isoformat() if row[5] else None,
|
||||
'is_active': row[6],
|
||||
'active_sessions': row[7]
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'license_key': license_data['license_key'],
|
||||
'device_limit': license_data['device_limit'],
|
||||
'devices': devices,
|
||||
'device_count': len(devices)
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Fehler beim Abrufen der Geräte: {str(e)}")
|
||||
return jsonify({'error': 'Fehler beim Abrufen der Geräte'}), 500
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@api_bp.route("/license/<int:license_id>/register-device", methods=["POST"])
|
||||
@login_required
|
||||
def register_device(license_id):
|
||||
"""Registriere ein neues Gerät für eine Lizenz"""
|
||||
data = request.get_json()
|
||||
|
||||
device_id = data.get('device_id')
|
||||
device_name = data.get('device_name')
|
||||
device_type = data.get('device_type', 'unknown')
|
||||
|
||||
if not device_id or not device_name:
|
||||
return jsonify({'error': 'Geräte-ID und Name erforderlich'}), 400
|
||||
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Hole Lizenz-Info
|
||||
license_data = get_license_by_id(license_id)
|
||||
if not license_data:
|
||||
return jsonify({'error': 'Lizenz nicht gefunden'}), 404
|
||||
|
||||
# Prüfe Gerätelimit
|
||||
cur.execute("""
|
||||
SELECT COUNT(*) FROM device_registrations
|
||||
WHERE license_key = %s AND is_active = true
|
||||
""", (license_data['license_key'],))
|
||||
|
||||
active_device_count = cur.fetchone()[0]
|
||||
|
||||
if active_device_count >= license_data['device_limit']:
|
||||
return jsonify({'error': 'Gerätelimit erreicht'}), 400
|
||||
|
||||
# Prüfe ob Gerät bereits registriert
|
||||
cur.execute("""
|
||||
SELECT id, is_active FROM device_registrations
|
||||
WHERE license_key = %s AND device_id = %s
|
||||
""", (license_data['license_key'], device_id))
|
||||
|
||||
existing = cur.fetchone()
|
||||
|
||||
if existing:
|
||||
if existing[1]: # is_active
|
||||
return jsonify({'error': 'Gerät bereits registriert'}), 400
|
||||
else:
|
||||
# Reaktiviere Gerät
|
||||
cur.execute("""
|
||||
UPDATE device_registrations
|
||||
SET is_active = true, last_seen = CURRENT_TIMESTAMP
|
||||
WHERE id = %s
|
||||
""", (existing[0],))
|
||||
else:
|
||||
# Registriere neues Gerät
|
||||
cur.execute("""
|
||||
INSERT INTO device_registrations
|
||||
(license_key, device_id, device_name, device_type, is_active)
|
||||
VALUES (%s, %s, %s, %s, true)
|
||||
""", (license_data['license_key'], device_id, device_name, device_type))
|
||||
|
||||
conn.commit()
|
||||
|
||||
# Audit-Log
|
||||
log_audit('DEVICE_REGISTER', 'license', license_id,
|
||||
additional_info=f"Gerät {device_name} ({device_id}) registriert")
|
||||
|
||||
return jsonify({'success': True})
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
logging.error(f"Fehler beim Registrieren des Geräts: {str(e)}")
|
||||
return jsonify({'error': 'Fehler beim Registrieren des Geräts'}), 500
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@api_bp.route("/license/<int:license_id>/deactivate-device/<int:device_id>", methods=["POST"])
|
||||
@login_required
|
||||
def deactivate_device(license_id, device_id):
|
||||
"""Deaktiviere ein Gerät einer Lizenz"""
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Prüfe ob Gerät zur Lizenz gehört
|
||||
cur.execute("""
|
||||
SELECT dr.device_name, dr.device_id, l.license_key
|
||||
FROM device_registrations dr
|
||||
JOIN licenses l ON dr.license_key = l.license_key
|
||||
WHERE dr.id = %s AND l.id = %s
|
||||
""", (device_id, license_id))
|
||||
|
||||
device = cur.fetchone()
|
||||
if not device:
|
||||
return jsonify({'error': 'Gerät nicht gefunden'}), 404
|
||||
|
||||
# Deaktiviere Gerät
|
||||
cur.execute("""
|
||||
UPDATE device_registrations
|
||||
SET is_active = false
|
||||
WHERE id = %s
|
||||
""", (device_id,))
|
||||
|
||||
# Beende aktive Sessions
|
||||
cur.execute("""
|
||||
UPDATE sessions
|
||||
SET active = false, logout_time = CURRENT_TIMESTAMP
|
||||
WHERE license_key = %s AND device_id = %s AND active = true
|
||||
""", (device[2], device[1]))
|
||||
|
||||
conn.commit()
|
||||
|
||||
# Audit-Log
|
||||
log_audit('DEVICE_DEACTIVATE', 'license', license_id,
|
||||
additional_info=f"Gerät {device[0]} ({device[1]}) deaktiviert")
|
||||
|
||||
return jsonify({'success': True})
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
logging.error(f"Fehler beim Deaktivieren des Geräts: {str(e)}")
|
||||
return jsonify({'error': 'Fehler beim Deaktivieren des Geräts'}), 500
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@api_bp.route("/licenses/bulk-delete", methods=["POST"])
|
||||
@login_required
|
||||
def bulk_delete_licenses():
|
||||
"""Lösche mehrere Lizenzen gleichzeitig"""
|
||||
data = request.get_json()
|
||||
license_ids = data.get('license_ids', [])
|
||||
|
||||
if not license_ids:
|
||||
return jsonify({'error': 'Keine Lizenzen ausgewählt'}), 400
|
||||
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
deleted_count = 0
|
||||
|
||||
for license_id in license_ids:
|
||||
# Hole Lizenz-Info für Audit
|
||||
cur.execute("SELECT license_key FROM licenses WHERE id = %s", (license_id,))
|
||||
result = cur.fetchone()
|
||||
|
||||
if result:
|
||||
license_key = result[0]
|
||||
|
||||
# Lösche Sessions
|
||||
cur.execute("DELETE FROM sessions WHERE license_key = %s", (license_key,))
|
||||
|
||||
# Lösche Geräte-Registrierungen
|
||||
cur.execute("DELETE FROM device_registrations WHERE license_key = %s", (license_key,))
|
||||
|
||||
# Lösche Lizenz
|
||||
cur.execute("DELETE FROM licenses WHERE id = %s", (license_id,))
|
||||
|
||||
# Audit-Log
|
||||
log_audit('BULK_DELETE', 'license', license_id,
|
||||
old_values={'license_key': license_key})
|
||||
|
||||
deleted_count += 1
|
||||
|
||||
conn.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'deleted_count': deleted_count
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
logging.error(f"Fehler beim Bulk-Löschen: {str(e)}")
|
||||
return jsonify({'error': 'Fehler beim Löschen der Lizenzen'}), 500
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@api_bp.route("/license/<int:license_id>/quick-edit", methods=['POST'])
|
||||
@login_required
|
||||
def quick_edit_license(license_id):
|
||||
"""Schnellbearbeitung einer Lizenz"""
|
||||
data = request.get_json()
|
||||
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Hole aktuelle Lizenz für Vergleich
|
||||
current_license = get_license_by_id(license_id)
|
||||
if not current_license:
|
||||
return jsonify({'error': 'Lizenz nicht gefunden'}), 404
|
||||
|
||||
# Update nur die übergebenen Felder
|
||||
updates = []
|
||||
params = []
|
||||
old_values = {}
|
||||
new_values = {}
|
||||
|
||||
if 'device_limit' in data:
|
||||
updates.append("device_limit = %s")
|
||||
params.append(int(data['device_limit']))
|
||||
old_values['device_limit'] = current_license['device_limit']
|
||||
new_values['device_limit'] = int(data['device_limit'])
|
||||
|
||||
if 'valid_until' in data:
|
||||
updates.append("valid_until = %s")
|
||||
params.append(data['valid_until'])
|
||||
old_values['valid_until'] = str(current_license['valid_until'])
|
||||
new_values['valid_until'] = data['valid_until']
|
||||
|
||||
if 'active' in data:
|
||||
updates.append("active = %s")
|
||||
params.append(bool(data['active']))
|
||||
old_values['active'] = current_license['active']
|
||||
new_values['active'] = bool(data['active'])
|
||||
|
||||
if not updates:
|
||||
return jsonify({'error': 'Keine Änderungen angegeben'}), 400
|
||||
|
||||
# Führe Update aus
|
||||
params.append(license_id)
|
||||
cur.execute(f"""
|
||||
UPDATE licenses
|
||||
SET {', '.join(updates)}
|
||||
WHERE id = %s
|
||||
""", params)
|
||||
|
||||
conn.commit()
|
||||
|
||||
# Audit-Log
|
||||
log_audit('QUICK_EDIT', 'license', license_id,
|
||||
old_values=old_values,
|
||||
new_values=new_values)
|
||||
|
||||
return jsonify({'success': True})
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
logging.error(f"Fehler bei Schnellbearbeitung: {str(e)}")
|
||||
return jsonify({'error': 'Fehler bei der Bearbeitung'}), 500
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@api_bp.route("/license/<int:license_id>/resources")
|
||||
@login_required
|
||||
def get_license_resources(license_id):
|
||||
"""Hole alle Ressourcen einer Lizenz"""
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Hole Lizenz-Info
|
||||
license_data = get_license_by_id(license_id)
|
||||
if not license_data:
|
||||
return jsonify({'error': 'Lizenz nicht gefunden'}), 404
|
||||
|
||||
# Hole zugewiesene Ressourcen
|
||||
cur.execute("""
|
||||
SELECT
|
||||
rp.id,
|
||||
rp.resource_type,
|
||||
rp.resource_value,
|
||||
rp.is_test,
|
||||
rp.status_changed_at,
|
||||
lr.assigned_at,
|
||||
lr.assigned_by
|
||||
FROM resource_pools rp
|
||||
JOIN license_resources lr ON rp.id = lr.resource_id
|
||||
WHERE lr.license_id = %s
|
||||
ORDER BY rp.resource_type, rp.resource_value
|
||||
""", (license_id,))
|
||||
|
||||
resources = []
|
||||
for row in cur.fetchall():
|
||||
resources.append({
|
||||
'id': row[0],
|
||||
'type': row[1],
|
||||
'value': row[2],
|
||||
'is_test': row[3],
|
||||
'status_changed_at': row[4].isoformat() if row[4] else None,
|
||||
'assigned_at': row[5].isoformat() if row[5] else None,
|
||||
'assigned_by': row[6]
|
||||
})
|
||||
|
||||
# Gruppiere nach Typ
|
||||
grouped = {}
|
||||
for resource in resources:
|
||||
res_type = resource['type']
|
||||
if res_type not in grouped:
|
||||
grouped[res_type] = []
|
||||
grouped[res_type].append(resource)
|
||||
|
||||
return jsonify({
|
||||
'license_key': license_data['license_key'],
|
||||
'resources': resources,
|
||||
'grouped': grouped,
|
||||
'total_count': len(resources)
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Fehler beim Abrufen der Ressourcen: {str(e)}")
|
||||
return jsonify({'error': 'Fehler beim Abrufen der Ressourcen'}), 500
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@api_bp.route("/resources/allocate", methods=['POST'])
|
||||
@login_required
|
||||
def allocate_resources():
|
||||
"""Weise Ressourcen einer Lizenz zu"""
|
||||
data = request.get_json()
|
||||
|
||||
license_id = data.get('license_id')
|
||||
resource_ids = data.get('resource_ids', [])
|
||||
|
||||
if not license_id or not resource_ids:
|
||||
return jsonify({'error': 'Lizenz-ID und Ressourcen erforderlich'}), 400
|
||||
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Prüfe Lizenz
|
||||
license_data = get_license_by_id(license_id)
|
||||
if not license_data:
|
||||
return jsonify({'error': 'Lizenz nicht gefunden'}), 404
|
||||
|
||||
allocated_count = 0
|
||||
errors = []
|
||||
|
||||
for resource_id in resource_ids:
|
||||
try:
|
||||
# Prüfe ob Ressource verfügbar ist
|
||||
cur.execute("""
|
||||
SELECT resource_value, status, is_test
|
||||
FROM resource_pools
|
||||
WHERE id = %s
|
||||
""", (resource_id,))
|
||||
|
||||
resource = cur.fetchone()
|
||||
if not resource:
|
||||
errors.append(f"Ressource {resource_id} nicht gefunden")
|
||||
continue
|
||||
|
||||
if resource[1] != 'available':
|
||||
errors.append(f"Ressource {resource[0]} ist nicht verfügbar")
|
||||
continue
|
||||
|
||||
# Prüfe Test/Produktion Kompatibilität
|
||||
if resource[2] != license_data['is_test']:
|
||||
errors.append(f"Ressource {resource[0]} ist {'Test' if resource[2] else 'Produktion'}, Lizenz ist {'Test' if license_data['is_test'] else 'Produktion'}")
|
||||
continue
|
||||
|
||||
# Weise Ressource zu
|
||||
cur.execute("""
|
||||
UPDATE resource_pools
|
||||
SET status = 'allocated',
|
||||
allocated_to_license = %s,
|
||||
status_changed_at = CURRENT_TIMESTAMP,
|
||||
status_changed_by = %s
|
||||
WHERE id = %s
|
||||
""", (license_id, session['username'], resource_id))
|
||||
|
||||
# Erstelle Verknüpfung
|
||||
cur.execute("""
|
||||
INSERT INTO license_resources (license_id, resource_id, assigned_by)
|
||||
VALUES (%s, %s, %s)
|
||||
""", (license_id, resource_id, session['username']))
|
||||
|
||||
# History-Eintrag
|
||||
cur.execute("""
|
||||
INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address)
|
||||
VALUES (%s, %s, 'allocated', %s, %s)
|
||||
""", (resource_id, license_id, session['username'], get_client_ip()))
|
||||
|
||||
allocated_count += 1
|
||||
|
||||
except Exception as e:
|
||||
errors.append(f"Fehler bei Ressource {resource_id}: {str(e)}")
|
||||
|
||||
conn.commit()
|
||||
|
||||
# Audit-Log
|
||||
if allocated_count > 0:
|
||||
log_audit('RESOURCE_ALLOCATE', 'license', license_id,
|
||||
additional_info=f"{allocated_count} Ressourcen zugewiesen")
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'allocated_count': allocated_count,
|
||||
'errors': errors
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
logging.error(f"Fehler beim Zuweisen der Ressourcen: {str(e)}")
|
||||
return jsonify({'error': 'Fehler beim Zuweisen der Ressourcen'}), 500
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@api_bp.route("/resources/check-availability", methods=['GET'])
|
||||
@login_required
|
||||
def check_resource_availability():
|
||||
"""Prüfe Verfügbarkeit von Ressourcen"""
|
||||
resource_type = request.args.get('type')
|
||||
count = int(request.args.get('count', 1))
|
||||
is_test = request.args.get('is_test', 'false') == 'true'
|
||||
|
||||
if not resource_type:
|
||||
return jsonify({'error': 'Ressourcen-Typ erforderlich'}), 400
|
||||
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Zähle verfügbare Ressourcen
|
||||
cur.execute("""
|
||||
SELECT COUNT(*)
|
||||
FROM resource_pools
|
||||
WHERE resource_type = %s
|
||||
AND status = 'available'
|
||||
AND is_test = %s
|
||||
""", (resource_type, is_test))
|
||||
|
||||
available_count = cur.fetchone()[0]
|
||||
|
||||
return jsonify({
|
||||
'resource_type': resource_type,
|
||||
'requested': count,
|
||||
'available': available_count,
|
||||
'sufficient': available_count >= count,
|
||||
'is_test': is_test
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Fehler beim Prüfen der Verfügbarkeit: {str(e)}")
|
||||
return jsonify({'error': 'Fehler beim Prüfen der Verfügbarkeit'}), 500
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@api_bp.route("/global-search", methods=['GET'])
|
||||
@login_required
|
||||
def global_search():
|
||||
"""Globale Suche über alle Entitäten"""
|
||||
query = request.args.get('q', '').strip()
|
||||
|
||||
if not query or len(query) < 3:
|
||||
return jsonify({'error': 'Suchbegriff muss mindestens 3 Zeichen haben'}), 400
|
||||
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
results = {
|
||||
'licenses': [],
|
||||
'customers': [],
|
||||
'resources': [],
|
||||
'sessions': []
|
||||
}
|
||||
|
||||
try:
|
||||
# Suche in Lizenzen
|
||||
cur.execute("""
|
||||
SELECT id, license_key, customer_name, active
|
||||
FROM licenses
|
||||
WHERE license_key ILIKE %s
|
||||
OR customer_name ILIKE %s
|
||||
OR customer_email ILIKE %s
|
||||
LIMIT 10
|
||||
""", (f'%{query}%', f'%{query}%', f'%{query}%'))
|
||||
|
||||
for row in cur.fetchall():
|
||||
results['licenses'].append({
|
||||
'id': row[0],
|
||||
'license_key': row[1],
|
||||
'customer_name': row[2],
|
||||
'active': row[3]
|
||||
})
|
||||
|
||||
# Suche in Kunden
|
||||
cur.execute("""
|
||||
SELECT id, name, email
|
||||
FROM customers
|
||||
WHERE name ILIKE %s OR email ILIKE %s
|
||||
LIMIT 10
|
||||
""", (f'%{query}%', f'%{query}%'))
|
||||
|
||||
for row in cur.fetchall():
|
||||
results['customers'].append({
|
||||
'id': row[0],
|
||||
'name': row[1],
|
||||
'email': row[2]
|
||||
})
|
||||
|
||||
# Suche in Ressourcen
|
||||
cur.execute("""
|
||||
SELECT id, resource_type, resource_value, status
|
||||
FROM resource_pools
|
||||
WHERE resource_value ILIKE %s
|
||||
LIMIT 10
|
||||
""", (f'%{query}%',))
|
||||
|
||||
for row in cur.fetchall():
|
||||
results['resources'].append({
|
||||
'id': row[0],
|
||||
'type': row[1],
|
||||
'value': row[2],
|
||||
'status': row[3]
|
||||
})
|
||||
|
||||
# Suche in Sessions
|
||||
cur.execute("""
|
||||
SELECT id, license_key, username, device_id, active
|
||||
FROM sessions
|
||||
WHERE username ILIKE %s OR device_id ILIKE %s
|
||||
ORDER BY login_time DESC
|
||||
LIMIT 10
|
||||
""", (f'%{query}%', f'%{query}%'))
|
||||
|
||||
for row in cur.fetchall():
|
||||
results['sessions'].append({
|
||||
'id': row[0],
|
||||
'license_key': row[1],
|
||||
'username': row[2],
|
||||
'device_id': row[3],
|
||||
'active': row[4]
|
||||
})
|
||||
|
||||
return jsonify(results)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Fehler bei der globalen Suche: {str(e)}")
|
||||
return jsonify({'error': 'Fehler bei der Suche'}), 500
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@api_bp.route("/generate-license-key", methods=['POST'])
|
||||
@login_required
|
||||
def api_generate_key():
|
||||
"""API Endpoint zur Generierung eines neuen Lizenzschlüssels"""
|
||||
try:
|
||||
# Lizenztyp aus Request holen (default: full)
|
||||
data = request.get_json() or {}
|
||||
license_type = data.get('type', 'full')
|
||||
|
||||
# Key generieren
|
||||
key = generate_license_key(license_type)
|
||||
|
||||
# Prüfen ob Key bereits existiert (sehr unwahrscheinlich aber sicher ist sicher)
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
# Wiederhole bis eindeutiger Key gefunden
|
||||
attempts = 0
|
||||
while attempts < 10: # Max 10 Versuche
|
||||
cur.execute("SELECT 1 FROM licenses WHERE license_key = %s", (key,))
|
||||
if not cur.fetchone():
|
||||
break # Key ist eindeutig
|
||||
key = generate_license_key(license_type)
|
||||
attempts += 1
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
# Log für Audit
|
||||
log_audit('GENERATE_KEY', 'license',
|
||||
additional_info={'type': license_type, 'key': key})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'key': key,
|
||||
'type': license_type
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Fehler bei Key-Generierung: {str(e)}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Fehler bei der Key-Generierung'
|
||||
}), 500
|
||||
|
||||
|
||||
@api_bp.route("/customers", methods=['GET'])
|
||||
@login_required
|
||||
def api_customers():
|
||||
"""API Endpoint für die Kundensuche mit Select2"""
|
||||
try:
|
||||
# Suchparameter
|
||||
search = request.args.get('q', '').strip()
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = 20
|
||||
customer_id = request.args.get('id', type=int)
|
||||
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
# Einzelnen Kunden per ID abrufen
|
||||
if customer_id:
|
||||
cur.execute("""
|
||||
SELECT c.id, c.name, c.email,
|
||||
COUNT(l.id) as license_count
|
||||
FROM customers c
|
||||
LEFT JOIN licenses l ON c.id = l.customer_id
|
||||
WHERE c.id = %s
|
||||
GROUP BY c.id, c.name, c.email
|
||||
""", (customer_id,))
|
||||
|
||||
customer = cur.fetchone()
|
||||
results = []
|
||||
if customer:
|
||||
results.append({
|
||||
'id': customer[0],
|
||||
'text': f"{customer[1]} ({customer[2]})",
|
||||
'name': customer[1],
|
||||
'email': customer[2],
|
||||
'license_count': customer[3]
|
||||
})
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
return jsonify({
|
||||
'results': results,
|
||||
'pagination': {'more': False}
|
||||
})
|
||||
|
||||
# SQL Query mit optionaler Suche
|
||||
elif search:
|
||||
cur.execute("""
|
||||
SELECT c.id, c.name, c.email,
|
||||
COUNT(l.id) as license_count
|
||||
FROM customers c
|
||||
LEFT JOIN licenses l ON c.id = l.customer_id
|
||||
WHERE LOWER(c.name) LIKE LOWER(%s)
|
||||
OR LOWER(c.email) LIKE LOWER(%s)
|
||||
GROUP BY c.id, c.name, c.email
|
||||
ORDER BY c.name
|
||||
LIMIT %s OFFSET %s
|
||||
""", (f'%{search}%', f'%{search}%', per_page, (page - 1) * per_page))
|
||||
else:
|
||||
cur.execute("""
|
||||
SELECT c.id, c.name, c.email,
|
||||
COUNT(l.id) as license_count
|
||||
FROM customers c
|
||||
LEFT JOIN licenses l ON c.id = l.customer_id
|
||||
GROUP BY c.id, c.name, c.email
|
||||
ORDER BY c.name
|
||||
LIMIT %s OFFSET %s
|
||||
""", (per_page, (page - 1) * per_page))
|
||||
|
||||
customers = cur.fetchall()
|
||||
|
||||
# Format für Select2
|
||||
results = []
|
||||
for customer in customers:
|
||||
results.append({
|
||||
'id': customer[0],
|
||||
'text': f"{customer[1]} - {customer[2]} ({customer[3]} Lizenzen)",
|
||||
'name': customer[1],
|
||||
'email': customer[2],
|
||||
'license_count': customer[3]
|
||||
})
|
||||
|
||||
# Gesamtanzahl für Pagination
|
||||
if search:
|
||||
cur.execute("""
|
||||
SELECT COUNT(*) FROM customers
|
||||
WHERE LOWER(name) LIKE LOWER(%s)
|
||||
OR LOWER(email) LIKE LOWER(%s)
|
||||
""", (f'%{search}%', f'%{search}%'))
|
||||
else:
|
||||
cur.execute("SELECT COUNT(*) FROM customers")
|
||||
|
||||
total_count = cur.fetchone()[0]
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
# Select2 Response Format
|
||||
return jsonify({
|
||||
'results': results,
|
||||
'pagination': {
|
||||
'more': (page * per_page) < total_count
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Fehler bei Kundensuche: {str(e)}")
|
||||
return jsonify({
|
||||
'results': [],
|
||||
'pagination': {'more': False},
|
||||
'error': str(e)
|
||||
}), 500
|
||||
@@ -24,12 +24,39 @@ def test_customers():
|
||||
def customers():
|
||||
show_test = request.args.get('show_test', 'false').lower() == 'true'
|
||||
search = request.args.get('search', '').strip()
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = 20
|
||||
sort = request.args.get('sort', 'name')
|
||||
order = request.args.get('order', 'asc')
|
||||
|
||||
customers_list = get_customers(show_test=show_test, search=search)
|
||||
|
||||
# Sortierung
|
||||
if sort == 'name':
|
||||
customers_list.sort(key=lambda x: x['name'].lower(), reverse=(order == 'desc'))
|
||||
elif sort == 'email':
|
||||
customers_list.sort(key=lambda x: x['email'].lower(), reverse=(order == 'desc'))
|
||||
elif sort == 'created_at':
|
||||
customers_list.sort(key=lambda x: x['created_at'], reverse=(order == 'desc'))
|
||||
|
||||
# Paginierung
|
||||
total_customers = len(customers_list)
|
||||
total_pages = (total_customers + per_page - 1) // per_page
|
||||
start = (page - 1) * per_page
|
||||
end = start + per_page
|
||||
paginated_customers = customers_list[start:end]
|
||||
|
||||
return render_template("customers.html",
|
||||
customers=customers_list,
|
||||
customers=paginated_customers,
|
||||
show_test=show_test,
|
||||
search=search)
|
||||
search=search,
|
||||
page=page,
|
||||
per_page=per_page,
|
||||
total_pages=total_pages,
|
||||
total_customers=total_customers,
|
||||
sort=sort,
|
||||
order=order,
|
||||
current_order=order)
|
||||
|
||||
|
||||
@customer_bp.route("/customer/edit/<int:customer_id>", methods=["GET", "POST"])
|
||||
|
||||
@@ -363,12 +363,18 @@
|
||||
<!-- Sidebar Navigation -->
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<ul class="sidebar-nav">
|
||||
<li class="nav-item {% if request.endpoint in ['customers.customers_licenses', 'customers.edit_customer', 'customers.create_customer', 'licenses.edit_license', 'licenses.create_license', 'batch.batch_create'] %}has-active-child{% endif %}">
|
||||
<li class="nav-item {% if request.endpoint in ['customers.customers', 'customers.customers_licenses', 'customers.edit_customer', 'customers.create_customer', 'licenses.edit_license', 'licenses.create_license', 'batch.batch_create'] %}has-active-child{% endif %}">
|
||||
<a class="nav-link has-submenu {% if request.endpoint == 'customers.customers_licenses' %}active{% endif %}" href="{{ url_for('customers.customers_licenses') }}">
|
||||
<i class="bi bi-people"></i>
|
||||
<span>Kunden & Lizenzen</span>
|
||||
</a>
|
||||
<ul class="sidebar-submenu">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'customers.customers' %}active{% endif %}" href="{{ url_for('customers.customers') }}">
|
||||
<i class="bi bi-list"></i>
|
||||
<span>Alle Kunden</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'customers.create_customer' %}active{% endif %}" href="{{ url_for('customers.create_customer') }}">
|
||||
<i class="bi bi-person-plus"></i>
|
||||
@@ -392,7 +398,7 @@
|
||||
<li class="nav-item {% if request.endpoint in ['resources.resources', 'resources.add_resources'] %}has-active-child{% endif %}">
|
||||
<a class="nav-link has-submenu {% if request.endpoint == 'resources.resources' %}active{% endif %}" href="{{ url_for('resources.resources') }}">
|
||||
<i class="bi bi-box-seam"></i>
|
||||
<span>Resource Pool</span>
|
||||
<span>Ressourcen Pool</span>
|
||||
</a>
|
||||
<ul class="sidebar-submenu">
|
||||
<li class="nav-item">
|
||||
|
||||
@@ -1,488 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Kunden & Lizenzen - AccountForger Admin{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-5">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2>Kunden & Lizenzen</h2>
|
||||
<div>
|
||||
<a href="/create" class="btn btn-success">
|
||||
<i class="bi bi-plus-circle"></i> Neue Lizenz
|
||||
</a>
|
||||
<a href="/batch" class="btn btn-primary">
|
||||
<i class="bi bi-stack"></i> Batch-Lizenzen
|
||||
</a>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-info dropdown-toggle" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-download"></i> Export
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="/export/licenses?format=excel"><i class="bi bi-file-earmark-excel"></i> Excel</a></li>
|
||||
<li><a class="dropdown-item" href="/export/licenses?format=csv"><i class="bi bi-file-earmark-csv"></i> CSV</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Kundenliste (Links) -->
|
||||
<div class="col-md-4 col-lg-3">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-people"></i> Kunden
|
||||
<span class="badge bg-secondary float-end">{{ customers|length if customers else 0 }}</span>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<!-- Suchfeld -->
|
||||
<div class="p-3 border-bottom">
|
||||
<input type="text" class="form-control mb-2" id="customerSearch"
|
||||
placeholder="Kunde suchen..." autocomplete="off">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="showTestCustomers"
|
||||
{% if request.args.get('show_test', 'false').lower() == 'true' %}checked{% endif %}
|
||||
onchange="toggleTestCustomers()">
|
||||
<label class="form-check-label" for="showTestCustomers">
|
||||
<small class="text-muted">Testkunden anzeigen</small>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kundenliste -->
|
||||
<div class="customer-list" style="max-height: 600px; overflow-y: auto;">
|
||||
{% if customers %}
|
||||
{% for customer in customers %}
|
||||
<div class="customer-item p-3 border-bottom {% if customer[0] == selected_customer_id %}active{% endif %}"
|
||||
data-customer-id="{{ customer[0] }}"
|
||||
data-customer-name="{{ customer[1]|lower }}"
|
||||
data-customer-email="{{ customer[2]|lower }}"
|
||||
onclick="loadCustomerLicenses({{ customer[0] }})"
|
||||
style="cursor: pointer;">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div class="flex-grow-1">
|
||||
<h6 class="mb-1">{{ customer[1] }}</h6>
|
||||
<small class="text-muted">{{ customer[2] }}</small>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<span class="badge bg-primary">{{ customer[4] }}</span>
|
||||
{% if customer[5] > 0 %}
|
||||
<span class="badge bg-success">{{ customer[5] }}</span>
|
||||
{% endif %}
|
||||
{% if customer[6] > 0 %}
|
||||
<span class="badge bg-danger">{{ customer[6] }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="p-4 text-center text-muted">
|
||||
<i class="bi bi-inbox" style="font-size: 3rem; opacity: 0.3;"></i>
|
||||
<p class="mt-3 mb-2">Keine Kunden vorhanden</p>
|
||||
<small class="d-block mb-3">Erstellen Sie eine neue Lizenz, um automatisch einen Kunden anzulegen.</small>
|
||||
<a href="/create" class="btn btn-sm btn-primary">
|
||||
<i class="bi bi-plus-circle"></i> Neue Lizenz erstellen
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lizenzdetails (Rechts) -->
|
||||
<div class="col-md-8 col-lg-9">
|
||||
<div class="card">
|
||||
<div class="card-header bg-light">
|
||||
{% if selected_customer %}
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h5 class="mb-0">{{ selected_customer[1] }}</h5>
|
||||
<small class="text-muted">{{ selected_customer[2] }}</small>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/customer/edit/{{ selected_customer[0] }}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-pencil"></i> Bearbeiten
|
||||
</a>
|
||||
<button class="btn btn-sm btn-success" onclick="showNewLicenseModal({{ selected_customer[0] }})">
|
||||
<i class="bi bi-plus"></i> Neue Lizenz
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<h5 class="mb-0">Wählen Sie einen Kunden aus</h5>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="licenseContainer">
|
||||
{% if selected_customer %}
|
||||
{% if licenses %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Lizenzschlüssel</th>
|
||||
<th>Typ</th>
|
||||
<th>Gültig von</th>
|
||||
<th>Gültig bis</th>
|
||||
<th>Status</th>
|
||||
<th>Ressourcen</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for license in licenses %}
|
||||
<tr>
|
||||
<td>
|
||||
<code>{{ license[1] }}</code>
|
||||
<button class="btn btn-sm btn-link" onclick="copyToClipboard('{{ license[1] }}')">
|
||||
<i class="bi bi-clipboard"></i>
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge {% if license[2] == 'full' %}bg-primary{% else %}bg-secondary{% endif %}">
|
||||
{{ license[2]|upper }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ license[3].strftime('%d.%m.%Y') if license[3] else '-' }}</td>
|
||||
<td>{{ license[4].strftime('%d.%m.%Y') if license[4] else '-' }}</td>
|
||||
<td>
|
||||
<span class="badge
|
||||
{% if license[6] == 'aktiv' %}bg-success
|
||||
{% elif license[6] == 'läuft bald ab' %}bg-warning
|
||||
{% elif license[6] == 'abgelaufen' %}bg-danger
|
||||
{% else %}bg-secondary{% endif %}">
|
||||
{{ license[6] }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if license[7] > 0 %}🌐 {{ license[7] }}{% endif %}
|
||||
{% if license[8] > 0 %}📡 {{ license[8] }}{% endif %}
|
||||
{% if license[9] > 0 %}📱 {{ license[9] }}{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-outline-primary" onclick="toggleLicenseStatus({{ license[0] }}, {{ license[5] }})">
|
||||
<i class="bi bi-power"></i>
|
||||
</button>
|
||||
<a href="/license/edit/{{ license[0] }}" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-inbox text-muted" style="font-size: 3rem;"></i>
|
||||
<p class="text-muted mt-3">Keine Lizenzen für diesen Kunden vorhanden</p>
|
||||
<button class="btn btn-success" onclick="showNewLicenseModal({{ selected_customer[0] }})">
|
||||
<i class="bi bi-plus"></i> Erste Lizenz erstellen
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-arrow-left text-muted" style="font-size: 3rem;"></i>
|
||||
<p class="text-muted mt-3">Wählen Sie einen Kunden aus der Liste aus</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal für neue Lizenz -->
|
||||
<div class="modal fade" id="newLicenseModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Neue Lizenz erstellen</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Möchten Sie eine neue Lizenz für <strong id="modalCustomerName"></strong> erstellen?</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||
<button type="button" class="btn btn-success" id="createLicenseBtn">Zur Lizenzerstellung</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.customer-item {
|
||||
transition: all 0.2s ease;
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
.customer-item:hover {
|
||||
background-color: #f8f9fa;
|
||||
border-left-color: #dee2e6;
|
||||
}
|
||||
.customer-item.active {
|
||||
background-color: #e7f3ff;
|
||||
border-left-color: #0d6efd;
|
||||
}
|
||||
.card {
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.075);
|
||||
}
|
||||
.table-hover tbody tr:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
</style>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
// Globale Variablen und Funktionen
|
||||
let currentCustomerId = {{ selected_customer_id or 'null' }};
|
||||
|
||||
// Lade Lizenzen eines Kunden
|
||||
function loadCustomerLicenses(customerId) {
|
||||
const searchTerm = e.target.value.toLowerCase();
|
||||
const customerItems = document.querySelectorAll('.customer-item');
|
||||
|
||||
customerItems.forEach(item => {
|
||||
const name = item.dataset.customerName;
|
||||
const email = item.dataset.customerEmail;
|
||||
if (name.includes(searchTerm) || email.includes(searchTerm)) {
|
||||
item.style.display = 'block';
|
||||
} else {
|
||||
item.style.display = 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
// Aktiven Status aktualisieren
|
||||
document.querySelectorAll('.customer-item').forEach(item => {
|
||||
item.classList.remove('active');
|
||||
});
|
||||
document.querySelector(`[data-customer-id="${customerId}"]`).classList.add('active');
|
||||
|
||||
// URL aktualisieren ohne Reload (behalte show_test Parameter)
|
||||
const currentUrl = new URL(window.location);
|
||||
currentUrl.searchParams.set('customer_id', customerId);
|
||||
window.history.pushState({}, '', currentUrl.toString());
|
||||
|
||||
// Lade Lizenzen via AJAX
|
||||
const container = document.getElementById('licenseContainer');
|
||||
const cardHeader = document.querySelector('.card-header.bg-light');
|
||||
container.innerHTML = '<div class="text-center py-5"><div class="spinner-border text-primary" role="status"></div></div>';
|
||||
|
||||
fetch(`/api/customer/${customerId}/licenses`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// Update header with customer info
|
||||
const customerItem = document.querySelector(`[data-customer-id="${customerId}"]`);
|
||||
const customerName = customerItem.querySelector('h6').textContent;
|
||||
const customerEmail = customerItem.querySelector('small').textContent;
|
||||
|
||||
cardHeader.innerHTML = `
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h5 class="mb-0">${customerName}</h5>
|
||||
<small class="text-muted">${customerEmail}</small>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/customer/edit/${customerId}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-pencil"></i> Bearbeiten
|
||||
</a>
|
||||
<button class="btn btn-sm btn-success" onclick="showNewLicenseModal(${customerId})">
|
||||
<i class="bi bi-plus"></i> Neue Lizenz
|
||||
</button>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
updateLicenseView(customerId, data.licenses);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
container.innerHTML = '<div class="alert alert-danger">Fehler beim Laden der Lizenzen</div>';
|
||||
});
|
||||
}
|
||||
|
||||
// Aktualisiere Lizenzansicht
|
||||
function updateLicenseView(customerId, licenses) {
|
||||
currentCustomerId = customerId;
|
||||
const container = document.getElementById('licenseContainer');
|
||||
|
||||
if (licenses.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-inbox text-muted" style="font-size: 3rem;"></i>
|
||||
<p class="text-muted mt-3">Keine Lizenzen für diesen Kunden vorhanden</p>
|
||||
<button class="btn btn-success" onclick="showNewLicenseModal(${customerId})">
|
||||
<i class="bi bi-plus"></i> Erste Lizenz erstellen
|
||||
</button>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
let html = `
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Lizenzschlüssel</th>
|
||||
<th>Typ</th>
|
||||
<th>Gültig von</th>
|
||||
<th>Gültig bis</th>
|
||||
<th>Status</th>
|
||||
<th>Ressourcen</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>`;
|
||||
|
||||
licenses.forEach(license => {
|
||||
const statusClass = license.status === 'aktiv' ? 'bg-success' :
|
||||
license.status === 'läuft bald ab' ? 'bg-warning' :
|
||||
license.status === 'abgelaufen' ? 'bg-danger' : 'bg-secondary';
|
||||
|
||||
const typeClass = license.license_type === 'full' ? 'bg-primary' : 'bg-secondary';
|
||||
|
||||
html += `
|
||||
<tr>
|
||||
<td>
|
||||
<code>${license.license_key}</code>
|
||||
<button class="btn btn-sm btn-link" onclick="copyToClipboard('${license.license_key}')">
|
||||
<i class="bi bi-clipboard"></i>
|
||||
</button>
|
||||
</td>
|
||||
<td><span class="badge ${typeClass}">${license.license_type.toUpperCase()}</span></td>
|
||||
<td>${license.valid_from || '-'}</td>
|
||||
<td>${license.valid_until || '-'}</td>
|
||||
<td><span class="badge ${statusClass}">${license.status}</span></td>
|
||||
<td>
|
||||
${license.domain_count > 0 ? '🌐 ' + license.domain_count : ''}
|
||||
${license.ipv4_count > 0 ? '📡 ' + license.ipv4_count : ''}
|
||||
${license.phone_count > 0 ? '📱 ' + license.phone_count : ''}
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-outline-primary" onclick="toggleLicenseStatus(${license.id}, ${license.is_active})">
|
||||
<i class="bi bi-power"></i>
|
||||
</button>
|
||||
<a href="/license/edit/${license.id}" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
html += '</tbody></table></div>';
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// Toggle Lizenzstatus
|
||||
function toggleLicenseStatus(licenseId, currentStatus) {
|
||||
const newStatus = !currentStatus;
|
||||
|
||||
fetch(`/api/license/${licenseId}/toggle`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ is_active: newStatus })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// Reload current customer licenses
|
||||
if (currentCustomerId) {
|
||||
loadCustomerLicenses(currentCustomerId);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => console.error('Error:', error));
|
||||
}
|
||||
|
||||
// Zeige Modal für neue Lizenz
|
||||
function showNewLicenseModal(customerId) {
|
||||
const customerItem = document.querySelector(`[data-customer-id="${customerId}"]`);
|
||||
if (!customerItem) {
|
||||
console.error('Kunde nicht gefunden:', customerId);
|
||||
return;
|
||||
}
|
||||
|
||||
const customerName = customerItem.querySelector('h6').textContent;
|
||||
|
||||
document.getElementById('modalCustomerName').textContent = customerName;
|
||||
document.getElementById('createLicenseBtn').onclick = function() {
|
||||
window.location.href = `/create?customer_id=${customerId}`;
|
||||
};
|
||||
|
||||
// Check if bootstrap is loaded
|
||||
if (typeof bootstrap === 'undefined') {
|
||||
console.error('Bootstrap nicht geladen!');
|
||||
// Fallback: Direkt zur Erstellung
|
||||
if (confirm(`Neue Lizenz für ${customerName} erstellen?`)) {
|
||||
window.location.href = `/create?customer_id=${customerId}`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const modalElement = document.getElementById('newLicenseModal');
|
||||
const modal = new bootstrap.Modal(modalElement);
|
||||
modal.show();
|
||||
}
|
||||
|
||||
// Copy to clipboard
|
||||
function copyToClipboard(text) {
|
||||
const button = event.currentTarget;
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
// Zeige kurz Feedback
|
||||
button.innerHTML = '<i class="bi bi-check"></i>';
|
||||
setTimeout(() => {
|
||||
button.innerHTML = '<i class="bi bi-clipboard"></i>';
|
||||
}, 1000);
|
||||
}).catch(err => {
|
||||
console.error('Fehler beim Kopieren:', err);
|
||||
alert('Konnte nicht in die Zwischenablage kopieren');
|
||||
});
|
||||
}
|
||||
|
||||
// Toggle Testkunden
|
||||
function toggleTestCustomers() {
|
||||
const showTest = document.getElementById('showTestCustomers').checked;
|
||||
const currentUrl = new URL(window.location);
|
||||
currentUrl.searchParams.set('show_test', showTest);
|
||||
window.location.href = currentUrl.toString();
|
||||
}
|
||||
|
||||
// Keyboard navigation
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.target.id === 'customerSearch') return; // Nicht bei Suche
|
||||
|
||||
const activeItem = document.querySelector('.customer-item.active');
|
||||
if (!activeItem) return;
|
||||
|
||||
let targetItem = null;
|
||||
|
||||
if (e.key === 'ArrowUp') {
|
||||
targetItem = activeItem.previousElementSibling;
|
||||
} else if (e.key === 'ArrowDown') {
|
||||
targetItem = activeItem.nextElementSibling;
|
||||
}
|
||||
|
||||
if (targetItem && targetItem.classList.contains('customer-item')) {
|
||||
e.preventDefault();
|
||||
const customerId = parseInt(targetItem.dataset.customerId);
|
||||
loadCustomerLicenses(customerId);
|
||||
}
|
||||
});
|
||||
|
||||
}); // Ende DOMContentLoaded
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,188 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test-Skript zur Verifizierung aller Blueprint-Routes
|
||||
Prüft ob alle Routes korrekt registriert sind und erreichbar sind
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from app import app
|
||||
from flask import url_for
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def test_all_routes():
|
||||
"""Test alle registrierten Routes"""
|
||||
|
||||
print("=== Blueprint Route Test ===\n")
|
||||
|
||||
# Sammle alle Routes
|
||||
routes_by_blueprint = {}
|
||||
|
||||
with app.test_request_context():
|
||||
for rule in app.url_map.iter_rules():
|
||||
# Skip static files
|
||||
if rule.endpoint == 'static':
|
||||
continue
|
||||
|
||||
# Blueprint name is before the dot
|
||||
parts = rule.endpoint.split('.')
|
||||
if len(parts) == 2:
|
||||
blueprint_name = parts[0]
|
||||
function_name = parts[1]
|
||||
else:
|
||||
blueprint_name = 'app'
|
||||
function_name = rule.endpoint
|
||||
|
||||
if blueprint_name not in routes_by_blueprint:
|
||||
routes_by_blueprint[blueprint_name] = []
|
||||
|
||||
routes_by_blueprint[blueprint_name].append({
|
||||
'rule': str(rule),
|
||||
'endpoint': rule.endpoint,
|
||||
'methods': sorted(rule.methods - {'HEAD', 'OPTIONS'}),
|
||||
'function': function_name
|
||||
})
|
||||
|
||||
# Sortiere und zeige Routes nach Blueprint
|
||||
for blueprint_name in sorted(routes_by_blueprint.keys()):
|
||||
routes = routes_by_blueprint[blueprint_name]
|
||||
print(f"\n📦 Blueprint: {blueprint_name}")
|
||||
print(f" Anzahl Routes: {len(routes)}")
|
||||
print(" " + "-" * 50)
|
||||
|
||||
for route in sorted(routes, key=lambda x: x['rule']):
|
||||
methods = ', '.join(route['methods'])
|
||||
print(f" {route['rule']:<40} [{methods:<15}] -> {route['function']}")
|
||||
|
||||
# Zusammenfassung
|
||||
print("\n=== Zusammenfassung ===")
|
||||
total_routes = sum(len(routes) for routes in routes_by_blueprint.values())
|
||||
print(f"Gesamt Blueprints: {len(routes_by_blueprint)}")
|
||||
print(f"Gesamt Routes: {total_routes}")
|
||||
|
||||
# Erwartete Blueprints prüfen
|
||||
expected_blueprints = ['auth', 'admin', 'license', 'customer', 'resource',
|
||||
'session', 'batch', 'api', 'export']
|
||||
|
||||
print("\n=== Blueprint Status ===")
|
||||
for bp in expected_blueprints:
|
||||
if bp in routes_by_blueprint:
|
||||
print(f"✅ {bp:<10} - {len(routes_by_blueprint[bp])} routes")
|
||||
else:
|
||||
print(f"❌ {bp:<10} - FEHLT!")
|
||||
|
||||
# Prüfe ob noch Routes direkt in app.py sind
|
||||
if 'app' in routes_by_blueprint:
|
||||
print(f"\n⚠️ WARNUNG: {len(routes_by_blueprint['app'])} Routes sind noch direkt in app.py!")
|
||||
for route in routes_by_blueprint['app']:
|
||||
print(f" - {route['rule']}")
|
||||
|
||||
def test_route_accessibility():
|
||||
"""Test ob wichtige Routes erreichbar sind"""
|
||||
print("\n\n=== Route Erreichbarkeits-Test ===\n")
|
||||
|
||||
test_client = app.test_client()
|
||||
|
||||
# Wichtige Routes zum Testen (ohne Login)
|
||||
public_routes = [
|
||||
('GET', '/login', 'Login-Seite'),
|
||||
('GET', '/heartbeat', 'Session Heartbeat'),
|
||||
]
|
||||
|
||||
for method, route, description in public_routes:
|
||||
try:
|
||||
if method == 'GET':
|
||||
response = test_client.get(route)
|
||||
elif method == 'POST':
|
||||
response = test_client.post(route)
|
||||
|
||||
status = "✅" if response.status_code in [200, 302, 401] else "❌"
|
||||
print(f"{status} {method:<6} {route:<30} - Status: {response.status_code} ({description})")
|
||||
|
||||
# Bei Fehler mehr Details
|
||||
if response.status_code >= 400 and response.status_code != 401:
|
||||
print(f" ⚠️ Fehler-Details: {response.data[:200]}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ {method:<6} {route:<30} - FEHLER: {str(e)}")
|
||||
|
||||
def check_duplicate_routes():
|
||||
"""Prüfe ob es doppelte Route-Definitionen gibt"""
|
||||
print("\n\n=== Doppelte Routes Check ===\n")
|
||||
|
||||
route_paths = {}
|
||||
duplicates_found = False
|
||||
|
||||
for rule in app.url_map.iter_rules():
|
||||
if rule.endpoint == 'static':
|
||||
continue
|
||||
|
||||
path = str(rule)
|
||||
if path in route_paths:
|
||||
print(f"⚠️ DUPLIKAT gefunden:")
|
||||
print(f" Route: {path}")
|
||||
print(f" 1. Endpoint: {route_paths[path]}")
|
||||
print(f" 2. Endpoint: {rule.endpoint}")
|
||||
duplicates_found = True
|
||||
else:
|
||||
route_paths[path] = rule.endpoint
|
||||
|
||||
if not duplicates_found:
|
||||
print("✅ Keine doppelten Routes gefunden!")
|
||||
|
||||
def check_template_references():
|
||||
"""Prüfe ob Template-Dateien für die Routes existieren"""
|
||||
print("\n\n=== Template Verfügbarkeits-Check ===\n")
|
||||
|
||||
template_dir = os.path.join(os.path.dirname(__file__), 'templates')
|
||||
|
||||
# Sammle alle verfügbaren Templates
|
||||
available_templates = []
|
||||
if os.path.exists(template_dir):
|
||||
for root, dirs, files in os.walk(template_dir):
|
||||
for file in files:
|
||||
if file.endswith(('.html', '.jinja2')):
|
||||
rel_path = os.path.relpath(os.path.join(root, file), template_dir)
|
||||
available_templates.append(rel_path.replace('\\', '/'))
|
||||
|
||||
print(f"Gefundene Templates: {len(available_templates)}")
|
||||
|
||||
# Wichtige Templates prüfen
|
||||
required_templates = [
|
||||
'login.html',
|
||||
'index.html',
|
||||
'profile.html',
|
||||
'licenses.html',
|
||||
'customers.html',
|
||||
'resources.html',
|
||||
'sessions.html',
|
||||
'audit.html',
|
||||
'backups.html'
|
||||
]
|
||||
|
||||
for template in required_templates:
|
||||
if template in available_templates:
|
||||
print(f"✅ {template}")
|
||||
else:
|
||||
print(f"❌ {template} - FEHLT!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🔍 Starte Blueprint-Verifizierung...\n")
|
||||
|
||||
try:
|
||||
test_all_routes()
|
||||
test_route_accessibility()
|
||||
check_duplicate_routes()
|
||||
check_template_references()
|
||||
|
||||
print("\n\n✅ Test abgeschlossen!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n\n❌ Fehler beim Test: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
@@ -1,21 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Test if blueprints can be imported successfully"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
try:
|
||||
from routes.auth_routes import auth_bp
|
||||
print("✓ auth_routes blueprint imported successfully")
|
||||
print(f" Routes: {[str(r) for r in auth_bp.url_values_defaults]}")
|
||||
except Exception as e:
|
||||
print(f"✗ Error importing auth_routes: {e}")
|
||||
|
||||
try:
|
||||
from routes.admin_routes import admin_bp
|
||||
print("✓ admin_routes blueprint imported successfully")
|
||||
except Exception as e:
|
||||
print(f"✗ Error importing admin_routes: {e}")
|
||||
|
||||
print("\nBlueprints are ready to use!")
|
||||
@@ -29,7 +29,7 @@ def log_audit(action, entity_type, entity_id=None, old_values=None, new_values=N
|
||||
ip_address, user_agent, additional_info)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
""", (username, action, entity_type, entity_id, old_json, new_json,
|
||||
ip_address, user_agent, additional_info))
|
||||
ip_address, user_agent, Json(additional_info) if isinstance(additional_info, dict) else additional_info))
|
||||
|
||||
conn.commit()
|
||||
except Exception as e:
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren