aktiv-inaktiv der Lizenzen ist gefixt

Dieser Commit ist enthalten in:
2025-06-18 19:09:36 +02:00
Ursprung eb9f86b918
Commit 4bfe1983a3
49 geänderte Dateien mit 58 neuen und 39933 gelöschten Zeilen

Datei anzeigen

@@ -70,7 +70,8 @@
"Bash(then echo \"- $template\")", "Bash(then echo \"- $template\")",
"Bash(fi)", "Bash(fi)",
"Bash(done)", "Bash(done)",
"Bash(docker compose:*)" "Bash(docker compose:*)",
"Bash(true)"
], ],
"deny": [] "deny": []
} }

Dateidiff unterdrückt, weil mindestens eine Zeile zu lang ist

Datei anzeigen

@@ -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.

Datei anzeigen

@@ -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')`

Datei anzeigen

@@ -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

Datei anzeigen

@@ -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

Datei anzeigen

@@ -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

Datei anzeigen

@@ -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)
```

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 anzeigen

@@ -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

Datei anzeigen

@@ -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()

Datei anzeigen

@@ -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()

Datei anzeigen

@@ -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

Datei anzeigen

@@ -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;

Datei anzeigen

@@ -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;

Datei anzeigen

@@ -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;

Datei anzeigen

@@ -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;

Datei anzeigen

@@ -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;

Datei anzeigen

@@ -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()

Datei anzeigen

@@ -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äre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Datei anzeigen

@@ -65,22 +65,22 @@ def toggle_license(license_id):
if not license_data: if not license_data:
return jsonify({'error': 'Lizenz nicht gefunden'}), 404 return jsonify({'error': 'Lizenz nicht gefunden'}), 404
new_status = not license_data['active'] new_status = not license_data['is_active']
# Update status # 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() conn.commit()
# Log change # Log change
log_audit('TOGGLE', 'license', license_id, log_audit('TOGGLE', 'license', license_id,
old_values={'active': license_data['active']}, old_values={'is_active': license_data['is_active']},
new_values={'active': new_status}) 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: except Exception as e:
conn.rollback() 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 return jsonify({'error': 'Fehler beim Umschalten der Lizenz'}), 500
finally: finally:
cur.close() cur.close()
@@ -104,8 +104,8 @@ def bulk_activate_licenses():
# Update all selected licenses # Update all selected licenses
cur.execute(""" cur.execute("""
UPDATE licenses UPDATE licenses
SET active = true SET is_active = true
WHERE id = ANY(%s) AND active = false WHERE id = ANY(%s) AND is_active = false
RETURNING id RETURNING id
""", (license_ids,)) """, (license_ids,))
@@ -115,7 +115,7 @@ def bulk_activate_licenses():
# Log changes # Log changes
for license_id in updated_ids: for license_id in updated_ids:
log_audit('BULK_ACTIVATE', 'license', license_id, log_audit('BULK_ACTIVATE', 'license', license_id,
new_values={'active': True}) new_values={'is_active': True})
return jsonify({ return jsonify({
'success': True, 'success': True,
@@ -148,8 +148,8 @@ def bulk_deactivate_licenses():
# Update all selected licenses # Update all selected licenses
cur.execute(""" cur.execute("""
UPDATE licenses UPDATE licenses
SET active = false SET is_active = false
WHERE id = ANY(%s) AND active = true WHERE id = ANY(%s) AND is_active = true
RETURNING id RETURNING id
""", (license_ids,)) """, (license_ids,))
@@ -159,7 +159,7 @@ def bulk_deactivate_licenses():
# Log changes # Log changes
for license_id in updated_ids: for license_id in updated_ids:
log_audit('BULK_DEACTIVATE', 'license', license_id, log_audit('BULK_DEACTIVATE', 'license', license_id,
new_values={'active': False}) new_values={'is_active': False})
return jsonify({ return jsonify({
'success': True, 'success': True,
@@ -451,10 +451,10 @@ def quick_edit_license(license_id):
new_values['valid_until'] = data['valid_until'] new_values['valid_until'] = data['valid_until']
if 'active' in data: if 'active' in data:
updates.append("active = %s") updates.append("is_active = %s")
params.append(bool(data['active'])) params.append(bool(data['active']))
old_values['active'] = current_license['active'] old_values['is_active'] = current_license['is_active']
new_values['active'] = bool(data['active']) new_values['is_active'] = bool(data['active'])
if not updates: if not updates:
return jsonify({'error': 'Keine Änderungen angegeben'}), 400 return jsonify({'error': 'Keine Änderungen angegeben'}), 400
@@ -797,7 +797,7 @@ def global_search():
try: try:
# Suche in Lizenzen # Suche in Lizenzen
cur.execute(""" cur.execute("""
SELECT id, license_key, customer_name, active SELECT id, license_key, customer_name, is_active
FROM licenses FROM licenses
WHERE license_key ILIKE %s WHERE license_key ILIKE %s
OR customer_name ILIKE %s OR customer_name ILIKE %s
@@ -810,7 +810,7 @@ def global_search():
'id': row[0], 'id': row[0],
'license_key': row[1], 'license_key': row[1],
'customer_name': row[2], 'customer_name': row[2],
'active': row[3] 'is_active': row[3]
}) })
# Suche in Kunden # Suche in Kunden

Datei anzeigen

@@ -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

Datei anzeigen

@@ -24,12 +24,39 @@ def test_customers():
def customers(): def customers():
show_test = request.args.get('show_test', 'false').lower() == 'true' show_test = request.args.get('show_test', 'false').lower() == 'true'
search = request.args.get('search', '').strip() 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) 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", return render_template("customers.html",
customers=customers_list, customers=paginated_customers,
show_test=show_test, 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"]) @customer_bp.route("/customer/edit/<int:customer_id>", methods=["GET", "POST"])

Datei anzeigen

@@ -363,12 +363,18 @@
<!-- Sidebar Navigation --> <!-- Sidebar Navigation -->
<aside class="sidebar" id="sidebar"> <aside class="sidebar" id="sidebar">
<ul class="sidebar-nav"> <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') }}"> <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> <i class="bi bi-people"></i>
<span>Kunden & Lizenzen</span> <span>Kunden & Lizenzen</span>
</a> </a>
<ul class="sidebar-submenu"> <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"> <li class="nav-item">
<a class="nav-link {% if request.endpoint == 'customers.create_customer' %}active{% endif %}" href="{{ url_for('customers.create_customer') }}"> <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> <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 %}"> <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') }}"> <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> <i class="bi bi-box-seam"></i>
<span>Resource Pool</span> <span>Ressourcen Pool</span>
</a> </a>
<ul class="sidebar-submenu"> <ul class="sidebar-submenu">
<li class="nav-item"> <li class="nav-item">

Datei anzeigen

@@ -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 %}

Datei anzeigen

@@ -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()

Datei anzeigen

@@ -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!")

Datei anzeigen

@@ -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) ip_address, user_agent, additional_info)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
""", (username, action, entity_type, entity_id, old_json, new_json, """, (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() conn.commit()
except Exception as e: except Exception as e: