diff --git a/MULTITENANT_INTEGRATION.md b/MULTITENANT_INTEGRATION.md index 7f871cf..dfbe6c4 100644 --- a/MULTITENANT_INTEGRATION.md +++ b/MULTITENANT_INTEGRATION.md @@ -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 **Fragen?** diff --git a/Web/settings.py b/Web/settings.py index 8e4e485..d12c065 100644 --- a/Web/settings.py +++ b/Web/settings.py @@ -157,8 +157,36 @@ SSL_KEY = _get(_conf, ['ssl', 'key'], DEFAULTS['ssl']['key']) SCHOOL_PERIODS = _get(_conf, ['schoolPeriods'], DEFAULTS['schoolPeriods']) # Optional feature modules -LIBRARY_MODULE_ENABLED = bool(_get(_conf, ['modules', 'library', 'enabled'], DEFAULTS['modules']['library']['enabled'])) -STUDENT_CARDS_MODULE_ENABLED = bool(_get(_conf, ['modules', 'student_cards', 'enabled'], DEFAULTS['modules']['student_cards']['enabled'])) +TENANT_CONFIGS = _get(_conf, ['tenants'], {}) + + +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_MAX_BORROW_DAYS = int(_get(_conf, ['modules', 'student_cards', 'max_borrow_days'], DEFAULTS['modules']['student_cards']['max_borrow_days'])) diff --git a/Web/tenant.py b/Web/tenant.py index 6a48d3f..5db42f1 100644 --- a/Web/tenant.py +++ b/Web/tenant.py @@ -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 functools import wraps import logging +import settings as cfg logger = logging.getLogger(__name__) # Tenant registry: maps subdomain/tenant_id to database name 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: @@ -27,6 +59,7 @@ class TenantContext: self.tenant_id = None self.db_name = None self.subdomain = None + self.config = {} def resolve_tenant(self): """ @@ -40,6 +73,7 @@ class TenantContext: tenant_from_header = request.headers.get('X-Tenant-ID', '').strip() if 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) # Priority 2: Subdomain extraction @@ -56,10 +90,12 @@ class TenantContext: if potential_subdomain not in ('www', 'api', 'admin', 'app', 'mail'): self.subdomain = potential_subdomain self.tenant_id = potential_subdomain + self.config = get_tenant_config(potential_subdomain) return self._get_db_name(potential_subdomain) # Fallback to default tenant if no subdomain detected self.tenant_id = 'default' + self.config = get_tenant_config('default') return self._get_db_name('default') def _get_db_name(self, tenant_id): diff --git a/config.json b/config.json index 8a8dfa3..84ae9bf 100755 --- a/config.json +++ b/config.json @@ -50,6 +50,8 @@ } }, + "tenants": {}, + "allowed_extensions": [ "png", "jpg", "jpeg", "gif", "hevc", "heif",