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:
eric 2026-02-21 17:26:10 +00:00
parent 7ee66397e1
commit 83b1842392
5 changed files with 124 additions and 12 deletions

86
app.py
View file

@ -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)