Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b00aab1fa | |||
| 6d4ac073a5 | |||
| 73ab1a37d2 | |||
| 0eb1ffe9d9 | |||
| 0ea1294237 | |||
| a7ff2f1552 | |||
| f847faea3e | |||
| 4c58dceb02 | |||
| db80693c6a |
@@ -1,8 +1,6 @@
|
||||
services:
|
||||
app:
|
||||
working_dir: /app/Web
|
||||
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
|
||||
image: ghcr.io/aiirondev/legendary-octo-garbanzo:v0.7.42
|
||||
build: null
|
||||
ports:
|
||||
- "10000:8000"
|
||||
|
||||
@@ -134,14 +134,22 @@ jobs:
|
||||
echo "push_dev=false" >> "$GITHUB_OUTPUT"
|
||||
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
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |
|
||||
TAG="${{ steps.meta.outputs.tag }}"
|
||||
git config user.name "github-actions[bot]"
|
||||
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 push origin "$TAG"
|
||||
git push origin HEAD:${{ github.ref_name }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
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
|
||||
|
||||
- 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
|
||||
run: |
|
||||
mkdir -p release-bundle
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
v0.7.42
|
||||
v0.7.70
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# Web package initialization
|
||||
+56
-13
@@ -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 werkzeug.utils import secure_filename
|
||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
from werkzeug.exceptions import HTTPException
|
||||
from werkzeug.routing import BuildError
|
||||
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.items as it
|
||||
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 Web.modules.inventarsystem.pdf_export as pdf_export
|
||||
import datetime
|
||||
@@ -42,7 +55,6 @@ from urllib.parse import urlparse, urlunparse
|
||||
import requests
|
||||
import csv
|
||||
import ipaddress
|
||||
import os
|
||||
import json
|
||||
import datetime
|
||||
import time
|
||||
@@ -63,7 +75,6 @@ except Exception:
|
||||
# import qrcode
|
||||
# from qrcode.constants import ERROR_CORRECT_L
|
||||
import threading
|
||||
import sys
|
||||
import shutil
|
||||
import uuid
|
||||
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-------------------------------------------------------"""
|
||||
|
||||
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
|
||||
# if not os.path.exists(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'):
|
||||
return redirect(url_for('home'))
|
||||
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'))
|
||||
|
||||
@@ -433,6 +444,41 @@ def handle_not_found(e):
|
||||
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.'):
|
||||
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
|
||||
@@ -2369,11 +2415,7 @@ def preview_file(filename):
|
||||
if denied:
|
||||
return denied
|
||||
|
||||
# Check production path first
|
||||
prod_path = "/var/Inventarsystem/Web/previews"
|
||||
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)):
|
||||
return send_from_directory(dev_path, filename)
|
||||
|
||||
@@ -2640,7 +2682,7 @@ def home():
|
||||
|
||||
if not cfg.MODULES.is_enabled('inventory'):
|
||||
if cfg.MODULES.is_enabled('library'):
|
||||
return redirect(url_for('library_view'))
|
||||
return redirect('/library')
|
||||
else:
|
||||
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
|
||||
)
|
||||
|
||||
@app.route('/library/export', defaults={'scope': 'all'})
|
||||
@app.route('/library/export/<scope>')
|
||||
def library_export_excel(scope):
|
||||
def library_export_excel(scope='all'):
|
||||
if 'username' not in session:
|
||||
return redirect(url_for('login'))
|
||||
|
||||
@@ -2737,7 +2780,7 @@ def library_export_excel(scope):
|
||||
filename = f"Bibliothek_Ausgeliehen_{username}.xlsx"
|
||||
elif scope == 'all_borrowed':
|
||||
if not is_admin_user:
|
||||
return redirect(url_for('library_view'))
|
||||
return redirect('/library')
|
||||
query['Verfuegbar'] = False
|
||||
filename = "Bibliothek_Alle_Ausleihen.xlsx"
|
||||
elif scope == 'schulbuecher':
|
||||
@@ -2751,7 +2794,7 @@ def library_export_excel(scope):
|
||||
client.close()
|
||||
|
||||
excel_file = excel_export.generate_library_excel(items)
|
||||
return flask.send_file(
|
||||
return send_file(
|
||||
excel_file,
|
||||
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
as_attachment=True,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# Web.modules package initialization
|
||||
@@ -0,0 +1 @@
|
||||
# Web.modules.bibliothek package initialization
|
||||
@@ -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:
|
||||
try:
|
||||
# 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(
|
||||
str(ausleihung['_id']),
|
||||
original_status,
|
||||
|
||||
@@ -135,7 +135,7 @@ def _get_int_env(name, default):
|
||||
return int(default)
|
||||
|
||||
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:
|
||||
if l.startswith('INVENTAR_APP_IMAGE='):
|
||||
return l.split(':', 1)[1].strip()
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# Web.modules.emailservice package initialization
|
||||
@@ -0,0 +1 @@
|
||||
# Web.modules.log package initialization
|
||||
@@ -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,
|
||||
}
|
||||
Executable
+45
@@ -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
|
||||
@@ -124,7 +124,7 @@
|
||||
</div>
|
||||
<div class="head-actions">
|
||||
<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>
|
||||
|
||||
|
||||
+28
-2
@@ -113,6 +113,28 @@ def _normalize_db_name(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):
|
||||
aliases = []
|
||||
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):
|
||||
"""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)
|
||||
for candidate_name in _module_name_candidates(module_name):
|
||||
enabled = _get_nested_value(config, ['modules', candidate_name, 'enabled'], None)
|
||||
if enabled is not None:
|
||||
return bool(enabled)
|
||||
|
||||
return bool(default)
|
||||
|
||||
|
||||
class TenantContext:
|
||||
|
||||
+5
-4
@@ -373,7 +373,7 @@ case "$COMMAND" in
|
||||
APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1)
|
||||
if [ -n "$APP_CONTAINER" ]; then
|
||||
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()
|
||||
sanitized = ''.join(c for c in tenant_id if c.isalnum() or c == '_')
|
||||
db_name = f'inventar_{sanitized}'
|
||||
@@ -441,7 +441,7 @@ print(f'Tenant {sys.argv[1]} database initialized. Default admin: admin / admin1
|
||||
APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1)
|
||||
if [ -n "$APP_CONTAINER" ]; then
|
||||
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()
|
||||
db_name = ctx._get_db_name(sys.argv[1])
|
||||
client = MongoClient(settings.MONGODB_HOST, int(settings.MONGODB_PORT))
|
||||
@@ -488,7 +488,7 @@ print(f'Database for tenant {sys.argv[1]} dropped.')
|
||||
APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1)
|
||||
if [ -n "$APP_CONTAINER" ]; then
|
||||
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))
|
||||
db = client[f'{settings.MONGODB_DB}_{sys.argv[1]}']
|
||||
db.sessions.drop() # Force sign-out / session clear
|
||||
@@ -535,8 +535,9 @@ PY
|
||||
if [ -n "$APP_CONTAINER" ]; then
|
||||
docker exec -i "$APP_CONTAINER" python3 - <<'PY'
|
||||
import sys
|
||||
sys.path.insert(0, '/app')
|
||||
sys.path.insert(0, '/app/Web')
|
||||
import settings
|
||||
from Web.modules.database import settings
|
||||
from pymongo import MongoClient
|
||||
client = MongoClient(settings.MONGODB_HOST, int(settings.MONGODB_PORT))
|
||||
prefix = 'inventar_'
|
||||
|
||||
Reference in New Issue
Block a user