**Backend (config.py):** - Add dynamic template loading from USER_TEMPLATE_IMAGES (.env) - Add template metadata loading from templates.json - Implement automatic type extraction from image names - Remove hardcoded template definitions (dev/prod) - Maintain legacy USER_TEMPLATE_IMAGE for backward compatibility **Configuration:** - Add templates.json with metadata for template-01, template-02, template-next - Update .env.example with new USER_TEMPLATE_IMAGES variable (semicolon-separated) - Document automatic template type extraction **Installation (install.sh):** - Implement auto-detection for all user-template-* directories - Replace hardcoded template builds with dynamic loop - Calculate TOTAL_BUILDS dynamically - Add special handling for Next.js templates **Documentation:** - Move MVP_DEPLOYMENT_GUIDE.md to docs/install/DEPLOYMENT_GUIDE.md - Add "Dynamic Template System" section to CLAUDE.md - Update docs/install/README.md with Quick Links and dynamic system info - Add references to deployment guide in CLAUDE.md **Templates:** - Reorganize user-template/ → user-template-01/ (Nginx Basic) - Add user-template-02/ (Nginx Advanced) - Keep user-template-next/ unchanged **Benefits:** - Unlimited number of templates (no longer hardcoded to 2) - Metadata-driven display in dashboard - Automatic image discovery and building - Extensible without code changes Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
225 lines
7.7 KiB
Python
225 lines
7.7 KiB
Python
import os
|
|
import json
|
|
from pathlib import Path
|
|
from dotenv import load_dotenv
|
|
|
|
load_dotenv()
|
|
|
|
class Config:
|
|
# ========================================
|
|
# Sicherheit
|
|
# ========================================
|
|
SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production')
|
|
|
|
# JWT-Konfiguration
|
|
JWT_SECRET_KEY = os.getenv('JWT_SECRET_KEY', SECRET_KEY)
|
|
JWT_ACCESS_TOKEN_EXPIRES = int(os.getenv('JWT_ACCESS_TOKEN_EXPIRES', 3600)) # 1 Stunde
|
|
JWT_TOKEN_LOCATION = ['headers']
|
|
JWT_HEADER_NAME = 'Authorization'
|
|
JWT_HEADER_TYPE = 'Bearer'
|
|
|
|
# CORS-Konfiguration
|
|
CORS_ORIGINS = os.getenv('CORS_ORIGINS', 'http://localhost:3000').split(',')
|
|
|
|
# Session-Sicherheit
|
|
SESSION_COOKIE_SECURE = os.getenv('BASE_DOMAIN', 'localhost') != 'localhost'
|
|
SESSION_COOKIE_HTTPONLY = True
|
|
SESSION_COOKIE_SAMESITE = 'Lax'
|
|
PERMANENT_SESSION_LIFETIME = 3600 # 1 Stunde
|
|
|
|
# ========================================
|
|
# Datenbank
|
|
# ========================================
|
|
SQLALCHEMY_DATABASE_URI = os.getenv(
|
|
'DATABASE_URL',
|
|
'sqlite:////app/data/users.db' # 4 slashes: sqlite:// + /app/data/users.db
|
|
)
|
|
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
|
|
|
# ========================================
|
|
# Docker-Konfiguration - DYNAMISCHES TEMPLATE-SYSTEM
|
|
# ========================================
|
|
DOCKER_SOCKET = os.getenv('DOCKER_SOCKET', 'unix://var/run/docker.sock')
|
|
|
|
# LEGACY: Wird noch für alte spawn_container() verwendet
|
|
USER_TEMPLATE_IMAGE = os.getenv('USER_TEMPLATE_IMAGE', 'user-template-01:latest')
|
|
|
|
# ========================================
|
|
# Dynamisches Template-Loading
|
|
# ========================================
|
|
|
|
@staticmethod
|
|
def _extract_type_from_image(image_name: str) -> str:
|
|
"""
|
|
Extrahiert Container-Typ aus Image-Namen
|
|
|
|
Examples:
|
|
'user-template-01:latest' → 'template-01'
|
|
'user-template-next:latest' → 'template-next'
|
|
'custom-nginx:v1.0' → 'custom-nginx'
|
|
"""
|
|
# Entferne Tag (:latest, :v1.0, etc.)
|
|
base_name = image_name.split(':')[0]
|
|
|
|
# Entferne 'user-' Prefix falls vorhanden
|
|
if base_name.startswith('user-'):
|
|
base_name = base_name[5:] # 'user-template-01' → 'template-01'
|
|
|
|
return base_name
|
|
|
|
@staticmethod
|
|
def _load_template_images() -> list:
|
|
"""Lädt Template-Image-Liste aus USER_TEMPLATE_IMAGES (semikolon-getrennt)"""
|
|
raw_images = os.getenv('USER_TEMPLATE_IMAGES', '')
|
|
if not raw_images:
|
|
# Fallback für Kompatibilität
|
|
return ['user-template-01:latest']
|
|
|
|
return [img.strip() for img in raw_images.split(';') if img.strip()]
|
|
|
|
@staticmethod
|
|
def _load_templates_config() -> dict:
|
|
"""Lädt Template-Konfiguration aus templates.json"""
|
|
config_path = Path(__file__).parent / 'templates.json'
|
|
|
|
if not config_path.exists():
|
|
return {}
|
|
|
|
try:
|
|
with open(config_path, 'r', encoding='utf-8') as f:
|
|
data = json.load(f)
|
|
|
|
# Konvertiere Array zu Dictionary (key=type)
|
|
return {
|
|
template['type']: template
|
|
for template in data.get('templates', [])
|
|
}
|
|
except (json.JSONDecodeError, KeyError) as e:
|
|
import sys
|
|
print(f"[CONFIG] Warnung: Fehler beim Laden von templates.json: {e}", file=sys.stderr)
|
|
return {}
|
|
|
|
@staticmethod
|
|
def _build_container_templates() -> dict:
|
|
"""
|
|
Baut CONTAINER_TEMPLATES Dictionary aus:
|
|
1. TEMPLATE_IMAGES (Liste der verfügbaren Images)
|
|
2. TEMPLATES_CONFIG (Metadaten aus templates.json)
|
|
"""
|
|
templates = {}
|
|
|
|
for image in Config.TEMPLATE_IMAGES:
|
|
# Extrahiere Typ aus Image-Namen
|
|
container_type = Config._extract_type_from_image(image)
|
|
|
|
# Hole Metadaten aus JSON (falls vorhanden)
|
|
config = Config.TEMPLATES_CONFIG.get(container_type, {})
|
|
|
|
# Verwende JSON-Metadaten oder Fallback
|
|
templates[container_type] = {
|
|
'image': image,
|
|
'display_name': config.get('display_name', container_type.replace('-', ' ').title()),
|
|
'description': config.get('description', f'Container basierend auf {image}')
|
|
}
|
|
|
|
return templates
|
|
|
|
# Dynamisches Template-Loading initialisieren
|
|
TEMPLATE_IMAGES = _load_template_images.__func__()
|
|
TEMPLATES_CONFIG = _load_templates_config.__func__()
|
|
CONTAINER_TEMPLATES = _build_container_templates.__func__()
|
|
|
|
# ========================================
|
|
# Traefik/Domain-Konfiguration
|
|
# ========================================
|
|
BASE_DOMAIN = os.getenv('BASE_DOMAIN', 'localhost')
|
|
SPAWNER_SUBDOMAIN = os.getenv('SPAWNER_SUBDOMAIN', 'spawner')
|
|
TRAEFIK_NETWORK = os.getenv('TRAEFIK_NETWORK', 'web')
|
|
TRAEFIK_CERTRESOLVER = os.getenv('TRAEFIK_CERTRESOLVER', 'lets-encrypt')
|
|
TRAEFIK_ENTRYPOINT = os.getenv('TRAEFIK_ENTRYPOINT', 'websecure')
|
|
|
|
# Vollständige Spawner-URL
|
|
SPAWNER_URL = f"{SPAWNER_SUBDOMAIN}.{BASE_DOMAIN}"
|
|
|
|
# ========================================
|
|
# Application-Settings
|
|
# ========================================
|
|
# HTTPS automatisch für Nicht-Localhost
|
|
PREFERRED_URL_SCHEME = 'https' if BASE_DOMAIN != 'localhost' else 'http'
|
|
|
|
# Spawner-Port (nur für Debugging wichtig)
|
|
SPAWNER_PORT = int(os.getenv('SPAWNER_PORT', 5000))
|
|
|
|
# ========================================
|
|
# Optionale Einstellungen
|
|
# ========================================
|
|
# Logging
|
|
LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO')
|
|
LOG_FILE = os.getenv('LOG_FILE', '/app/logs/spawner.log')
|
|
|
|
# Container-Limits (für container_manager.py)
|
|
DEFAULT_MEMORY_LIMIT = os.getenv('DEFAULT_MEMORY_LIMIT', '512m')
|
|
DEFAULT_CPU_QUOTA = int(os.getenv('DEFAULT_CPU_QUOTA', 50000)) # 0.5 CPU
|
|
|
|
# Container-Cleanup
|
|
CONTAINER_IDLE_TIMEOUT = int(os.getenv('CONTAINER_IDLE_TIMEOUT', 3600)) # 1h in Sekunden
|
|
|
|
# ========================================
|
|
# SMTP / Email-Konfiguration
|
|
# ========================================
|
|
SMTP_HOST = os.getenv('SMTP_HOST', 'localhost')
|
|
SMTP_PORT = int(os.getenv('SMTP_PORT', 587))
|
|
SMTP_USER = os.getenv('SMTP_USER', '')
|
|
SMTP_PASSWORD = os.getenv('SMTP_PASSWORD', '')
|
|
SMTP_FROM = os.getenv('SMTP_FROM', 'noreply@localhost')
|
|
SMTP_USE_TLS = os.getenv('SMTP_USE_TLS', 'true').lower() == 'true'
|
|
|
|
# Frontend-URL fuer Email-Links
|
|
FRONTEND_URL = os.getenv(
|
|
'FRONTEND_URL',
|
|
f"{PREFERRED_URL_SCHEME}://{SPAWNER_SUBDOMAIN}.{BASE_DOMAIN}"
|
|
)
|
|
|
|
# ========================================
|
|
# Magic Link Passwordless Auth
|
|
# ========================================
|
|
MAGIC_LINK_TOKEN_EXPIRY = int(os.getenv('MAGIC_LINK_TOKEN_EXPIRY', 900)) # 15 Minuten
|
|
MAGIC_LINK_RATE_LIMIT = int(os.getenv('MAGIC_LINK_RATE_LIMIT', 3)) # Max 3 pro Stunde
|
|
|
|
|
|
class DevelopmentConfig(Config):
|
|
"""Konfiguration für Entwicklung"""
|
|
DEBUG = True
|
|
TESTING = False
|
|
|
|
|
|
class ProductionConfig(Config):
|
|
"""Konfiguration für Produktion"""
|
|
DEBUG = False
|
|
TESTING = False
|
|
|
|
# Strikte Session-Sicherheit
|
|
SESSION_COOKIE_SECURE = True
|
|
|
|
# Optional: PostgreSQL statt SQLite
|
|
# SQLALCHEMY_DATABASE_URI = os.getenv(
|
|
# 'DATABASE_URL',
|
|
# 'postgresql://spawner:password@postgres:5432/spawner'
|
|
# )
|
|
|
|
|
|
class TestingConfig(Config):
|
|
"""Konfiguration für Tests"""
|
|
TESTING = True
|
|
SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
|
|
WTF_CSRF_ENABLED = False
|
|
|
|
|
|
# Config-Dict für einfaches Laden
|
|
config = {
|
|
'development': DevelopmentConfig,
|
|
'production': ProductionConfig,
|
|
'testing': TestingConfig,
|
|
'default': DevelopmentConfig
|
|
}
|