diff --git a/API_REFERENCE.md b/API_REFERENCE.md index 3207cd7..5e11572 100644 --- a/API_REFERENCE.md +++ b/API_REFERENCE.md @@ -1,5 +1,31 @@ # V2-Docker API Reference +## Authentication + +### API Key Authentication + +All License Server API endpoints require authentication using an API key. The API key must be included in the request headers. + +**Header Format:** +``` +X-API-Key: AF-2025-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +``` + +**API Key Management:** +- API keys can be managed through the Admin Panel under Administration → API Keys +- Keys follow the format: `AF-YYYY-[32 random characters]` +- Only one system API key is active at a time +- Regenerating the key will immediately invalidate the old key + +**Error Response (401 Unauthorized):** +```json +{ + "error": "Invalid or missing API key", + "code": "INVALID_API_KEY", + "status": 401 +} +``` + ## License Server API ### Public Endpoints @@ -42,7 +68,8 @@ Activate a license on a new system. **Headers:** ``` -X-API-Key: your-api-key +X-API-Key: AF-2025-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +Content-Type: application/json ``` **Request:** @@ -76,7 +103,8 @@ Verify an active license. **Headers:** ``` -X-API-Key: your-api-key +X-API-Key: AF-2025-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +Content-Type: application/json ``` **Request:** diff --git a/SYSTEM_DOCUMENTATION.md b/SYSTEM_DOCUMENTATION.md index 2c9ecd9..005c4e5 100644 --- a/SYSTEM_DOCUMENTATION.md +++ b/SYSTEM_DOCUMENTATION.md @@ -273,7 +273,6 @@ lead_institutions - `DATABASE_URL`: PostgreSQL Verbindung - `SECRET_KEY`: Flask Session Secret - `JWT_SECRET`: JWT Token Signierung -- `API_KEY`: Lizenzserver API Key #### Optional mit Defaults - `MONITORING_ENABLED`: "true" (Feature Flag) diff --git a/v2_adminpanel/init.sql b/v2_adminpanel/init.sql index 9207a8c..896ba45 100644 --- a/v2_adminpanel/init.sql +++ b/v2_adminpanel/init.sql @@ -640,3 +640,65 @@ CREATE INDEX IF NOT EXISTS idx_session_history_ended_at ON session_history(ended INSERT INTO client_configs (client_name, api_key, current_version, minimum_version) VALUES ('Account Forger', 'AF-' || gen_random_uuid()::text, '1.0.0', '1.0.0') ON CONFLICT DO NOTHING; + +-- ===================== SYSTEM API KEY TABLE ===================== +-- Single API key for system-wide authentication +CREATE TABLE IF NOT EXISTS system_api_key ( + id INTEGER PRIMARY KEY DEFAULT 1 CHECK (id = 1), -- Ensures single row + api_key VARCHAR(255) NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + regenerated_at TIMESTAMP WITH TIME ZONE, + last_used_at TIMESTAMP WITH TIME ZONE, + usage_count INTEGER DEFAULT 0, + created_by VARCHAR(50), + regenerated_by VARCHAR(50) +); + +-- Function to generate API key with AF-YYYY- prefix +CREATE OR REPLACE FUNCTION generate_api_key() RETURNS VARCHAR AS $$ +DECLARE + year_part VARCHAR(4); + random_part VARCHAR(32); +BEGIN + year_part := to_char(CURRENT_DATE, 'YYYY'); + random_part := upper(substring(md5(random()::text || clock_timestamp()::text) from 1 for 32)); + RETURN 'AF-' || year_part || '-' || random_part; +END; +$$ LANGUAGE plpgsql; + +-- Initialize with a default API key if none exists +INSERT INTO system_api_key (api_key, created_by) +SELECT generate_api_key(), 'system' +WHERE NOT EXISTS (SELECT 1 FROM system_api_key); + +-- Audit trigger for API key changes +CREATE OR REPLACE FUNCTION audit_api_key_changes() RETURNS TRIGGER AS $$ +BEGIN + IF TG_OP = 'UPDATE' AND OLD.api_key != NEW.api_key THEN + INSERT INTO audit_log ( + timestamp, + username, + action, + entity_type, + entity_id, + old_values, + new_values, + additional_info + ) VALUES ( + CURRENT_TIMESTAMP, + COALESCE(NEW.regenerated_by, 'system'), + 'api_key_regenerated', + 'system_api_key', + NEW.id, + jsonb_build_object('api_key', LEFT(OLD.api_key, 8) || '...'), + jsonb_build_object('api_key', LEFT(NEW.api_key, 8) || '...'), + 'API Key regenerated' + ); + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER audit_system_api_key_changes +AFTER UPDATE ON system_api_key +FOR EACH ROW EXECUTE FUNCTION audit_api_key_changes(); diff --git a/v2_adminpanel/routes/admin_routes.py b/v2_adminpanel/routes/admin_routes.py index e38d237..947410c 100644 --- a/v2_adminpanel/routes/admin_routes.py +++ b/v2_adminpanel/routes/admin_routes.py @@ -6,6 +6,7 @@ from flask import Blueprint, render_template, request, redirect, session, url_fo import requests import config +from config import DATABASE_CONFIG from auth.decorators import login_required from utils.audit import log_audit from utils.backup import create_backup, restore_backup @@ -934,50 +935,55 @@ def license_config(): # Get client configuration cur.execute(""" SELECT id, client_name, api_key, heartbeat_interval, session_timeout, - current_version, minimum_version, download_url, whats_new, - created_at, updated_at + current_version, minimum_version, created_at, updated_at FROM client_configs WHERE client_name = 'Account Forger' """) client_config = cur.fetchone() - # Get active sessions - cur.execute(""" - SELECT ls.id, ls.session_token, l.license_key, c.name as customer_name, - ls.hardware_id, ls.ip_address, ls.client_version, - ls.started_at AT TIME ZONE 'Europe/Berlin' as started_at, - ls.last_heartbeat AT TIME ZONE 'Europe/Berlin' as last_heartbeat, - EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - ls.last_heartbeat)) as seconds_since_heartbeat - FROM license_sessions ls - JOIN licenses l ON ls.license_id = l.id - LEFT JOIN customers c ON l.customer_id = c.id - ORDER BY ls.last_heartbeat DESC - LIMIT 5 - """) - active_sessions = cur.fetchall() + # Get active sessions - table doesn't exist, use empty list + active_sessions = [] - # Get feature flags - cur.execute(""" - SELECT * FROM feature_flags - ORDER BY feature_name - """) - feature_flags = cur.fetchall() + # Get feature flags - table doesn't exist, use empty list + feature_flags = [] - # Get rate limits + # Get rate limits - table doesn't exist, use empty list + rate_limits = [] + + # Get system API key cur.execute(""" - SELECT * FROM api_rate_limits - ORDER BY api_key + SELECT api_key, created_at, regenerated_at, last_used_at, + usage_count, created_by, regenerated_by + FROM system_api_key + WHERE id = 1 """) - rate_limits = cur.fetchall() + api_key_data = cur.fetchone() + + if api_key_data: + system_api_key = { + 'api_key': api_key_data[0], + 'created_at': api_key_data[1], + 'regenerated_at': api_key_data[2], + 'last_used_at': api_key_data[3], + 'usage_count': api_key_data[4], + 'created_by': api_key_data[5], + 'regenerated_by': api_key_data[6] + } + else: + system_api_key = None return render_template('license_config.html', client_config=client_config, active_sessions=active_sessions, feature_flags=feature_flags, - rate_limits=rate_limits + rate_limits=rate_limits, + system_api_key=system_api_key ) except Exception as e: + import traceback + current_app.logger.error(f"Error in license_config: {str(e)}") + current_app.logger.error(traceback.format_exc()) flash(f'Fehler beim Laden der Konfiguration: {str(e)}', 'error') return render_template('license_config.html') finally: @@ -1042,8 +1048,6 @@ def update_client_config(): UPDATE client_configs SET current_version = %s, minimum_version = %s, - download_url = %s, - whats_new = %s, heartbeat_interval = %s, session_timeout = %s, updated_at = CURRENT_TIMESTAMP @@ -1051,8 +1055,6 @@ def update_client_config(): """, ( request.form.get('current_version'), request.form.get('minimum_version'), - '', # download_url - no longer used - '', # whats_new - no longer used 30, # heartbeat_interval - fixed 60 # session_timeout - fixed )) @@ -1272,3 +1274,92 @@ def get_analytics_token(): token = jwt.encode(payload, jwt_secret, algorithm='HS256') return jsonify({'token': token}) + + +# ===================== API KEY MANAGEMENT ===================== + +@admin_bp.route("/api-key/regenerate", methods=["POST"]) +@login_required +def regenerate_api_key(): + """Regenerate the system API key""" + import string + import random + + conn = get_connection() + cur = conn.cursor() + + try: + # Generate new API key + year_part = datetime.now().strftime('%Y') + random_part = ''.join(random.choices(string.ascii_uppercase + string.digits, k=32)) + new_api_key = f"AF-{year_part}-{random_part}" + + # Update the API key + cur.execute(""" + UPDATE system_api_key + SET api_key = %s, + regenerated_at = CURRENT_TIMESTAMP, + regenerated_by = %s + WHERE id = 1 + """, (new_api_key, session.get('username'))) + + conn.commit() + + flash('API Key wurde erfolgreich regeneriert', 'success') + + # Log action + log_audit('API_KEY_REGENERATED', 'system_api_key', 1, + additional_info="API Key regenerated") + + except Exception as e: + conn.rollback() + flash(f'Fehler beim Regenerieren des API Keys: {str(e)}', 'error') + + finally: + cur.close() + conn.close() + + return redirect(url_for('admin.license_config')) + + +@admin_bp.route("/test-api-key") +@login_required +def test_api_key(): + """Test route to check API key in database""" + try: + conn = get_connection() + cur = conn.cursor() + + # Test if table exists + cur.execute(""" + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_name = 'system_api_key' + ); + """) + table_exists = cur.fetchone()[0] + + # Get API key if table exists + api_key = None + if table_exists: + cur.execute("SELECT api_key FROM system_api_key WHERE id = 1;") + result = cur.fetchone() + if result: + api_key = result[0] + + return jsonify({ + 'table_exists': table_exists, + 'api_key': api_key, + 'database': DATABASE_CONFIG['dbname'] + }) + + except Exception as e: + return jsonify({ + 'error': str(e), + 'database': DATABASE_CONFIG.get('dbname', 'unknown') + }) + finally: + if 'cur' in locals(): + cur.close() + if 'conn' in locals(): + conn.close() diff --git a/v2_adminpanel/templates/license_config.html b/v2_adminpanel/templates/license_config.html index b03d966..f3be6f1 100644 --- a/v2_adminpanel/templates/license_config.html +++ b/v2_adminpanel/templates/license_config.html @@ -98,6 +98,112 @@ + +
import requests
+
+headers = {
+ "X-API-Key": "{{ system_api_key.api_key }}",
+ "Content-Type": "application/json"
+}
+
+response = requests.post(
+ "{{ request.url_root }}api/license/verify",
+ headers=headers,
+ json={"license_key": "YOUR_LICENSE_KEY"}
+)
+