API Key Config ist fertig
Dieser Commit ist enthalten in:
@@ -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();
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -98,6 +98,112 @@
|
||||
</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) -->
|
||||
<div class="accordion mb-4" id="technicalSettings">
|
||||
<!-- 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
|
||||
function refreshSessions() {
|
||||
fetch('{{ url_for("admin.license_live_stats") }}')
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren