API Key Config ist fertig

Dieser Commit ist enthalten in:
2025-06-22 12:03:49 +02:00
Ursprung b420452551
Commit 1b5b7d0381
7 geänderte Dateien mit 398 neuen und 40 gelöschten Zeilen

Datei anzeigen

@@ -1,5 +1,31 @@
# V2-Docker API Reference # 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 ## License Server API
### Public Endpoints ### Public Endpoints
@@ -42,7 +68,8 @@ Activate a license on a new system.
**Headers:** **Headers:**
``` ```
X-API-Key: your-api-key X-API-Key: AF-2025-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Content-Type: application/json
``` ```
**Request:** **Request:**
@@ -76,7 +103,8 @@ Verify an active license.
**Headers:** **Headers:**
``` ```
X-API-Key: your-api-key X-API-Key: AF-2025-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Content-Type: application/json
``` ```
**Request:** **Request:**

Datei anzeigen

@@ -273,7 +273,6 @@ lead_institutions
- `DATABASE_URL`: PostgreSQL Verbindung - `DATABASE_URL`: PostgreSQL Verbindung
- `SECRET_KEY`: Flask Session Secret - `SECRET_KEY`: Flask Session Secret
- `JWT_SECRET`: JWT Token Signierung - `JWT_SECRET`: JWT Token Signierung
- `API_KEY`: Lizenzserver API Key
#### Optional mit Defaults #### Optional mit Defaults
- `MONITORING_ENABLED`: "true" (Feature Flag) - `MONITORING_ENABLED`: "true" (Feature Flag)

Datei anzeigen

@@ -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) 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') VALUES ('Account Forger', 'AF-' || gen_random_uuid()::text, '1.0.0', '1.0.0')
ON CONFLICT DO NOTHING; 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();

Datei anzeigen

