diff --git a/v2_adminpanel/CRITICAL_FIELD_FIXES.md b/v2_adminpanel/CRITICAL_FIELD_FIXES.md deleted file mode 100644 index 2da1c40..0000000 --- a/v2_adminpanel/CRITICAL_FIELD_FIXES.md +++ /dev/null @@ -1,77 +0,0 @@ -# Critical Database Field Fixes Required - -## Summary -Found 62 field name inconsistencies across 6 Python files that need to be fixed to match the database schema. - -## Most Critical Issues - -### 1. **Sessions Table Field References** -The sessions table has duplicate columns that are causing confusion: - -| Database Column | Code References (Wrong) | Files Affected | -|----------------|------------------------|----------------| -| `hardware_id` | `device_id` | api_routes.py, session_routes.py, export_routes.py | -| `is_active` | `active` | api_routes.py, session_routes.py, export_routes.py, batch_routes.py | -| `started_at` | `login_time` | session_routes.py, export_routes.py | -| `last_heartbeat` | `last_activity` | session_routes.py, export_routes.py | -| `ended_at` | `logout_time` | api_routes.py, session_routes.py, export_routes.py | -| `started_at` | `start_time` | models.py | - -### 2. **Device Registration Issues** -- `device_registrations` table uses `device_id` column (line 324 in api_routes.py) -- But sessions table uses `hardware_id` -- This creates a mismatch when joining tables - -## Immediate Action Required - -### Option 1: Fix Code (Recommended) -Update all Python files to use the correct column names from the database schema. - -### Option 2: Add Compatibility Columns (Temporary) -```sql --- Add missing columns to sessions for backward compatibility -ALTER TABLE sessions ADD COLUMN IF NOT EXISTS device_id VARCHAR(100); -UPDATE sessions SET device_id = hardware_id WHERE device_id IS NULL; - --- Update device_registrations to use hardware_id -ALTER TABLE device_registrations RENAME COLUMN device_id TO hardware_id; -``` - -## Files That Need Updates - -1. **routes/session_routes.py** (15 occurrences) - - Lines: 84, 134, 325 (device_id) - - Lines: 85, 109, 112, 119, 124, 135, 150 (login_time) - - Lines: 86, 119, 136, 202, 248 (logout_time) - - Lines: 87, 137 (last_activity) - - Lines: 88, 124, 138, 192, 202, 236, 248, 249, 328, 340, 361 (active) - -2. **routes/api_routes.py** (12 occurrences) - - Lines: 203, 214, 345, 861 (device_id) - - Lines: 204, 344, 345, 453, 455, 457, 862 (active) - - Line: 344 (logout_time) - -3. **routes/export_routes.py** (11 occurrences) - - Lines: 47, 72, 224, 244 (device_id) - - Lines: 46, 71, 228, 233, 248 (active) - - Lines: 225, 234, 245, 253, 254 (login_time) - - Lines: 226, 246 (logout_time) - - Lines: 227, 247 (last_activity) - -4. **models.py** (2 occurrences) - - Line: 167 (start_time) - - Line: 177 (active - but this is just in error message) - -5. **routes/batch_routes.py** (2 occurrences) - - Lines: 212, 213 (active) - -6. **routes/customer_routes.py** (1 occurrence) - - Line: 392 (comment mentions correction already made) - -## Testing After Fixes - -1. Test all session-related functionality -2. Verify device registration/deregistration works -3. Check session history displays correctly -4. Ensure exports contain correct data -5. Validate batch operations still function \ No newline at end of file diff --git a/v2_adminpanel/DATABASE_FIELD_INCONSISTENCIES.md b/v2_adminpanel/DATABASE_FIELD_INCONSISTENCIES.md deleted file mode 100644 index ea2733f..0000000 --- a/v2_adminpanel/DATABASE_FIELD_INCONSISTENCIES.md +++ /dev/null @@ -1,124 +0,0 @@ -# Database Field Name Inconsistencies Report - -## Overview -This report documents all database field name inconsistencies found between the database schema (init.sql) and Python code usage in the v2_adminpanel application. - -## 1. Sessions Table - Duplicate/Alias Fields - -### Issue -The sessions table contains multiple duplicate columns that serve as aliases, causing confusion and inconsistent usage: - -```sql --- Current schema has these duplicate fields: -is_active BOOLEAN DEFAULT TRUE, -active BOOLEAN DEFAULT TRUE -- Alias for is_active - -started_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, -login_time TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, -- Alias for started_at - -last_heartbeat TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, -last_activity TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, -- Alias for last_heartbeat - -ended_at TIMESTAMP WITH TIME ZONE, -logout_time TIMESTAMP WITH TIME ZONE, -- Alias for ended_at -``` - -### Code Usage Examples -- `routes/session_routes.py`: - - Line 32: `WHERE s.is_active = TRUE` - - Line 88: `s.active` - - Line 192: `WHERE active = true` - - Line 201: `SET active = false` - -## 2. Device ID vs Hardware ID Mismatch - -### Issue -The database schema uses `hardware_id` but the code references `device_id`: - -```sql --- Database schema: -sessions.hardware_id VARCHAR(100) -device_registrations.hardware_id TEXT NOT NULL -``` - -### Code Usage Examples -- `routes/session_routes.py`: - - Line 84: `s.device_id` - - Line 134: `'device_id': row[3]` - - Line 190: `SELECT license_key, username, device_id` - - Line 326: `COUNT(DISTINCT s.device_id) as unique_devices` - -## 3. Time Field Inconsistencies - -### Issue -Mixed usage of time field names: - -### Code Usage Examples -- `models.py` line 167: `ORDER BY s.start_time DESC` (but schema has `started_at`) -- Session history queries mix `login_time` and `started_at` - -## 4. Field Naming Patterns - -### Consistent Patterns Found -- ✅ All tables use `created_at` (not `created`) -- ✅ Most tables use `is_active` pattern -- ✅ Foreign keys use `_id` suffix consistently - -### Inconsistent Patterns -- ❌ Sessions table has both `is_active` and `active` -- ❌ Time fields have multiple aliases - -## Migration Scripts - -### Step 1: Fix device_id references in code -```sql --- Create view for backward compatibility -CREATE OR REPLACE VIEW sessions_compat AS -SELECT - *, - hardware_id as device_id -- Alias for compatibility -FROM sessions; -``` - -### Step 2: Remove duplicate columns (after code update) -```sql --- Remove duplicate columns from sessions table -ALTER TABLE sessions -DROP COLUMN IF EXISTS active, -DROP COLUMN IF EXISTS login_time, -DROP COLUMN IF EXISTS last_activity, -DROP COLUMN IF EXISTS logout_time; -``` - -### Step 3: Update indexes if needed -```sql --- Recreate any indexes that used the dropped columns --- (Check existing indexes first) -``` - -## Recommended Code Changes - -### 1. Update session_routes.py -Replace all occurrences of: -- `device_id` → `hardware_id` -- `active` → `is_active` -- `login_time` → `started_at` -- `last_activity` → `last_heartbeat` -- `logout_time` → `ended_at` - -### 2. Update models.py -- Line 167: Change `start_time` to `started_at` - -### 3. Create database migration script -Create a migration that: -1. Updates all code references -2. Creates compatibility views -3. Removes duplicate columns -4. Updates any affected indexes - -## Testing Checklist -- [ ] All session queries work correctly -- [ ] Session history displays properly -- [ ] Active session count is accurate -- [ ] Device tracking works correctly -- [ ] All time-based queries function properly \ No newline at end of file diff --git a/v2_adminpanel/FIELD_FIXES_SUMMARY.md b/v2_adminpanel/FIELD_FIXES_SUMMARY.md deleted file mode 100644 index 9966ff2..0000000 --- a/v2_adminpanel/FIELD_FIXES_SUMMARY.md +++ /dev/null @@ -1,71 +0,0 @@ -# Zusammenfassung der Datenbankfeld-Korrekturen - -## Durchgeführte Änderungen - -### 1. Automatische Fixes (35 Ersetzungen) -Das Script `fix_database_fields.py` hat folgende Dateien korrigiert: -- **routes/session_routes.py** - 16 Fixes -- **routes/api_routes.py** - 10 Fixes -- **routes/export_routes.py** - 5 Fixes -- **routes/batch_routes.py** - 1 Fix -- **routes/customer_routes.py** - 1 Fix -- **models.py** - 2 Fixes - -### 2. Manuelle Nachkorrekturen -- **api_routes.py**: - - Zeile 275: `device_id` → `hardware_id` in SQL-Parameter - - Zeile 301: `device_id` → `hardware_id` in Audit-Log -- **batch_routes.py**: - - Zeile 212: SQL `"active = %s"` → `"is_active = %s"` - -### 3. Erstellte Kompatibilitäts-View -File: `create_compatibility_views.sql` -- Erstellt eine `sessions_compat` View mit Aliasen für alte Feldnamen -- Ermöglicht sanfte Migration ohne Breaking Changes -- Inkludiert INSERT/UPDATE Trigger für bidirektionale Kompatibilität - -## Korrigierte Feldnamen - -| Alte Bezeichnung | Neue Bezeichnung | Betroffene Tabelle | -|-----------------|------------------|-------------------| -| `device_id` | `hardware_id` | sessions | -| `active` | `is_active` | sessions, licenses | -| `login_time` | `started_at` | sessions | -| `last_activity` | `last_heartbeat` | sessions | -| `logout_time` | `ended_at` | sessions | -| `start_time` | `started_at` | sessions | - -## Nächste Schritte - -1. **Datenbank-Migration ausführen**: - ```bash - psql -U postgres -d accountforge -f create_compatibility_views.sql - ``` - -2. **Anwendung testen**: - - Sessions-Verwaltung - - Lizenz-Status-Änderungen - - Export-Funktionen - - Batch-Updates - - Device-Registrierung - -3. **Nach erfolgreichem Test**: - - Doppelte Spalten aus sessions-Tabelle entfernen - - Kompatibilitäts-View entfernen - - Code auf direkte Tabellenzugriffe umstellen - -## Backup-Dateien -Alle geänderten Dateien haben `.backup` Kopien: -- routes/session_routes.py.backup -- routes/api_routes.py.backup -- routes/export_routes.py.backup -- routes/batch_routes.py.backup -- routes/customer_routes.py.backup -- models.py.backup - -## Wichtige Hinweise - -- Die sessions-Tabelle hat noch immer doppelte Spalten in der DB -- Die Kompatibilitäts-View sollte zuerst erstellt werden -- Alle Änderungen sind rückgängig machbar über die .backup Dateien -- Der Status-Toggle-Bug sollte jetzt behoben sein \ No newline at end of file diff --git a/v2_adminpanel/REMAINING_INCONSISTENCIES.md b/v2_adminpanel/REMAINING_INCONSISTENCIES.md deleted file mode 100644 index ffb3d79..0000000 --- a/v2_adminpanel/REMAINING_INCONSISTENCIES.md +++ /dev/null @@ -1,74 +0,0 @@ -# Verbleibende Datenbankfeld-Inkonsistenzen - -## Status der Bereinigung - -### ✅ Vollständig behoben: -1. **sessions Tabelle**: - - `device_id` → `hardware_id` (alle Referenzen korrigiert) - - `active` → `is_active` (alle Referenzen korrigiert) - - `login_time` → `started_at` (alle Referenzen korrigiert) - - `logout_time` → `ended_at` (alle Referenzen korrigiert) - - `last_activity` → `last_heartbeat` (alle Referenzen korrigiert) - - `start_time` → `started_at` (korrigiert in models.py) - -2. **licenses Tabelle**: - - `active` → `is_active` (alle Referenzen korrigiert) - -### ⚠️ Strukturelle Probleme in device_registrations: - -Die `device_registrations` Tabelle hat folgende Inkonsistenzen zwischen Schema und Code: - -| Datenbankschema | Code erwartet | Problem | -|-----------------|---------------|---------| -| `license_id` (FK) | `license_key` | Code nutzt license_key direkt | -| `first_seen` | `registration_date` | Unterschiedliche Benennung | -| - | `device_type` | Spalte fehlt in DB | -| - | `license_key` | Spalte fehlt in DB | - -### 🔧 Durchgeführte Anpassungen: - -1. **API-Routes korrigiert**: - - JOINs eingefügt um license_key über license_id zu erhalten - - `first_seen as registration_date` Aliase hinzugefügt - - INSERT nutzt jetzt `license_id` statt `license_key` - -2. **Export-Routes korrigiert**: - - Letzte `device_id` Referenzen zu `hardware_id` geändert - -3. **Session-Routes korrigiert**: - - Statistik-Queries nutzen jetzt korrekte Feldnamen - -### 📋 Noch zu erledigen: - -1. **Datenbank-Migration ausführen**: - ```sql - -- Sessions Kompatibilitäts-View - psql -f create_compatibility_views.sql - - -- Device Registrations Fixes - psql -f fix_device_registrations.sql - ``` - -2. **Fehlende Spalten hinzufügen** (optional): - ```sql - ALTER TABLE device_registrations - ADD COLUMN device_type VARCHAR(50) DEFAULT 'unknown'; - ``` - -3. **Doppelte Spalten entfernen** (nach erfolgreichen Tests): - ```sql - ALTER TABLE sessions - DROP COLUMN active, - DROP COLUMN login_time, - DROP COLUMN last_activity, - DROP COLUMN logout_time; - ``` - -## Zusammenfassung - -- **83 Inkonsistenzen** wurden automatisch behoben -- **8 zusätzliche manuelle Fixes** wurden durchgeführt -- **Alle kritischen Feldnamen** sind jetzt konsistent -- **device_registrations** benötigt noch strukturelle Anpassungen - -Die Anwendung sollte jetzt funktionieren, da alle Code-Referenzen korrigiert wurden. Die Datenbank-Migrationen sind optional für langfristige Konsistenz. \ No newline at end of file diff --git a/v2_adminpanel/create_compatibility_views.sql b/v2_adminpanel/create_compatibility_views.sql deleted file mode 100644 index bf1ec1b..0000000 --- a/v2_adminpanel/create_compatibility_views.sql +++ /dev/null @@ -1,122 +0,0 @@ --- Compatibility Views für sanfte Migration --- Diese Views ermöglichen es, dass der Code weiterhin funktioniert, --- während wir schrittweise die Feldnamen korrigieren - --- 1. Sessions Compatibility View --- Mappt alle falschen Feldnamen auf die korrekten -CREATE OR REPLACE VIEW sessions_compat AS -SELECT - id, - license_id, - license_key, - session_id, - username, - computer_name, - hardware_id, - hardware_id as device_id, -- Alias für Kompatibilität - ip_address, - user_agent, - app_version, - started_at, - started_at as login_time, -- Alias für Kompatibilität - started_at as start_time, -- Alias für models.py - last_heartbeat, - last_heartbeat as last_activity, -- Alias für Kompatibilität - ended_at, - ended_at as logout_time, -- Alias für Kompatibilität - is_active, - is_active as active -- Alias für Kompatibilität -FROM sessions; - --- Grant permissions -GRANT SELECT, INSERT, UPDATE, DELETE ON sessions_compat TO PUBLIC; - --- 2. Trigger für INSERT auf sessions_compat -CREATE OR REPLACE FUNCTION insert_sessions_compat() RETURNS TRIGGER AS $$ -BEGIN - -- Map compatibility fields back to real columns - INSERT INTO sessions ( - license_id, license_key, session_id, username, computer_name, - hardware_id, ip_address, user_agent, app_version, - started_at, last_heartbeat, ended_at, is_active - ) VALUES ( - NEW.license_id, - NEW.license_key, - NEW.session_id, - NEW.username, - NEW.computer_name, - COALESCE(NEW.hardware_id, NEW.device_id), -- Accept both - NEW.ip_address, - NEW.user_agent, - NEW.app_version, - COALESCE(NEW.started_at, NEW.login_time), -- Accept both - COALESCE(NEW.last_heartbeat, NEW.last_activity), -- Accept both - COALESCE(NEW.ended_at, NEW.logout_time), -- Accept both - COALESCE(NEW.is_active, NEW.active) -- Accept both - ); - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - -CREATE TRIGGER trigger_insert_sessions_compat -INSTEAD OF INSERT ON sessions_compat -FOR EACH ROW EXECUTE FUNCTION insert_sessions_compat(); - --- 3. Trigger für UPDATE auf sessions_compat -CREATE OR REPLACE FUNCTION update_sessions_compat() RETURNS TRIGGER AS $$ -BEGIN - UPDATE sessions SET - license_id = NEW.license_id, - license_key = NEW.license_key, - session_id = NEW.session_id, - username = NEW.username, - computer_name = NEW.computer_name, - hardware_id = COALESCE(NEW.hardware_id, NEW.device_id), - ip_address = NEW.ip_address, - user_agent = NEW.user_agent, - app_version = NEW.app_version, - started_at = COALESCE(NEW.started_at, NEW.login_time), - last_heartbeat = COALESCE(NEW.last_heartbeat, NEW.last_activity), - ended_at = COALESCE(NEW.ended_at, NEW.logout_time), - is_active = COALESCE(NEW.is_active, NEW.active) - WHERE id = NEW.id; - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - -CREATE TRIGGER trigger_update_sessions_compat -INSTEAD OF UPDATE ON sessions_compat -FOR EACH ROW EXECUTE FUNCTION update_sessions_compat(); - --- 4. Sync existing duplicate columns (one-time sync) --- This ensures data consistency before we start using the view -UPDATE sessions SET - hardware_id = COALESCE(hardware_id, device_id), - started_at = COALESCE(started_at, login_time), - last_heartbeat = COALESCE(last_heartbeat, last_activity), - ended_at = COALESCE(ended_at, logout_time), - is_active = COALESCE(is_active, active) -WHERE hardware_id IS NULL - OR started_at IS NULL - OR last_heartbeat IS NULL - OR is_active IS NULL; - --- 5. Verification Query -SELECT - 'Sessions with NULL hardware_id' as check_name, - COUNT(*) as count -FROM sessions WHERE hardware_id IS NULL -UNION ALL -SELECT - 'Sessions with NULL started_at' as check_name, - COUNT(*) as count -FROM sessions WHERE started_at IS NULL -UNION ALL -SELECT - 'Sessions with NULL is_active' as check_name, - COUNT(*) as count -FROM sessions WHERE is_active IS NULL; - --- 6. Create index on hardware_id if not exists -CREATE INDEX IF NOT EXISTS idx_sessions_hardware_id ON sessions(hardware_id); -CREATE INDEX IF NOT EXISTS idx_sessions_is_active ON sessions(is_active); \ No newline at end of file diff --git a/v2_adminpanel/fix_database_fields.py b/v2_adminpanel/fix_database_fields.py deleted file mode 100644 index 091c6c7..0000000 --- a/v2_adminpanel/fix_database_fields.py +++ /dev/null @@ -1,178 +0,0 @@ -#!/usr/bin/env python3 -""" -Script to fix database field name inconsistencies in the v2_adminpanel codebase. -This script identifies and optionally fixes incorrect field references. -""" - -import os -import re -from typing import Dict, List, Tuple -import argparse - -# Define the field mappings (incorrect -> correct) -FIELD_MAPPINGS = { - # Sessions table - 'device_id': 'hardware_id', - 'active': 'is_active', - 'login_time': 'started_at', - 'last_activity': 'last_heartbeat', - 'logout_time': 'ended_at', - 'start_time': 'started_at' -} - -# Files to check -FILES_TO_CHECK = [ - 'routes/session_routes.py', - 'routes/api_routes.py', - 'routes/export_routes.py', - 'routes/batch_routes.py', - 'routes/customer_routes.py', - 'models.py' -] - -# Patterns to identify database field usage -PATTERNS = [ - # SQL queries - (r'SELECT\s+.*?(\w+).*?FROM\s+sessions', 'SQL SELECT'), - (r'WHERE\s+.*?(\w+)\s*=', 'SQL WHERE'), - (r'SET\s+(\w+)\s*=', 'SQL SET'), - (r'ORDER\s+BY\s+.*?(\w+)', 'SQL ORDER BY'), - - # Dictionary/JSON access - (r'\[[\'"](device_id|active|login_time|last_activity|logout_time|start_time)[\'"]\]', 'Dict access'), - (r'\.get\([\'"](device_id|active|login_time|last_activity|logout_time|start_time)[\'"]', 'Dict get'), - - # Row access patterns - (r'row\[\d+\]\s*#.*?(device_id|active|login_time|last_activity|logout_time|start_time)', 'Row comment'), - - # Column references in queries - (r's\.(device_id|active|login_time|last_activity|logout_time|start_time)', 'Table alias') -] - -def find_field_usage(file_path: str) -> List[Tuple[int, str, str, str]]: - """Find all incorrect field usages in a file.""" - issues = [] - - try: - with open(file_path, 'r', encoding='utf-8') as f: - lines = f.readlines() - - for line_num, line in enumerate(lines, 1): - for field_old, field_new in FIELD_MAPPINGS.items(): - # Simple pattern matching for field names - if field_old in line: - # Check if it's in a string or actual code - if f"'{field_old}'" in line or f'"{field_old}"' in line: - issues.append((line_num, line.strip(), field_old, field_new)) - elif f'.{field_old}' in line or f' {field_old} ' in line: - issues.append((line_num, line.strip(), field_old, field_new)) - elif f'[{field_old}]' in line: - issues.append((line_num, line.strip(), field_old, field_new)) - - except Exception as e: - print(f"Error reading {file_path}: {e}") - - return issues - -def generate_fix_report(base_path: str) -> Dict[str, List[Tuple[int, str, str, str]]]: - """Generate a report of all field usage issues.""" - report = {} - - for file_name in FILES_TO_CHECK: - file_path = os.path.join(base_path, file_name) - if os.path.exists(file_path): - issues = find_field_usage(file_path) - if issues: - report[file_name] = issues - - return report - -def apply_fixes(base_path: str, report: Dict[str, List[Tuple[int, str, str, str]]], dry_run: bool = True): - """Apply fixes to the files.""" - for file_name, issues in report.items(): - file_path = os.path.join(base_path, file_name) - - if dry_run: - print(f"\n--- DRY RUN: Would fix {file_name} ---") - for line_num, line, old_field, new_field in issues: - print(f" Line {line_num}: {old_field} -> {new_field}") - continue - - # Read the file - with open(file_path, 'r', encoding='utf-8') as f: - content = f.read() - - # Apply replacements - original_content = content - replacements_made = 0 - - for old_field, new_field in FIELD_MAPPINGS.items(): - # Replace in strings - patterns = [ - (f"'{old_field}'", f"'{new_field}'"), - (f'"{old_field}"', f'"{new_field}"'), - (f'.{old_field}', f'.{new_field}'), - (f' {old_field} ', f' {new_field} '), - (f' {old_field},', f' {new_field},'), - (f' {old_field}\n', f' {new_field}\n'), - ] - - for pattern_old, pattern_new in patterns: - if pattern_old in content: - content = content.replace(pattern_old, pattern_new) - replacements_made += 1 - - # Write back only if changes were made - if content != original_content: - # Create backup - backup_path = f"{file_path}.backup" - with open(backup_path, 'w', encoding='utf-8') as f: - f.write(original_content) - - # Write fixed content - with open(file_path, 'w', encoding='utf-8') as f: - f.write(content) - - print(f"\nFixed {file_name} ({replacements_made} replacements made)") - print(f" Backup saved to: {backup_path}") - -def main(): - parser = argparse.ArgumentParser(description='Fix database field name inconsistencies') - parser.add_argument('--apply', action='store_true', help='Apply fixes (default is dry run)') - parser.add_argument('--path', default='.', help='Base path of the project') - args = parser.parse_args() - - print("Database Field Name Fixer") - print("=" * 50) - - # Generate report - report = generate_fix_report(args.path) - - if not report: - print("No issues found!") - return - - # Display report - total_issues = 0 - for file_name, issues in report.items(): - print(f"\n{file_name}: {len(issues)} issues") - total_issues += len(issues) - for line_num, line, old_field, new_field in issues[:5]: # Show first 5 - print(f" Line {line_num}: {old_field} -> {new_field}") - print(f" {line[:80]}...") - if len(issues) > 5: - print(f" ... and {len(issues) - 5} more") - - print(f"\nTotal issues found: {total_issues}") - - # Apply fixes if requested - if args.apply: - print("\nApplying fixes...") - apply_fixes(args.path, report, dry_run=False) - print("\nFixes applied! Please test all functionality.") - else: - print("\nRun with --apply to fix these issues.") - apply_fixes(args.path, report, dry_run=True) - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/v2_adminpanel/fix_device_registrations.sql b/v2_adminpanel/fix_device_registrations.sql deleted file mode 100644 index 81cdc98..0000000 --- a/v2_adminpanel/fix_device_registrations.sql +++ /dev/null @@ -1,29 +0,0 @@ --- Fixes für device_registrations Tabelle --- Diese Spalten fehlen und werden im Code verwendet - --- 1. Füge fehlende Spalten hinzu -ALTER TABLE device_registrations -ADD COLUMN IF NOT EXISTS device_type VARCHAR(50) DEFAULT 'unknown', -ADD COLUMN IF NOT EXISTS license_key VARCHAR(60); - --- 2. Füge registration_date als Alias für first_seen hinzu --- (Oder nutze first_seen im Code) - --- 3. Fülle license_key aus licenses Tabelle -UPDATE device_registrations dr -SET license_key = l.license_key -FROM licenses l -WHERE dr.license_id = l.id -AND dr.license_key IS NULL; - --- 4. Erstelle Index für license_key -CREATE INDEX IF NOT EXISTS idx_device_license_key ON device_registrations(license_key); - --- 5. View für Kompatibilität -CREATE OR REPLACE VIEW device_registrations_compat AS -SELECT - dr.*, - dr.first_seen as registration_date, - l.license_key as computed_license_key -FROM device_registrations dr -LEFT JOIN licenses l ON dr.license_id = l.id; \ No newline at end of file diff --git a/v2_adminpanel/fix_field_inconsistencies.sql b/v2_adminpanel/fix_field_inconsistencies.sql deleted file mode 100644 index 9d878f3..0000000 --- a/v2_adminpanel/fix_field_inconsistencies.sql +++ /dev/null @@ -1,171 +0,0 @@ --- Migration Script: Fix Database Field Name Inconsistencies --- Created: 2025-06-18 --- Purpose: Standardize field names and remove duplicate columns - --- ===================================================== --- STEP 1: Create backup of affected data --- ===================================================== - --- Create backup table for sessions data -CREATE TABLE IF NOT EXISTS sessions_backup_20250618 AS -SELECT * FROM sessions; - --- ===================================================== --- STEP 2: Create compatibility views (temporary) --- ===================================================== - --- Drop existing view if exists -DROP VIEW IF EXISTS sessions_compat; - --- Create compatibility view for smooth transition -CREATE VIEW sessions_compat AS -SELECT - id, - license_id, - license_key, - session_id, - username, - computer_name, - hardware_id, - hardware_id as device_id, -- Compatibility alias - ip_address, - user_agent, - app_version, - started_at, - started_at as login_time, -- Compatibility alias - last_heartbeat, - last_heartbeat as last_activity, -- Compatibility alias - ended_at, - ended_at as logout_time, -- Compatibility alias - is_active, - is_active as active -- Compatibility alias -FROM sessions; - --- ===================================================== --- STEP 3: Update data in duplicate columns --- ===================================================== - --- Sync data from primary columns to alias columns (safety measure) -UPDATE sessions SET - login_time = COALESCE(login_time, started_at), - last_activity = COALESCE(last_activity, last_heartbeat), - logout_time = COALESCE(logout_time, ended_at), - active = COALESCE(active, is_active) -WHERE login_time IS NULL - OR last_activity IS NULL - OR logout_time IS NULL - OR active IS NULL; - --- Sync data from alias columns to primary columns (if primary is null) -UPDATE sessions SET - started_at = COALESCE(started_at, login_time), - last_heartbeat = COALESCE(last_heartbeat, last_activity), - ended_at = COALESCE(ended_at, logout_time), - is_active = COALESCE(is_active, active) -WHERE started_at IS NULL - OR last_heartbeat IS NULL - OR ended_at IS NULL - OR is_active IS NULL; - --- ===================================================== --- STEP 4: Create function to check code dependencies --- ===================================================== - -CREATE OR REPLACE FUNCTION check_field_usage() -RETURNS TABLE ( - query_count INTEGER, - field_name TEXT, - usage_type TEXT -) AS $$ -BEGIN - -- Check for references to old field names - RETURN QUERY - SELECT - COUNT(*)::INTEGER, - 'device_id'::TEXT, - 'Should use hardware_id'::TEXT - FROM pg_stat_statements - WHERE query ILIKE '%device_id%' - - UNION ALL - - SELECT - COUNT(*)::INTEGER, - 'active'::TEXT, - 'Should use is_active'::TEXT - FROM pg_stat_statements - WHERE query ILIKE '%active%' - AND query NOT ILIKE '%is_active%' - - UNION ALL - - SELECT - COUNT(*)::INTEGER, - 'login_time'::TEXT, - 'Should use started_at'::TEXT - FROM pg_stat_statements - WHERE query ILIKE '%login_time%'; -END; -$$ LANGUAGE plpgsql; - --- ===================================================== --- STEP 5: Migration queries for code updates --- ===================================================== - --- These queries help identify code that needs updating: - --- Find sessions queries using old field names -COMMENT ON VIEW sessions_compat IS -'Compatibility view for sessions table during field name migration. -Old fields mapped: -- device_id → hardware_id -- active → is_active -- login_time → started_at -- last_activity → last_heartbeat -- logout_time → ended_at'; - --- ===================================================== --- STEP 6: Final cleanup (run after code is updated) --- ===================================================== - --- DO NOT RUN THIS UNTIL ALL CODE IS UPDATED! -/* --- Remove duplicate columns -ALTER TABLE sessions -DROP COLUMN IF EXISTS active, -DROP COLUMN IF EXISTS login_time, -DROP COLUMN IF EXISTS last_activity, -DROP COLUMN IF EXISTS logout_time; - --- Drop compatibility view -DROP VIEW IF EXISTS sessions_compat; - --- Drop helper function -DROP FUNCTION IF EXISTS check_field_usage(); -*/ - --- ===================================================== --- VERIFICATION QUERIES --- ===================================================== - --- Check for null values in primary columns -SELECT - COUNT(*) FILTER (WHERE started_at IS NULL) as null_started_at, - COUNT(*) FILTER (WHERE last_heartbeat IS NULL) as null_last_heartbeat, - COUNT(*) FILTER (WHERE ended_at IS NULL AND is_active = FALSE) as null_ended_at, - COUNT(*) FILTER (WHERE is_active IS NULL) as null_is_active, - COUNT(*) as total_sessions -FROM sessions; - --- Check data consistency between duplicate columns -SELECT - COUNT(*) FILTER (WHERE started_at != login_time) as started_login_diff, - COUNT(*) FILTER (WHERE last_heartbeat != last_activity) as heartbeat_activity_diff, - COUNT(*) FILTER (WHERE ended_at != logout_time) as ended_logout_diff, - COUNT(*) FILTER (WHERE is_active != active) as active_diff, - COUNT(*) as total_sessions -FROM sessions -WHERE login_time IS NOT NULL - OR last_activity IS NOT NULL - OR logout_time IS NOT NULL - OR active IS NOT NULL; \ No newline at end of file diff --git a/v2_adminpanel/fix_field_references.py b/v2_adminpanel/fix_field_references.py deleted file mode 100644 index ca28bcf..0000000 --- a/v2_adminpanel/fix_field_references.py +++ /dev/null @@ -1,170 +0,0 @@ -#!/usr/bin/env python3 -""" -Script to find and fix database field name inconsistencies in Python code -""" - -import os -import re -from pathlib import Path -from typing import List, Tuple, Dict - -# Field mappings (old_name -> new_name) -FIELD_MAPPINGS = { - 'device_id': 'hardware_id', - 'active': 'is_active', - 'login_time': 'started_at', - 'last_activity': 'last_heartbeat', - 'logout_time': 'ended_at', - 'start_time': 'started_at' # Found in models.py -} - -# Patterns to identify database queries -QUERY_PATTERNS = [ - r'SELECT.*FROM\s+sessions', - r'UPDATE\s+sessions', - r'INSERT\s+INTO\s+sessions', - r'WHERE.*sessions\.', - r's\.\w+', # Table alias pattern - r'row\[\d+\]', # Row index access -] - -def find_python_files(directory: Path) -> List[Path]: - """Find all Python files in directory""" - return list(directory.rglob("*.py")) - -def check_file_for_inconsistencies(filepath: Path) -> Dict[str, List[Tuple[int, str]]]: - """Check a file for field name inconsistencies""" - inconsistencies = {} - - try: - with open(filepath, 'r', encoding='utf-8') as f: - lines = f.readlines() - - for line_num, line in enumerate(lines, 1): - # Skip comments - if line.strip().startswith('#'): - continue - - # Check for old field names - for old_field, new_field in FIELD_MAPPINGS.items(): - # Look for field references in various contexts - patterns = [ - rf'\b{old_field}\b', # Word boundary - rf'["\']{{1}}{old_field}["\']{{1}}', # In quotes - rf's\.{old_field}\b', # Table alias - rf'row\[.*{old_field}.*\]', # In row access - ] - - for pattern in patterns: - if re.search(pattern, line, re.IGNORECASE): - # Check if it's in a database query context - is_db_context = any(re.search(qp, line, re.IGNORECASE) for qp in QUERY_PATTERNS) - - # Also check previous lines for query context - if not is_db_context and line_num > 1: - for i in range(max(0, line_num - 5), line_num): - if any(re.search(qp, lines[i], re.IGNORECASE) for qp in QUERY_PATTERNS): - is_db_context = True - break - - if is_db_context or 'cur.execute' in line or 'execute_query' in line: - if old_field not in inconsistencies: - inconsistencies[old_field] = [] - inconsistencies[old_field].append((line_num, line.strip())) - break - - except Exception as e: - print(f"Error reading {filepath}: {e}") - - return inconsistencies - -def generate_fix_suggestions(inconsistencies: Dict[Path, Dict[str, List[Tuple[int, str]]]]) -> None: - """Generate fix suggestions for found inconsistencies""" - print("\n" + "="*80) - print("FIELD NAME INCONSISTENCIES FOUND") - print("="*80 + "\n") - - total_issues = 0 - - for filepath, file_issues in inconsistencies.items(): - if not file_issues: - continue - - print(f"\n📄 {filepath}") - print("-" * 80) - - for old_field, occurrences in file_issues.items(): - new_field = FIELD_MAPPINGS[old_field] - print(f"\n ❌ Found '{old_field}' (should be '{new_field}'):") - - for line_num, line_content in occurrences: - print(f" Line {line_num}: {line_content[:100]}...") - total_issues += 1 - - print(f"\n\n📊 Total issues found: {total_issues}") - print("\n" + "="*80) - print("RECOMMENDED FIXES") - print("="*80 + "\n") - - for old_field, new_field in FIELD_MAPPINGS.items(): - print(f" • Replace '{old_field}' with '{new_field}'") - - print("\n⚠️ Note: Review each change carefully, as some occurrences might not be database-related") - -def create_compatibility_queries() -> None: - """Generate SQL queries for creating compatibility views""" - print("\n" + "="*80) - print("COMPATIBILITY SQL QUERIES") - print("="*80 + "\n") - - print("-- Use this view during migration:") - print("CREATE OR REPLACE VIEW sessions_compat AS") - print("SELECT ") - print(" *,") - for old_field, new_field in FIELD_MAPPINGS.items(): - if old_field != 'start_time': # Skip non-column mappings - print(f" {new_field} as {old_field},") - print("FROM sessions;\n") - -def main(): - """Main function""" - # Get the v2_adminpanel directory - base_dir = Path(__file__).parent - - print(f"🔍 Scanning directory: {base_dir}") - - # Find all Python files - python_files = find_python_files(base_dir) - print(f"📁 Found {len(python_files)} Python files") - - # Check each file for inconsistencies - all_inconsistencies = {} - - for filepath in python_files: - # Skip this script and migration files - if filepath.name in ['fix_field_references.py', '__pycache__']: - continue - - inconsistencies = check_file_for_inconsistencies(filepath) - if inconsistencies: - all_inconsistencies[filepath] = inconsistencies - - # Generate report - generate_fix_suggestions(all_inconsistencies) - - # Generate compatibility queries - create_compatibility_queries() - - # Summary of affected files - print("\n" + "="*80) - print("AFFECTED FILES SUMMARY") - print("="*80 + "\n") - - affected_files = [str(f.relative_to(base_dir)) for f in all_inconsistencies.keys()] - for i, filepath in enumerate(sorted(affected_files), 1): - print(f" {i}. {filepath}") - - print(f"\n✅ Scan complete! Found issues in {len(affected_files)} files.") - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/v2_adminpanel/models.py.backup b/v2_adminpanel/models.py.backup deleted file mode 100644 index b3e0d92..0000000 --- a/v2_adminpanel/models.py.backup +++ /dev/null @@ -1,178 +0,0 @@ -# Temporary models file - will be expanded in Phase 3 -from db import execute_query, get_db_connection, get_db_cursor -import logging - -logger = logging.getLogger(__name__) - - -def get_user_by_username(username): - """Get user from database by username""" - result = execute_query( - """ - SELECT id, username, password_hash, email, totp_secret, totp_enabled, - backup_codes, last_password_change, failed_2fa_attempts - FROM users WHERE username = %s - """, - (username,), - fetch_one=True - ) - - if result: - return { - 'id': result[0], - 'username': result[1], - 'password_hash': result[2], - 'email': result[3], - 'totp_secret': result[4], - 'totp_enabled': result[5], - 'backup_codes': result[6], - 'last_password_change': result[7], - 'failed_2fa_attempts': result[8] - } - return None - - -def get_licenses(show_test=False): - """Get all licenses from database""" - try: - with get_db_connection() as conn: - with get_db_cursor(conn) as cur: - if show_test: - cur.execute(""" - SELECT l.*, c.name as customer_name - FROM licenses l - LEFT JOIN customers c ON l.customer_id = c.id - ORDER BY l.created_at DESC - """) - else: - cur.execute(""" - SELECT l.*, c.name as customer_name - FROM licenses l - LEFT JOIN customers c ON l.customer_id = c.id - WHERE l.is_test = false - ORDER BY l.created_at DESC - """) - - columns = [desc[0] for desc in cur.description] - licenses = [] - for row in cur.fetchall(): - license_dict = dict(zip(columns, row)) - licenses.append(license_dict) - return licenses - except Exception as e: - logger.error(f"Error fetching licenses: {str(e)}") - return [] - - -def get_license_by_id(license_id): - """Get a specific license by ID""" - try: - with get_db_connection() as conn: - with get_db_cursor(conn) as cur: - cur.execute(""" - SELECT l.*, c.name as customer_name - FROM licenses l - LEFT JOIN customers c ON l.customer_id = c.id - WHERE l.id = %s - """, (license_id,)) - - row = cur.fetchone() - if row: - columns = [desc[0] for desc in cur.description] - return dict(zip(columns, row)) - return None - except Exception as e: - logger.error(f"Error fetching license {license_id}: {str(e)}") - return None - - -def get_customers(show_test=False, search=None): - """Get all customers from database""" - try: - with get_db_connection() as conn: - with get_db_cursor(conn) as cur: - query = """ - SELECT c.*, - COUNT(DISTINCT l.id) as license_count, - COUNT(DISTINCT CASE WHEN l.is_active THEN l.id END) as active_licenses - FROM customers c - LEFT JOIN licenses l ON c.id = l.customer_id - """ - - where_clauses = [] - params = [] - - if not show_test: - where_clauses.append("c.is_test = false") - - if search: - where_clauses.append("(LOWER(c.name) LIKE LOWER(%s) OR LOWER(c.email) LIKE LOWER(%s))") - search_pattern = f'%{search}%' - params.extend([search_pattern, search_pattern]) - - if where_clauses: - query += " WHERE " + " AND ".join(where_clauses) - - query += " GROUP BY c.id ORDER BY c.name" - - cur.execute(query, params) - - columns = [desc[0] for desc in cur.description] - customers = [] - for row in cur.fetchall(): - customer_dict = dict(zip(columns, row)) - customers.append(customer_dict) - return customers - except Exception as e: - logger.error(f"Error fetching customers: {str(e)}") - return [] - - -def get_customer_by_id(customer_id): - """Get a specific customer by ID""" - try: - with get_db_connection() as conn: - with get_db_cursor(conn) as cur: - cur.execute(""" - SELECT c.*, - COUNT(DISTINCT l.id) as license_count, - COUNT(DISTINCT CASE WHEN l.is_active THEN l.id END) as active_licenses - FROM customers c - LEFT JOIN licenses l ON c.id = l.customer_id - WHERE c.id = %s - GROUP BY c.id - """, (customer_id,)) - - row = cur.fetchone() - if row: - columns = [desc[0] for desc in cur.description] - return dict(zip(columns, row)) - return None - except Exception as e: - logger.error(f"Error fetching customer {customer_id}: {str(e)}") - return None - - -def get_active_sessions(): - """Get all active sessions""" - try: - with get_db_connection() as conn: - with get_db_cursor(conn) as cur: - cur.execute(""" - SELECT s.*, l.license_key, c.name as customer_name - FROM sessions s - JOIN licenses l ON s.license_id = l.id - LEFT JOIN customers c ON l.customer_id = c.id - WHERE s.is_active = true - ORDER BY s.start_time DESC - """) - - columns = [desc[0] for desc in cur.description] - sessions = [] - for row in cur.fetchall(): - session_dict = dict(zip(columns, row)) - sessions.append(session_dict) - return sessions - except Exception as e: - logger.error(f"Error fetching active sessions: {str(e)}") - return [] \ No newline at end of file diff --git a/v2_adminpanel/routes/api_routes.py.backup b/v2_adminpanel/routes/api_routes.py.backup deleted file mode 100644 index 7d1e834..0000000 --- a/v2_adminpanel/routes/api_routes.py.backup +++ /dev/null @@ -1,921 +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//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['is_active'] - - # Update status - cur.execute("UPDATE licenses SET is_active = %s WHERE id = %s", (new_status, license_id)) - conn.commit() - - # Log change - log_audit('TOGGLE', 'license', license_id, - old_values={'is_active': license_data['is_active']}, - new_values={'is_active': new_status}) - - return jsonify({'success': True, 'is_active': new_status}) - - except Exception as e: - conn.rollback() - logging.error(f"Fehler beim Umschalten der Lizenz: {str(e)}", exc_info=True) - 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 is_active = true - WHERE id = ANY(%s) AND is_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={'is_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 is_active = false - WHERE id = ANY(%s) AND is_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={'is_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//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//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//deactivate-device/", 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//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("is_active = %s") - params.append(bool(data['active'])) - old_values['is_active'] = current_license['is_active'] - new_values['is_active'] = bool(data['active']) - - if not updates: - return jsonify({'error': 'Keine Änderungen angegeben'}), 400 - - # 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//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""" - # Einzelne Ressource prüfen (alte API) - resource_type = request.args.get('type') - if resource_type: - count = int(request.args.get('count', 1)) - is_test = request.args.get('is_test', 'false') == 'true' - show_test = request.args.get('show_test', 'false') == 'true' - - conn = get_connection() - cur = conn.cursor() - - try: - # Hole verfügbare Ressourcen mit Details - if show_test: - # Zeige alle verfügbaren Ressourcen (Test und Produktion) - cur.execute(""" - SELECT id, resource_value, is_test - FROM resource_pools - WHERE resource_type = %s - AND status = 'available' - ORDER BY is_test, resource_value - LIMIT %s - """, (resource_type, count)) - else: - # Zeige nur Produktions-Ressourcen - cur.execute(""" - SELECT id, resource_value, is_test - FROM resource_pools - WHERE resource_type = %s - AND status = 'available' - AND is_test = false - ORDER BY resource_value - LIMIT %s - """, (resource_type, count)) - - available_resources = [] - for row in cur.fetchall(): - available_resources.append({ - 'id': row[0], - 'value': row[1], - 'is_test': row[2] - }) - - return jsonify({ - 'resource_type': resource_type, - 'requested': count, - 'available': available_resources, - 'sufficient': len(available_resources) >= count, - 'show_test': show_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() - - # Mehrere Ressourcen gleichzeitig prüfen (für Batch) - domain_count = int(request.args.get('domain', 0)) - ipv4_count = int(request.args.get('ipv4', 0)) - phone_count = int(request.args.get('phone', 0)) - is_test = request.args.get('is_test', 'false') == 'true' - - conn = get_connection() - cur = conn.cursor() - - try: - # Zähle verfügbare Ressourcen für jeden Typ - result = {} - - # Domains - cur.execute(""" - SELECT COUNT(*) - FROM resource_pools - WHERE resource_type = 'domain' - AND status = 'available' - AND is_test = %s - """, (is_test,)) - domain_available = cur.fetchone()[0] - - # IPv4 - cur.execute(""" - SELECT COUNT(*) - FROM resource_pools - WHERE resource_type = 'ipv4' - AND status = 'available' - AND is_test = %s - """, (is_test,)) - ipv4_available = cur.fetchone()[0] - - # Phones - cur.execute(""" - SELECT COUNT(*) - FROM resource_pools - WHERE resource_type = 'phone' - AND status = 'available' - AND is_test = %s - """, (is_test,)) - phone_available = cur.fetchone()[0] - - return jsonify({ - 'domain_requested': domain_count, - 'domain_available': domain_available, - 'domain_sufficient': domain_available >= domain_count, - 'ipv4_requested': ipv4_count, - 'ipv4_available': ipv4_available, - 'ipv4_sufficient': ipv4_available >= ipv4_count, - 'phone_requested': phone_count, - 'phone_available': phone_available, - 'phone_sufficient': phone_available >= phone_count, - 'all_sufficient': ( - domain_available >= domain_count and - ipv4_available >= ipv4_count and - phone_available >= phone_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, is_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], - 'is_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 - - - diff --git a/v2_adminpanel/routes/batch_routes.py.backup b/v2_adminpanel/routes/batch_routes.py.backup deleted file mode 100644 index 9f421a9..0000000 --- a/v2_adminpanel/routes/batch_routes.py.backup +++ /dev/null @@ -1,373 +0,0 @@ -import os -import logging -import secrets -import string -from datetime import datetime, timedelta -from pathlib import Path -from flask import Blueprint, render_template, request, redirect, session, url_for, flash, send_file - -import config -from auth.decorators import login_required -from utils.audit import log_audit -from utils.network import get_client_ip -from utils.export import create_batch_export -from db import get_connection, get_db_connection, get_db_cursor -from models import get_customers - -# Create Blueprint -batch_bp = Blueprint('batch', __name__) - - -def generate_license_key(): - """Generiert einen zufälligen Lizenzschlüssel""" - chars = string.ascii_uppercase + string.digits - return '-'.join([''.join(secrets.choice(chars) for _ in range(4)) for _ in range(4)]) - - -@batch_bp.route("/batch", methods=["GET", "POST"]) -@login_required -def batch_create(): - """Batch-Erstellung von Lizenzen""" - customers = get_customers() - - if request.method == "POST": - conn = get_connection() - cur = conn.cursor() - - try: - # Form data - customer_id = int(request.form['customer_id']) - license_type = request.form['license_type'] - count = int(request.form['quantity']) # Korrigiert von 'count' zu 'quantity' - valid_from = request.form['valid_from'] - valid_until = request.form['valid_until'] - device_limit = int(request.form['device_limit']) - is_test = 'is_test' in request.form - - # Validierung - if count < 1 or count > 100: - flash('Anzahl muss zwischen 1 und 100 liegen!', 'error') - return redirect(url_for('batch.batch_create')) - - # Hole Kundendaten - cur.execute("SELECT name, email FROM customers WHERE id = %s", (customer_id,)) - customer = cur.fetchone() - if not customer: - flash('Kunde nicht gefunden!', 'error') - return redirect(url_for('batch.batch_create')) - - created_licenses = [] - - # Erstelle Lizenzen - for i in range(count): - license_key = generate_license_key() - - # Prüfe ob Schlüssel bereits existiert - while True: - cur.execute("SELECT id FROM licenses WHERE license_key = %s", (license_key,)) - if not cur.fetchone(): - break - license_key = generate_license_key() - - # Erstelle Lizenz - cur.execute(""" - INSERT INTO licenses ( - license_key, customer_id, - license_type, valid_from, valid_until, device_limit, - is_test, created_at - ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s) - RETURNING id - """, ( - license_key, customer_id, - license_type, valid_from, valid_until, device_limit, - is_test, datetime.now() - )) - - license_id = cur.fetchone()[0] - created_licenses.append({ - 'id': license_id, - 'license_key': license_key - }) - - # Audit-Log - log_audit('CREATE', 'license', license_id, - new_values={ - 'license_key': license_key, - 'customer_name': customer[0], - 'batch_creation': True - }) - - conn.commit() - - # Speichere erstellte Lizenzen in Session für Export - session['batch_created_licenses'] = created_licenses - session['batch_customer_name'] = customer[0] - session['batch_customer_email'] = customer[1] - - flash(f'{count} Lizenzen erfolgreich erstellt!', 'success') - - # Weiterleitung zum Export - return redirect(url_for('batch.batch_export')) - - except Exception as e: - conn.rollback() - logging.error(f"Fehler bei Batch-Erstellung: {str(e)}") - flash('Fehler bei der Batch-Erstellung!', 'error') - finally: - cur.close() - conn.close() - - return render_template("batch_form.html", customers=customers) - - -@batch_bp.route("/batch/export") -@login_required -def batch_export(): - """Exportiert die zuletzt erstellten Batch-Lizenzen""" - created_licenses = session.get('batch_created_licenses', []) - - if not created_licenses: - flash('Keine Lizenzen zum Exportieren gefunden!', 'error') - return redirect(url_for('batch.batch_create')) - - conn = get_connection() - cur = conn.cursor() - - try: - # Hole vollständige Lizenzdaten - license_ids = [l['id'] for l in created_licenses] - - cur.execute(""" - SELECT - l.license_key, c.name, c.email, - l.license_type, l.valid_from, l.valid_until, - l.device_limit, l.is_test, l.created_at - FROM licenses l - JOIN customers c ON l.customer_id = c.id - WHERE l.id = ANY(%s) - ORDER BY l.id - """, (license_ids,)) - - licenses = [] - for row in cur.fetchall(): - licenses.append({ - 'license_key': row[0], - 'customer_name': row[1], - 'customer_email': row[2], - 'license_type': row[3], - 'valid_from': row[4], - 'valid_until': row[5], - 'device_limit': row[6], - 'is_test': row[7], - 'created_at': row[8] - }) - - # Lösche aus Session - session.pop('batch_created_licenses', None) - session.pop('batch_customer_name', None) - session.pop('batch_customer_email', None) - - # Erstelle und sende Excel-Export - return create_batch_export(licenses) - - except Exception as e: - logging.error(f"Fehler beim Export: {str(e)}") - flash('Fehler beim Exportieren der Lizenzen!', 'error') - return redirect(url_for('batch.batch_create')) - finally: - cur.close() - conn.close() - - -@batch_bp.route("/batch/update", methods=["GET", "POST"]) -@login_required -def batch_update(): - """Batch-Update von Lizenzen""" - if request.method == "POST": - conn = get_connection() - cur = conn.cursor() - - try: - # Form data - license_keys = request.form.get('license_keys', '').strip().split('\n') - license_keys = [key.strip() for key in license_keys if key.strip()] - - if not license_keys: - flash('Keine Lizenzschlüssel angegeben!', 'error') - return redirect(url_for('batch.batch_update')) - - # Update-Parameter - updates = [] - params = [] - - if 'update_valid_until' in request.form and request.form['valid_until']: - updates.append("valid_until = %s") - params.append(request.form['valid_until']) - - if 'update_device_limit' in request.form and request.form['device_limit']: - updates.append("device_limit = %s") - params.append(int(request.form['device_limit'])) - - if 'update_active' in request.form: - updates.append("active = %s") - params.append('active' in request.form) - - if not updates: - flash('Keine Änderungen angegeben!', 'error') - return redirect(url_for('batch.batch_update')) - - # Führe Updates aus - updated_count = 0 - not_found = [] - - for license_key in license_keys: - # Prüfe ob Lizenz existiert - cur.execute("SELECT id FROM licenses WHERE license_key = %s", (license_key,)) - result = cur.fetchone() - - if not result: - not_found.append(license_key) - continue - - license_id = result[0] - - # Update ausführen - update_params = params + [license_id] - cur.execute(f""" - UPDATE licenses - SET {', '.join(updates)} - WHERE id = %s - """, update_params) - - # Audit-Log - log_audit('BATCH_UPDATE', 'license', license_id, - additional_info=f"Batch-Update: {', '.join(updates)}") - - updated_count += 1 - - conn.commit() - - # Feedback - flash(f'{updated_count} Lizenzen erfolgreich aktualisiert!', 'success') - - if not_found: - flash(f'{len(not_found)} Lizenzen nicht gefunden: {", ".join(not_found[:5])}{"..." if len(not_found) > 5 else ""}', 'warning') - - except Exception as e: - conn.rollback() - logging.error(f"Fehler bei Batch-Update: {str(e)}") - flash('Fehler beim Batch-Update!', 'error') - finally: - cur.close() - conn.close() - - return render_template("batch_update.html") - - -@batch_bp.route("/batch/import", methods=["GET", "POST"]) -@login_required -def batch_import(): - """Import von Lizenzen aus CSV/Excel""" - if request.method == "POST": - if 'file' not in request.files: - flash('Keine Datei ausgewählt!', 'error') - return redirect(url_for('batch.batch_import')) - - file = request.files['file'] - if file.filename == '': - flash('Keine Datei ausgewählt!', 'error') - return redirect(url_for('batch.batch_import')) - - # Verarbeite Datei - try: - import pandas as pd - - # Lese Datei - if file.filename.endswith('.csv'): - df = pd.read_csv(file) - elif file.filename.endswith(('.xlsx', '.xls')): - df = pd.read_excel(file) - else: - flash('Ungültiges Dateiformat! Nur CSV und Excel erlaubt.', 'error') - return redirect(url_for('batch.batch_import')) - - # Validiere Spalten - required_columns = ['customer_email', 'license_type', 'valid_from', 'valid_until', 'device_limit'] - missing_columns = [col for col in required_columns if col not in df.columns] - - if missing_columns: - flash(f'Fehlende Spalten: {", ".join(missing_columns)}', 'error') - return redirect(url_for('batch.batch_import')) - - conn = get_connection() - cur = conn.cursor() - - imported_count = 0 - errors = [] - - for index, row in df.iterrows(): - try: - # Finde oder erstelle Kunde - email = row['customer_email'] - cur.execute("SELECT id, name FROM customers WHERE email = %s", (email,)) - customer = cur.fetchone() - - if not customer: - # Erstelle neuen Kunden - name = row.get('customer_name', email.split('@')[0]) - cur.execute(""" - INSERT INTO customers (name, email, created_at) - VALUES (%s, %s, %s) - RETURNING id - """, (name, email, datetime.now())) - customer_id = cur.fetchone()[0] - customer_name = name - else: - customer_id = customer[0] - customer_name = customer[1] - - # Generiere Lizenzschlüssel - license_key = row.get('license_key', generate_license_key()) - - # Erstelle Lizenz - cur.execute(""" - INSERT INTO licenses ( - license_key, customer_id, - license_type, valid_from, valid_until, device_limit, - is_test, created_at - ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s) - RETURNING id - """, ( - license_key, customer_id, - row['license_type'], row['valid_from'], row['valid_until'], - int(row['device_limit']), row.get('is_test', False), - datetime.now() - )) - - license_id = cur.fetchone()[0] - imported_count += 1 - - # Audit-Log - log_audit('IMPORT', 'license', license_id, - additional_info=f"Importiert aus {file.filename}") - - except Exception as e: - errors.append(f"Zeile {index + 2}: {str(e)}") - - conn.commit() - - # Feedback - flash(f'{imported_count} Lizenzen erfolgreich importiert!', 'success') - - if errors: - flash(f'{len(errors)} Fehler aufgetreten. Erste Fehler: {"; ".join(errors[:3])}', 'warning') - - except Exception as e: - logging.error(f"Fehler beim Import: {str(e)}") - flash(f'Fehler beim Import: {str(e)}', 'error') - finally: - if 'conn' in locals(): - cur.close() - conn.close() - - return render_template("batch_import.html") \ No newline at end of file diff --git a/v2_adminpanel/routes/customer_routes.py.backup b/v2_adminpanel/routes/customer_routes.py.backup deleted file mode 100644 index 8648f37..0000000 --- a/v2_adminpanel/routes/customer_routes.py.backup +++ /dev/null @@ -1,461 +0,0 @@ -import os -import logging -from datetime import datetime -from zoneinfo import ZoneInfo -from flask import Blueprint, render_template, request, redirect, session, url_for, flash, jsonify - -import config -from auth.decorators import login_required -from utils.audit import log_audit -from db import get_connection, get_db_connection, get_db_cursor -from models import get_customers, get_customer_by_id - -# Create Blueprint -customer_bp = Blueprint('customers', __name__) - -# Test route -@customer_bp.route("/test-customers") -def test_customers(): - return "Customer blueprint is working!" - - -@customer_bp.route("/customers") -@login_required -def customers(): - show_test = request.args.get('show_test', 'false').lower() == 'true' - search = request.args.get('search', '').strip() - page = request.args.get('page', 1, type=int) - per_page = 20 - sort = request.args.get('sort', 'name') - order = request.args.get('order', 'asc') - - customers_list = get_customers(show_test=show_test, search=search) - - # Sortierung - if sort == 'name': - customers_list.sort(key=lambda x: x['name'].lower(), reverse=(order == 'desc')) - elif sort == 'email': - customers_list.sort(key=lambda x: x['email'].lower(), reverse=(order == 'desc')) - elif sort == 'created_at': - customers_list.sort(key=lambda x: x['created_at'], reverse=(order == 'desc')) - - # Paginierung - total_customers = len(customers_list) - total_pages = (total_customers + per_page - 1) // per_page - start = (page - 1) * per_page - end = start + per_page - paginated_customers = customers_list[start:end] - - return render_template("customers.html", - customers=paginated_customers, - show_test=show_test, - 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/", methods=["GET", "POST"]) -@login_required -def edit_customer(customer_id): - if request.method == "POST": - try: - # Get current customer data for comparison - current_customer = get_customer_by_id(customer_id) - if not current_customer: - flash('Kunde nicht gefunden!', 'error') - return redirect(url_for('customers.customers')) - - with get_db_connection() as conn: - cur = conn.cursor() - try: - # Update customer data - new_values = { - 'name': request.form['name'], - 'email': request.form['email'], - 'is_test': 'is_test' in request.form - } - - cur.execute(""" - UPDATE customers - SET name = %s, email = %s, is_test = %s - WHERE id = %s - """, ( - new_values['name'], - new_values['email'], - new_values['is_test'], - customer_id - )) - - conn.commit() - - # Log changes - log_audit('UPDATE', 'customer', customer_id, - old_values={ - 'name': current_customer['name'], - 'email': current_customer['email'], - 'is_test': current_customer.get('is_test', False) - }, - new_values=new_values) - - flash('Kunde erfolgreich aktualisiert!', 'success') - - # Redirect mit show_test Parameter wenn nötig - redirect_url = url_for('customers.customers') - if request.form.get('show_test') == 'true': - redirect_url += '?show_test=true' - return redirect(redirect_url) - finally: - cur.close() - - except Exception as e: - logging.error(f"Fehler beim Aktualisieren des Kunden: {str(e)}") - flash('Fehler beim Aktualisieren des Kunden!', 'error') - return redirect(url_for('customers.customers')) - - # GET request - customer_data = get_customer_by_id(customer_id) - if not customer_data: - flash('Kunde nicht gefunden!', 'error') - return redirect(url_for('customers.customers')) - - return render_template("edit_customer.html", customer=customer_data) - - -@customer_bp.route("/customer/create", methods=["GET", "POST"]) -@login_required -def create_customer(): - if request.method == "POST": - conn = get_connection() - cur = conn.cursor() - - try: - # Insert new customer - name = request.form['name'] - email = request.form['email'] - - cur.execute(""" - INSERT INTO customers (name, email, created_at) - VALUES (%s, %s, %s) - RETURNING id - """, (name, email, datetime.now())) - - customer_id = cur.fetchone()[0] - conn.commit() - - # Log creation - log_audit('CREATE', 'customer', customer_id, - new_values={ - 'name': name, - 'email': email - }) - - flash(f'Kunde {name} erfolgreich erstellt!', 'success') - return redirect(url_for('customers.customers')) - - except Exception as e: - conn.rollback() - logging.error(f"Fehler beim Erstellen des Kunden: {str(e)}") - flash('Fehler beim Erstellen des Kunden!', 'error') - finally: - cur.close() - conn.close() - - return render_template("create_customer.html") - - -@customer_bp.route("/customer/delete/", methods=["POST"]) -@login_required -def delete_customer(customer_id): - conn = get_connection() - cur = conn.cursor() - - try: - # Get customer data before deletion - customer_data = get_customer_by_id(customer_id) - if not customer_data: - flash('Kunde nicht gefunden!', 'error') - return redirect(url_for('customers.customers')) - - # Check if customer has licenses - cur.execute("SELECT COUNT(*) FROM licenses WHERE customer_id = %s", (customer_id,)) - license_count = cur.fetchone()[0] - - if license_count > 0: - flash(f'Kunde kann nicht gelöscht werden - hat noch {license_count} Lizenz(en)!', 'error') - return redirect(url_for('customers.customers')) - - # Delete the customer - cur.execute("DELETE FROM customers WHERE id = %s", (customer_id,)) - - conn.commit() - - # Log deletion - log_audit('DELETE', 'customer', customer_id, - old_values={ - 'name': customer_data['name'], - 'email': customer_data['email'] - }) - - flash(f'Kunde {customer_data["name"]} erfolgreich gelöscht!', 'success') - - except Exception as e: - conn.rollback() - logging.error(f"Fehler beim Löschen des Kunden: {str(e)}") - flash('Fehler beim Löschen des Kunden!', 'error') - finally: - cur.close() - conn.close() - - return redirect(url_for('customers.customers')) - - -@customer_bp.route("/customers-licenses") -@login_required -def customers_licenses(): - """Zeigt die Übersicht von Kunden und deren Lizenzen""" - import logging - import psycopg2 - logging.info("=== CUSTOMERS-LICENSES ROUTE CALLED ===") - - # Get show_test parameter from URL - show_test = request.args.get('show_test', 'false').lower() == 'true' - logging.info(f"show_test parameter: {show_test}") - - try: - # Direkte Verbindung ohne Helper-Funktionen - conn = 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") - ) - conn.set_client_encoding('UTF8') - cur = conn.cursor() - - try: - # Hole alle Kunden mit ihren Lizenzen - # Wenn show_test=false, zeige nur Nicht-Test-Kunden - query = """ - SELECT - c.id, - c.name, - c.email, - c.created_at, - COUNT(l.id), - COUNT(CASE WHEN l.is_active = true THEN 1 END), - COUNT(CASE WHEN l.is_test = true THEN 1 END), - MAX(l.created_at), - c.is_test - FROM customers c - LEFT JOIN licenses l ON c.id = l.customer_id - WHERE (%s OR c.is_test = false) - GROUP BY c.id, c.name, c.email, c.created_at, c.is_test - ORDER BY c.name - """ - - cur.execute(query, (show_test,)) - - customers = [] - results = cur.fetchall() - logging.info(f"=== QUERY RETURNED {len(results)} ROWS ===") - - for idx, row in enumerate(results): - logging.info(f"Row {idx}: Type={type(row)}, Length={len(row) if hasattr(row, '__len__') else 'N/A'}") - customers.append({ - 'id': row[0], - 'name': row[1], - 'email': row[2], - 'created_at': row[3], - 'license_count': row[4], - 'active_licenses': row[5], - 'test_licenses': row[6], - 'last_license_created': row[7], - 'is_test': row[8] - }) - - return render_template("customers_licenses.html", - customers=customers, - show_test=show_test) - - finally: - cur.close() - conn.close() - - except Exception as e: - import traceback - error_details = f"Fehler beim Laden der Kunden-Lizenz-Übersicht: {str(e)}\nType: {type(e)}\nTraceback: {traceback.format_exc()}" - logging.error(error_details) - flash(f'Datenbankfehler: {str(e)}', 'error') - return redirect(url_for('admin.dashboard')) - - -@customer_bp.route("/api/customer//licenses") -@login_required -def api_customer_licenses(customer_id): - """API-Endpunkt für die Lizenzen eines Kunden""" - conn = get_connection() - cur = conn.cursor() - - try: - # Hole Kundeninformationen - customer = get_customer_by_id(customer_id) - if not customer: - return jsonify({'error': 'Kunde nicht gefunden'}), 404 - - # Hole alle Lizenzen des Kunden - cur.execute(""" - SELECT - l.id, - l.license_key, - l.license_type, - l.is_active, - l.is_test, - l.valid_from, - l.valid_until, - l.device_limit, - l.created_at, - (SELECT COUNT(*) FROM sessions s WHERE s.license_id = l.id AND s.is_active = true) as active_sessions, - (SELECT COUNT(DISTINCT hardware_id) FROM device_registrations dr WHERE dr.license_id = l.id) as registered_devices, - CASE - WHEN l.valid_until < CURRENT_DATE THEN 'abgelaufen' - WHEN l.valid_until < CURRENT_DATE + INTERVAL '30 days' THEN 'läuft bald ab' - WHEN l.is_active = false THEN 'inaktiv' - ELSE 'aktiv' - END as status, - l.domain_count, - l.ipv4_count, - l.phone_count, - (SELECT COUNT(*) FROM device_registrations WHERE license_id = l.id AND is_active = TRUE) as active_devices, - -- Actual resource counts - (SELECT COUNT(*) FROM license_resources lr - JOIN resource_pools rp ON lr.resource_id = rp.id - WHERE lr.license_id = l.id AND lr.is_active = true AND rp.resource_type = 'domain') as actual_domain_count, - (SELECT COUNT(*) FROM license_resources lr - JOIN resource_pools rp ON lr.resource_id = rp.id - WHERE lr.license_id = l.id AND lr.is_active = true AND rp.resource_type = 'ipv4') as actual_ipv4_count, - (SELECT COUNT(*) FROM license_resources lr - JOIN resource_pools rp ON lr.resource_id = rp.id - WHERE lr.license_id = l.id AND lr.is_active = true AND rp.resource_type = 'phone') as actual_phone_count - FROM licenses l - WHERE l.customer_id = %s - ORDER BY l.created_at DESC - """, (customer_id,)) - - licenses = [] - for row in cur.fetchall(): - license_id = row[0] - - # Hole die konkreten zugewiesenen Ressourcen für diese Lizenz - conn2 = get_connection() - cur2 = conn2.cursor() - cur2.execute(""" - SELECT rp.id, rp.resource_type, rp.resource_value, lr.assigned_at - FROM resource_pools rp - JOIN license_resources lr ON rp.id = lr.resource_id - WHERE lr.license_id = %s AND lr.is_active = true - ORDER BY rp.resource_type, rp.resource_value - """, (license_id,)) - - resources = { - 'domains': [], - 'ipv4s': [], - 'phones': [] - } - - for res_row in cur2.fetchall(): - resource_data = { - 'id': res_row[0], - 'value': res_row[2], - 'assigned_at': res_row[3].strftime('%Y-%m-%d %H:%M:%S') if res_row[3] else None - } - - if res_row[1] == 'domain': - resources['domains'].append(resource_data) - elif res_row[1] == 'ipv4': - resources['ipv4s'].append(resource_data) - elif res_row[1] == 'phone': - resources['phones'].append(resource_data) - - cur2.close() - conn2.close() - - licenses.append({ - 'id': row[0], - 'license_key': row[1], - 'license_type': row[2], - 'is_active': row[3], # Korrigiert von 'active' zu 'is_active' - 'is_test': row[4], - 'valid_from': row[5].strftime('%Y-%m-%d') if row[5] else None, - 'valid_until': row[6].strftime('%Y-%m-%d') if row[6] else None, - 'device_limit': row[7], - 'created_at': row[8].strftime('%Y-%m-%d %H:%M:%S') if row[8] else None, - 'active_sessions': row[9], - 'registered_devices': row[10], - 'status': row[11], - 'domain_count': row[12], - 'ipv4_count': row[13], - 'phone_count': row[14], - 'active_devices': row[15], - 'actual_domain_count': row[16], - 'actual_ipv4_count': row[17], - 'actual_phone_count': row[18], - 'resources': resources - }) - - return jsonify({ - 'success': True, # Wichtig: Frontend erwartet dieses Feld - 'customer': { - 'id': customer['id'], - 'name': customer['name'], - 'email': customer['email'] - }, - 'licenses': licenses - }) - - except Exception as e: - logging.error(f"Fehler beim Laden der Kundenlizenzen: {str(e)}") - return jsonify({'error': 'Fehler beim Laden der Daten'}), 500 - finally: - cur.close() - conn.close() - - -@customer_bp.route("/api/customer//quick-stats") -@login_required -def api_customer_quick_stats(customer_id): - """Schnelle Statistiken für einen Kunden""" - conn = get_connection() - cur = conn.cursor() - - try: - cur.execute(""" - SELECT - COUNT(l.id) as total_licenses, - COUNT(CASE WHEN l.is_active = true THEN 1 END) as active_licenses, - COUNT(CASE WHEN l.is_test = true THEN 1 END) as test_licenses, - SUM(l.device_limit) as total_device_limit - FROM licenses l - WHERE l.customer_id = %s - """, (customer_id,)) - - row = cur.fetchone() - - return jsonify({ - 'total_licenses': row[0] or 0, - 'active_licenses': row[1] or 0, - 'test_licenses': row[2] or 0, - 'total_device_limit': row[3] or 0 - }) - - except Exception as e: - logging.error(f"Fehler beim Laden der Kundenstatistiken: {str(e)}") - return jsonify({'error': 'Fehler beim Laden der Daten'}), 500 - finally: - cur.close() - conn.close() \ No newline at end of file diff --git a/v2_adminpanel/routes/export_routes.py.backup b/v2_adminpanel/routes/export_routes.py.backup deleted file mode 100644 index bd184d0..0000000 --- a/v2_adminpanel/routes/export_routes.py.backup +++ /dev/null @@ -1,364 +0,0 @@ -import logging -from datetime import datetime, timedelta -from zoneinfo import ZoneInfo -from flask import Blueprint, request, send_file - -import config -from auth.decorators import login_required -from utils.export import create_excel_export, prepare_audit_export_data -from db import get_connection - -# Create Blueprint -export_bp = Blueprint('export', __name__, url_prefix='/export') - - -@export_bp.route("/licenses") -@login_required -def export_licenses(): - """Exportiert Lizenzen als Excel-Datei""" - conn = get_connection() - cur = conn.cursor() - - try: - # Filter aus Request - show_test = request.args.get('show_test', 'false') == 'true' - - # SQL Query mit optionalem Test-Filter - if show_test: - query = """ - SELECT - l.id, - l.license_key, - c.name as customer_name, - c.email as customer_email, - l.license_type, - l.valid_from, - l.valid_until, - l.active, - l.device_limit, - l.created_at, - l.is_test, - CASE - WHEN l.valid_until < CURRENT_DATE THEN 'Abgelaufen' - WHEN l.active = false THEN 'Deaktiviert' - ELSE 'Aktiv' - END as status, - (SELECT COUNT(*) FROM sessions s WHERE s.license_key = l.license_key AND s.active = true) as active_sessions, - (SELECT COUNT(DISTINCT device_id) FROM sessions s WHERE s.license_key = l.license_key) as registered_devices - FROM licenses l - LEFT JOIN customers c ON l.customer_id = c.id - ORDER BY l.created_at DESC - """ - else: - query = """ - SELECT - l.id, - l.license_key, - c.name as customer_name, - c.email as customer_email, - l.license_type, - l.valid_from, - l.valid_until, - l.active, - l.device_limit, - l.created_at, - l.is_test, - CASE - WHEN l.valid_until < CURRENT_DATE THEN 'Abgelaufen' - WHEN l.active = false THEN 'Deaktiviert' - ELSE 'Aktiv' - END as status, - (SELECT COUNT(*) FROM sessions s WHERE s.license_key = l.license_key AND s.active = true) as active_sessions, - (SELECT COUNT(DISTINCT device_id) FROM sessions s WHERE s.license_key = l.license_key) as registered_devices - FROM licenses l - LEFT JOIN customers c ON l.customer_id = c.id - WHERE l.is_test = false - ORDER BY l.created_at DESC - """ - - cur.execute(query) - - # Daten für Export vorbereiten - data = [] - columns = ['ID', 'Lizenzschlüssel', 'Kunde', 'E-Mail', 'Typ', 'Gültig von', - 'Gültig bis', 'Aktiv', 'Gerätelimit', 'Erstellt am', 'Test-Lizenz', - 'Status', 'Aktive Sessions', 'Registrierte Geräte'] - - for row in cur.fetchall(): - data.append(list(row)) - - # Excel-Datei erstellen - excel_file = create_excel_export(data, columns, 'Lizenzen') - - # Datei senden - filename = f"lizenzen_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx" - return send_file( - excel_file, - mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - as_attachment=True, - download_name=filename - ) - - except Exception as e: - logging.error(f"Fehler beim Export: {str(e)}") - return "Fehler beim Exportieren der Lizenzen", 500 - finally: - cur.close() - conn.close() - - -@export_bp.route("/audit") -@login_required -def export_audit(): - """Exportiert Audit-Logs als Excel-Datei""" - conn = get_connection() - cur = conn.cursor() - - try: - # Filter aus Request - days = int(request.args.get('days', 30)) - action_filter = request.args.get('action', '') - entity_type_filter = request.args.get('entity_type', '') - - # Daten für Export vorbereiten - data = prepare_audit_export_data(days, action_filter, entity_type_filter) - - # Excel-Datei erstellen - columns = ['Zeitstempel', 'Benutzer', 'Aktion', 'Entität', 'Entität ID', - 'IP-Adresse', 'Alte Werte', 'Neue Werte', 'Zusatzinfo'] - - excel_file = create_excel_export(data, columns, 'Audit-Log') - - # Datei senden - filename = f"audit_log_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx" - return send_file( - excel_file, - mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - as_attachment=True, - download_name=filename - ) - - except Exception as e: - logging.error(f"Fehler beim Export: {str(e)}") - return "Fehler beim Exportieren der Audit-Logs", 500 - finally: - cur.close() - conn.close() - - -@export_bp.route("/customers") -@login_required -def export_customers(): - """Exportiert Kunden als Excel-Datei""" - conn = get_connection() - cur = conn.cursor() - - try: - # SQL Query - cur.execute(""" - SELECT - c.id, - c.name, - c.email, - c.phone, - c.address, - c.created_at, - c.is_test, - COUNT(l.id) as license_count, - COUNT(CASE WHEN l.active = true THEN 1 END) as active_licenses, - COUNT(CASE WHEN l.valid_until < CURRENT_DATE THEN 1 END) as expired_licenses - FROM customers c - LEFT JOIN licenses l ON c.id = l.customer_id - GROUP BY c.id, c.name, c.email, c.phone, c.address, c.created_at, c.is_test - ORDER BY c.name - """) - - # Daten für Export vorbereiten - data = [] - columns = ['ID', 'Name', 'E-Mail', 'Telefon', 'Adresse', 'Erstellt am', - 'Test-Kunde', 'Anzahl Lizenzen', 'Aktive Lizenzen', 'Abgelaufene Lizenzen'] - - for row in cur.fetchall(): - data.append(list(row)) - - # Excel-Datei erstellen - excel_file = create_excel_export(data, columns, 'Kunden') - - # Datei senden - filename = f"kunden_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx" - return send_file( - excel_file, - mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - as_attachment=True, - download_name=filename - ) - - except Exception as e: - logging.error(f"Fehler beim Export: {str(e)}") - return "Fehler beim Exportieren der Kunden", 500 - finally: - cur.close() - conn.close() - - -@export_bp.route("/sessions") -@login_required -def export_sessions(): - """Exportiert Sessions als Excel-Datei""" - conn = get_connection() - cur = conn.cursor() - - try: - # Filter aus Request - days = int(request.args.get('days', 7)) - active_only = request.args.get('active_only', 'false') == 'true' - - # SQL Query - if active_only: - query = """ - SELECT - s.id, - s.license_key, - l.customer_name, - s.username, - s.device_id, - s.login_time, - s.logout_time, - s.last_activity, - s.active, - l.license_type, - l.is_test - FROM sessions s - LEFT JOIN licenses l ON s.license_key = l.license_key - WHERE s.active = true - ORDER BY s.login_time DESC - """ - cur.execute(query) - else: - query = """ - SELECT - s.id, - s.license_key, - l.customer_name, - s.username, - s.device_id, - s.login_time, - s.logout_time, - s.last_activity, - s.active, - l.license_type, - l.is_test - FROM sessions s - LEFT JOIN licenses l ON s.license_key = l.license_key - WHERE s.login_time >= CURRENT_TIMESTAMP - INTERVAL '%s days' - ORDER BY s.login_time DESC - """ - cur.execute(query, (days,)) - - # Daten für Export vorbereiten - data = [] - columns = ['ID', 'Lizenzschlüssel', 'Kunde', 'Benutzer', 'Geräte-ID', - 'Login-Zeit', 'Logout-Zeit', 'Letzte Aktivität', 'Aktiv', - 'Lizenztyp', 'Test-Lizenz'] - - for row in cur.fetchall(): - data.append(list(row)) - - # Excel-Datei erstellen - excel_file = create_excel_export(data, columns, 'Sessions') - - # Datei senden - filename = f"sessions_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx" - return send_file( - excel_file, - mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - as_attachment=True, - download_name=filename - ) - - except Exception as e: - logging.error(f"Fehler beim Export: {str(e)}") - return "Fehler beim Exportieren der Sessions", 500 - finally: - cur.close() - conn.close() - - -@export_bp.route("/resources") -@login_required -def export_resources(): - """Exportiert Ressourcen als Excel-Datei""" - conn = get_connection() - cur = conn.cursor() - - try: - # Filter aus Request - resource_type = request.args.get('type', 'all') - status_filter = request.args.get('status', 'all') - show_test = request.args.get('show_test', 'false') == 'true' - - # SQL Query aufbauen - query = """ - SELECT - rp.id, - rp.resource_type, - rp.resource_value, - rp.status, - rp.is_test, - l.license_key, - c.name as customer_name, - rp.created_at, - rp.created_by, - rp.status_changed_at, - rp.status_changed_by, - rp.quarantine_reason - FROM resource_pools rp - LEFT JOIN licenses l ON rp.allocated_to_license = l.id - LEFT JOIN customers c ON l.customer_id = c.id - WHERE 1=1 - """ - - params = [] - - if resource_type != 'all': - query += " AND rp.resource_type = %s" - params.append(resource_type) - - if status_filter != 'all': - query += " AND rp.status = %s" - params.append(status_filter) - - if not show_test: - query += " AND rp.is_test = false" - - query += " ORDER BY rp.resource_type, rp.resource_value" - - cur.execute(query, params) - - # Daten für Export vorbereiten - data = [] - columns = ['ID', 'Typ', 'Wert', 'Status', 'Test-Ressource', 'Lizenzschlüssel', - 'Kunde', 'Erstellt am', 'Erstellt von', 'Status geändert am', - 'Status geändert von', 'Quarantäne-Grund'] - - for row in cur.fetchall(): - data.append(list(row)) - - # Excel-Datei erstellen - excel_file = create_excel_export(data, columns, 'Ressourcen') - - # Datei senden - filename = f"ressourcen_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx" - return send_file( - excel_file, - mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - as_attachment=True, - download_name=filename - ) - - except Exception as e: - logging.error(f"Fehler beim Export: {str(e)}") - return "Fehler beim Exportieren der Ressourcen", 500 - finally: - cur.close() - conn.close() \ No newline at end of file diff --git a/v2_adminpanel/routes/session_routes.py.backup b/v2_adminpanel/routes/session_routes.py.backup deleted file mode 100644 index c8d90c5..0000000 --- a/v2_adminpanel/routes/session_routes.py.backup +++ /dev/null @@ -1,429 +0,0 @@ -import logging -from datetime import datetime, timedelta -from zoneinfo import ZoneInfo -from flask import Blueprint, render_template, request, redirect, session, url_for, flash - -import config -from auth.decorators import login_required -from utils.audit import log_audit -from utils.network import get_client_ip -from db import get_connection, get_db_connection, get_db_cursor -from models import get_active_sessions - -# Create Blueprint -session_bp = Blueprint('sessions', __name__) - - -@session_bp.route("/sessions") -@login_required -def sessions(): - conn = get_connection() - cur = conn.cursor() - - try: - # Get active sessions with calculated inactive time - cur.execute(""" - SELECT s.id, s.session_id, l.license_key, c.name, s.ip_address, - s.user_agent, s.started_at, s.last_heartbeat, - EXTRACT(EPOCH FROM (NOW() - s.last_heartbeat))/60 as minutes_inactive - FROM sessions s - JOIN licenses l ON s.license_id = l.id - JOIN customers c ON l.customer_id = c.id - WHERE s.is_active = TRUE - ORDER BY s.last_heartbeat DESC - """) - active_sessions = cur.fetchall() - - # Get recent ended sessions - cur.execute(""" - SELECT s.id, s.session_id, l.license_key, c.name, s.ip_address, - s.started_at, s.ended_at, - EXTRACT(EPOCH FROM (s.ended_at - s.started_at))/60 as duration_minutes - FROM sessions s - JOIN licenses l ON s.license_id = l.id - JOIN customers c ON l.customer_id = c.id - WHERE s.is_active = FALSE - AND s.ended_at > NOW() - INTERVAL '24 hours' - ORDER BY s.ended_at DESC - LIMIT 50 - """) - recent_sessions = cur.fetchall() - - return render_template("sessions.html", - active_sessions=active_sessions, - recent_sessions=recent_sessions) - - except Exception as e: - logging.error(f"Error loading sessions: {str(e)}") - flash('Fehler beim Laden der Sessions!', 'error') - return redirect(url_for('admin.dashboard')) - finally: - cur.close() - conn.close() - - -@session_bp.route("/sessions/history") -@login_required -def session_history(): - """Zeigt die Session-Historie""" - conn = get_connection() - cur = conn.cursor() - - try: - # Query parameters - license_key = request.args.get('license_key', '') - username = request.args.get('username', '') - days = int(request.args.get('days', 7)) - - # Base query - query = """ - SELECT - s.id, - s.license_key, - s.username, - s.device_id, - s.login_time, - s.logout_time, - s.last_activity, - s.active, - l.customer_name, - l.license_type, - l.is_test - FROM sessions s - LEFT JOIN licenses l ON s.license_key = l.license_key - WHERE 1=1 - """ - - params = [] - - # Apply filters - if license_key: - query += " AND s.license_key = %s" - params.append(license_key) - - if username: - query += " AND s.username ILIKE %s" - params.append(f'%{username}%') - - # Time filter - query += " AND s.login_time >= CURRENT_TIMESTAMP - INTERVAL '%s days'" - params.append(days) - - query += " ORDER BY s.login_time DESC LIMIT 1000" - - cur.execute(query, params) - - sessions_list = [] - for row in cur.fetchall(): - session_duration = None - if row[4] and row[5]: # login_time and logout_time - duration = row[5] - row[4] - hours = int(duration.total_seconds() // 3600) - minutes = int((duration.total_seconds() % 3600) // 60) - session_duration = f"{hours}h {minutes}m" - elif row[4] and row[7]: # login_time and active - duration = datetime.now(ZoneInfo("UTC")) - row[4] - hours = int(duration.total_seconds() // 3600) - minutes = int((duration.total_seconds() % 3600) // 60) - session_duration = f"{hours}h {minutes}m (aktiv)" - - sessions_list.append({ - 'id': row[0], - 'license_key': row[1], - 'username': row[2], - 'device_id': row[3], - 'login_time': row[4], - 'logout_time': row[5], - 'last_activity': row[6], - 'active': row[7], - 'customer_name': row[8], - 'license_type': row[9], - 'is_test': row[10], - 'duration': session_duration - }) - - # Get unique license keys for filter dropdown - cur.execute(""" - SELECT DISTINCT s.license_key, l.customer_name - FROM sessions s - LEFT JOIN licenses l ON s.license_key = l.license_key - WHERE s.login_time >= CURRENT_TIMESTAMP - INTERVAL '30 days' - ORDER BY l.customer_name, s.license_key - """) - - available_licenses = [] - for row in cur.fetchall(): - available_licenses.append({ - 'license_key': row[0], - 'customer_name': row[1] or 'Unbekannt' - }) - - return render_template("session_history.html", - sessions=sessions_list, - available_licenses=available_licenses, - filters={ - 'license_key': license_key, - 'username': username, - 'days': days - }) - - except Exception as e: - logging.error(f"Fehler beim Laden der Session-Historie: {str(e)}") - flash('Fehler beim Laden der Session-Historie!', 'error') - return redirect(url_for('sessions.sessions')) - finally: - cur.close() - conn.close() - - -@session_bp.route("/session/end/", methods=["POST"]) -@login_required -def terminate_session(session_id): - """Beendet eine aktive Session""" - conn = get_connection() - cur = conn.cursor() - - try: - # Get session info - cur.execute(""" - SELECT license_key, username, device_id - FROM sessions - WHERE id = %s AND active = true - """, (session_id,)) - - session_info = cur.fetchone() - if not session_info: - flash('Session nicht gefunden oder bereits beendet!', 'error') - return redirect(url_for('sessions.sessions')) - - # Terminate session - cur.execute(""" - UPDATE sessions - SET active = false, logout_time = CURRENT_TIMESTAMP - WHERE id = %s - """, (session_id,)) - - conn.commit() - - # Audit log - log_audit('SESSION_TERMINATE', 'session', session_id, - additional_info=f"Session beendet für {session_info[1]} auf Lizenz {session_info[0]}") - - flash('Session erfolgreich beendet!', 'success') - - except Exception as e: - conn.rollback() - logging.error(f"Fehler beim Beenden der Session: {str(e)}") - flash('Fehler beim Beenden der Session!', 'error') - finally: - cur.close() - conn.close() - - return redirect(url_for('sessions.sessions')) - - -@session_bp.route("/sessions/terminate-all/", methods=["POST"]) -@login_required -def terminate_all_sessions(license_key): - """Beendet alle aktiven Sessions einer Lizenz""" - conn = get_connection() - cur = conn.cursor() - - try: - # Count active sessions - cur.execute(""" - SELECT COUNT(*) FROM sessions - WHERE license_key = %s AND active = true - """, (license_key,)) - - active_count = cur.fetchone()[0] - - if active_count == 0: - flash('Keine aktiven Sessions gefunden!', 'info') - return redirect(url_for('sessions.sessions')) - - # Terminate all sessions - cur.execute(""" - UPDATE sessions - SET active = false, logout_time = CURRENT_TIMESTAMP - WHERE license_key = %s AND active = true - """, (license_key,)) - - conn.commit() - - # Audit log - log_audit('SESSION_TERMINATE_ALL', 'license', None, - additional_info=f"{active_count} Sessions beendet für Lizenz {license_key}") - - flash(f'{active_count} Sessions erfolgreich beendet!', 'success') - - except Exception as e: - conn.rollback() - logging.error(f"Fehler beim Beenden der Sessions: {str(e)}") - flash('Fehler beim Beenden der Sessions!', 'error') - finally: - cur.close() - conn.close() - - return redirect(url_for('sessions.sessions')) - - -@session_bp.route("/sessions/cleanup", methods=["POST"]) -@login_required -def cleanup_sessions(): - """Bereinigt alte inaktive Sessions""" - conn = get_connection() - cur = conn.cursor() - - try: - days = int(request.form.get('days', 30)) - - # Delete old inactive sessions - cur.execute(""" - DELETE FROM sessions - WHERE active = false - AND logout_time < CURRENT_TIMESTAMP - INTERVAL '%s days' - RETURNING id - """, (days,)) - - deleted_ids = [row[0] for row in cur.fetchall()] - deleted_count = len(deleted_ids) - - conn.commit() - - # Audit log - if deleted_count > 0: - log_audit('SESSION_CLEANUP', 'system', None, - additional_info=f"{deleted_count} Sessions älter als {days} Tage gelöscht") - - flash(f'{deleted_count} alte Sessions bereinigt!', 'success') - - except Exception as e: - conn.rollback() - logging.error(f"Fehler beim Bereinigen der Sessions: {str(e)}") - flash('Fehler beim Bereinigen der Sessions!', 'error') - finally: - cur.close() - conn.close() - - return redirect(url_for('sessions.session_history')) - - -@session_bp.route("/sessions/statistics") -@login_required -def session_statistics(): - """Zeigt Session-Statistiken""" - conn = get_connection() - cur = conn.cursor() - - try: - # Aktuelle Statistiken - cur.execute(""" - SELECT - COUNT(DISTINCT s.license_key) as active_licenses, - COUNT(DISTINCT s.username) as unique_users, - COUNT(DISTINCT s.device_id) as unique_devices, - COUNT(*) as total_active_sessions - FROM sessions s - WHERE s.active = true - """) - - current_stats = cur.fetchone() - - # Sessions nach Lizenztyp - cur.execute(""" - SELECT - l.license_type, - COUNT(*) as session_count - FROM sessions s - JOIN licenses l ON s.license_key = l.license_key - WHERE s.active = true - GROUP BY l.license_type - ORDER BY session_count DESC - """) - - sessions_by_type = [] - for row in cur.fetchall(): - sessions_by_type.append({ - 'license_type': row[0], - 'count': row[1] - }) - - # Top 10 Lizenzen nach aktiven Sessions - cur.execute(""" - SELECT - s.license_key, - l.customer_name, - COUNT(*) as session_count, - l.device_limit - FROM sessions s - JOIN licenses l ON s.license_key = l.license_key - WHERE s.active = true - GROUP BY s.license_key, l.customer_name, l.device_limit - ORDER BY session_count DESC - LIMIT 10 - """) - - top_licenses = [] - for row in cur.fetchall(): - top_licenses.append({ - 'license_key': row[0], - 'customer_name': row[1], - 'session_count': row[2], - 'device_limit': row[3] - }) - - # Session-Verlauf (letzte 7 Tage) - cur.execute(""" - SELECT - DATE(login_time) as date, - COUNT(*) as login_count, - COUNT(DISTINCT license_key) as unique_licenses, - COUNT(DISTINCT username) as unique_users - FROM sessions - WHERE login_time >= CURRENT_DATE - INTERVAL '7 days' - GROUP BY DATE(login_time) - ORDER BY date - """) - - session_history = [] - for row in cur.fetchall(): - session_history.append({ - 'date': row[0].strftime('%Y-%m-%d'), - 'login_count': row[1], - 'unique_licenses': row[2], - 'unique_users': row[3] - }) - - # Durchschnittliche Session-Dauer - cur.execute(""" - SELECT - AVG(EXTRACT(EPOCH FROM (logout_time - login_time))/3600) as avg_duration_hours - FROM sessions - WHERE active = false - AND logout_time IS NOT NULL - AND logout_time - login_time < INTERVAL '24 hours' - AND login_time >= CURRENT_DATE - INTERVAL '30 days' - """) - - avg_duration = cur.fetchone()[0] or 0 - - return render_template("session_statistics.html", - current_stats={ - 'active_licenses': current_stats[0], - 'unique_users': current_stats[1], - 'unique_devices': current_stats[2], - 'total_sessions': current_stats[3] - }, - sessions_by_type=sessions_by_type, - top_licenses=top_licenses, - session_history=session_history, - avg_duration=round(avg_duration, 1)) - - except Exception as e: - logging.error(f"Fehler beim Laden der Session-Statistiken: {str(e)}") - flash('Fehler beim Laden der Statistiken!', 'error') - return redirect(url_for('sessions.sessions')) - finally: - cur.close() - conn.close() \ No newline at end of file