Compare commits

..

14 Commits

Author SHA1 Message Date
github-actions[bot] 17745c64e0 chore: bump version to v0.7.72 2026-05-20 17:37:17 +00:00
Aiirondev_dev a0279f82e1 Merge remote-tracking branch 'refs/remotes/origin/main' 2026-05-20 19:35:26 +02:00
Aiirondev_dev 8d1e3865d3 fix: improve environment variable handling and path resolution in restart_app_container function 2026-05-20 19:35:11 +02:00
github-actions[bot] 79be502aa1 chore: bump version to v0.7.71 2026-05-20 17:27:29 +00:00
Aiirondev_dev d0f54f0dca fix: ensure deterministic config path resolution in manage-tenant.sh 2026-05-20 19:26:00 +02:00
github-actions[bot] 2b00aab1fa chore: bump version to v0.7.70 2026-05-20 16:45:03 +00:00
Aiirondev_dev 6d4ac073a5 feat: Add module name candidate resolution for tenant feature modules 2026-05-20 18:42:55 +02:00
Aiirondev_dev 73ab1a37d2 feat: Update import paths for database settings in manage-tenant.sh 2026-05-20 18:31:17 +02:00
github-actions[bot] 0eb1ffe9d9 chore: bump version to v0.7.69 2026-05-20 16:24:14 +00:00
Aiirondev_dev 0ea1294237 feat: Update version handling in release workflow and improve directory creation 2026-05-20 18:23:34 +02:00
Aiirondev_dev a7ff2f1552 feat: Refactor logging module structure and implement audit logging functionality 2026-05-20 18:01:08 +02:00
Aiirondev_dev f847faea3e feat: Improve module import paths and add package initialization files 2026-05-20 17:43:06 +02:00
Aiirondev_dev 4c58dceb02 feat: Enhance error handling for URL resolution and unexpected exceptions 2026-05-20 17:06:22 +02:00
Aiirondev_dev db80693c6a fix: Update link to library admin in invoice template 2026-05-20 16:54:17 +02:00
16 changed files with 323 additions and 33 deletions
+1 -3
View File
@@ -1,8 +1,6 @@
services: services:
app: app:
working_dir: /app/Web image: ghcr.io/aiirondev/legendary-octo-garbanzo:v0.7.42
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 build: null
ports: ports:
- "10000:8000" - "10000:8000"
+19
View File
@@ -134,14 +134,22 @@ jobs:
echo "push_dev=false" >> "$GITHUB_OUTPUT" echo "push_dev=false" >> "$GITHUB_OUTPUT"
fi fi
- name: Update .release-version file
run: |
echo "${{ steps.meta.outputs.tag }}" > .release-version
cat .release-version
- name: Create and push tag for manual releases - name: Create and push tag for manual releases
if: github.event_name == 'workflow_dispatch' if: github.event_name == 'workflow_dispatch'
run: | run: |
TAG="${{ steps.meta.outputs.tag }}" TAG="${{ steps.meta.outputs.tag }}"
git config user.name "github-actions[bot]" git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com" git config user.email "github-actions[bot]@users.noreply.github.com"
git add .release-version
git commit -m "chore: bump version to $TAG" || true
git tag "$TAG" git tag "$TAG"
git push origin "$TAG" git push origin "$TAG"
git push origin HEAD:${{ github.ref_name }}
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
@@ -198,6 +206,17 @@ jobs:
# development tar omitted: dev releases will be versioned (vX.Y.Z-dev) and handled by update.sh using the tag # development tar omitted: dev releases will be versioned (vX.Y.Z-dev) and handled by update.sh using the tag
- name: Commit .release-version for tag pushes
if: github.event_name == 'push'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
if ! git diff --quiet .release-version; then
git add .release-version
git commit -m "chore: update version to ${{ steps.meta.outputs.tag }}"
git push origin HEAD:${{ github.ref_name }}
fi
- name: Create release-only docker bundle - name: Create release-only docker bundle
run: | run: |
mkdir -p release-bundle mkdir -p release-bundle
+1 -1
View File
@@ -1 +1 @@
v0.7.42 v0.7.72
+1
View File
@@ -0,0 +1 @@
# Web package initialization
+56 -13
View File
@@ -28,11 +28,24 @@ Features:
from flask import Flask, render_template, request, redirect, url_for, session, flash, send_from_directory, get_flashed_messages, jsonify, Response, make_response, send_file, abort from flask import Flask, render_template, request, redirect, url_for, session, flash, send_from_directory, get_flashed_messages, jsonify, Response, make_response, send_file, abort
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from werkzeug.middleware.proxy_fix import ProxyFix from werkzeug.middleware.proxy_fix import ProxyFix
from werkzeug.exceptions import HTTPException
from werkzeug.routing import BuildError
from jinja2 import TemplateNotFound from jinja2 import TemplateNotFound
import os
import sys
# Ensure imports work regardless of whether gunicorn starts in /app or /app/Web.
_CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
_PROJECT_ROOT = os.path.dirname(_CURRENT_DIR)
if _PROJECT_ROOT not in sys.path:
sys.path.insert(0, _PROJECT_ROOT)
if _CURRENT_DIR not in sys.path:
sys.path.insert(0, _CURRENT_DIR)
import Web.modules.database.user as us import Web.modules.database.user as us
import Web.modules.database.items as it import Web.modules.database.items as it
import Web.modules.database.ausleihung as au import Web.modules.database.ausleihung as au
import Web.modules.logs.audit_log as al import Web.modules.log.audit_log as al
import push_notifications as pn import push_notifications as pn
import Web.modules.inventarsystem.pdf_export as pdf_export import Web.modules.inventarsystem.pdf_export as pdf_export
import datetime import datetime
@@ -42,7 +55,6 @@ from urllib.parse import urlparse, urlunparse
import requests import requests
import csv import csv
import ipaddress import ipaddress
import os
import json import json
import datetime import datetime
import time import time
@@ -63,7 +75,6 @@ except Exception:
# import qrcode # import qrcode
# from qrcode.constants import ERROR_CORRECT_L # from qrcode.constants import ERROR_CORRECT_L
import threading import threading
import sys
import shutil import shutil
import uuid import uuid
from PIL import Image, ImageOps from PIL import Image, ImageOps
@@ -109,7 +120,7 @@ app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1)
"""--------------------------------------------------------------Path Init-------------------------------------------------------""" """--------------------------------------------------------------Path Init-------------------------------------------------------"""
if not os.path.exists(app.config['UPLOAD_FOLDER']): if not os.path.exists(app.config['UPLOAD_FOLDER']):
os.makedirs(app.config['UPLOAD_FOLDER']) os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
# QR Code directory creation deactivated # QR Code directory creation deactivated
# if not os.path.exists(app.config['QR_CODE_FOLDER']): # if not os.path.exists(app.config['QR_CODE_FOLDER']):
# os.makedirs(app.config['QR_CODE_FOLDER']) # os.makedirs(app.config['QR_CODE_FOLDER'])
@@ -373,7 +384,7 @@ def _enforce_module_access():
if name != 'inventory' and cfg.MODULES.is_enabled('inventory'): if name != 'inventory' and cfg.MODULES.is_enabled('inventory'):
return redirect(url_for('home')) return redirect(url_for('home'))
elif name != 'library' and cfg.MODULES.is_enabled('library'): elif name != 'library' and cfg.MODULES.is_enabled('library'):
return redirect(url_for('library_view')) return redirect('/library')
return redirect(url_for('my_borrowed_items')) return redirect(url_for('my_borrowed_items'))
@@ -433,6 +444,41 @@ def handle_not_found(e):
return redirect(url_for('login')) return redirect(url_for('login'))
@app.errorhandler(BuildError)
def handle_build_error(e):
"""Handle url_for endpoint mismatches without crashing the whole request."""
app.logger.error(f"BuildError while resolving endpoint: {e}", exc_info=True)
if request.is_json or request.path.startswith('/api/'):
return jsonify({'error': 'Route resolution failed', 'status': 500}), 500
# Known fallback for legacy library endpoint naming issues.
if 'library_view' in str(e):
return redirect('/library')
if 'username' in session:
return redirect(url_for('home'))
return redirect(url_for('login'))
@app.errorhandler(Exception)
def handle_unexpected_exception(e):
"""Last-resort handler to avoid raw 500 pages for unhandled exceptions."""
if isinstance(e, HTTPException):
return e
app.logger.error('Unhandled exception', exc_info=True)
if request.is_json or request.path.startswith('/api/'):
return jsonify({'error': 'Internal server error', 'status': 500}), 500
if 'username' in session:
try:
return render_template('error.html', error_code=500, error_message='Interner Serverfehler.'), 500
except Exception:
return 'Internal Server Error', 500
return redirect(url_for('login'))
def _csrf_error_response(message='CSRF token fehlt oder ist ungültig.'): def _csrf_error_response(message='CSRF token fehlt oder ist ungültig.'):
if request.is_json or request.path.startswith('/api/') or request.path in {'/download_book_cover', '/proxy_image', '/log_mobile_issue'}: if request.is_json or request.path.startswith('/api/') or request.path in {'/download_book_cover', '/proxy_image', '/log_mobile_issue'}:
return jsonify({'error': message}), 400 return jsonify({'error': message}), 400
@@ -2369,11 +2415,7 @@ def preview_file(filename):
if denied: if denied:
return denied return denied
# Check production path first
prod_path = "/var/Inventarsystem/Web/previews"
dev_path = app.config['PREVIEW_FOLDER'] dev_path = app.config['PREVIEW_FOLDER']
if os.path.exists(os.path.join(prod_path, filename)):
return send_from_directory(prod_path, filename)
if os.path.exists(os.path.join(dev_path, filename)): if os.path.exists(os.path.join(dev_path, filename)):
return send_from_directory(dev_path, filename) return send_from_directory(dev_path, filename)
@@ -2640,7 +2682,7 @@ def home():
if not cfg.MODULES.is_enabled('inventory'): if not cfg.MODULES.is_enabled('inventory'):
if cfg.MODULES.is_enabled('library'): if cfg.MODULES.is_enabled('library'):
return redirect(url_for('library_view')) return redirect('/library')
else: else:
return "Weder Inventar- noch Bibliotheks-Modul sind aktiviert.", 403 return "Weder Inventar- noch Bibliotheks-Modul sind aktiviert.", 403
@@ -2716,8 +2758,9 @@ def tutorial_page():
student_max_borrow_days=cfg.STUDENT_MAX_BORROW_DAYS student_max_borrow_days=cfg.STUDENT_MAX_BORROW_DAYS
) )
@app.route('/library/export', defaults={'scope': 'all'})
@app.route('/library/export/<scope>') @app.route('/library/export/<scope>')
def library_export_excel(scope): def library_export_excel(scope='all'):
if 'username' not in session: if 'username' not in session:
return redirect(url_for('login')) return redirect(url_for('login'))
@@ -2737,7 +2780,7 @@ def library_export_excel(scope):
filename = f"Bibliothek_Ausgeliehen_{username}.xlsx" filename = f"Bibliothek_Ausgeliehen_{username}.xlsx"
elif scope == 'all_borrowed': elif scope == 'all_borrowed':
if not is_admin_user: if not is_admin_user:
return redirect(url_for('library_view')) return redirect('/library')
query['Verfuegbar'] = False query['Verfuegbar'] = False
filename = "Bibliothek_Alle_Ausleihen.xlsx" filename = "Bibliothek_Alle_Ausleihen.xlsx"
elif scope == 'schulbuecher': elif scope == 'schulbuecher':
@@ -2751,7 +2794,7 @@ def library_export_excel(scope):
client.close() client.close()
excel_file = excel_export.generate_library_excel(items) excel_file = excel_export.generate_library_excel(items)
return flask.send_file( return send_file(
excel_file, excel_file,
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
as_attachment=True, as_attachment=True,
+1
View File
@@ -0,0 +1 @@
# Web.modules package initialization
+1
View File
@@ -0,0 +1 @@
# Web.modules.bibliothek package initialization
+1 -1
View File
@@ -103,7 +103,7 @@ def get_current_status(ausleihung, log_changes=False, user=None):
if log_changes and new_status != original_status and '_id' in ausleihung: if log_changes and new_status != original_status and '_id' in ausleihung:
try: try:
# Importieren Sie das Modul nur bei Bedarf, um zirkuläre Importe zu vermeiden # Importieren Sie das Modul nur bei Bedarf, um zirkuläre Importe zu vermeiden
import Web.modules.logs.ausleihung_log as ausleihung_log import Web.modules.log.ausleihung_log as ausleihung_log
ausleihung_log.log_status_change( ausleihung_log.log_status_change(
str(ausleihung['_id']), str(ausleihung['_id']),
original_status, original_status,
+1 -1
View File
@@ -135,7 +135,7 @@ def _get_int_env(name, default):
return int(default) return int(default)
def get_version(): def get_version():
with open(os.path.join(BASE_DIR, '..', '.docker-build.env'), 'r') as f: with open(os.path.join(BASE_DIR, '..', '..', '..', '.docker-build.env'), 'r') as f:
for l in f: for l in f:
if l.startswith('INVENTAR_APP_IMAGE='): if l.startswith('INVENTAR_APP_IMAGE='):
return l.split(':', 1)[1].strip() return l.split(':', 1)[1].strip()
+1
View File
@@ -0,0 +1 @@
# Web.modules.emailservice package initialization
+1
View File
@@ -0,0 +1 @@
# Web.modules.log package initialization
+149
View File
@@ -0,0 +1,149 @@
"""
Tamper-evident audit logging helpers.
The audit chain stores each entry with a hash of the previous entry.
Any mutation in history breaks the chain verification.
"""
import datetime
import hashlib
import json
import random
import time
from pymongo.errors import DuplicateKeyError
def _stable_json(value):
"""Serialize dictionaries in a stable way for deterministic hashing."""
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False, default=str)
def _entry_hash(prev_hash, payload):
"""Build the chained entry hash from previous hash + canonical payload."""
base = f"{prev_hash}|{_stable_json(payload)}"
return hashlib.sha256(base.encode("utf-8")).hexdigest()
def append_audit_event(db, event_type, actor, payload, request_ip=None, source="web", max_retries=5):
"""
Append an audit event to a tamper-evident chain.
Args:
db: MongoDB database handle.
event_type (str): Event category.
actor (str): User/system who performed the action.
payload (dict): Event details.
request_ip (str, optional): Request origin.
source (str): Source subsystem.
Returns:
dict: Inserted audit entry.
"""
logs = db["audit_log"]
attempts = 0
while attempts <= max_retries:
previous = logs.find_one(sort=[("chain_index", -1)])
prev_hash = previous.get("entry_hash", "") if previous else ""
chain_index = int(previous.get("chain_index", 0)) + 1 if previous else 1
timestamp = datetime.datetime.utcnow()
entry_payload = {
"event_type": event_type,
"actor": actor or "system",
"source": source,
"ip": request_ip or "",
"payload": payload or {},
"timestamp": timestamp.isoformat() + "Z",
}
entry_hash = _entry_hash(prev_hash, entry_payload)
entry = {
**entry_payload,
"created_at": timestamp,
"prev_hash": prev_hash,
"entry_hash": entry_hash,
"chain_index": chain_index,
}
try:
logs.insert_one(entry)
return entry
except DuplicateKeyError:
attempts += 1
if attempts > max_retries:
raise
# Exponential backoff with jitter to avoid retry storms.
delay = min(0.25, (0.005 * (2 ** attempts)) + random.random() * 0.01)
time.sleep(delay)
def ensure_audit_indexes(db):
"""Create indexes required for fast and safe audit operations."""
logs = db["audit_log"]
logs.create_index("chain_index", unique=True, name="audit_chain_index_unique")
logs.create_index("created_at", name="audit_created_at_idx")
logs.create_index("event_type", name="audit_event_type_idx")
def verify_audit_chain(db):
"""Verify hash chain integrity across all stored audit entries."""
logs = db["audit_log"]
entries = list(logs.find({}, {"_id": 1, "event_type": 1, "actor": 1, "source": 1, "ip": 1, "payload": 1, "timestamp": 1, "prev_hash": 1, "entry_hash": 1, "chain_index": 1}).sort("chain_index", 1))
previous_hash = ""
previous_index = 0
mismatches = []
for entry in entries:
chain_index = int(entry.get("chain_index", 0))
prev_hash = entry.get("prev_hash", "")
entry_hash = entry.get("entry_hash", "")
payload = {
"event_type": entry.get("event_type", ""),
"actor": entry.get("actor", ""),
"source": entry.get("source", ""),
"ip": entry.get("ip", ""),
"payload": entry.get("payload", {}),
"timestamp": entry.get("timestamp", ""),
}
expected_hash = _entry_hash(previous_hash, payload)
if chain_index != previous_index + 1:
mismatches.append({
"chain_index": chain_index,
"error": "chain_index_gap",
"expected": previous_index + 1,
"found": chain_index,
})
if prev_hash != previous_hash:
mismatches.append({
"chain_index": chain_index,
"error": "prev_hash_mismatch",
"expected": previous_hash,
"found": prev_hash,
})
if entry_hash != expected_hash:
mismatches.append({
"chain_index": chain_index,
"error": "entry_hash_mismatch",
"expected": expected_hash,
"found": entry_hash,
})
previous_hash = entry_hash
previous_index = chain_index
return {
"ok": len(mismatches) == 0,
"count": len(entries),
"last_chain_index": previous_index,
"last_hash": previous_hash,
"mismatches": mismatches,
}
+45
View File
@@ -0,0 +1,45 @@
'''
Copyright 2025-2026 AIIrondev
Licensed under the Inventarsystem EULA (Endbenutzer-Lizenzvertrag).
See Legal/LICENSE for the full license text.
Unauthorized commercial use, SaaS hosting, or removal of branding is prohibited.
For commercial licensing inquiries: https://github.com/AIIrondev
'''
"""
Funktion zum Protokollieren von Statusänderungen bei Ausleihungen
"""
import os
import datetime
from bson.objectid import ObjectId
def log_status_change(ausleihung_id, old_status, new_status, user=None):
"""
Protokolliert eine Statusänderung einer Ausleihung in einer Log-Datei.
Args:
ausleihung_id: Die ID der Ausleihung
old_status: Der alte Status
new_status: Der neue Status
user: Der Benutzer, der die Änderung vorgenommen hat (optional)
"""
try:
# Erstelle Log-Verzeichnis, falls es nicht existiert
log_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'logs')
if not os.path.exists(log_dir):
os.makedirs(log_dir)
# Log-Datei für Statusänderungen
log_file = os.path.join(log_dir, 'ausleihungen_status_changes.log')
# Protokolliere die Änderung
timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
user_info = f" by {user}" if user else ""
with open(log_file, 'a', encoding='utf-8') as f:
f.write(f"{timestamp}: Ausleihung {ausleihung_id} - Status changed from '{old_status}' to '{new_status}'{user_info}\n")
return True
except Exception as e:
print(f"Fehler beim Protokollieren der Statusänderung: {e}")
return False
+1 -1
View File
@@ -124,7 +124,7 @@
</div> </div>
<div class="head-actions"> <div class="head-actions">
<a class="btn btn-outline-secondary" href="{{ url_for('library_loans_admin') }}">Zur Bibliotheks-Ausleihenverwaltung</a> <a class="btn btn-outline-secondary" href="{{ url_for('library_loans_admin') }}">Zur Bibliotheks-Ausleihenverwaltung</a>
<a class="btn btn-secondary" href="{{ url_for('library_view') }}">Bibliothek öffnen</a> <a class="btn btn-secondary" href="{{ url_for('library_admin') }}">Bibliothek öffnen</a>
</div> </div>
</div> </div>
+28 -2
View File
@@ -113,6 +113,28 @@ def _normalize_db_name(db_name):
return db_name return db_name
def _module_name_candidates(module_name):
normalized = str(module_name or '').strip().lower().replace('-', '_')
if not normalized:
return []
candidates = [normalized]
alias_groups = {
'inventory': {'inventory', 'inventar'},
'library': {'library', 'bib', 'bibliothek'},
'student_cards': {'student_cards', 'studentcards', 'schuelerausweise', 'schueler_ausweise'},
}
for canonical_name, aliases in alias_groups.items():
if normalized in aliases:
for alias in (canonical_name, *sorted(aliases)):
if alias not in candidates:
candidates.append(alias)
break
return candidates
def _tenant_db_aliases(tenant_id): def _tenant_db_aliases(tenant_id):
aliases = [] aliases = []
env_map = _parse_tenant_db_map() env_map = _parse_tenant_db_map()
@@ -153,8 +175,12 @@ def _resolve_db_alias(tenant_id, db_name):
def is_tenant_module_enabled(module_name, tenant_id=None, default=False): def is_tenant_module_enabled(module_name, tenant_id=None, default=False):
"""Resolve whether a feature module is enabled for the current tenant.""" """Resolve whether a feature module is enabled for the current tenant."""
config = get_tenant_config(tenant_id) config = get_tenant_config(tenant_id)
enabled = _get_nested_value(config, ['modules', module_name, 'enabled'], default) for candidate_name in _module_name_candidates(module_name):
return bool(enabled) enabled = _get_nested_value(config, ['modules', candidate_name, 'enabled'], None)
if enabled is not None:
return bool(enabled)
return bool(default)
class TenantContext: class TenantContext:
+16 -11
View File
@@ -9,7 +9,9 @@ if [ ! -f "docker-compose-multitenant.yml" ]; then
exit 1 exit 1
fi fi
CONFIG_FILE="$PWD/config.json" # Resolve script directory so config paths are deterministic even when called via sudo
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="$SCRIPT_DIR/config.json"
ensure_runtime_config_json() { ensure_runtime_config_json() {
local config_path backup_path local config_path backup_path
@@ -212,13 +214,15 @@ EOF
} }
restart_app_container() { restart_app_container() {
local env_file="$PWD/.docker-build.env" local workdir="$SCRIPT_DIR"
local env_file="$SCRIPT_DIR/.docker-build.env"
local compose_args=() local compose_args=()
ensure_runtime_config_json ensure_runtime_config_json
# If HOST_WORKDIR is set (called from container), use absolute paths so docker daemon resolves them correctly # If HOST_WORKDIR is set (called from container), use absolute paths so docker daemon resolves them correctly
if [ -n "$HOST_WORKDIR" ]; then if [ -n "${HOST_WORKDIR:-}" ]; then
workdir="$HOST_WORKDIR"
compose_args+=( -f "$(readlink -f "$HOST_WORKDIR/docker-compose-multitenant.yml")" ) compose_args+=( -f "$(readlink -f "$HOST_WORKDIR/docker-compose-multitenant.yml")" )
if [ -f "$HOST_WORKDIR/.docker-compose.runtime.override.yml" ]; then if [ -f "$HOST_WORKDIR/.docker-compose.runtime.override.yml" ]; then
compose_args+=( -f "$(readlink -f "$HOST_WORKDIR/.docker-compose.runtime.override.yml")" ) compose_args+=( -f "$(readlink -f "$HOST_WORKDIR/.docker-compose.runtime.override.yml")" )
@@ -228,9 +232,9 @@ restart_app_container() {
fi fi
else else
# Normal case: called directly from host # Normal case: called directly from host
compose_args+=( -f "$PWD/docker-compose-multitenant.yml" ) compose_args+=( -f "$workdir/docker-compose-multitenant.yml" )
if [ -f "$PWD/.docker-compose.runtime.override.yml" ]; then if [ -f "$workdir/.docker-compose.runtime.override.yml" ]; then
compose_args+=( -f "$PWD/.docker-compose.runtime.override.yml" ) compose_args+=( -f "$workdir/.docker-compose.runtime.override.yml" )
fi fi
if [ -f "$env_file" ]; then if [ -f "$env_file" ]; then
compose_args+=( --env-file "$env_file" ) compose_args+=( --env-file "$env_file" )
@@ -238,7 +242,7 @@ restart_app_container() {
fi fi
# Pass along COMPOSE_PROJECT_NAME if set so the internal docker-compose sees it # Pass along COMPOSE_PROJECT_NAME if set so the internal docker-compose sees it
if [ -n "$COMPOSE_PROJECT_NAME" ]; then if [ -n "${COMPOSE_PROJECT_NAME:-}" ]; then
compose_args=( -p "$COMPOSE_PROJECT_NAME" "${compose_args[@]}" ) compose_args=( -p "$COMPOSE_PROJECT_NAME" "${compose_args[@]}" )
fi fi
@@ -373,7 +377,7 @@ case "$COMMAND" in
APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1) APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1)
if [ -n "$APP_CONTAINER" ]; then if [ -n "$APP_CONTAINER" ]; then
docker exec $APP_CONTAINER python3 -c " docker exec $APP_CONTAINER python3 -c "
import sys, re; sys.path.insert(0, '/app/Web'); import settings; from pymongo import MongoClient; import hashlib import sys, re; sys.path.insert(0, '/app'); sys.path.insert(0, '/app/Web'); from Web.modules.database import settings; from pymongo import MongoClient; import hashlib
tenant_id = sys.argv[1].lower() tenant_id = sys.argv[1].lower()
sanitized = ''.join(c for c in tenant_id if c.isalnum() or c == '_') sanitized = ''.join(c for c in tenant_id if c.isalnum() or c == '_')
db_name = f'inventar_{sanitized}' db_name = f'inventar_{sanitized}'
@@ -441,7 +445,7 @@ print(f'Tenant {sys.argv[1]} database initialized. Default admin: admin / admin1
APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1) APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1)
if [ -n "$APP_CONTAINER" ]; then if [ -n "$APP_CONTAINER" ]; then
docker exec $APP_CONTAINER python3 -c " docker exec $APP_CONTAINER python3 -c "
import sys; sys.path.insert(0, '/app/Web'); from tenant import TenantContext; import settings; from pymongo import MongoClient import sys; sys.path.insert(0, '/app'); sys.path.insert(0, '/app/Web'); from tenant import TenantContext; from Web.modules.database import settings; from pymongo import MongoClient
ctx = TenantContext() ctx = TenantContext()
db_name = ctx._get_db_name(sys.argv[1]) db_name = ctx._get_db_name(sys.argv[1])
client = MongoClient(settings.MONGODB_HOST, int(settings.MONGODB_PORT)) client = MongoClient(settings.MONGODB_HOST, int(settings.MONGODB_PORT))
@@ -488,7 +492,7 @@ print(f'Database for tenant {sys.argv[1]} dropped.')
APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1) APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1)
if [ -n "$APP_CONTAINER" ]; then if [ -n "$APP_CONTAINER" ]; then
docker exec $APP_CONTAINER python3 -c " docker exec $APP_CONTAINER python3 -c "
import sys; sys.path.insert(0, '/app/Web'); import settings; from pymongo import MongoClient import sys; sys.path.insert(0, '/app'); sys.path.insert(0, '/app/Web'); from Web.modules.database import settings; from pymongo import MongoClient
client = MongoClient(settings.MONGODB_HOST, int(settings.MONGODB_PORT)) client = MongoClient(settings.MONGODB_HOST, int(settings.MONGODB_PORT))
db = client[f'{settings.MONGODB_DB}_{sys.argv[1]}'] db = client[f'{settings.MONGODB_DB}_{sys.argv[1]}']
db.sessions.drop() # Force sign-out / session clear db.sessions.drop() # Force sign-out / session clear
@@ -535,8 +539,9 @@ PY
if [ -n "$APP_CONTAINER" ]; then if [ -n "$APP_CONTAINER" ]; then
docker exec -i "$APP_CONTAINER" python3 - <<'PY' docker exec -i "$APP_CONTAINER" python3 - <<'PY'
import sys import sys
sys.path.insert(0, '/app')
sys.path.insert(0, '/app/Web') sys.path.insert(0, '/app/Web')
import settings from Web.modules.database import settings
from pymongo import MongoClient from pymongo import MongoClient
client = MongoClient(settings.MONGODB_HOST, int(settings.MONGODB_PORT)) client = MongoClient(settings.MONGODB_HOST, int(settings.MONGODB_PORT))
prefix = 'inventar_' prefix = 'inventar_'