from flask import Flask, render_template, redirect, url_for, jsonify from flask_login import LoginManager, login_required, current_user from flask_jwt_extended import JWTManager from flask_cors import CORS from flasgger import Swagger from sqlalchemy import text from models import db, User, AdminTakeoverSession from auth import auth_bp from api import api_bp, check_if_token_revoked from admin_api import admin_bp from config import Config from container_manager import ContainerManager import logging from logging.handlers import RotatingFileHandler import os # Flask-App initialisieren app = Flask(__name__) app.config.from_object(Config) # Datenbank initialisieren db.init_app(app) # CORS initialisieren CORS(app, resources={ r"/api/*": { "origins": app.config.get('CORS_ORIGINS', ['http://localhost:3000']), "methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"], "allow_headers": ["Content-Type", "Authorization"], "supports_credentials": True } }) # JWT initialisieren jwt = JWTManager(app) # Swagger/OpenAPI initialisieren swagger_config = { "headers": [], "specs": [ { "endpoint": 'openapi', "route": '/openapi.json', "rule_filter": lambda rule: True, "model_filter": lambda tag: True, } ], "static_url_path": "/flasgger_static", "swagger_ui": True, "specs_route": "/swagger", "title": "Container Spawner API", "description": "API für Container-Spawner mit Admin-Debug-Endpoints", "version": "2.0.0", "termsOfService": "", "contact": { "name": "API Support" } } swagger = Swagger(app, config=swagger_config) @jwt.token_in_blocklist_loader def check_if_token_in_blocklist(jwt_header, jwt_payload): return check_if_token_revoked(jwt_header, jwt_payload) @jwt.expired_token_loader def expired_token_callback(jwt_header, jwt_payload): return jsonify({'error': 'Token abgelaufen'}), 401 @jwt.invalid_token_loader def invalid_token_callback(error): return jsonify({'error': 'Ungültiger Token'}), 401 @jwt.unauthorized_loader def missing_token_callback(error): return jsonify({'error': 'Authentifizierung erforderlich'}), 401 # ======================================== # Logging konfigurieren # ======================================== log_file = app.config.get('LOG_FILE', '/app/logs/spawner.log') log_dir = os.path.dirname(log_file) # Erstelle Log-Verzeichnis falls nicht vorhanden if log_dir and not os.path.exists(log_dir): os.makedirs(log_dir, exist_ok=True) # Rotating File Handler (max 10MB pro Datei, 5 Backups) if log_file: file_handler = RotatingFileHandler( log_file, maxBytes=10485760, # 10MB backupCount=5 ) file_handler.setLevel(logging.INFO) formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') file_handler.setFormatter(formatter) app.logger.addHandler(file_handler) app.logger.setLevel(logging.INFO) # Flask-Login initialisieren login_manager = LoginManager() login_manager.init_app(app) login_manager.login_view = 'auth.login' login_manager.login_message = 'Bitte melde dich an, um auf diese Seite zuzugreifen.' login_manager.login_message_category = 'error' # Blueprints registrieren app.register_blueprint(auth_bp) app.register_blueprint(api_bp) app.register_blueprint(admin_bp) @login_manager.user_loader def load_user(user_id): """Lädt User für Flask-Login""" return User.query.get(int(user_id)) @app.route('/') def index(): """Startseite - Redirect zu Dashboard oder Login""" if current_user.is_authenticated: return redirect(url_for('dashboard')) return redirect(url_for('auth.login')) @app.route('/dashboard') @login_required def dashboard(): """Dashboard - zeigt Container-Status und Service-URL""" container_mgr = ContainerManager() container_status = 'unknown' if current_user.container_id: container_status = container_mgr.get_container_status(current_user.container_id) # Service-URL für den User (pfad-basiert) scheme = app.config['PREFERRED_URL_SCHEME'] spawner_domain = f"{app.config['SPAWNER_SUBDOMAIN']}.{app.config['BASE_DOMAIN']}" service_url = f"{scheme}://{spawner_domain}/{current_user.slug}" return render_template('dashboard.html', user=current_user, service_url=service_url, container_status=container_status) @app.route('/container/restart') @login_required def restart_container(): """Startet Container des Users neu""" container_mgr = ContainerManager() # Alten Container stoppen falls vorhanden if current_user.container_id: container_mgr.stop_container(current_user.container_id) container_mgr.remove_container(current_user.container_id) # Neuen Container starten try: container_id, port = container_mgr.spawn_container(current_user.id, current_user.slug) current_user.container_id = container_id current_user.container_port = port db.session.commit() except Exception as e: app.logger.error(f"Container-Restart fehlgeschlagen: {str(e)}") return redirect(url_for('dashboard')) @app.route('/health') def health(): """Health-Check für Docker und Monitoring""" db_status = 'ok' docker_status = 'warning' try: # DB-Check (KRITISCH) db.session.execute(text('SELECT 1')) except Exception as e: db_status = f'error: {str(e)}' app.logger.error(f"Database health check failed: {str(e)}") try: # Docker-Check (OPTIONAL) container_mgr = ContainerManager() container_mgr._get_client().ping() docker_status = 'ok' except Exception as e: docker_status = f'warning: {str(e)}' app.logger.warning(f"Docker health check failed (non-critical): {str(e)}") # Status 503 nur wenn DATABASE down ist, nicht wenn Docker down ist status_code = 200 if db_status == 'ok' else 503 response = { 'status': 'healthy' if status_code == 200 else 'unhealthy', 'database': db_status, 'docker': docker_status, 'version': '1.0.0' } if status_code != 200: app.logger.error(f"Health check CRITICAL: {response}") else: app.logger.info(f"Health check OK") return response, status_code # Datenbank-Tabellen erstellen beim ersten Start with app.app_context(): db.create_all() app.logger.info('Datenbank-Tabellen erstellt') if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=False)