feat: Add password login system and upgrade agent models
- App-level password auth via Flask session (APP_PASSWORD in .env) - login_required decorator on all routes - Login page, logout button in navbar, 7-day session lifetime - Upgrade musik_rechte_advisor and negotiator from Opus 4.0 to Opus 4.6 - Fix orchestrator session cookie overflow (kb/prompts no longer stored in session) - Change app port from 5000 to 5050 (5000 occupied by Zou/Kitsu) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7ee66397e1
commit
83b1842392
5 changed files with 124 additions and 12 deletions
86
app.py
86
app.py
|
|
@ -15,7 +15,8 @@ from email.mime.text import MIMEText
|
|||
from email.mime.multipart import MIMEMultipart
|
||||
from email.header import decode_header
|
||||
from flask import Flask, render_template, request, redirect, url_for, session, flash, Response, send_from_directory, jsonify
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from functools import wraps
|
||||
from dotenv import load_dotenv
|
||||
from telegram import Update
|
||||
from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes
|
||||
|
|
@ -397,9 +398,21 @@ AGENT_KEYWORDS = {
|
|||
'musik_rechte_advisor': ['musik', 'akm', 'gema', 'lizenz', 'rechte', 'urheber', 'copyright', 'verwertungsgesellschaft'],
|
||||
'document_editor': ['dokument', 'vertrag', 'brief', 'text', 'bearbeiten', 'erstellen', 'schreiben'],
|
||||
}
|
||||
app.secret_key = 'agent-orchestration-secret-key-2026'
|
||||
app.secret_key = os.getenv('SECRET_KEY', 'agent-orchestration-secret-key-2026')
|
||||
app.config['UPLOAD_FOLDER'] = 'uploads'
|
||||
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024
|
||||
app.permanent_session_lifetime = timedelta(days=7)
|
||||
|
||||
# ── App Password ─────────────────────────────────────────────────────────────
|
||||
APP_PASSWORD = os.getenv('APP_PASSWORD', '')
|
||||
|
||||
def login_required(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not session.get('authenticated'):
|
||||
return redirect(url_for('login'))
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
# Email Configuration - loaded AFTER load_dotenv()
|
||||
EMAIL_CONFIG = {
|
||||
|
|
@ -1221,10 +1234,6 @@ def load_agent_prompts():
|
|||
return prompts
|
||||
|
||||
def init_orchestrator_session():
|
||||
if 'orchestrator_kb' not in session:
|
||||
session['orchestrator_kb'] = load_knowledge_base()
|
||||
if 'orchestrator_prompts' not in session:
|
||||
session['orchestrator_prompts'] = load_agent_prompts()
|
||||
if 'orchestrator_chat' not in session:
|
||||
session['orchestrator_chat'] = []
|
||||
|
||||
|
|
@ -2207,7 +2216,26 @@ start_task_beat()
|
|||
start_orchestrator_beat()
|
||||
|
||||
|
||||
@app.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
if session.get('authenticated'):
|
||||
return redirect(url_for('index'))
|
||||
error = None
|
||||
if request.method == 'POST':
|
||||
if request.form.get('password') == APP_PASSWORD:
|
||||
session['authenticated'] = True
|
||||
session.permanent = True
|
||||
return redirect(url_for('index'))
|
||||
error = 'Falsches Passwort'
|
||||
return render_template('login.html', error=error)
|
||||
|
||||
@app.route('/logout')
|
||||
def logout():
|
||||
session.clear()
|
||||
return redirect(url_for('login'))
|
||||
|
||||
@app.route('/')
|
||||
@login_required
|
||||
def index():
|
||||
# Hole die 5 neuesten Tasks aus DB
|
||||
all_tasks = get_tasks()
|
||||
|
|
@ -2215,6 +2243,7 @@ def index():
|
|||
return render_template('index.html', agents=AGENTS, recent_tasks=recent_tasks)
|
||||
|
||||
@app.route('/chat', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def chat():
|
||||
# Chat-Verlauf aus Session laden
|
||||
if 'chat_history' not in session:
|
||||
|
|
@ -2225,6 +2254,7 @@ def chat():
|
|||
|
||||
|
||||
@app.route('/chat/send', methods=['POST'])
|
||||
@login_required
|
||||
def chat_send():
|
||||
"""Führt einen Agent aus und gibt die Antwort per Server-Sent Events LIVE zurück."""
|
||||
data = request.get_json()
|
||||
|
|
@ -2317,6 +2347,7 @@ Die Wissensdatenbank liegt unter: {kb_file}
|
|||
|
||||
|
||||
@app.route('/chat/save', methods=['POST'])
|
||||
@login_required
|
||||
def chat_save():
|
||||
"""Speichert eine Chat-Nachricht in der Session."""
|
||||
data = request.get_json()
|
||||
|
|
@ -2337,6 +2368,7 @@ def chat_save():
|
|||
return jsonify({'success': True})
|
||||
|
||||
@app.route('/tasks', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def task_list():
|
||||
if request.method == 'POST':
|
||||
title = request.form.get('title', '').strip()
|
||||
|
|
@ -2358,6 +2390,7 @@ def task_list():
|
|||
return render_template('tasks.html', agents=AGENTS, tasks=all_tasks)
|
||||
|
||||
@app.route('/tasks/update/<int:task_id>/<status>')
|
||||
@login_required
|
||||
def update_task(task_id, status):
|
||||
# Update in Datenbank
|
||||
update_task_db(task_id, status=status)
|
||||
|
|
@ -2371,6 +2404,7 @@ def update_task(task_id, status):
|
|||
return redirect(url_for('task_list'))
|
||||
|
||||
@app.route('/api/agent-stream', methods=['POST'])
|
||||
@login_required
|
||||
def agent_stream():
|
||||
"""Server-Sent Events Endpoint – echtes Streaming direkt aus opencode JSON-Output."""
|
||||
data = request.get_json()
|
||||
|
|
@ -2444,6 +2478,7 @@ def agent_stream():
|
|||
|
||||
|
||||
@app.route('/orchestrator', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def orchestrator():
|
||||
init_orchestrator_session()
|
||||
|
||||
|
|
@ -2451,8 +2486,8 @@ def orchestrator():
|
|||
prompt = request.form.get('prompt', '').strip()
|
||||
|
||||
if prompt:
|
||||
kb = session.get('orchestrator_kb', '')
|
||||
agent_prompts = session.get('orchestrator_prompts', {})
|
||||
kb = load_knowledge_base()
|
||||
agent_prompts = load_agent_prompts()
|
||||
selected_agent = delegate_to_agent(prompt)
|
||||
agent_info = AGENTS.get(selected_agent, {})
|
||||
agent_name = agent_info.get('name', selected_agent)
|
||||
|
|
@ -2474,7 +2509,7 @@ def orchestrator():
|
|||
return render_template('orchestrator.html',
|
||||
agents=AGENTS,
|
||||
chat_history=chat_display,
|
||||
knowledge_loaded=bool(session.get('orchestrator_kb')))
|
||||
knowledge_loaded=bool(load_knowledge_base()))
|
||||
|
||||
@app.route('/orchestrator/clear', methods=['POST'])
|
||||
def orchestrator_clear():
|
||||
|
|
@ -2484,6 +2519,7 @@ def orchestrator_clear():
|
|||
return jsonify({'success': True})
|
||||
|
||||
@app.route('/agents', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def agents():
|
||||
agents_dir = os.path.join(os.path.dirname(__file__), 'agents')
|
||||
agents_list = []
|
||||
|
|
@ -2581,6 +2617,7 @@ def agents():
|
|||
|
||||
|
||||
@app.route('/files', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def files():
|
||||
if request.method == 'POST':
|
||||
if 'file' not in request.files:
|
||||
|
|
@ -2613,6 +2650,7 @@ def files():
|
|||
|
||||
|
||||
@app.route('/files/delete/<filename>')
|
||||
@login_required
|
||||
def delete_file(filename):
|
||||
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
|
||||
if os.path.exists(filepath):
|
||||
|
|
@ -2622,12 +2660,14 @@ def delete_file(filename):
|
|||
|
||||
|
||||
@app.route('/files/download/<filename>')
|
||||
@login_required
|
||||
def download_file(filename):
|
||||
"""Liefert eine hochgeladene Datei zum Download/Anzeige."""
|
||||
return send_from_directory(app.config['UPLOAD_FOLDER'], filename, as_attachment=False)
|
||||
|
||||
|
||||
@app.route('/files/agent/<agent_key>/<filename>')
|
||||
@login_required
|
||||
def download_agent_file(agent_key, filename):
|
||||
"""Liefert eine Datei aus dem Work-Ordner eines Agenten."""
|
||||
if agent_key not in AGENTS:
|
||||
|
|
@ -2650,6 +2690,7 @@ def download_agent_file(agent_key, filename):
|
|||
|
||||
|
||||
@app.route('/files/agent/<agent_key>/view/<filename>')
|
||||
@login_required
|
||||
def view_agent_file(agent_key, filename):
|
||||
"""Gibt Inhalt einer Agent-Datei als JSON zurück."""
|
||||
if agent_key not in AGENTS:
|
||||
|
|
@ -2678,6 +2719,7 @@ def view_agent_file(agent_key, filename):
|
|||
|
||||
|
||||
@app.route('/files/agent/<agent_key>/delete/<filename>')
|
||||
@login_required
|
||||
def delete_agent_file(agent_key, filename):
|
||||
"""Löscht eine Datei aus dem Work-Ordner eines Agenten."""
|
||||
if agent_key not in AGENTS:
|
||||
|
|
@ -2707,6 +2749,7 @@ def delete_agent_file(agent_key, filename):
|
|||
|
||||
|
||||
@app.route('/files/email/view/<filename>')
|
||||
@login_required
|
||||
def view_email_file(filename):
|
||||
"""Gibt Inhalt einer Email-Vorlage als JSON oder direkten Text zurück."""
|
||||
email_dir = os.path.join(os.path.dirname(__file__), 'emails')
|
||||
|
|
@ -2727,6 +2770,7 @@ def view_email_file(filename):
|
|||
|
||||
|
||||
@app.route('/files/email/save/<filename>', methods=['POST'])
|
||||
@login_required
|
||||
def save_email_file(filename):
|
||||
"""Speichert den Inhalt einer Email-Vorlage (JSON POST)."""
|
||||
email_dir = os.path.join(os.path.dirname(__file__), 'emails')
|
||||
|
|
@ -2744,6 +2788,7 @@ def save_email_file(filename):
|
|||
|
||||
|
||||
@app.route('/files/email/delete/<filename>')
|
||||
@login_required
|
||||
def delete_email_file(filename):
|
||||
"""Löscht eine Email-Vorlage."""
|
||||
email_dir = os.path.join(os.path.dirname(__file__), 'emails')
|
||||
|
|
@ -2760,6 +2805,7 @@ def delete_email_file(filename):
|
|||
|
||||
|
||||
@app.route('/files/project/view/<filename>')
|
||||
@login_required
|
||||
def view_project_file(filename):
|
||||
"""Gibt Inhalt einer Projektdatei als JSON zurück."""
|
||||
base_dir = os.path.dirname(__file__)
|
||||
|
|
@ -2785,6 +2831,7 @@ def view_project_file(filename):
|
|||
|
||||
|
||||
@app.route('/files/project/<filename>')
|
||||
@login_required
|
||||
def download_project_file(filename):
|
||||
"""Liefert eine Projektdatei zum Download."""
|
||||
base_dir = os.path.dirname(__file__)
|
||||
|
|
@ -2807,6 +2854,7 @@ def download_project_file(filename):
|
|||
|
||||
|
||||
@app.route('/files/project/delete/<filename>')
|
||||
@login_required
|
||||
def delete_project_file(filename):
|
||||
"""Löscht eine Projektdatei."""
|
||||
base_dir = os.path.dirname(__file__)
|
||||
|
|
@ -2836,6 +2884,7 @@ def delete_project_file(filename):
|
|||
|
||||
|
||||
@app.route('/emails', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def emails():
|
||||
"""Email Management Interface"""
|
||||
if request.method == 'POST':
|
||||
|
|
@ -2865,6 +2914,7 @@ def emails():
|
|||
|
||||
|
||||
@app.route('/emails/<email_id>')
|
||||
@login_required
|
||||
def view_email(email_id):
|
||||
"""View single email content"""
|
||||
if not (EMAIL_CONFIG['email_address'] and EMAIL_CONFIG['email_password']):
|
||||
|
|
@ -2875,6 +2925,7 @@ def view_email(email_id):
|
|||
|
||||
|
||||
@app.route('/email-log')
|
||||
@login_required
|
||||
def email_log_view():
|
||||
"""Zeigt das Email-Verarbeitungs-Log."""
|
||||
with email_log_lock:
|
||||
|
|
@ -2883,6 +2934,7 @@ def email_log_view():
|
|||
|
||||
|
||||
@app.route('/settings', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def settings():
|
||||
"""App-Einstellungen & Poller-Einstellungen zur Laufzeit ändern."""
|
||||
if request.method == 'POST':
|
||||
|
|
@ -2936,6 +2988,7 @@ def settings():
|
|||
|
||||
|
||||
@app.route('/settings/journal-clear', methods=['POST'])
|
||||
@login_required
|
||||
def journal_clear():
|
||||
"""Löscht abgeschlossene Journal-Einträge (completed, skipped, error)."""
|
||||
con = sqlite3.connect(EMAIL_JOURNAL_DB)
|
||||
|
|
@ -2949,6 +3002,7 @@ def journal_clear():
|
|||
|
||||
|
||||
@app.route('/team')
|
||||
@login_required
|
||||
def team():
|
||||
"""Zeigt alle Team-Members an."""
|
||||
team_members = get_team_members(active_only=False)
|
||||
|
|
@ -2956,6 +3010,7 @@ def team():
|
|||
|
||||
|
||||
@app.route('/team/add', methods=['POST'])
|
||||
@login_required
|
||||
def team_add():
|
||||
"""Fügt ein neues Team-Mitglied hinzu."""
|
||||
name = request.form.get('name', '').strip()
|
||||
|
|
@ -2988,6 +3043,7 @@ def team_add():
|
|||
|
||||
|
||||
@app.route('/team/<int:member_id>/activate', methods=['POST'])
|
||||
@login_required
|
||||
def team_activate(member_id):
|
||||
"""Aktiviert ein Team-Mitglied."""
|
||||
con = sqlite3.connect(EMAIL_JOURNAL_DB)
|
||||
|
|
@ -3000,6 +3056,7 @@ def team_activate(member_id):
|
|||
|
||||
|
||||
@app.route('/team/<int:member_id>/deactivate', methods=['POST'])
|
||||
@login_required
|
||||
def team_deactivate(member_id):
|
||||
"""Deaktiviert ein Team-Mitglied."""
|
||||
con = sqlite3.connect(EMAIL_JOURNAL_DB)
|
||||
|
|
@ -3012,6 +3069,7 @@ def team_deactivate(member_id):
|
|||
|
||||
|
||||
@app.route('/team/edit', methods=['POST'])
|
||||
@login_required
|
||||
def team_edit():
|
||||
"""Bearbeitet ein Team-Mitglied."""
|
||||
member_id = request.form.get('member_id')
|
||||
|
|
@ -3051,6 +3109,7 @@ def team_edit():
|
|||
|
||||
|
||||
@app.route('/api/telegram-qr')
|
||||
@login_required
|
||||
def telegram_qr():
|
||||
"""Generiert QR-Code für Telegram Bot."""
|
||||
if not TELEGRAM_CONFIG['bot_token'] or not TELEGRAM_CONFIG['bot_username']:
|
||||
|
|
@ -3076,6 +3135,7 @@ def telegram_qr():
|
|||
|
||||
# ── Task API ────────────────────────────────────────────────────────────────
|
||||
@app.route('/api/tasks', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def api_tasks():
|
||||
"""API zum Erstellen und Abrufen von Tasks."""
|
||||
if request.method == 'POST':
|
||||
|
|
@ -3121,6 +3181,7 @@ def api_tasks():
|
|||
|
||||
|
||||
@app.route('/api/tasks/<int:task_id>', methods=['PUT'])
|
||||
@login_required
|
||||
def update_task_api(task_id):
|
||||
"""API zum Aktualisieren eines Tasks."""
|
||||
data = request.get_json()
|
||||
|
|
@ -3140,6 +3201,7 @@ def update_task_api(task_id):
|
|||
|
||||
|
||||
@app.route('/api/models', methods=['GET'])
|
||||
@login_required
|
||||
def get_models():
|
||||
"""Gibt die Liste der verfügbaren KI-Modelle zurück."""
|
||||
force_refresh = request.args.get('refresh', 'false').lower() == 'true'
|
||||
|
|
@ -3148,6 +3210,7 @@ def get_models():
|
|||
|
||||
|
||||
@app.route('/api/agent/<agent_name>/model', methods=['POST'])
|
||||
@login_required
|
||||
def set_agent_model(agent_name):
|
||||
"""Setzt das Modell für einen Agenten."""
|
||||
data = request.get_json()
|
||||
|
|
@ -3158,6 +3221,7 @@ def set_agent_model(agent_name):
|
|||
|
||||
|
||||
@app.route('/api/agent/<agent_name>/delete', methods=['DELETE'])
|
||||
@login_required
|
||||
def delete_agent(agent_name):
|
||||
"""Löscht einen Agenten (den gesamten Ordner)."""
|
||||
import shutil
|
||||
|
|
@ -3188,6 +3252,7 @@ def delete_agent(agent_name):
|
|||
|
||||
|
||||
@app.route('/api/agent/<agent_name>/reminders', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def agent_reminders(agent_name):
|
||||
agents_dir = os.path.join(os.path.dirname(__file__), 'agents')
|
||||
agent_path = os.path.join(agents_dir, agent_name)
|
||||
|
|
@ -3214,6 +3279,7 @@ def agent_reminders(agent_name):
|
|||
|
||||
|
||||
@app.route('/api/orchestrator-distribute', methods=['POST'])
|
||||
@login_required
|
||||
def distribute_tasks():
|
||||
"""Erstellt einen Planungs-Task für den Orchestrator - dieser weist dann die richtigen Agenten zu."""
|
||||
data = request.get_json()
|
||||
|
|
@ -3311,4 +3377,4 @@ if __name__ == '__main__':
|
|||
start_telegram_thread()
|
||||
|
||||
# Flask App starten
|
||||
app.run(debug=False, host='0.0.0.0', port=5000, threaded=True)
|
||||
app.run(debug=False, host='0.0.0.0', port=5050, threaded=True)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue