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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user