Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0114fec928 | |||
| b16ad56b98 | |||
| d2c21bc519 | |||
| 4461644cb6 | |||
| 73db5c5fe0 | |||
| d609775eb6 |
+6
-3
@@ -542,7 +542,8 @@ def _enforce_module_access():
|
||||
msg = {
|
||||
'library': "Bibliotheks-Modul ist deaktiviert.",
|
||||
'inventory': "Inventar-Modul ist deaktiviert.",
|
||||
'student_cards': "Schülerausweis-Modul ist deaktiviert."
|
||||
'student_cards': "Schülerausweis-Modul ist deaktiviert.",
|
||||
'terminplaner': "Termin-Modul ist deaktiviert."
|
||||
}.get(name, f"{name.capitalize()}-Modul ist deaktiviert.")
|
||||
|
||||
if request.path.startswith('/api/') or request.is_json:
|
||||
@@ -554,6 +555,8 @@ def _enforce_module_access():
|
||||
return redirect(url_for('home_admin'))
|
||||
elif name != 'library' and cfg.MODULES.is_enabled('library'):
|
||||
return redirect('/library')
|
||||
elif name != 'terminplaner' and cfg.MODULES.is_enabled('terminplaner'):
|
||||
return redirect('/terminplaner')
|
||||
|
||||
return redirect(url_for('my_borrowed_items'))
|
||||
|
||||
@@ -706,7 +709,7 @@ def _action_access_allowed(permissions, action_key):
|
||||
def _permission_denied_fallback_endpoint(permissions, current_endpoint=None):
|
||||
username = session.get('username')
|
||||
|
||||
for candidate in ('home_admin', 'my_borrowed_items', 'tutorial_page', 'notifications_view', 'impressum'):
|
||||
for candidate in ('home_admin', 'library_view', 'terminplaner', 'my_borrowed_items', 'tutorial_page', 'notifications_view', 'impressum'):
|
||||
if current_endpoint and candidate == current_endpoint:
|
||||
continue
|
||||
if _page_access_allowed(permissions, candidate):
|
||||
@@ -13163,7 +13166,7 @@ def get_optimal_image_quality(img, target_size_kb=80):
|
||||
|
||||
@app.route('/health')
|
||||
def health_check():
|
||||
return 'OK', 200
|
||||
return jsonify({"status": "healthy"}), 200
|
||||
|
||||
|
||||
@app.route('/api/push/subscribe', methods=['POST'])
|
||||
|
||||
@@ -843,6 +843,8 @@
|
||||
let keyboardScanBuffer = '';
|
||||
let keyboardLastKeyAt = 0;
|
||||
const KEYBOARD_SCAN_INTERCHAR_MS = 100; // max time between keystrokes to consider them one scan
|
||||
const physicalScanQueue = [];
|
||||
let keyboardScanProcessing = false;
|
||||
let editLibraryState = {
|
||||
itemId: '',
|
||||
seriesGroupId: '',
|
||||
@@ -1188,38 +1190,49 @@
|
||||
// Only active when explicitly enabled
|
||||
if (!keyboardScannerEnabled) return;
|
||||
|
||||
// Ignore if focus is in an input/textarea/contenteditable to avoid interfering with typing
|
||||
const active = document.activeElement;
|
||||
if (active && (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA' || active.isContentEditable)) return;
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
// If Enter/Return pressed -> finalize buffer
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
const code = keyboardScanBuffer.trim();
|
||||
keyboardScanBuffer = '';
|
||||
keyboardLastKeyAt = 0;
|
||||
if (!code) return;
|
||||
|
||||
// Process exactly like a camera scan - centralized logic handles the rest!
|
||||
handleScanSuccess(code);
|
||||
// Scanner keystrokes must not be submitted into the focused form field.
|
||||
processPhysicalScan(code);
|
||||
return;
|
||||
}
|
||||
|
||||
// Only accept common printable characters; ignore modifier keys
|
||||
if (e.key.length === 1) {
|
||||
// Physical scanners in this workflow emit numeric codes only.
|
||||
if (e.key.length === 1 && /\d/.test(e.key)) {
|
||||
// If time gap too big, start new buffer
|
||||
if (keyboardLastKeyAt && (now - keyboardLastKeyAt) > KEYBOARD_SCAN_INTERCHAR_MS) {
|
||||
keyboardScanBuffer = '';
|
||||
}
|
||||
keyboardScanBuffer += e.key;
|
||||
keyboardLastKeyAt = now;
|
||||
// Prevent default so scanner input doesn't accidentally move focus or trigger shortcuts
|
||||
// Prevent default so scanner input is captured even while an input has focus.
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
function handleScanSuccess(decodedText) {
|
||||
async function processPhysicalScan(code) {
|
||||
physicalScanQueue.push(code);
|
||||
if (keyboardScanProcessing) return;
|
||||
|
||||
keyboardScanProcessing = true;
|
||||
try {
|
||||
while (physicalScanQueue.length > 0) {
|
||||
await handleScanSuccess(physicalScanQueue.shift());
|
||||
}
|
||||
} finally {
|
||||
keyboardScanProcessing = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleScanSuccess(decodedText) {
|
||||
const scannedCode = typeof normalizeScannedCode === "function" ? normalizeScannedCode(decodedText) : decodedText;
|
||||
if (!scannedCode) return;
|
||||
|
||||
@@ -1235,26 +1248,27 @@
|
||||
|
||||
// 1. Modus: Nur Rückgabe
|
||||
if (mode === 'return_only') {
|
||||
returnByCode(scannedCode);
|
||||
await returnByCode(scannedCode);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Modus: Nur Ausweis
|
||||
if (mode === 'card_only') {
|
||||
setActiveStudentCard(scannedCode);
|
||||
setScanStatus(`Ausweis gesetzt: ${activeStudentCardId}`, 'ok');
|
||||
setScanStatus(`Ausweis gesetzt: ${activeStudentCardId}. Bitte den nächsten Ausweis scannen.`, 'ok');
|
||||
showSmallConfirm(`Ausweis erkannt: ${activeStudentCardId}. Sie können den nächsten scannen.`, 'ok');
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Modus: Schnellmodus (1x Ausweis, 1x Buch)
|
||||
if (mode === 'quick_toggle') {
|
||||
processQuickToggleScan(scannedCode);
|
||||
await processQuickToggleScan(scannedCode);
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. Modus: Dauermodus (1x Ausweis, Nx Bücher)
|
||||
if (mode === 'continuous') {
|
||||
processContinuousScan(scannedCode);
|
||||
await processContinuousScan(scannedCode);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1341,7 +1355,8 @@
|
||||
// 2. Wenn kein Ausweis gesetzt ist, wird der Code als Ausweis interpretiert
|
||||
if (!activeStudentCardId) {
|
||||
setActiveStudentCard(scannedCode);
|
||||
setScanStatus(`Ausweis gesetzt: ${activeStudentCardId}`, 'ok');
|
||||
setScanStatus(`Ausweis gesetzt: ${activeStudentCardId}. Bitte den nächsten Mediencode scannen.`, 'ok');
|
||||
showSmallConfirm(`Ausweis erkannt: ${activeStudentCardId}. Bitte jetzt den nächsten Mediencode scannen.`, 'ok');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -3418,14 +3418,24 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
`<form method="POST" action="{{ url_for('ausleihen', id='') }}${item._id}">
|
||||
${isGroupedItem ? `
|
||||
<div class="grouped-borrow-controls" style="display:flex; gap:8px; flex-wrap:wrap; margin-bottom:8px; align-items:center;">
|
||||
<label style="font-size:0.85rem; margin:0;">Anzahl:</label>
|
||||
<input type="number" name="exemplare_count" min="1" max="${availableGroupedCount}" value="1" style="width:74px; padding:4px;">
|
||||
<label style="font-size:0.85rem; margin:0;">oder Code:</label>
|
||||
<select id="specific-item-card-${item._id}" name="specific_item_id" style="max-width:190px; padding:4px;">
|
||||
<option value="">Automatisch wählen</option>
|
||||
${groupedAvailableUnits.map(unit => `<option value="${unit.id}">${unit.code}</option>`).join('')}
|
||||
</select>
|
||||
</div>` : ''}
|
||||
<label style="font-size:0.85rem; margin:0;">Anzahl:</label>
|
||||
<input type="number"
|
||||
name="exemplare_count"
|
||||
min="1"
|
||||
max="${availableGroupedCount}"
|
||||
value="1"
|
||||
style="width:74px; padding:4px;"
|
||||
oninput="if(this.value){ this.form.querySelector('[name=specific_item_id]').value = ''; }">
|
||||
|
||||
<label style="font-size:0.85rem; margin:0;">oder Code:</label>
|
||||
<select id="specific-item-card-${item._id}"
|
||||
name="specific_item_id"
|
||||
style="max-width:190px; padding:4px;"
|
||||
onchange="if(this.value !== ''){ this.form.querySelector('[name=exemplare_count]').value = '1'; }">
|
||||
<option value="">Automatisch wählen</option>
|
||||
${groupedAvailableUnits.map(unit => `<option value="${unit.id}">${unit.code}</option>`).join('')}
|
||||
</select>
|
||||
</div>` : ''}
|
||||
<button class="ausleihen" type="submit">Ausleihen</button>
|
||||
</form>`
|
||||
: isBorrowedByMe ?
|
||||
|
||||
+106
-72
@@ -430,6 +430,7 @@ PY
|
||||
initialize_tenant_database() {
|
||||
local tenant_id="$1"
|
||||
local mode="$2"
|
||||
local admin_password="${3:-admin123}"
|
||||
|
||||
APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1)
|
||||
if [ -z "$APP_CONTAINER" ]; then
|
||||
@@ -438,7 +439,8 @@ initialize_tenant_database() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
docker exec -i "$APP_CONTAINER" python3 - "$tenant_id" "$mode" <<'PY'
|
||||
# Fix: We inject the password securely via a temporary environment variable (-e ADMIN_PASS)
|
||||
docker exec -i -e ADMIN_PASS="$admin_password" "$APP_CONTAINER" python3 - "$tenant_id" "$mode" <<'PY'
|
||||
import sys, os, re, datetime, hashlib
|
||||
sys.path.insert(0, "/app")
|
||||
sys.path.insert(0, "/app/Web")
|
||||
@@ -449,6 +451,13 @@ import Web.modules.database.user as us
|
||||
|
||||
tenant_id = sys.argv[1].lower()
|
||||
mode = sys.argv[2]
|
||||
|
||||
# Fix: Read the password directly from the environment variable instead of sys.argv
|
||||
admin_password = os.environ.get("ADMIN_PASS", "admin123")
|
||||
# Failsafe in case of empty string
|
||||
if not admin_password.strip():
|
||||
admin_password = "admin123"
|
||||
|
||||
sanitized = "".join(c for c in tenant_id if c.isalnum() or c == "_")
|
||||
db_name = f"inventar_{sanitized}" if sanitized else settings.MONGODB_DB
|
||||
|
||||
@@ -458,7 +467,7 @@ users = db["users"]
|
||||
permission_defaults = us.build_default_permission_payload("full_access")
|
||||
admin_doc = {
|
||||
"Username": "admin",
|
||||
"Password": us.hashing("admin123"),
|
||||
"Password": us.hashing(admin_password),
|
||||
"Admin": True,
|
||||
"active_ausleihung": None,
|
||||
"name": dp.encrypt_text("admin"),
|
||||
@@ -488,7 +497,7 @@ if mode == "trial":
|
||||
|
||||
client.close()
|
||||
|
||||
print(f"Tenant {sys.argv[1]} database initialized. Default admin: admin / admin123")
|
||||
print(f"Tenant {sys.argv[1]} database initialized. Default admin: admin / {admin_password}")
|
||||
PY
|
||||
}
|
||||
|
||||
@@ -812,12 +821,26 @@ case "$COMMAND" in
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PORT_ARG="${3:-}"
|
||||
if [ -n "$PORT_ARG" ]; then
|
||||
if ! printf '%s\n' "$PORT_ARG" | grep -qE '^[0-9]+$'; then
|
||||
echo "Error: Port must be a numeric value."
|
||||
exit 1
|
||||
ARG3="${3:-}"
|
||||
ARG4="${4:-}"
|
||||
|
||||
PORT_ARG=""
|
||||
PASSWORD_ARG="admin123"
|
||||
|
||||
# Check if the 3rd argument is numeric (Port) or text (Password)
|
||||
if [ -n "$ARG3" ]; then
|
||||
if printf '%s\n' "$ARG3" | grep -qE '^[0-9]+$'; then
|
||||
PORT_ARG="$ARG3"
|
||||
if [ -n "$ARG4" ]; then
|
||||
PASSWORD_ARG="$ARG4"
|
||||
fi
|
||||
else
|
||||
# If ARG3 is not numeric, treat it as the password without setting a port
|
||||
PASSWORD_ARG="$ARG3"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$PORT_ARG" ]; then
|
||||
register_tenant_port "$TENANT_ID" "$PORT_ARG"
|
||||
update_runtime_ports "$PORT_ARG"
|
||||
sync_tenant_port_map
|
||||
@@ -829,11 +852,9 @@ case "$COMMAND" in
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
|
||||
echo "Adding new tenant '$TENANT_ID'..."
|
||||
echo "Initializing database for $TENANT_ID..."
|
||||
initialize_tenant_database "$TENANT_ID" "standard"
|
||||
initialize_tenant_database "$TENANT_ID" "standard" "$PASSWORD_ARG"
|
||||
echo "Tenant '$TENANT_ID' successfully added. Ready to use."
|
||||
;;
|
||||
|
||||
@@ -872,81 +893,94 @@ case "$COMMAND" in
|
||||
;;
|
||||
|
||||
remove)
|
||||
FORCE_REMOVE=false
|
||||
TENANT_ARG="${2:-}"
|
||||
FORCE_REMOVE=false
|
||||
|
||||
if [ "$TENANT_ARG" = "--yes" ] || [ "$TENANT_ARG" = "-y" ]; then
|
||||
FORCE_REMOVE=true
|
||||
TENANT_ID="${3:-}"
|
||||
else
|
||||
TENANT_ID="$TENANT_ARG"
|
||||
# Fix: Prüfe sowohl Position 2 als auch 3 auf das -y Flag
|
||||
if [ "${2:-}" = "--yes" ] || [ "${2:-}" = "-y" ]; then
|
||||
FORCE_REMOVE=true
|
||||
TENANT_ID="${3:-}"
|
||||
elif [ "${3:-}" = "--yes" ] || [ "${3:-}" = "-y" ]; then
|
||||
FORCE_REMOVE=true
|
||||
TENANT_ID="${2:-}"
|
||||
else
|
||||
TENANT_ID="${2:-}"
|
||||
fi
|
||||
|
||||
if [ -z "$TENANT_ID" ]; then
|
||||
echo "Error: Please provide a tenant_id to remove."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$FORCE_REMOVE" != true ]; then
|
||||
echo -n "WARNING: Are you sure you want to permanently delete all data for tenant '$TENANT_ID'? (y/N) "
|
||||
read confirm
|
||||
if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then
|
||||
echo "Removal canceled."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$TENANT_ID" ]; then
|
||||
echo "Error: Please provide a tenant_id to remove."
|
||||
exit 1
|
||||
fi
|
||||
echo "Removing tenant '$TENANT_ID'..."
|
||||
APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1)
|
||||
port_to_remove=""
|
||||
|
||||
if [ "$FORCE_REMOVE" != true ]; then
|
||||
echo -n "WARNING: Are you sure you want to permanently delete all data for tenant '$TENANT_ID'? (y/N) "
|
||||
read confirm
|
||||
if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then
|
||||
echo "Removal canceled."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
if [ -n "$APP_CONTAINER" ]; then
|
||||
# MongoDB-Datenbank via PyMongo direkt im Container droppen
|
||||
# Fix 1: Wir hängen '|| true' an, damit das Skript nicht abstürzt, falls der Befehl fehlschlägt.
|
||||
docker exec -i "$APP_CONTAINER" python3 -c '
|
||||
import sys, os
|
||||
try:
|
||||
import pymongo
|
||||
tenant_id = sys.argv[1]
|
||||
# In Docker Compose heißt der Host oft "mongodb" statt "localhost"
|
||||
mongo_uri = os.environ.get("MONGO_URI", "mongodb://mongodb:27017/")
|
||||
client = pymongo.MongoClient(mongo_uri, serverSelectionTimeoutMS=2000)
|
||||
|
||||
echo "Removing tenant '$TENANT_ID'..."
|
||||
APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1)
|
||||
port_to_remove=""
|
||||
# Fix 2: Wir versuchen beide Formate zu löschen, da "list" zeigt, dass sie "test" und nicht "inventar_test" heißen.
|
||||
dbs_to_drop = [f"inventar_{tenant_id}", tenant_id]
|
||||
existing_dbs = client.list_database_names()
|
||||
|
||||
if [ -n "$APP_CONTAINER" ]; then
|
||||
# MongoDB-Datenbank via PyMongo direkt im Container droppen
|
||||
docker exec -i "$APP_CONTAINER" python3 -c '
|
||||
import sys, os
|
||||
try:
|
||||
import pymongo
|
||||
tenant_id = sys.argv[1]
|
||||
mongo_uri = os.environ.get("MONGO_URI", "mongodb://localhost:27017/")
|
||||
client = pymongo.MongoClient(mongo_uri, serverSelectionTimeoutMS=2000)
|
||||
db_name = f"inventar_{tenant_id}"
|
||||
if db_name in client.list_database_names():
|
||||
for db_name in dbs_to_drop:
|
||||
if db_name in existing_dbs:
|
||||
client.drop_database(db_name)
|
||||
print(f"Dropped database: {db_name}")
|
||||
except Exception as e:
|
||||
print(f"Error dropping database: {e}", file=sys.stderr)
|
||||
' "$TENANT_ID" > /dev/null 2>&1
|
||||
except Exception as e:
|
||||
print(f"Error dropping database: {e}", file=sys.stderr)
|
||||
' "$TENANT_ID" > /dev/null 2>&1 || true
|
||||
|
||||
# Konfiguration und Port via Host-Funktion bereinigen und Port ermitteln
|
||||
if port_to_remove="$(remove_tenant_port "$TENANT_ID" 2>/dev/null)"; then
|
||||
:
|
||||
else
|
||||
port_to_remove=""
|
||||
fi
|
||||
|
||||
echo "Tenant '$TENANT_ID' database and config removed."
|
||||
# Konfiguration und Port via Host-Funktion bereinigen und Port ermitteln
|
||||
if port_to_remove="$(remove_tenant_port "$TENANT_ID" 2>/dev/null)"; then
|
||||
:
|
||||
else
|
||||
echo "Warning: Application container not running. Tenant database may still exist in MongoDB."
|
||||
if port_to_remove="$(remove_tenant_port "$TENANT_ID" 2>/dev/null)"; then
|
||||
:
|
||||
else
|
||||
echo "Warning: tenant '$TENANT_ID' was not configured in config.json or could not be removed."
|
||||
fi
|
||||
port_to_remove=""
|
||||
fi
|
||||
|
||||
if [ -n "$port_to_remove" ]; then
|
||||
remove_runtime_port "$port_to_remove"
|
||||
fi
|
||||
remove_tenant_nginx_config "$TENANT_ID"
|
||||
sync_tenant_port_map
|
||||
if [ -n "$(docker ps -qf 'name=app' | head -n 1)" ]; then
|
||||
restart_app_container
|
||||
fi
|
||||
if [ -n "$port_to_remove" ]; then
|
||||
echo "Removed tenant '$TENANT_ID' and cleaned runtime port $port_to_remove."
|
||||
echo "Tenant '$TENANT_ID' database and config removed."
|
||||
else
|
||||
echo "Warning: Application container not running. Tenant database may still exist in MongoDB."
|
||||
if port_to_remove="$(remove_tenant_port "$TENANT_ID" 2>/dev/null)"; then
|
||||
:
|
||||
else
|
||||
echo "Removed tenant '$TENANT_ID'. No port mapping was present."
|
||||
echo "Warning: tenant '$TENANT_ID' was not configured in config.json or could not be removed."
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$port_to_remove" ]; then
|
||||
remove_runtime_port "$port_to_remove" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
remove_tenant_nginx_config "$TENANT_ID"
|
||||
sync_tenant_port_map
|
||||
|
||||
if [ -n "$(docker ps -qf 'name=app' | head -n 1)" ]; then
|
||||
restart_app_container || true
|
||||
fi
|
||||
|
||||
if [ -n "$port_to_remove" ]; then
|
||||
echo "Removed tenant '$TENANT_ID' and cleaned runtime port $port_to_remove."
|
||||
else
|
||||
echo "Removed tenant '$TENANT_ID'. No port mapping was present."
|
||||
fi
|
||||
;;
|
||||
|
||||
restart-tenant)
|
||||
|
||||
Reference in New Issue
Block a user