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.
This commit is contained in:
2026-05-20 15:30:23 +02:00
parent b3fc470c88
commit 0c27d7ac86
42 changed files with 122 additions and 1055 deletions
+366
View File
@@ -0,0 +1,366 @@
# 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