@@ -6,6 +6,7 @@ from flask import Blueprint, render_template, request, redirect, session, url_fo
import requests import requests
import config import config
from config import DATABASE_CONFIG
from auth.decorators import login_required from auth.decorators import login_required
from utils.audit import log_audit from utils.audit import log_audit
from utils.backup import create_backup, restore_backup from utils.backup import create_backup, restore_backup
@@ -934,50 +935,55 @@ def license_config():
# Get client configuration # Get client configuration
cur.execute(""" cur.execute("""
SELECT id, client_name, api_key, heartbeat_interval, session_timeout, SELECT id, client_name, api_key, heartbeat_interval, session_timeout,
current_version, minimum_version, download_url, whats_new, current_version, minimum_version, created_at, updated_at
created_at, updated_at
FROM client_configs FROM client_configs
WHERE client_name = 'Account Forger' WHERE client_name = 'Account Forger'
""") """)
client_config = cur.fetchone() client_config = cur.fetchone()
# Get active sessions # Get active sessions - table doesn't exist, use empty list
cur.execute(""" active_sessions = []
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 feature flags # Get feature flags - table doesn't exist, use empty list
cur.execute(""" feature_flags = []
SELECT * FROM feature_flags
ORDER BY feature_name
""")
feature_flags = cur.fetchall()
# Get rate limits # Get rate limits - table doesn't exist, use empty list
rate_limits = []
# Get system API key
cur.execute(""" cur.execute("""
SELECT * FROM api_rate_limits SELECT api_key, created_at, regenerated_at, last_used_at,
ORDER BY api_key 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', return render_template('license_config.html',
client_config=client_config, client_config=client_config,
active_sessions=active_sessions, active_sessions=active_sessions,
feature_flags=feature_flags, feature_flags=feature_flags,
rate_limits=rate_limits rate_limits=rate_limits,
system_api_key=system_api_key
) )
except Exception as e: 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') flash(f'Fehler beim Laden der Konfiguration: {str(e)}', 'error')
return render_template('license_config.html') return render_template('license_config.html')
finally: finally:
@@ -1042,8 +1048,6 @@ def update_client_config():
UPDATE client_configs UPDATE client_configs
SET current_version = %s, SET current_version = %s,
minimum_version = %s, minimum_version = %s,
download_url = %s,
whats_new = %s,
heartbeat_interval = %s, heartbeat_interval = %s,
session_timeout = %s, session_timeout = %s,
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
@@ -1051,8 +1055,6 @@ def update_client_config():
""", ( """, (
request.form.get('current_version'), request.form.get('current_version'),
request.form.get('minimum_version'), request.form.get('minimum_version'),
'', # download_url - no longer used
'', # whats_new - no longer used
30, # heartbeat_interval - fixed 30, # heartbeat_interval - fixed
60 # session_timeout - fixed 60 # session_timeout - fixed
)) ))
@@ -1272,3 +1274,92 @@ def get_analytics_token():
token = jwt.encode(payload, jwt_secret, algorithm='HS256') token = jwt.encode(payload, jwt_secret, algorithm='HS256')
return jsonify({'token': token}) 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()

Datei anzeigen

@@ -98,6 +98,112 @@
</div> </div>
</div> </div>
<!-- System API Key Section -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header bg-warning text-dark">
<h5 class="mb-0"><i class="bi bi-key"></i> System API Key</h5>
</div>
<div class="card-body">
{% if system_api_key %}
<div class="row mb-3">
<div class="col-md-12">
<label class="form-label fw-bold">Aktueller API Key:</label>
<div class="input-group">
<input type="text" class="form-control font-monospace" id="systemApiKey"
value="{{ system_api_key.api_key }}" readonly>
<button class="btn btn-outline-secondary" type="button" onclick="copySystemApiKey()">
<i class="bi bi-clipboard"></i> Kopieren
</button>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<h6>Key Informationen:</h6>
<ul class="list-unstyled small">
<li><strong>Erstellt:</strong>
{% if system_api_key.created_at %}
{{ system_api_key.created_at.strftime('%d.%m.%Y %H:%M') }}
{% else %}
N/A
{% endif %}
</li>
<li><strong>Erstellt von:</strong> {{ system_api_key.created_by or 'System' }}</li>
{% if system_api_key.regenerated_at %}
<li><strong>Zuletzt regeneriert:</strong>
{{ system_api_key.regenerated_at.strftime('%d.%m.%Y %H:%M') }}
</li>
<li><strong>Regeneriert von:</strong> {{ system_api_key.regenerated_by }}</li>
{% else %}
<li><strong>Zuletzt regeneriert:</strong> Nie</li>
{% endif %}
</ul>
</div>
<div class="col-md-6">
<h6>Nutzungsstatistiken:</h6>
<ul class="list-unstyled small">
<li><strong>Letzte Nutzung:</strong>
{% if system_api_key.last_used_at %}
{{ system_api_key.last_used_at.strftime('%d.%m.%Y %H:%M') }}
{% else %}
Noch nie genutzt
{% endif %}
</li>
<li><strong>Gesamte Anfragen:</strong> {{ system_api_key.usage_count or 0 }}</li>
</ul>
</div>
</div>
<hr>
<div class="row">
<div class="col-md-12">
<form action="{{ url_for('admin.regenerate_api_key') }}" method="POST"
onsubmit="return confirmRegenerate()">
<button type="submit" class="btn btn-warning">
<i class="bi bi-arrow-clockwise"></i> API Key regenerieren
</button>
<span class="text-muted ms-2">
<i class="bi bi-exclamation-triangle"></i>
Dies wird den aktuellen Key ungültig machen!
</span>
</form>
</div>
</div>
<div class="mt-3">
<details>
<summary class="text-primary" style="cursor: pointer;">Verwendungsbeispiel anzeigen</summary>
<div class="mt-2">
<pre class="bg-light p-3"><code>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"}
)</code></pre>
</div>
</details>
</div>
{% else %}
<div class="alert alert-danger">
<i class="bi bi-exclamation-triangle"></i> Kein System API Key gefunden!
Bitte kontaktieren Sie den Administrator.
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Technical Settings (collapsible) --> <!-- Technical Settings (collapsible) -->
<div class="accordion mb-4" id="technicalSettings"> <div class="accordion mb-4" id="technicalSettings">
<!-- Feature Flags --> <!-- Feature Flags -->
@@ -213,6 +319,32 @@ function copyToClipboard(text) {
}); });
} }
function copySystemApiKey() {
const apiKeyInput = document.getElementById('systemApiKey');
apiKeyInput.select();
apiKeyInput.setSelectionRange(0, 99999);
navigator.clipboard.writeText(apiKeyInput.value).then(function() {
const button = event.currentTarget;
const originalHTML = button.innerHTML;
button.innerHTML = '<i class="bi bi-check"></i> Kopiert!';
button.classList.remove('btn-outline-secondary');
button.classList.add('btn-success');
setTimeout(() => {
button.innerHTML = originalHTML;
button.classList.remove('btn-success');
button.classList.add('btn-outline-secondary');
}, 2000);
});
}
function confirmRegenerate() {
return confirm('Sind Sie sicher, dass Sie den API Key regenerieren möchten?\n\n' +
'Dies wird den aktuellen Key ungültig machen und alle bestehenden ' +
'Integrationen müssen mit dem neuen Key aktualisiert werden.');
}
// Auto-refresh sessions every 30 seconds // Auto-refresh sessions every 30 seconds
function refreshSessions() { function refreshSessions() {
fetch('{{ url_for("admin.license_live_stats") }}') fetch('{{ url_for("admin.license_live_stats") }}')

Datei anzeigen

@@ -20,6 +20,7 @@ from app.schemas.license import (
) )
from app.core.security import get_api_key from app.core.security import get_api_key
from app.core.config import settings from app.core.config import settings
from app.core.api_key_auth import validate_api_key
router = APIRouter() router = APIRouter()
@@ -27,7 +28,7 @@ router = APIRouter()
async def activate_license( async def activate_license(
request: LicenseActivationRequest, request: LicenseActivationRequest,
db: Session = Depends(get_db), db: Session = Depends(get_db),
api_key = Depends(get_api_key) api_key: str = Depends(validate_api_key)
): ):
license = db.query(License).filter( license = db.query(License).filter(
License.license_key == request.license_key, License.license_key == request.license_key,
@@ -106,7 +107,7 @@ async def activate_license(
async def verify_license( async def verify_license(
request: LicenseVerificationRequest, request: LicenseVerificationRequest,
db: Session = Depends(get_db), db: Session = Depends(get_db),
api_key = Depends(get_api_key) api_key: str = Depends(validate_api_key)
): ):
activation = db.query(Activation).filter( activation = db.query(Activation).filter(
Activation.id == request.activation_id, Activation.id == request.activation_id,
@@ -180,7 +181,7 @@ async def verify_license(
async def get_license_info( async def get_license_info(
license_key: str, license_key: str,
db: Session = Depends(get_db), db: Session = Depends(get_db),
api_key = Depends(get_api_key) api_key: str = Depends(validate_api_key)
): ):
license = db.query(License).filter( license = db.query(License).filter(
License.license_key == license_key License.license_key == license_key
@@ -219,7 +220,7 @@ async def get_license_info(
async def start_session( async def start_session(
request: SessionStartRequest, request: SessionStartRequest,
db: Session = Depends(get_db), db: Session = Depends(get_db),
api_key = Depends(get_api_key) api_key: str = Depends(validate_api_key)
): ):
# Verify API key matches client config # Verify API key matches client config
from sqlalchemy import text from sqlalchemy import text
@@ -320,7 +321,7 @@ async def start_session(
async def session_heartbeat( async def session_heartbeat(
request: SessionHeartbeatRequest, request: SessionHeartbeatRequest,
db: Session = Depends(get_db), db: Session = Depends(get_db),
api_key = Depends(get_api_key) api_key: str = Depends(validate_api_key)
): ):
# Update heartbeat # Update heartbeat
result = db.execute( result = db.execute(
@@ -350,7 +351,7 @@ async def session_heartbeat(
async def end_session( async def end_session(
request: SessionEndRequest, request: SessionEndRequest,
db: Session = Depends(get_db), db: Session = Depends(get_db),
api_key = Depends(get_api_key) api_key: str = Depends(validate_api_key)
): ):
# Get session info before deleting # Get session info before deleting
session_info = db.execute( session_info = db.execute(

Datei anzeigen

@@ -0,0 +1,45 @@
from fastapi import HTTPException, Request, Depends
from sqlalchemy.orm import Session
from datetime import datetime
import logging
from app.db.database import get_db
logger = logging.getLogger(__name__)
async def validate_api_key(request: Request, db: Session = Depends(get_db)):
"""Validate API key from X-API-Key header against system_api_key table"""
api_key = request.headers.get("X-API-Key")
if not api_key:
logger.warning("API request without API key")
raise HTTPException(
status_code=401,
detail="API key required",
headers={"WWW-Authenticate": "ApiKey"}
)
# Query the system API key
cursor = db.execute(
"SELECT api_key FROM system_api_key WHERE id = 1"
)
result = cursor.fetchone()
if not result or result[0] != api_key:
logger.warning(f"Invalid API key attempt: {api_key[:8]}...")
raise HTTPException(
status_code=401,
detail="Invalid API key"
)
# Update usage statistics
db.execute("""
UPDATE system_api_key
SET last_used_at = CURRENT_TIMESTAMP,
usage_count = usage_count + 1
WHERE id = 1
""")
db.commit()
return api_key