- Add rotating file handler to app.py for logging to /app/logs/spawner.log - Configure max 10MB per file with 5 backup files - Update admin_api.py debug endpoint to read from Flask log file - Implement clear-logs functionality to truncate log file - Update documentation with Flask log file details - Creates log directory automatically if missing
187 lines
5.8 KiB
Python
187 lines
5.8 KiB
Python
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 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)
|
|
|
|
@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)
|