Compare commits

...

11 Commits

Author SHA1 Message Date
Aiirondev_dev 6c73f79866 Enhance module navigation: update inventory and library navbar logic for improved module access and user session handling 2026-05-08 12:03:38 +02:00
Aiirondev_dev 5cb59f4b63 Enhance module management: add inventory module support in settings, update templates and app logic for module access control, and extend tenant management script for module configuration 2026-05-08 11:24:53 +02:00
Aiirondev_dev 326fc4ef91 Enhance Docker configuration: update .docker-build.env and .docker-compose.runtime.override.yml for improved resource allocation and service settings 2026-05-01 10:51:43 +02:00
Aiirondev_dev 12cd365f36 Enhance filter retrieval: update get_filter API to return full filter lists and modify frontend to utilize them for improved item filtering 2026-04-29 17:17:54 +02:00
Aiirondev_dev b06828cc7e Enhance logging: change log level to DEBUG and add detailed database connection logging in user and tenant modules
Co-authored-by: Copilot <copilot@github.com>
2026-04-29 13:44:22 +02:00
Aiirondev_dev 79f07d8631 Enhance logging: update logger configuration to use 'app' and set log level to INFO
Co-authored-by: Copilot <copilot@github.com>
2026-04-29 13:30:52 +02:00
Aiirondev_dev 2ef5056727 Enhance logging: add detailed context and error handling in tenant resolution and user authentication
Co-authored-by: Copilot <copilot@github.com>
2026-04-29 12:57:21 +02:00
Aiirondev_dev 46745dba49 Enhance user authentication: add logging for missing tenant databases during login attempts
Co-authored-by: Copilot <copilot@github.com>
2026-04-29 12:47:18 +02:00
Aiirondev_dev c71da2fabf Enhance MongoDB configuration: add support for MongoDB URI and update connection logic 2026-04-29 11:50:14 +02:00
Aiirondev_dev e822ea36bf Enhance logging: implement rotating file handler and log tenant resolution details in context manager 2026-04-29 11:32:42 +02:00
Aiirondev_dev cd767caac7 Enhance login page: display active tenant and database information in an alert 2026-04-29 11:24:10 +02:00
11 changed files with 674 additions and 278 deletions
+9
View File
@@ -1,3 +1,12 @@
NUITKA_BUILD=0
INVENTAR_HTTP_PORT=10000
INVENTAR_HTTP_PORTS=10000
INVENTAR_APP_IMAGE=ghcr.io/aiirondev/legendary-octo-garbanzo:latest
INVENTAR_APP_CPUS=1.0
INVENTAR_APP_MEM_LIMIT=384m
INVENTAR_APP_MEM_SWAP_LIMIT=768m
INVENTAR_WORKERS=4
INVENTAR_THREADS=2
INVENTAR_WORKER_TIMEOUT=30
INVENTAR_WORKER_CONNECTIONS=100
+3 -1
View File
@@ -1,6 +1,8 @@
services:
app:
working_dir: /app/Web
command: ["gunicorn", "app:app", "--bind", "0.0.0.0:8000", "--workers", "2", "--timeout", "30", "--graceful-timeout", "20", "--max-requests", "200", "--max-requests-jitter", "50", "--log-level", "info", "--access-logfile", "-", "--error-logfile", "-"]
command: ["gunicorn", "app:app", "--bind", "0.0.0.0:8000", "--workers", "4", "--threads", "2", "--timeout", "30", "--graceful-timeout", "20", "--worker-connections", "100", "--max-requests", "200", "--max-requests-jitter", "50", "--log-level", "info", "--access-logfile", "-", "--error-logfile", "-"]
image: ghcr.io/aiirondev/legendary-octo-garbanzo:latest
build: null
ports:
- "10000:8000"
+90 -5
View File
@@ -50,6 +50,7 @@ import socket
import io
import html
import logging
from logging.handlers import RotatingFileHandler
import secrets
import importlib
try:
@@ -80,7 +81,15 @@ from tenant import get_tenant_context
app = Flask(__name__, static_folder='static') # Correctly set static folder
app.logger.setLevel(logging.WARNING)
app.logger.setLevel(logging.DEBUG)
if not os.path.exists(cfg.LOGS_FOLDER):
os.makedirs(cfg.LOGS_FOLDER, exist_ok=True)
log_file_path = os.path.join(cfg.LOGS_FOLDER, 'application.log')
file_handler = RotatingFileHandler(log_file_path, maxBytes=10 * 1024 * 1024, backupCount=5, encoding='utf-8')
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(logging.Formatter('%(asctime)s %(levelname)s %(name)s %(message)s'))
app.logger.handlers = []
app.logger.addHandler(file_handler)
app.secret_key = cfg.SECRET_KEY
app.debug = cfg.DEBUG
app.config['UPLOAD_FOLDER'] = cfg.UPLOAD_FOLDER
@@ -392,12 +401,59 @@ def _is_library_module_path(path):
)
return path.startswith(library_prefixes)
def _is_inventory_module_path(path):
"""Return True when the current request path belongs to the inventory module."""
if not path:
return False
if path == '/' or path == '/home':
return True
inventory_prefixes = (
'/scanner',
'/inventory',
'/upload_admin',
'/manage_filters',
'/manage_locations',
'/admin_borrowings',
'/admin_damaged_items'
)
return path.startswith(inventory_prefixes)
@app.before_request
def _enforce_module_access():
endpoint = request.endpoint or ''
if endpoint == 'static' or endpoint.startswith('static') or request.path.startswith('/api/'):
return None
if not cfg.LIBRARY_MODULE_ENABLED and _is_library_module_path(request.path):
return "Bibliotheks-Modul ist deaktiviert.", 403
if not cfg.INVENTORY_MODULE_ENABLED and _is_inventory_module_path(request.path):
return "Inventar-Modul ist deaktiviert.", 403
def _get_current_module(path):
"""Resolve the active UI module for navbar separation."""
# Check if referer indicates we came from library
referrer = request.referrer or ''
if cfg.LIBRARY_MODULE_ENABLED and _is_library_module_path(path):
session['last_module'] = 'library'
return 'library'
return 'inventory'
if cfg.INVENTORY_MODULE_ENABLED and _is_inventory_module_path(path):
session['last_module'] = 'inventory'
return 'inventory'
last_module = session.get('last_module')
if last_module == 'library' and cfg.LIBRARY_MODULE_ENABLED:
return 'library'
if last_module == 'inventory' and cfg.INVENTORY_MODULE_ENABLED:
return 'inventory'
# Default fallback: prefer inventory if enabled, otherwise library, else inventory
if cfg.INVENTORY_MODULE_ENABLED:
return 'inventory'
return 'library' if cfg.LIBRARY_MODULE_ENABLED else 'inventory'
def _get_current_user_permissions():
@@ -1109,6 +1165,7 @@ def inject_version():
'csrf_token': csrf_token,
'CURRENT_MODULE': current_module,
'school_periods': SCHOOL_PERIODS,
'inventory_module_enabled': cfg.INVENTORY_MODULE_ENABLED,
'library_module_enabled': cfg.LIBRARY_MODULE_ENABLED,
'student_cards_module_enabled': cfg.STUDENT_CARDS_MODULE_ENABLED,
'is_admin': is_admin,
@@ -2732,6 +2789,13 @@ def home():
if 'username' not in session:
flash('Bitte mit registriertem Konto anmelden!', 'error')
return redirect(url_for('login'))
if not cfg.INVENTORY_MODULE_ENABLED:
if cfg.LIBRARY_MODULE_ENABLED:
return redirect(url_for('library_view'))
else:
return "Weder Inventar- noch Bibliotheks-Modul sind aktiviert.", 403
elif not us.check_admin(session['username']):
return render_template(
'main.html',
@@ -2764,6 +2828,13 @@ def home_admin():
if 'username' not in session:
flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error')
return redirect(url_for('login'))
if not cfg.INVENTORY_MODULE_ENABLED:
if cfg.LIBRARY_MODULE_ENABLED:
return redirect(url_for('library_admin'))
else:
return "Weder Inventar- noch Bibliotheks-Modul sind aktiviert.", 403
if not us.check_admin(session['username']):
flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error')
return redirect(url_for('login'))
@@ -4048,13 +4119,22 @@ def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
ctx = get_tenant_context()
current_tenant_id = ctx.tenant_id if ctx else None
current_tenant_db = ctx.db_name if ctx else cfg.MONGODB_DB
app.logger.info(f"Login attempt: username={username!r} tenant={current_tenant_id or 'default'} db={current_tenant_db} host={request.host} ip={request.remote_addr}")
app.logger.info(f"Debug login context: headers={dict(request.headers)} tenant_config={ctx.config if ctx else None} remote_addr={request.remote_addr} host={request.host}")
app.logger.info(f"Raw login payload: username={username!r} password={password!r}")
app.logger.info(f"Active MongoDB config: uri={getattr(cfg, 'MONGODB_URI', None)!r} host={cfg.MONGODB_HOST!r} port={cfg.MONGODB_PORT!r} default_db={cfg.MONGODB_DB!r}")
if not username or not password:
app.logger.warning(f"Login blocked: missing credentials tenant={current_tenant_id or 'default'} host={request.host} ip={request.remote_addr}")
flash('Bitte alle Felder ausfüllen', 'error')
return redirect(url_for('login'))
user = us.check_nm_pwd(username, password)
if user:
app.logger.info(f"Login success: username={username!r} tenant={current_tenant_id or 'default'} db={current_tenant_db} host={request.host} ip={request.remote_addr}")
session['username'] = username
is_admin_user = bool(user.get('Admin', False))
session['admin'] = is_admin_user
@@ -4075,6 +4155,7 @@ def login():
else:
return redirect(url_for('home'))
else:
app.logger.warning(f"Login failed: username={username!r} tenant={current_tenant_id or 'default'} db={current_tenant_db} host={request.host} ip={request.remote_addr}")
flash('Ungültige Anmeldedaten', 'error')
get_flashed_messages()
return render_template('login.html')
@@ -6599,11 +6680,15 @@ def zurueckgeben(id):
def get_filter():
"""
API endpoint to retrieve available item filters/categories.
Returns:
dict: Dictionary of available filters
dict: Dictionary of available filters by filter field
"""
return it.get_filters()
return jsonify({
'filter1': it.get_primary_filters(),
'filter2': it.get_secondary_filters(),
'filter3': it.get_tertiary_filters(),
})
@app.route('/get_ausleihung_by_item/<id>')
+11 -1
View File
@@ -74,6 +74,9 @@ DEFAULTS = {
"10": {"start": "16:00", "end": "16:45", "label": "10. Stunde (16:00 - 16:45)"}
},
'modules': {
'inventory': {
'enabled': True
},
'library': {
'enabled': False
},
@@ -131,11 +134,15 @@ PORT = _get(_conf, ['port'], DEFAULTS['port'])
MONGODB_HOST = _get(_conf, ['mongodb', 'host'], DEFAULTS['mongodb']['host'])
MONGODB_PORT = _get(_conf, ['mongodb', 'port'], DEFAULTS['mongodb']['port'])
MONGODB_DB = _get(_conf, ['mongodb', 'db'], DEFAULTS['mongodb']['db'])
MONGODB_URI = _get(_conf, ['mongodb', 'uri'], '')
# Optional environment overrides for containerized/runtime deployments.
MONGODB_HOST = os.getenv('INVENTAR_MONGODB_HOST', MONGODB_HOST)
MONGODB_PORT = int(os.getenv('INVENTAR_MONGODB_PORT', str(MONGODB_PORT)))
MONGODB_DB = os.getenv('INVENTAR_MONGODB_DB', MONGODB_DB)
MONGODB_URI = os.getenv('INVENTAR_MONGODB_URI', os.getenv('MONGO_URI', MONGODB_URI))
if isinstance(MONGODB_URI, str):
MONGODB_URI = MONGODB_URI.strip() or None
MONGODB_MAX_POOL_SIZE = _get_int_env('INVENTAR_MONGODB_MAX_POOL_SIZE', 20)
MONGODB_MIN_POOL_SIZE = _get_int_env('INVENTAR_MONGODB_MIN_POOL_SIZE', 0)
MONGODB_MAX_IDLE_TIME_MS = _get_int_env('INVENTAR_MONGODB_MAX_IDLE_TIME_MS', 300000)
@@ -186,6 +193,7 @@ class _TenantAwareBool:
return f"_TenantAwareBool(module_name={self.module_name!r}, value={self.resolve()!r})"
INVENTORY_MODULE_ENABLED = _TenantAwareBool('inventory', _get(_conf, ['modules', 'inventory', 'enabled'], DEFAULTS['modules']['inventory']['enabled']))
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']))
@@ -318,7 +326,9 @@ def MongoClient(*args, **kwargs):
}
client_kwargs.update(kwargs)
if len(args) >= 2 and not explicit_host and not explicit_port:
if MONGODB_URI and len(args) == 0 and not explicit_host and not explicit_port:
mongo_args = (MONGODB_URI,)
elif len(args) >= 2 and not explicit_host and not explicit_port:
mongo_args = args
else:
mongo_args = (host, port)
+239 -221
View File
@@ -1010,14 +1010,18 @@
<div class="module-selector-bar" id="moduleBar">
<span class="module-label">Module:</span>
<div class="module-tabs">
{% if inventory_module_enabled %}
<a href="{{ url_for('home') }}"
class="module-tab {% if CURRENT_MODULE == 'inventory' %}active{% endif %}"
id="inventoryModule"
data-module="inventory">
📦 Inventarsystem
</a>
{% if library_module_enabled %}
{% endif %}
{% if inventory_module_enabled and library_module_enabled %}
<div class="module-separator"></div>
{% endif %}
{% if library_module_enabled %}
<a href="{{ url_for('library_view') }}"
class="module-tab {% if CURRENT_MODULE == 'library' %}active{% endif %}"
id="libraryModule"
@@ -1028,234 +1032,248 @@
</div>
</div>
{% endif %}
{% if CURRENT_MODULE != 'library' %}
<nav class="navbar navbar-expand-lg navbar-dark" id="inventoryNavbar">
<div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('home') }}" data-icon="📦">Inventarsystem</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#inventoryNavContent">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="inventoryNavContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0" id="inventoryNavList">
{% if current_permissions.pages.get('home', True) %}
<li class="nav-item" data-nav-fixed="true">
<a class="nav-link {% if current_path == url_for('home') %}nav-active{% endif %}" href="{{ url_for('home') }}" data-tutorial-tip="Starten Sie hier mit dem Materialbestand und suchen Sie nach Artikeln.">Artikel</a>
</li>
{% endif %}
{% if 'username' in session %}
{% if current_permissions.pages.get('my_borrowed_items', True) %}
<li class="nav-item">
<a class="nav-link quick-link-pill {% if current_path == url_for('my_borrowed_items') %}nav-active{% endif %}" href="{{ url_for('my_borrowed_items') }}" data-tutorial-tip="Ihre aktuellen Ausleihen und Rückgaben finden Sie hier.">Meine Ausleihen</a>
</li>
{% endif %}
{% if current_permissions.pages.get('tutorial_page', True) %}
<li class="nav-item">
<a class="nav-link quick-link-pill {% if current_path == url_for('tutorial_page') %}nav-active{% endif %}" href="{{ url_for('tutorial_page') }}" data-tutorial-tip="Öffnen Sie das Tutorial, um das System Schritt für Schritt kennenzulernen.">Tutorial</a>
</li>
{% endif %}
{% endif %}
{% if 'username' in session and current_permissions.pages.get('upload_admin', True) and current_permissions.actions.get('can_insert', True) %}
<li class="nav-item">
<a class="nav-link nav-priority-link {% if current_path == url_for('upload_admin') %}nav-active{% endif %}" href="{{ url_for('upload_admin') }}" data-tutorial-tip="Hier können Sie neue Artikel ins Inventar einpflegen."> Hochladen</a>
</li>
{% endif %}
<li class="nav-item" data-nav-fixed="true">
<button id="themeToggleBtn" class="btn btn-link nav-link px-3" aria-label="Dark Mode umschalten" title="Theme umschalten">
<span class="theme-icon-light" style="display: none;">☀️</span>
<span class="theme-icon-dark" style="display: none;">🌙</span>
</button>
</li>
<li class="nav-item dropdown ms-lg-auto">
<a class="nav-link dropdown-toggle" href="#" id="invMoreDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false" title="Weitere Optionen">
Mehr Optionen
</a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="invMoreDropdown">
{% if 'username' in session and (session.get('admin', False) or is_admin) and current_permissions.actions.get('can_manage_settings', True) %}
<li><h6 class="dropdown-header">Verwaltung</h6></li>
{% if current_permissions.pages.get('manage_filters', True) %}
<li><a class="dropdown-item" href="{{ url_for('manage_filters') }}">Filter verwalten</a></li>
{% endif %}
{% if current_permissions.pages.get('manage_locations', True) %}
<li><a class="dropdown-item" href="{{ url_for('manage_locations') }}">Orte verwalten</a></li>
{% endif %}
{% if current_permissions.pages.get('admin_borrowings', True) %}
<li><a class="dropdown-item" href="{{ url_for('admin_borrowings') }}">Ausleihen</a></li>
{% endif %}
{% if current_permissions.pages.get('admin_damaged_items', True) %}
<li><a class="dropdown-item" href="{{ url_for('admin_damaged_items') }}">Defekte Items</a></li>
{% endif %}
{% if current_permissions.actions.get('can_view_logs', True) and current_permissions.pages.get('admin_audit_dashboard', True) %}
<li><a class="dropdown-item" href="{{ url_for('admin_audit_dashboard') }}">Audit Dashboard</a></li>
{% endif %}
{% if current_permissions.actions.get('can_view_logs', True) and current_permissions.pages.get('logs', True) %}
<li><a class="dropdown-item" href="{{ url_for('logs') }}">Logs</a></li>
{% endif %}
<li><hr class="dropdown-divider"></li>
{% if current_permissions.actions.get('can_manage_users', True) %}
<li><h6 class="dropdown-header">System</h6></li>
{% if current_permissions.pages.get('user_del', True) %}
<li><a class="dropdown-item" href="{{ url_for('user_del') }}">Benutzer verwalten</a></li>
{% endif %}
{% if current_permissions.pages.get('register', True) %}
<li><a class="dropdown-item" href="{{ url_for('register') }}">Neuer Benutzer</a></li>
{% endif %}
<li><hr class="dropdown-divider"></li>
{% endif %}
{% endif %}
<li><a class="dropdown-item" href="{{ url_for('impressum') }}">Impressum</a></li>
<li><a class="dropdown-item" href="{{ url_for('license') }}">Lizenz</a></li>
</ul>
</li>
<li class="nav-item d-none" id="inv-nav-overflow-anchor" aria-hidden="true"></li>
</ul>
<div class="d-flex">
{% if 'username' in session %}
<div class="function-search-wrap">
<form class="function-search-form" data-function-search="true">
<input
class="function-search-input"
type="search"
name="function_search"
placeholder="Funktion suchen..."
list="function-search-options"
autocomplete="off"
>
<button class="function-search-btn" type="submit">Los</button>
</form>
</div>
{% if current_tenant_db %}
<span class="navbar-text tenant-badge" title="Aktive Tenant-Datenbank">{{ current_tenant_db }}</span>
{% endif %}
<span class="navbar-text text-light me-3">{{ session['username'] }}</span>
<div class="dropdown me-2 user-menu-wrap">
<button class="btn btn-secondary dropdown-toggle user-menu-btn" type="button" id="invUserMenuDropdown" data-bs-toggle="dropdown" aria-expanded="false" data-notification-button="true">
👤
<span class="user-notification-dot {% if unread_notification_count and unread_notification_count > 0 %}visible{% endif %}" aria-hidden="true"></span>
{% if 'username' in session %}
{% if CURRENT_MODULE != 'library' and inventory_module_enabled %}
<nav class="navbar navbar-expand-lg navbar-dark" id="inventoryNavbar">
<div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('home') }}" data-icon="📦">Inventarsystem</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#inventoryNavContent">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="inventoryNavContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0" id="inventoryNavList">
{% if current_permissions.pages.get('home', True) %}
<li class="nav-item" data-nav-fixed="true">
<a class="nav-link {% if current_path == url_for('home') %}nav-active{% endif %}" href="{{ url_for('home') }}" data-tutorial-tip="Starten Sie hier mit dem Materialbestand und suchen Sie nach Artikeln.">Artikel</a>
</li>
{% endif %}
{% if 'username' in session %}
{% if current_permissions.pages.get('my_borrowed_items', True) %}
<li class="nav-item">
<a class="nav-link quick-link-pill {% if current_path == url_for('my_borrowed_items') %}nav-active{% endif %}" href="{{ url_for('my_borrowed_items') }}" data-tutorial-tip="Ihre aktuellen Ausleihen und Rückgaben finden Sie hier.">Meine Ausleihen</a>
</li>
{% endif %}
{% if current_permissions.pages.get('tutorial_page', True) %}
<li class="nav-item">
<a class="nav-link quick-link-pill {% if current_path == url_for('tutorial_page') %}nav-active{% endif %}" href="{{ url_for('tutorial_page') }}" data-tutorial-tip="Öffnen Sie das Tutorial, um das System Schritt für Schritt kennenzulernen.">Tutorial</a>
</li>
{% endif %}
{% endif %}
{% if 'username' in session and current_permissions.pages.get('upload_admin', True) and current_permissions.actions.get('can_insert', True) %}
<li class="nav-item">
<a class="nav-link nav-priority-link {% if current_path == url_for('upload_admin') %}nav-active{% endif %}" href="{{ url_for('upload_admin') }}" data-tutorial-tip="Hier können Sie neue Artikel ins Inventar einpflegen."> Hochladen</a>
</li>
{% endif %}
<li class="nav-item" data-nav-fixed="true">
<button id="themeToggleBtn" class="btn btn-link nav-link px-3" aria-label="Dark Mode umschalten" title="Theme umschalten">
<span class="theme-icon-light" style="display: none;">☀️</span>
<span class="theme-icon-dark" style="display: none;">🌙</span>
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="invUserMenuDropdown">
{% if current_permissions.pages.get('notifications_view', True) %}
<li><a class="dropdown-item" href="{{ url_for('notifications_view') }}">Benachrichtigungen</a></li>
</li>
<li class="nav-item dropdown ms-lg-auto">
<a class="nav-link dropdown-toggle" href="#" id="invMoreDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false" title="Weitere Optionen">
Mehr Optionen
</a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="invMoreDropdown">
{% if 'username' in session and (session.get('admin', False) or is_admin) and current_permissions.actions.get('can_manage_settings', True) %}
<li><h6 class="dropdown-header">Verwaltung</h6></li>
{% if current_permissions.pages.get('manage_filters', True) %}
<li><a class="dropdown-item" href="{{ url_for('manage_filters') }}">Filter verwalten</a></li>
{% endif %}
{% if current_permissions.pages.get('manage_locations', True) %}
<li><a class="dropdown-item" href="{{ url_for('manage_locations') }}">Orte verwalten</a></li>
{% endif %}
{% if current_permissions.pages.get('admin_borrowings', True) %}
<li><a class="dropdown-item" href="{{ url_for('admin_borrowings') }}">Ausleihen</a></li>
{% endif %}
{% if current_permissions.pages.get('admin_damaged_items', True) %}
<li><a class="dropdown-item" href="{{ url_for('admin_damaged_items') }}">Defekte Items</a></li>
{% endif %}
{% if current_permissions.actions.get('can_view_logs', True) and current_permissions.pages.get('admin_audit_dashboard', True) %}
<li><a class="dropdown-item" href="{{ url_for('admin_audit_dashboard') }}">Audit Dashboard</a></li>
{% endif %}
{% if current_permissions.actions.get('can_view_logs', True) and current_permissions.pages.get('logs', True) %}
<li><a class="dropdown-item" href="{{ url_for('logs') }}">Logs</a></li>
{% endif %}
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{{ url_for('change_password') }}">Passwort ändern</a></li>
{% if current_permissions.actions.get('can_manage_users', True) %}
<li><h6 class="dropdown-header">System</h6></li>
{% if current_permissions.pages.get('user_del', True) %}
<li><a class="dropdown-item" href="{{ url_for('user_del') }}">Benutzer verwalten</a></li>
{% endif %}
{% if current_permissions.pages.get('register', True) %}
<li><a class="dropdown-item" href="{{ url_for('register') }}">Neuer Benutzer</a></li>
{% endif %}
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{{ url_for('logout') }}">Logout</a></li>
{% endif %}
{% endif %}
<li><a class="dropdown-item" href="{{ url_for('impressum') }}">Impressum</a></li>
<li><a class="dropdown-item" href="{{ url_for('license') }}">Lizenz</a></li>
</ul>
</div>
{% endif %}
</div>
</div>
</div>
</nav>
{% endif %}
{% if library_module_enabled and CURRENT_MODULE == 'library' %}
<nav class="navbar navbar-expand-lg navbar-dark" id="libraryNavbar">
<div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('library_view') }}" data-icon="📚">Bibliothek</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#libraryNavContent">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="libraryNavContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0" id="libraryNavList">
{% if current_permissions.pages.get('library_view', True) %}
<li class="nav-item" data-nav-fixed="true">
<a class="nav-link {% if current_path == url_for('library_view') %}nav-active{% endif %}" href="{{ url_for('library_view') }}" data-tutorial-tip="Die Bibliothek zeigt Ihnen alle Medien und verfügbaren Bücher.">Medien</a>
</li>
{% endif %}
{% if 'username' in session %}
{% if current_permissions.pages.get('my_borrowed_items', True) %}
<li class="nav-item">
<a class="nav-link quick-link-pill {% if current_path == url_for('my_borrowed_items') %}nav-active{% endif %}" href="{{ url_for('my_borrowed_items') }}" data-tutorial-tip="Hier sehen Sie Ihre bibliotheksbezogenen Ausleihen.">Meine Medien</a>
</li>
{% endif %}
{% if current_permissions.pages.get('tutorial_page', True) %}
<li class="nav-item">
<a class="nav-link quick-link-pill {% if current_path == url_for('tutorial_page') %}nav-active{% endif %}" href="{{ url_for('tutorial_page') }}" data-tutorial-tip="Nutzen Sie das Tutorial, um die Bibliotheksfunktionen kennenzulernen.">Tutorial</a>
</li>
{% endif %}
{% endif %}
{% if 'username' in session and current_permissions.actions.get('can_insert', True) and current_permissions.pages.get('library_admin', True) %}
<li class="nav-item">
<a class="nav-link nav-priority-link {% if current_path == url_for('library_admin') %}nav-active{% endif %}" href="{{ url_for('library_admin') }}" data-tutorial-tip="Neue Bibliotheksmedien können hier aufgenommen werden.">📖 Hochladen</a>
</li>
{% endif %}
<li class="nav-item" data-nav-fixed="true">
<button id="themeToggleBtn" class="btn btn-link nav-link px-3" aria-label="Dark Mode umschalten" title="Theme umschalten">
<span class="theme-icon-light" style="display: none;">☀️</span>
<span class="theme-icon-dark" style="display: none;">🌙</span>
</button>
</li>
<li class="nav-item dropdown ms-lg-auto">
<a class="nav-link dropdown-toggle" href="#" id="libMoreDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false" title="Weitere Optionen">
Mehr Optionen
</a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="libMoreDropdown">
{% if 'username' in session and (session.get('admin', False) or is_admin) and current_permissions.actions.get('can_manage_settings', True) %}
<li><h6 class="dropdown-header">Bibliotheks-Verwaltung</h6></li>
{% if current_permissions.pages.get('library_loans_admin', True) %}
<li><a class="dropdown-item" href="{{ url_for('library_loans_admin') }}">Ausleihen</a></li>
{% endif %}
{% if current_permissions.pages.get('admin_damaged_items', True) %}
<li><a class="dropdown-item" href="{{ url_for('admin_damaged_items') }}">Defekte Items</a></li>
{% endif %}
{% if student_cards_module_enabled %}
<li><a class="dropdown-item" href="{{ url_for('student_cards_admin') }}">Bibliotheksausweis</a></li>
{% endif %}
<li><hr class="dropdown-divider"></li>
{% if current_permissions.actions.get('can_manage_users', True) %}
<li><h6 class="dropdown-header">System</h6></li>
{% if current_permissions.pages.get('user_del', True) %}
<li><a class="dropdown-item" href="{{ url_for('user_del') }}">Benutzer verwalten</a></li>
{% endif %}
{% if current_permissions.pages.get('register', True) %}
<li><a class="dropdown-item" href="{{ url_for('register') }}">Neuer Benutzer</a></li>
{% endif %}
<li><hr class="dropdown-divider"></li>
{% endif %}
{% endif %}
<li><a class="dropdown-item" href="{{ url_for('impressum') }}">Impressum</a></li>
<li><a class="dropdown-item" href="{{ url_for('license') }}">Lizenz</a></li>
</ul>
</li>
<li class="nav-item d-none" id="lib-nav-overflow-anchor" aria-hidden="true"></li>
</ul>
<div class="d-flex">
{% if 'username' in session %}
<div class="function-search-wrap">
<form class="function-search-form" data-function-search="true">
<input
class="function-search-input"
type="search"
name="function_search"
placeholder="Funktion suchen..."
list="function-search-options"
autocomplete="off"
>
<button class="function-search-btn" type="submit">Los</button>
</form>
</div>
</li>
<li class="nav-item d-none" id="inv-nav-overflow-anchor" aria-hidden="true"></li>
</ul>
<div class="d-flex">
{% if 'username' in session %}
<div class="function-search-wrap">
<form class="function-search-form" data-function-search="true">
<input
class="function-search-input"
type="search"
name="function_search"
placeholder="Funktion suchen..."
list="function-search-options"
autocomplete="off"
>
<button class="function-search-btn" type="submit">Los</button>
</form>
</div>
{% if current_tenant_db %}
<span class="navbar-text tenant-badge" title="Aktive Tenant-Datenbank">{{ current_tenant_db }}</span>
{% endif %}
<span class="navbar-text text-light me-3">{{ session['username'] }}</span>
<div class="dropdown me-2 user-menu-wrap">
<button class="btn btn-secondary dropdown-toggle user-menu-btn" type="button" id="libUserMenuDropdown" data-bs-toggle="dropdown" aria-expanded="false" data-notification-button="true">
👤
<span class="user-notification-dot {% if unread_notification_count and unread_notification_count > 0 %}visible{% endif %}" aria-hidden="true"></span>
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="libUserMenuDropdown">
{% if current_permissions.pages.get('notifications_view', True) %}
<li><a class="dropdown-item" href="{{ url_for('notifications_view') }}">Benachrichtigungen</a></li>
{% endif %}
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{{ url_for('change_password') }}">Passwort ändern</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{{ url_for('logout') }}">Logout</a></li>
</ul>
</div>
{% endif %}
<div class="dropdown me-2 user-menu-wrap">
<button class="btn btn-secondary dropdown-toggle user-menu-btn" type="button" id="invUserMenuDropdown" data-bs-toggle="dropdown" aria-expanded="false" data-notification-button="true">
👤
<span class="user-notification-dot {% if unread_notification_count and unread_notification_count > 0 %}visible{% endif %}" aria-hidden="true"></span>
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="invUserMenuDropdown">
{% if current_permissions.pages.get('notifications_view', True) %}
<li><a class="dropdown-item" href="{{ url_for('notifications_view') }}">Benachrichtigungen</a></li>
{% endif %}
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{{ url_for('change_password') }}">Passwort ändern</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{{ url_for('logout') }}">Logout</a></li>
</ul>
</div>
{% endif %}
</div>
</div>
</div>
</nav>
{% endif %}
{% if library_module_enabled and CURRENT_MODULE == 'library' %}
<nav class="navbar navbar-expand-lg navbar-dark" id="libraryNavbar">
<div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('library_view') }}" data-icon="📚">Bibliothek</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#libraryNavContent">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="libraryNavContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0" id="libraryNavList">
{% if current_permissions.pages.get('library_view', True) %}
<li class="nav-item" data-nav-fixed="true">
<a class="nav-link {% if current_path == url_for('library_view') %}nav-active{% endif %}" href="{{ url_for('library_view') }}" data-tutorial-tip="Die Bibliothek zeigt Ihnen alle Medien und verfügbaren Bücher.">Medien</a>
</li>
{% endif %}
{% if 'username' in session %}
{% if current_permissions.pages.get('my_borrowed_items', True) %}
<li class="nav-item">
<a class="nav-link quick-link-pill {% if current_path == url_for('my_borrowed_items') %}nav-active{% endif %}" href="{{ url_for('my_borrowed_items') }}" data-tutorial-tip="Hier sehen Sie Ihre bibliotheksbezogenen Ausleihen.">Meine Medien</a>
</li>
{% endif %}
{% if current_permissions.pages.get('tutorial_page', True) %}
<li class="nav-item">
<a class="nav-link quick-link-pill {% if current_path == url_for('tutorial_page') %}nav-active{% endif %}" href="{{ url_for('tutorial_page') }}" data-tutorial-tip="Nutzen Sie das Tutorial, um die Bibliotheksfunktionen kennenzulernen.">Tutorial</a>
</li>
{% endif %}
{% endif %}
{% if 'username' in session and current_permissions.actions.get('can_insert', True) and current_permissions.pages.get('library_admin', True) %}
<li class="nav-item">
<a class="nav-link nav-priority-link {% if current_path == url_for('library_admin') %}nav-active{% endif %}" href="{{ url_for('library_admin') }}" data-tutorial-tip="Neue Bibliotheksmedien können hier aufgenommen werden.">📖 Hochladen</a>
</li>
{% endif %}
<li class="nav-item" data-nav-fixed="true">
<button id="themeToggleBtn" class="btn btn-link nav-link px-3" aria-label="Dark Mode umschalten" title="Theme umschalten">
<span class="theme-icon-light" style="display: none;">☀️</span>
<span class="theme-icon-dark" style="display: none;">🌙</span>
</button>
</li>
<li class="nav-item dropdown ms-lg-auto">
<a class="nav-link dropdown-toggle" href="#" id="libMoreDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false" title="Weitere Optionen">
Mehr Optionen
</a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="libMoreDropdown">
{% if 'username' in session and (session.get('admin', False) or is_admin) and current_permissions.actions.get('can_manage_settings', True) %}
<li><h6 class="dropdown-header">Bibliotheks-Verwaltung</h6></li>
{% if current_permissions.pages.get('library_loans_admin', True) %}
<li><a class="dropdown-item" href="{{ url_for('library_loans_admin') }}">Ausleihen</a></li>
{% endif %}
{% if current_permissions.pages.get('admin_damaged_items', True) %}
<li><a class="dropdown-item" href="{{ url_for('admin_damaged_items') }}">Defekte Items</a></li>
{% endif %}
{% if student_cards_module_enabled %}
<li><a class="dropdown-item" href="{{ url_for('student_cards_admin') }}">Bibliotheksausweis</a></li>
{% endif %}
<li><hr class="dropdown-divider"></li>
{% if current_permissions.actions.get('can_manage_users', True) %}
<li><h6 class="dropdown-header">System</h6></li>
{% if current_permissions.pages.get('user_del', True) %}
<li><a class="dropdown-item" href="{{ url_for('user_del') }}">Benutzer verwalten</a></li>
{% endif %}
{% if current_permissions.pages.get('register', True) %}
<li><a class="dropdown-item" href="{{ url_for('register') }}">Neuer Benutzer</a></li>
{% endif %}
<li><hr class="dropdown-divider"></li>
{% endif %}
{% endif %}
<li><a class="dropdown-item" href="{{ url_for('impressum') }}">Impressum</a></li>
<li><a class="dropdown-item" href="{{ url_for('license') }}">Lizenz</a></li>
</ul>
</li>
<li class="nav-item d-none" id="lib-nav-overflow-anchor" aria-hidden="true"></li>
</ul>
<div class="d-flex">
{% if 'username' in session %}
<div class="function-search-wrap">
<form class="function-search-form" data-function-search="true">
<input
class="function-search-input"
type="search"
name="function_search"
placeholder="Funktion suchen..."
list="function-search-options"
autocomplete="off"
>
<button class="function-search-btn" type="submit">Los</button>
</form>
</div>
<span class="navbar-text text-light me-3">{{ session['username'] }}</span>
<div class="dropdown me-2 user-menu-wrap">
<button class="btn btn-secondary dropdown-toggle user-menu-btn" type="button" id="libUserMenuDropdown" data-bs-toggle="dropdown" aria-expanded="false" data-notification-button="true">
👤
<span class="user-notification-dot {% if unread_notification_count and unread_notification_count > 0 %}visible{% endif %}" aria-hidden="true"></span>
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="libUserMenuDropdown">
{% if current_permissions.pages.get('notifications_view', True) %}
<li><a class="dropdown-item" href="{{ url_for('notifications_view') }}">Benachrichtigungen</a></li>
{% endif %}
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{{ url_for('change_password') }}">Passwort ändern</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{{ url_for('logout') }}">Logout</a></li>
</ul>
</div>
{% endif %}
</div>
</div>
</div>
</nav>
{% endif %}
{% else %}
<nav class="navbar navbar-expand-lg navbar-dark" id="loginNavbar">
<div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('home') }}" data-icon="🔹">Invario</a>
<ul class="navbar-nav ms-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('impressum') }}">Impressum</a>
</li>
</ul>
</div>
</nav>
{% endif %}
+9
View File
@@ -14,6 +14,15 @@
<div class="row justify-content-center">
<div class="col-md-6">
<h2 class="text-center">Login</h2>
{% if current_tenant_id or current_tenant_db %}
<div class="alert alert-info text-center small mb-4" role="alert">
{% if current_tenant_id %}
Aktiver Tenant: <strong>{{ current_tenant_id }}</strong> &middot; Datenbank: <strong>{{ current_tenant_db }}</strong>
{% else %}
Aktive Datenbank: <strong>{{ current_tenant_db }}</strong>
{% endif %}
</div>
{% endif %}
<form method="POST" action="{{ url_for('login') }}">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
+26 -10
View File
@@ -665,18 +665,30 @@
// Set up schedule form submission
setupScheduleFormSubmission();
// Fetch user status to get current username
fetch('/user_status')
.then(response => response.json())
.then(data => {
currentUsername = data.username;
// Fetch user status to get current username and full filter lists
Promise.all([
fetch('/user_status').then(response => response.json()),
fetch('/get_filter').then(response => response.json()),
])
.then(([userData, filterData]) => {
currentUsername = userData.username;
if (filterData.filter1) {
allFilter1Values = new Set(filterData.filter1.filter(value => value && typeof value === 'string' && value.trim() !== ''));
}
if (filterData.filter2) {
allFilter2Values = new Set(filterData.filter2.filter(value => value && typeof value === 'string' && value.trim() !== ''));
}
if (filterData.filter3) {
allFilter3Values = new Set(filterData.filter3.filter(value => value && typeof value === 'string' && value.trim() !== ''));
}
// Now load items with username information
loadItems();
// Setup card images display after a short delay to ensure items are loaded
setTimeout(setupCardImagesDisplay, 500);
})
.catch(error => {
console.error('Error fetching user status:', error);
console.error('Error fetching user status or filters:', error);
loadItems(); // Load items anyway in case of error
});
@@ -721,6 +733,10 @@
filter3: []
};
let allFilter1Values = new Set();
let allFilter2Values = new Set();
let allFilter3Values = new Set();
// Add this line to define allItems globally
let allItems = [];
@@ -1083,10 +1099,10 @@
}
});
// Populate filter options
populateFilterOptions('filter1-options', [...filter1Values].sort(), 1);
populateFilterOptions('filter2-options', [...filter2Values].sort(), 2);
populateFilterOptions('filter3-options', [...filter3Values].sort(), 3);
// Populate filter options using server-provided full filter lists when available.
populateFilterOptions('filter1-options', allFilter1Values.size ? [...allFilter1Values].sort() : [...filter1Values].sort(), 1);
populateFilterOptions('filter2-options', allFilter2Values.size ? [...allFilter2Values].sort() : [...filter2Values].sort(), 2);
populateFilterOptions('filter3-options', allFilter3Values.size ? [...allFilter3Values].sort() : [...filter3Values].sort(), 3);
// Start display from leftmost position on full reload only.
if (!append) {
+83 -4
View File
@@ -13,6 +13,7 @@ import logging
import os
import re
import settings as cfg
from settings import MongoClient
logger = logging.getLogger(__name__)
@@ -32,6 +33,21 @@ def _get_nested_value(source, path, default=None):
return current
def _parse_tenant_db_map():
mapping = {}
raw_map = os.getenv('INVENTAR_TENANT_DB_MAP', '').strip()
if not raw_map:
return mapping
for mapping_pair in re.split(r'[;,\s]+', raw_map):
if '=' not in mapping_pair:
continue
key, value = mapping_pair.split('=', 1)
mapping[key.strip()] = value.strip()
return mapping
def _parse_port_from_host(host):
"""Parse host header and return (hostname, port) when a numeric port is present."""
if host.startswith('['):
@@ -84,6 +100,52 @@ def get_tenant_config(tenant_id=None):
return TENANT_REGISTRY.get('default', {}) or {}
def _normalize_db_name(db_name):
if not db_name:
return None
db_name = str(db_name).strip()
if db_name and not db_name.startswith('inventar_'):
db_name = f'inventar_{db_name}'
return db_name
def _tenant_db_aliases(tenant_id):
aliases = []
env_map = _parse_tenant_db_map()
if tenant_id in env_map:
aliases.append(env_map[tenant_id])
if tenant_id.lower().startswith('schule'):
aliases.append(tenant_id.lower().replace('schule', 'school', 1))
elif tenant_id.lower().startswith('school'):
aliases.append(tenant_id.lower().replace('school', 'schule', 1))
return [alias for alias in aliases if alias]
def _resolve_db_alias(tenant_id, db_name):
try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
available_databases = client.list_database_names()
if db_name in available_databases:
return db_name
for alias in _tenant_db_aliases(tenant_id):
candidate = _normalize_db_name(alias)
if candidate in available_databases:
logger.warning(
"Tenant DB fallback applied for tenant %r: %r -> %r",
tenant_id,
db_name,
candidate,
)
return candidate
except Exception as exc:
logger.exception("Tenant DB alias resolution failed for %r: %s", tenant_id, exc)
return db_name
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)
@@ -123,12 +185,17 @@ class TenantContext:
host = request.host.lower()
_, port = _parse_port_from_host(host)
self.port = port
logger.info(f"Tenant resolution start: request.host={host} request.headers={dict(request.headers)}")
if port:
tenant_from_port = _tenant_id_for_port(port)
if tenant_from_port:
self.tenant_id = tenant_from_port
self.config = get_tenant_config(tenant_from_port)
logger.info(
f"Tenant resolution by port: host={host} port={port} tenant={tenant_from_port} config={self.config}"
)
return self._get_db_name(tenant_from_port)
logger.info(f"Tenant port not mapped: host={host} port={port}")
# Priority 3: Subdomain extraction
parts = host.split('.')
@@ -144,7 +211,11 @@ class TenantContext:
self.subdomain = potential_subdomain
self.tenant_id = potential_subdomain
self.config = get_tenant_config(potential_subdomain)
logger.info(
f"Tenant resolution by subdomain: host={host} tenant={potential_subdomain} config={self.config}"
)
return self._get_db_name(potential_subdomain)
logger.info(f"Tenant subdomain ignored: {potential_subdomain}")
# Fallback to default tenant if no tenant identifier found.
# If no explicit 'default' tenant config exists, use configured MongoDB DB.
@@ -161,11 +232,19 @@ class TenantContext:
def _get_db_name(self, tenant_id):
"""
Get MongoDB database name for tenant.
Format: inventar_<tenant_id>
Format: inventar_<tenant_id> unless tenant config overrides it.
"""
# Sanitize tenant_id for MongoDB database name
sanitized = ''.join(c if c.isalnum() or c == '_' else '' for c in tenant_id.lower())
db_name = f"inventar_{sanitized}"
explicit_db = None
if isinstance(self.config, dict):
explicit_db = self.config.get('db') or self.config.get('db_name')
if explicit_db:
db_name = _normalize_db_name(explicit_db)
else:
sanitized = ''.join(c if c.isalnum() or c == '_' else '' for c in tenant_id.lower())
db_name = f"inventar_{sanitized}"
db_name = _resolve_db_alias(tenant_id, db_name)
self.db_name = db_name
return db_name
+62 -7
View File
@@ -12,6 +12,7 @@ Provides methods for creating, validating, and retrieving user information.
'''
import hashlib
import copy
import logging
import re
import secrets
import string
@@ -19,6 +20,10 @@ from bson.objectid import ObjectId
import settings as cfg
from settings import MongoClient
logger = logging.getLogger('app')
logger.setLevel(logging.DEBUG)
logger.propagate = True
def normalize_student_card_id(card_id):
"""Normalize student card IDs for reliable lookup."""
@@ -421,25 +426,75 @@ def check_nm_pwd(username, password):
"""
db_name = cfg.MONGODB_DB
tenant_db = None
ctx = None
try:
from tenant import get_tenant_context
ctx = get_tenant_context()
if ctx and ctx.tenant_id:
tenant_db = ctx.db_name or ctx.resolve_tenant()
db_name = tenant_db
except Exception:
pass
except Exception as exc:
logger.exception(f"Failed to resolve tenant context in check_nm_pwd: {exc}")
logger.info(
"check_nm_pwd start: username=%r tenant=%r db=%r host=%r port=%r uri=%r",
username,
ctx.tenant_id if ctx else None,
db_name,
cfg.MONGODB_HOST,
cfg.MONGODB_PORT,
getattr(cfg, 'MONGODB_URI', None),
)
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
try:
hashed_password = hashing(password)
logger.info("check_nm_pwd password hash for username=%r: %s", username, hashed_password)
available_dbs = []
try:
available_dbs = client.list_database_names()
logger.debug("MongoDB connected. Available databases=%s", available_dbs)
except Exception as exc:
logger.exception("Unable to list MongoDB databases: %s", exc)
def find_user_in_db(database_name):
db = client[database_name]
users = db['users']
return users.find_one({'Username': username, 'Password': hashed_password}) or users.find_one({'username': username, 'Password': hashed_password})
db = client[db_name]
try:
existing_collections = db.list_collection_names()
except Exception as exc:
logger.exception("Unable to list collections for db=%r: %s", db_name, exc)
existing_collections = []
logger.debug("Tenant db=%r collections=%s", db_name, existing_collections)
return find_user_in_db(db_name)
users = db['users']
query = {'$or': [{'Username': username}, {'username': username}]}
logger.debug("Running user lookup on %r: %s", db_name, query)
user_record = users.find_one(query)
if user_record is None:
logger.warning("No user document found in db=%r for username=%r", db_name, username)
if db_name not in available_dbs:
logger.warning("Tenant database %r is missing from available MongoDB databases", db_name)
if 'users' not in existing_collections:
logger.warning("Tenant database %r has no users collection", db_name)
return None
logger.info("Found user document for username=%r in db=%r: %s", username, db_name, user_record)
stored_password = user_record.get('Password') or user_record.get('password')
if stored_password is None:
logger.warning("User document for username=%r in db=%r has no password field", username, db_name)
return None
if stored_password != hashed_password:
logger.warning(
"Password mismatch for username=%r in db=%r: provided_hash=%s stored_hash=%s",
username,
db_name,
hashed_password,
stored_password,
)
return None
return user_record
finally:
client.close()
+87 -29
View File
@@ -4,42 +4,44 @@
"ver": "0.6.44",
"host": "0.0.0.0",
"port": 8000,
"mongodb": {
"host": "localhost",
"port": 27017,
"db": "Inventarsystem"
},
"scheduler": {
"enabled": true,
"interval_minutes": 1,
"backup_interval_hours": 24
},
"ssl": {
"enabled": true,
"cert": "Web/certs/cert.pem",
"key": "Web/certs/key.pem"
},
"images": {
"thumbnail_size": [150, 150],
"preview_size": [400, 400]
"thumbnail_size": [
150,
150
],
"preview_size": [
400,
400
]
},
"upload": {
"max_size_mb": 10,
"image_max_size_mb": 15,
"video_max_size_mb": 100
},
"paths": {
"backups": "backups",
"logs": "logs"
},
"modules": {
"inventory": {
"enabled": true
},
"library": {
"enabled": true
},
@@ -49,27 +51,83 @@
"max_borrow_days": 365
}
},
"tenants": {},
"tenants": {
},
"allowed_extensions": [
"png", "jpg", "jpeg", "gif",
"hevc", "heif",
"mp4", "mov", "avi", "mkv", "webm",
"mp3", "wav", "ogg", "flac", "aac", "m4a", "opus",
"pdf", "docx", "xlsx", "pptx", "txt"
"png",
"jpg",
"jpeg",
"gif",
"hevc",
"heif",
"mp4",
"mov",
"avi",
"mkv",
"webm",
"mp3",
"wav",
"ogg",
"flac",
"aac",
"m4a",
"opus",
"pdf",
"docx",
"xlsx",
"pptx",
"txt"
],
"schoolPeriods": {
"1": { "start": "08:00", "end": "08:45", "label": "1. Stunde (08:00 - 08:45)" },
"2": { "start": "08:45", "end": "09:30", "label": "2. Stunde (08:45 - 09:30)" },
"3": { "start": "09:45", "end": "10:30", "label": "3. Stunde (09:45 - 10:30)" },
"4": { "start": "10:30", "end": "11:15", "label": "4. Stunde (10:30 - 11:15)" },
"5": { "start": "11:30", "end": "12:15", "label": "5. Stunde (11:30 - 12:15)" },
"6": { "start": "12:15", "end": "13:00", "label": "6. Stunde (12:15 - 13:00)" },
"7": { "start": "13:30", "end": "14:15", "label": "7. Stunde (13:30 - 14:15)" },
"8": { "start": "14:15", "end": "15:00", "label": "8. Stunde (14:15 - 15:00)" },
"9": { "start": "15:15", "end": "16:00", "label": "9. Stunde (15:15 - 16:00)" },
"10": { "start": "16:00", "end": "16:45", "label": "10. Stunde (16:00 - 16:45)" }
"1": {
"start": "08:00",
"end": "08:45",
"label": "1. Stunde (08:00 - 08:45)"
},
"2": {
"start": "08:45",
"end": "09:30",
"label": "2. Stunde (08:45 - 09:30)"
},
"3": {
"start": "09:45",
"end": "10:30",
"label": "3. Stunde (09:45 - 10:30)"
},
"4": {
"start": "10:30",
"end": "11:15",
"label": "4. Stunde (10:30 - 11:15)"
},
"5": {
"start": "11:30",
"end": "12:15",
"label": "5. Stunde (11:30 - 12:15)"
},
"6": {
"start": "12:15",
"end": "13:00",
"label": "6. Stunde (12:15 - 13:00)"
},
"7": {
"start": "13:30",
"end": "14:15",
"label": "7. Stunde (13:30 - 14:15)"
},
"8": {
"start": "14:15",
"end": "15:00",
"label": "8. Stunde (14:15 - 15:00)"
},
"9": {
"start": "15:15",
"end": "16:00",
"label": "9. Stunde (15:15 - 16:00)"
},
"10": {
"start": "16:00",
"end": "16:45",
"label": "10. Stunde (16:00 - 16:45)"
}
}
}
}
+55
View File
@@ -20,11 +20,13 @@ show_help() {
echo " restart-tenant <id> 'Restart' a single tenant (clears cache/sessions)"
echo " restart-all Restart all application containers (zero-downtime reload)"
echo " list List active tenants"
echo " module <tenant_id> <module>=<on|off>... Enable/disable modules for a tenant"
echo " -h, --help Show this help message"
echo ""
echo "Examples:"
echo " ./manage-tenant.sh add school_a 10001"
echo " ./manage-tenant.sh remove test_tenant"
echo " ./manage-tenant.sh module school_a inventory=off library=on"
echo " ./manage-tenant.sh restart-all"
echo " ./manage-tenant.sh -h"
exit 1
@@ -492,6 +494,59 @@ PY
fi
;;
module)
if [ -z "$TENANT_ID" ] || [ -z "${3:-}" ]; then
echo "Error: Usage: manage-tenant.sh module <tenant_id> <module_name>=<on|off> [...]"
exit 1
fi
# Pass all remaining arguments to python script
shift 2
if python3 - "$CONFIG_FILE" "$TENANT_ID" "$@" <<'PY'
import json, sys, os
path = sys.argv[1]
tenant_id = sys.argv[2]
module_args = sys.argv[3:]
if not os.path.isfile(path):
print(f"Error: config file not found: {path}", file=sys.stderr)
sys.exit(1)
with open(path, 'r', encoding='utf-8') as f:
cfg = json.load(f)
tenants = cfg.setdefault('tenants', {})
tenant_cfg = tenants.setdefault(tenant_id, {})
if 'port' not in tenant_cfg:
print(f"Warning: Tenant {tenant_id} doesn't have a port mapping in config.json.", file=sys.stderr)
modules = tenant_cfg.setdefault('modules', {})
for arg in module_args:
if '=' not in arg:
print(f"Warning: Ignoring invalid argument format '{arg}'. Expected name=on|off.", file=sys.stderr)
continue
mod_name, state_str = arg.split('=', 1)
state = str(state_str).lower() in ('on', '1', 'true', 'yes')
module_cfg = modules.setdefault(mod_name, {})
module_cfg['enabled'] = state
print(f"Module '{mod_name}' set to '{'on' if state else 'off'}' for tenant '{tenant_id}'.")
with open(path, 'w', encoding='utf-8') as f:
json.dump(cfg, f, indent=4, ensure_ascii=False)
PY
then
echo "Module configurations updated successfully."
if [ -n "$(docker ps -qf 'name=app' | head -n 1)" ]; then
restart_app_container
fi
else
echo "Failed to update module configuration."
exit 1
fi
;;
*)
echo "Unknown command: $COMMAND"
show_help