Merge remote-tracking branch 'refs/remotes/origin/main'
This commit is contained in:
@@ -322,6 +322,32 @@ INVENTAR_WORKER_CONNECTIONS=100
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 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
|
## Support & Debugging
|
||||||
|
|
||||||
**Fragen?**
|
**Fragen?**
|
||||||
|
|||||||
+30
-2
@@ -157,8 +157,36 @@ SSL_KEY = _get(_conf, ['ssl', 'key'], DEFAULTS['ssl']['key'])
|
|||||||
SCHOOL_PERIODS = _get(_conf, ['schoolPeriods'], DEFAULTS['schoolPeriods'])
|
SCHOOL_PERIODS = _get(_conf, ['schoolPeriods'], DEFAULTS['schoolPeriods'])
|
||||||
|
|
||||||
# Optional feature modules
|
# Optional feature modules
|
||||||
LIBRARY_MODULE_ENABLED = bool(_get(_conf, ['modules', 'library', 'enabled'], DEFAULTS['modules']['library']['enabled']))
|
TENANT_CONFIGS = _get(_conf, ['tenants'], {})
|
||||||
STUDENT_CARDS_MODULE_ENABLED = bool(_get(_conf, ['modules', 'student_cards', 'enabled'], DEFAULTS['modules']['student_cards']['enabled']))
|
|
||||||
|
|
||||||
|
class _TenantAwareBool:
|
||||||
|
def __init__(self, module_name, default):
|
||||||
|
self.module_name = module_name
|
||||||
|
self.default = bool(default)
|
||||||
|
|
||||||
|
def resolve(self):
|
||||||
|
try:
|
||||||
|
from tenant import is_tenant_module_enabled
|
||||||
|
return bool(is_tenant_module_enabled(self.module_name, default=self.default))
|
||||||
|
except Exception:
|
||||||
|
return self.default
|
||||||
|
|
||||||
|
def __bool__(self):
|
||||||
|
return self.resolve()
|
||||||
|
|
||||||
|
def __int__(self):
|
||||||
|
return int(self.resolve())
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return 'True' if self.resolve() else 'False'
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"_TenantAwareBool(module_name={self.module_name!r}, value={self.resolve()!r})"
|
||||||
|
|
||||||
|
|
||||||
|
LIBRARY_MODULE_ENABLED = _TenantAwareBool('library', _get(_conf, ['modules', 'library', 'enabled'], DEFAULTS['modules']['library']['enabled']))
|
||||||
|
STUDENT_CARDS_MODULE_ENABLED = _TenantAwareBool('student_cards', _get(_conf, ['modules', 'student_cards', 'enabled'], DEFAULTS['modules']['student_cards']['enabled']))
|
||||||
STUDENT_DEFAULT_BORROW_DAYS = int(_get(_conf, ['modules', 'student_cards', 'default_borrow_days'], DEFAULTS['modules']['student_cards']['default_borrow_days']))
|
STUDENT_DEFAULT_BORROW_DAYS = int(_get(_conf, ['modules', 'student_cards', 'default_borrow_days'], DEFAULTS['modules']['student_cards']['default_borrow_days']))
|
||||||
STUDENT_MAX_BORROW_DAYS = int(_get(_conf, ['modules', 'student_cards', 'max_borrow_days'], DEFAULTS['modules']['student_cards']['max_borrow_days']))
|
STUDENT_MAX_BORROW_DAYS = int(_get(_conf, ['modules', 'student_cards', 'max_borrow_days'], DEFAULTS['modules']['student_cards']['max_borrow_days']))
|
||||||
|
|
||||||
|
|||||||
@@ -10,11 +10,43 @@ Each tenant can support up to 20+ users with isolated data and resource pools.
|
|||||||
from flask import request, g, has_request_context
|
from flask import request, g, has_request_context
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
import logging
|
import logging
|
||||||
|
import settings as cfg
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Tenant registry: maps subdomain/tenant_id to database name
|
# Tenant registry: maps subdomain/tenant_id to database name
|
||||||
TENANT_REGISTRY = {}
|
TENANT_REGISTRY = {}
|
||||||
|
if isinstance(getattr(cfg, 'TENANT_CONFIGS', None), dict):
|
||||||
|
TENANT_REGISTRY.update(cfg.TENANT_CONFIGS)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_nested_value(source, path, default=None):
|
||||||
|
current = source
|
||||||
|
for key in path:
|
||||||
|
if isinstance(current, dict) and key in current:
|
||||||
|
current = current[key]
|
||||||
|
else:
|
||||||
|
return default
|
||||||
|
return current
|
||||||
|
|
||||||
|
|
||||||
|
def get_tenant_config(tenant_id=None):
|
||||||
|
"""Return the registered config for a tenant, falling back to default."""
|
||||||
|
if tenant_id is None:
|
||||||
|
ctx = get_tenant_context()
|
||||||
|
tenant_id = ctx.tenant_id if ctx and ctx.tenant_id else 'default'
|
||||||
|
|
||||||
|
if tenant_id in TENANT_REGISTRY:
|
||||||
|
return TENANT_REGISTRY[tenant_id] or {}
|
||||||
|
|
||||||
|
return TENANT_REGISTRY.get('default', {}) or {}
|
||||||
|
|
||||||
|
|
||||||
|
def is_tenant_module_enabled(module_name, tenant_id=None, default=False):
|
||||||
|
"""Resolve whether a feature module is enabled for the current tenant."""
|
||||||
|
config = get_tenant_config(tenant_id)
|
||||||
|
enabled = _get_nested_value(config, ['modules', module_name, 'enabled'], default)
|
||||||
|
return bool(enabled)
|
||||||
|
|
||||||
|
|
||||||
class TenantContext:
|
class TenantContext:
|
||||||
@@ -27,6 +59,7 @@ class TenantContext:
|
|||||||
self.tenant_id = None
|
self.tenant_id = None
|
||||||
self.db_name = None
|
self.db_name = None
|
||||||
self.subdomain = None
|
self.subdomain = None
|
||||||
|
self.config = {}
|
||||||
|
|
||||||
def resolve_tenant(self):
|
def resolve_tenant(self):
|
||||||
"""
|
"""
|
||||||
@@ -40,6 +73,7 @@ class TenantContext:
|
|||||||
tenant_from_header = request.headers.get('X-Tenant-ID', '').strip()
|
tenant_from_header = request.headers.get('X-Tenant-ID', '').strip()
|
||||||
if tenant_from_header:
|
if tenant_from_header:
|
||||||
self.tenant_id = tenant_from_header
|
self.tenant_id = tenant_from_header
|
||||||
|
self.config = get_tenant_config(tenant_from_header)
|
||||||
return self._get_db_name(tenant_from_header)
|
return self._get_db_name(tenant_from_header)
|
||||||
|
|
||||||
# Priority 2: Subdomain extraction
|
# Priority 2: Subdomain extraction
|
||||||
@@ -56,10 +90,12 @@ class TenantContext:
|
|||||||
if potential_subdomain not in ('www', 'api', 'admin', 'app', 'mail'):
|
if potential_subdomain not in ('www', 'api', 'admin', 'app', 'mail'):
|
||||||
self.subdomain = potential_subdomain
|
self.subdomain = potential_subdomain
|
||||||
self.tenant_id = potential_subdomain
|
self.tenant_id = potential_subdomain
|
||||||
|
self.config = get_tenant_config(potential_subdomain)
|
||||||
return self._get_db_name(potential_subdomain)
|
return self._get_db_name(potential_subdomain)
|
||||||
|
|
||||||
# Fallback to default tenant if no subdomain detected
|
# Fallback to default tenant if no subdomain detected
|
||||||
self.tenant_id = 'default'
|
self.tenant_id = 'default'
|
||||||
|
self.config = get_tenant_config('default')
|
||||||
return self._get_db_name('default')
|
return self._get_db_name('default')
|
||||||
|
|
||||||
def _get_db_name(self, tenant_id):
|
def _get_db_name(self, tenant_id):
|
||||||
|
|||||||
@@ -50,6 +50,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"tenants": {},
|
||||||
|
|
||||||
"allowed_extensions": [
|
"allowed_extensions": [
|
||||||
"png", "jpg", "jpeg", "gif",
|
"png", "jpg", "jpeg", "gif",
|
||||||
"hevc", "heif",
|
"hevc", "heif",
|
||||||
|
|||||||
Reference in New Issue
Block a user