Files
Inventarsystem/Legal/MULTITENANT_INTEGRATION.md
Aiirondev_dev 0c27d7ac86 feat: Add Excel, PDF export, and user generation modules
- Implemented `excel_export.py` for generating library item exports in Excel format.
- Created `pdf_export.py` for generating audit reports compliant with DIN 5008 standards, including detailed event tables and signature blocks.
- Developed `generate_user.py` for interactive user creation with validation for usernames and passwords.
- Introduced `module_registry.py` for managing module states and path matching.
- Added a basic `__init__.py` in the `terminplaner` module for initialization.
2026-05-20 15:30:23 +02:00

367 lines
9.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Multi-Tenant Integration in Flask App
Hier sind die konkreten Änderungen für `Web/app.py`, um Multi-Tenant Funktionalität zu aktivieren.
## Änderung 1: Imports hinzufügen
**VORHER** (Zeile 1-60 in app.py):
```python
from flask import Flask, render_template, request, ...
from werkzeug.utils import secure_filename
# ... weitere imports
```
**NACHHER** (Zusätzliche Imports):
```python
# Multi-Tenant Imports
from tenant import get_tenant_context, require_tenant, get_tenant_db
from session_manager import create_redis_session_interface
from query_cache import get_cache_manager, cached_query, invalidate_cache
```
---
## Änderung 2: Redis Session Backend konfigurieren
**NACH** `app = Flask(...)` (ca. Zeile 65):
```python
app = Flask(__name__, static_folder='static')
app.logger.setLevel(logging.WARNING)
app.secret_key = cfg.SECRET_KEY
# ========== MULTI-TENANT KONFIGURATION ==========
# Aktiviere Redis Session Backend statt Filesystem
if os.getenv('INVENTAR_SESSION_BACKEND') == 'redis':
try:
app.session_interface = create_redis_session_interface(app)
app.logger.info("Redis session backend enabled")
except Exception as e:
app.logger.warning(f"Redis session backend failed, using default: {e}")
# ================================================
```
---
## Änderung 3: Health Check Endpoint hinzufügen
**Neuer Route** (nach allen anderen Routes, vor `if __name__ == '__main__'`):
```python
@app.route('/health')
def health_check():
"""
Health check endpoint für Nginx Load Balancer.
Wird regelmäßig von Nginx aufgerufen (30s interval).
Rückgabe: 200 OK wenn app bereit, sonst 503
"""
try:
# Check Database Connection
from settings import MongoClient
mongo = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
mongo.admin.command('ping')
# Check Redis Connection (falls Redis Session aktiv)
if os.getenv('INVENTAR_SESSION_BACKEND') == 'redis':
cache_mgr = get_cache_manager()
if cache_mgr and cache_mgr.redis:
cache_mgr.redis.ping()
return {'status': 'healthy'}, 200
except Exception as e:
app.logger.error(f"Health check failed: {e}")
return {'status': 'unhealthy', 'error': str(e)}, 503
```
---
## Änderung 4: Tenant Context in bestehende Database-Calls
**WICHTIG**: Alle `MongoClient` Zugriffe müssen durch Tenant-Context gehen.
### Beispiel 1: Bestehender Code (VORHER)
```python
# VORHER: Direkter DB-Zugriff
@app.route('/items')
def get_items():
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB] # ← PROBLEM: Alle Tenants teilen sich die gleiche DB
items = db['items'].find().to_list(100)
return jsonify(items)
```
### Beispiel 1: Mit Tenant-Routing (NACHHER)
```python
# NACHHER: Tenant-Isolierte DB
@app.route('/items')
@require_tenant # ← Decorator setzt Tenant Context
def get_items():
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
ctx = get_tenant_context()
# Richtige Datenbank für diesen Tenant
db = client[ctx.db_name] # z.B. "inventar_schule1"
items = db['items'].find().to_list(100)
return jsonify(items)
```
### Oder kürzere Variante:
```python
@app.route('/items')
@require_tenant
def get_items():
db = get_tenant_db(MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT))
items = db['items'].find().to_list(100)
return jsonify(items)
```
---
## Änderung 5: Query Caching für häufige Abfragen
**VORHER**:
```python
def load_user_profile(user_id):
db = client[cfg.MONGODB_DB]
# Direkter DB-Zugriff bei jedem Request
return db['users'].find_one({'_id': ObjectId(user_id)})
```
**NACHHER**:
```python
@cached_query(category='user', ttl=7*24*3600) # 7 Tage Cache
def load_user_profile(user_id):
db = get_tenant_db(MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT))
return db['users'].find_one({'_id': ObjectId(user_id)})
```
Nach Update muss Cache invalidiert werden:
```python
def update_user_profile(user_id, updates):
db = get_tenant_db(MongoClient(...))
db['users'].update_one({'_id': ObjectId(user_id)}, {'$set': updates})
# Cache invalidieren nach Write
ctx = get_tenant_context()
invalidate_cache(ctx.tenant_id, 'user')
return True
```
---
## Änderung 6: Debugging - Tenant-Info in Logs
**Logging Helper** (am besten in einem bestehenden Logging-Block):
```python
def log_with_tenant(level, message):
"""Helper um Tenant-ID in Logs zu erfassen."""
ctx = get_tenant_context()
tenant_id = ctx.tenant_id if ctx else 'unknown'
prefixed_msg = f"[{tenant_id}] {message}"
if level == 'error':
app.logger.error(prefixed_msg)
elif level == 'warning':
app.logger.warning(prefixed_msg)
elif level == 'info':
app.logger.info(prefixed_msg)
else:
app.logger.debug(prefixed_msg)
# Nutzung:
log_with_tenant('info', 'User login successful')
# Output: "[schule1] User login successful"
```
---
## Änderung 7: Test-Routes (optional, für Debugging)
```python
@app.route('/debug/tenant')
def debug_tenant_info():
"""Debug-Endpoint: Zeigt aktuellen Tenant-Context."""
ctx = get_tenant_context()
cache_mgr = get_cache_manager()
return {
'tenant_id': ctx.tenant_id if ctx else None,
'db_name': ctx.db_name if ctx else None,
'subdomain': ctx.subdomain if ctx else None,
'cache_enabled': cache_mgr is not None,
'cache_stats': cache_mgr.get_stats(ctx.tenant_id) if ctx and cache_mgr else None,
'request_host': request.host,
'request_headers': dict(request.headers)
}
@app.route('/debug/cache/<action>', methods=['POST'])
def debug_cache_control(action):
"""Debug-Endpoint: Cache Kontrolle."""
ctx = get_tenant_context()
cache_mgr = get_cache_manager()
if action == 'clear':
if cache_mgr:
cache_mgr.invalidate_tenant(ctx.tenant_id)
return {'status': 'Cache cleared for tenant', 'tenant': ctx.tenant_id}
return {'status': 'unknown action'}, 400
```
---
## Änderung 8: Umgebungsvariablen (.env)
```bash
# Multi-Tenant Konfiguration
INVENTAR_MULTITENANT_ENABLED=true
INVENTAR_SESSION_BACKEND=redis
INVENTAR_REDIS_HOST=redis
INVENTAR_REDIS_PORT=6379
INVENTAR_REDIS_DB=0
# Query Caching
INVENTAR_QUERY_CACHE_ENABLED=true
INVENTAR_CACHE_DB=1
# Logging (auf WARNING reduzieren)
INVENTAR_LOG_LEVEL=WARNING
# Performance
INVENTAR_WORKERS=4
INVENTAR_WORKER_CLASS=gevent
INVENTAR_WORKER_CONNECTIONS=100
```
---
## Migration-Strategie
### Phase 1: Compatibility Mode (Keine Breaking Changes)
- ✓ Beide Mode laufen gleichzeitig (Single + Multi)
- ✓ Alte Routes funktionieren ohne Änderung
- ✓ Neue Routes können `@require_tenant` nutzen
- ✓ Session-Fallback wenn Redis nicht verfügbar
### Phase 2: Graduelle Migration
1. Starten mit SINGLE Instance + Single Database
```bash
docker-compose -f docker-compose-multitenant.yml up -d --scale app=1
```
2. Redis Session Backend aktivieren
```bash
INVENTAR_SESSION_BACKEND=redis
```
3. Einzelne Routes mit `@require_tenant` decorator markieren
4. Query Caching für häufige Abfragen hinzufügen
5. Testing mit Multi-Subdomain (test1.local, test2.local)
6. Full Multi-Tenant in Production
```bash
docker-compose -f docker-compose-multitenant.yml up -d --scale app=5
```
---
## Performance-Vergleich
### Single Instance (Vorher)
```
1 App Instance
- Memory: 200MB
- Startup: 8s
- Max Users: ~50 (gleichzeitig)
- DB Load: 100%
- Sessions: Filesystem I/O
```
### Multi-Instance (Nachher)
```
3 App Instances
- Memory: 3 × 100MB = 300MB (gesamt)
- Startup: 3s pro Instance
- Max Users: ~150 (gleichzeitig, 50 pro instance)
- DB Load: 30% (durch Caching)
- Sessions: Redis (keine I/O)
```
---
## Checkliste für Deployment
- [ ] Docker-compose-multitenant.yml durchgelesen
- [ ] Tenant-Module (tenant.py, session_manager.py, query_cache.py) im Web/ Ordner
- [ ] app.py mit Multi-Tenant Imports aktualisiert
- [ ] Redis Session Backend aktiviert (INVENTAR_SESSION_BACKEND=redis)
- [ ] Health Check Endpoint implementiert
- [ ] Nginx multitenant.conf konfiguriert
- [ ] SSL Wildcard Zertifikat erstellt
- [ ] DNS Wildcard Record konfiguriert (*.example.com)
- [ ] First Tenant als test registriert
- [ ] Health Checks funktionieren: curl https://test.example.com/health
- [ ] Cache Stats verfügbar: curl https://test.example.com/debug/tenant
- [ ] Load Test mit 2-3 Tenants durchgeführt
- [ ] Monitoring Setup (Docker Stats, Nginx Logs)
---
## Schul-Konfiguration pro Tenant
Die Datei `config.json` unterstützt jetzt einen `tenants`-Block. Damit kann jede Schule eigene Modul-Schalter bekommen, ohne dass das ganze System global umgestellt werden muss.
```json
{
"tenants": {
"schule1": {
"modules": {
"library": { "enabled": true },
"student_cards": { "enabled": false }
}
},
"schule2": {
"modules": {
"library": { "enabled": false }
}
}
}
}
```
Wenn ein Request über Subdomain oder `X-Tenant-ID` aufgelöst wird, liest die App diese Werte automatisch aus und blendet die Bibliothek bzw. andere Module nur für diesen Tenant ein oder aus.
---
## Support & Debugging
**Fragen?**
1. Logs prüfen: `docker-compose -f docker-compose-multitenant.yml logs -f app`
2. Tenant-Info prüfen: `curl https://your-tenant.com/debug/tenant`
3. Cache Stats: `curl https://your-tenant.com/debug/cache-stats`
4. Redis Stats: `docker exec inventarsystem-redis redis-cli info stats`
**Häufige Fehler:**
- `X-Tenant-ID Header missing` → Nginx nutzt alte Konfiguration
- `Redis connection refused` → Redis Container nicht gestartet
- `Database not found` → Tenant nicht registriert (auto-create bei erstem Request)
- `Out of memory` → Memory Limit zu niedrig oder zu viele Instanzen