Compare commits

...

8 Commits

Author SHA1 Message Date
Aiirondev_dev 0e85337fc5 changes to the scanning functrionality
Release Inventarsystem / release-docker (push) Successful in 2m22s
2026-09-08 18:36:34 +02:00
Aiirondev_dev 0114fec928 changes to the physical scanner processing
Release Inventarsystem / release-docker (push) Successful in 12m48s
2026-09-08 18:19:40 +02:00
Aiirondev_dev b16ad56b98 New password option fo the tenant adding and heatlth check
Release Inventarsystem / release-docker (push) Successful in 3m16s
2026-08-27 11:31:07 +02:00
Aiirondev_dev d2c21bc519 New password option fo the tenant adding and heatlth check
Release Inventarsystem / release-docker (push) Successful in 3m9s
2026-08-26 22:55:56 +02:00
Aiirondev_dev 4461644cb6 New password option fo the tenant adding
Release Inventarsystem / release-docker (push) Successful in 3m22s
2026-08-25 19:47:29 +02:00
Aiirondev_dev 73db5c5fe0 changes to the removal process
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-25 00:07:06 +02:00
Aiirondev_dev d609775eb6 Implementation of the switch between Anzahl oder Code
Release Inventarsystem / release-docker (push) Successful in 3m7s
2026-08-24 18:24:32 +02:00
Aiirondev_dev c1a6a145dc The School year rollover button is placed in the school settings.
Release Inventarsystem / release-docker (push) Successful in 2m16s
2026-08-24 18:00:35 +02:00
5 changed files with 358 additions and 178 deletions
+6 -66
View File
@@ -228,7 +228,6 @@ def rollover_student_card_classes(dry_run=False, *, max_class=None, graduate_lab
client.close()
# Admin route to trigger rollover manually
@app.route('/admin/trigger_school_year_rollover', methods=['POST'])
def admin_trigger_school_year_rollover():
if 'username' not in session:
@@ -543,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:
@@ -555,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'))
@@ -707,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):
@@ -3038,42 +3040,6 @@ def optimized_image(filename):
app.logger.error(f"Error serving optimized image {filename}: {str(e)}")
return Response("Optimized image not found", status=404)
# @app.route('/QRCodes/<filename>')
# def qrcode_file(filename):
# """
# Serve QR code files from the QRCodes directory.
#
# Args:
# filename (str): Name of the QR code file to serve
#
# Returns:
# flask.Response: The requested QR code file or placeholder image if not found
# """
# try:
# # Check production path first
# prod_path = "/var/Inventarsystem/Web/QRCodes"
# dev_path = app.config['QR_CODE_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)
#
# # Use a placeholder image if file not found - first try SVG, then PNG
# svg_placeholder_path = os.path.join(app.static_folder, 'img', 'no-image.svg')
# png_placeholder_path = os.path.join(app.static_folder, 'img', 'no-image.png')
#
# if os.path.exists(svg_placeholder_path):
# return send_from_directory(app.static_folder, 'img/no-image.svg')
# elif os.path.exists(png_placeholder_path):
# return send_from_directory(app.static_folder, 'img/no-image.png')
# else:
# return send_from_directory(app.static_folder, 'favicon.ico')
# except Exception as e:
# print(f"Error serving QR code {filename}: {str(e)}")
# return Response("QR code not found", status=404)
@app.route('/<path:filename>')
def catch_all_files(filename):
"""
@@ -3136,32 +3102,6 @@ def test_connection():
"""
return {'status': 'success', 'message': 'Connection successful', 'status_code': 200}
""" if sucess in deployment the funktion can be removed
@app.route('/user_status')
def user_status():
API endpoint to get the current user's status (username, admin status).
Used by JavaScript in templates to personalize the UI.
Returns:
JSON: User status information or error if not authenticated
if 'username' in session:
is_admin = us.check_admin(session['username'])
return jsonify({
'authenticated': True,
'username': session['username'],
'is_admin': is_admin
})
else:
return jsonify({
'authenticated': False,
'error': 'Not logged in'
}), 401
"""
##################################################### changes to be made to account for the new account permison managment system ##############################
@app.route('/')
def home():
@@ -13226,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'])
+83 -3
View File
@@ -120,9 +120,64 @@
<p>Diese Angaben erscheinen künftig im amtlichen Audit-PDF und in anderen Behördenberichten.</p>
</div>
</div>
<!-- Danger Zone: Schuljahreswechsel -->
<div class="card danger-zone" style="grid-column: 1 / -1; border-color: #fca5a5;">
<div class="card-header" style="background: #fef2f2; border-bottom: 1px solid #fca5a5;">
<h2 style="color: #991b1b;">Erweiterte Aktionen</h2>
</div>
<div class="card-body" style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 15px;">
<div>
<strong style="color: #7f1d1d; display: block; font-size: 1.05rem; margin-bottom: 4px;">Schuljahreswechsel durchführen</strong>
<span style="color: #991b1b; font-size: 0.9rem;">Diese Aktion bereitet das System auf das neue Schuljahr vor (z. B. Hochstufen von Klassen).</span>
</div>
<button type="button"
id="btn-rollover"
class="btn btn-danger"
onclick="triggerSchoolYearRollover()">
Schuljahreswechsel auslösen
</button>
</div>
</div>
</div>
</div>
<script>
async function triggerSchoolYearRollover() {
if (!confirm('Möchtest du den Schuljahreswechsel wirklich durchführen? Dies kann nicht rückgängig gemacht werden!')) {
return;
}
const btn = document.getElementById('btn-rollover');
btn.disabled = true;
btn.textContent = 'Wird ausgeführt...';
try {
const response = await fetch('/admin/trigger_school_year_rollover', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (response.ok && data.ok) {
alert('Schuljahreswechsel erfolgreich durchgeführt!\n\nZusammenfassung:\n' + JSON.stringify(data.summary, null, 2));
location.reload();
} else {
alert('Fehler: ' + (data.message || 'Schuljahreswechsel konnte nicht durchgeführt werden.'));
}
} catch (error) {
console.error('Rollover error:', error);
alert('Netzwerk- oder Serverfehler beim Ausführen des Schuljahreswechsels.');
} finally {
btn.disabled = false;
btn.textContent = 'Schuljahreswechsel auslösen';
}
}
</script>
<style>
.container {
max-width: 1100px;
@@ -245,12 +300,17 @@
cursor: pointer;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-primary {
background: #2563eb;
color: #fff;
}
.btn-primary:hover {
.btn-primary:hover:not(:disabled) {
background: #1d4ed8;
color: #fff;
}
@@ -261,11 +321,21 @@
border-color: #d1d5db;
}
.btn-secondary:hover {
.btn-secondary:hover:not(:disabled) {
background: #e5e7eb;
color: #111827;
}
.btn-danger {
background: #ef4444;
color: #fff;
}
.btn-danger:hover:not(:disabled) {
background: #dc2626;
color: #fff;
}
.preview-card p {
margin: 0 0 10px;
line-height: 1.45;
@@ -283,5 +353,15 @@
grid-template-columns: 1fr;
}
}
@media (max-width: 600px) {
.danger-zone .card-body {
flex-direction: column;
align-items: flex-start;
}
.danger-zone .btn {
width: 100%;
}
}
</style>
{% endblock %}
{% endblock %}
+145 -29
View File
@@ -451,6 +451,27 @@
height: 100%;
}
.physical-scanner-modal {
z-index: 1100;
}
.physical-scanner-modal .modal-content {
max-width: 520px;
text-align: center;
}
.physical-scanner-code {
min-height: 28px;
margin: 18px 0 8px;
padding: 12px;
border: 1px dashed #cbd5e1;
border-radius: 6px;
color: var(--ui-text);
font-family: monospace;
font-size: 1.2em;
letter-spacing: 2px;
}
.detail-gallery-container {
display: flex;
@@ -518,10 +539,9 @@
<input type="text" id="manualItemCode" placeholder="Manueller Mediencode (optional)" style="min-width:180px;">
<button id="resetCardBtn" class="button" type="button">Feld zurücksetzen</button>
<button id="toggleScannerBtn" class="button" type="button">Scanner starten</button>
<label style="display:flex; align-items:center; gap:8px; margin-left:6px;">
<input type="checkbox" id="keyboardScannerToggle">
<span style="font-size:0.9em;">Physischer Scanner</span>
</label>
<button id="physicalScannerBtn" class="button" type="button" style="margin-left:6px;">
Physischer Scanner starten
</button>
<button id="manualActionBtn" class="button" type="button" style="margin-left:6px; background:#4f46e5; color:white;">Code verarbeiten</button>
</div>
<div class="library-scan-reader-wrap" id="scanReaderWrap" style="display: none; margin-top: 15px;">
@@ -612,6 +632,16 @@
<div id="detailContent"></div>
</div>
</div>
<div id="physicalScannerModal" class="modal physical-scanner-modal" style="display: none;" role="dialog" aria-modal="true" aria-labelledby="physicalScannerTitle">
<div class="modal-content">
<span class="close" id="closePhysicalScannerBtn" role="button" tabindex="0" aria-label="Physischen Scanner schließen">&times;</span>
<h2 id="physicalScannerTitle">Physischer Scanner</h2>
<p id="physicalScannerInstruction">Scanner ist bereit. Bitte den angezeigten Code scannen.</p>
<div id="physicalScannerCode" class="physical-scanner-code" aria-live="polite"></div>
<p id="physicalScannerStatus" class="library-scan-status" aria-live="polite">Warte auf Scan...</p>
<button id="stopPhysicalScannerBtn" class="button" type="button">Scanner schließen</button>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2/dist/quagga.js"></script>
<script>
@@ -842,7 +872,10 @@
let keyboardScannerEnabled = false;
let keyboardScanBuffer = '';
let keyboardLastKeyAt = 0;
let physicalScannerModalOpen = false;
const KEYBOARD_SCAN_INTERCHAR_MS = 100; // max time between keystrokes to consider them one scan
const physicalScanQueue = [];
let keyboardScanProcessing = false;
let editLibraryState = {
itemId: '',
seriesGroupId: '',
@@ -1184,42 +1217,114 @@
// =========================================================================
// Keyboard scanner handling (physical scanners that send chars then Enter)
// =========================================================================
function getPhysicalScannerInstruction() {
const mode = document.getElementById('scanModeSelect')?.value || 'card_only';
if (mode === 'return_only') return 'Bitte den Mediencode scannen.';
if (mode === 'card_only') return 'Bitte den Bibliotheksausweis scannen.';
if (mode === 'quick_toggle' && !activeStudentCardId) return 'Bitte zuerst den Bibliotheksausweis scannen.';
if (mode === 'quick_toggle') return 'Bitte den Mediencode scannen.';
if (mode === 'continuous' && !activeStudentCardId) return 'Bitte zuerst den Bibliotheksausweis scannen.';
return 'Bitte den nächsten Mediencode scannen.';
}
function updatePhysicalScannerModal(message, kind = '') {
const instruction = document.getElementById('physicalScannerInstruction');
const status = document.getElementById('physicalScannerStatus');
if (instruction) instruction.textContent = getPhysicalScannerInstruction();
if (status) {
status.textContent = message;
status.classList.remove('ok', 'warn', 'error');
if (kind) status.classList.add(kind);
}
}
function openPhysicalScannerModal() {
const modal = document.getElementById('physicalScannerModal');
if (!modal) return;
physicalScannerModalOpen = true;
keyboardScannerEnabled = true;
keyboardScanBuffer = '';
keyboardLastKeyAt = 0;
modal.style.display = 'flex';
updatePhysicalScannerModal('Warte auf Scan...', 'warn');
document.addEventListener('keydown', keyboardScanKeydownHandler);
document.getElementById('closePhysicalScannerBtn')?.focus();
}
function closePhysicalScannerModal() {
physicalScannerModalOpen = false;
keyboardScannerEnabled = false;
keyboardScanBuffer = '';
keyboardLastKeyAt = 0;
document.removeEventListener('keydown', keyboardScanKeydownHandler);
const modal = document.getElementById('physicalScannerModal');
if (modal) modal.style.display = 'none';
const code = document.getElementById('physicalScannerCode');
if (code) code.textContent = '';
}
function keyboardScanKeydownHandler(e) {
// Only active when explicitly enabled
if (!keyboardScannerEnabled) return;
if (!keyboardScannerEnabled || !physicalScannerModalOpen) 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;
if (e.key === 'Escape') {
e.preventDefault();
closePhysicalScannerModal();
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.
const displayCode = document.getElementById('physicalScannerCode');
if (displayCode) displayCode.textContent = code;
updatePhysicalScannerModal('Code empfangen. Verarbeitung läuft...', 'warn');
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
const displayCode = document.getElementById('physicalScannerCode');
if (displayCode) displayCode.textContent = keyboardScanBuffer;
// 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());
if (physicalScannerModalOpen) {
updatePhysicalScannerModal('Aktion abgeschlossen. Bereit für den nächsten Scan.', 'ok');
}
}
} finally {
keyboardScanProcessing = false;
}
}
async function handleScanSuccess(decodedText) {
const scannedCode = typeof normalizeScannedCode === "function" ? normalizeScannedCode(decodedText) : decodedText;
if (!scannedCode) return;
@@ -1235,26 +1340,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 +1447,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;
}
@@ -1649,7 +1756,7 @@
const toggleBtn = document.getElementById('toggleScannerBtn');
const resetBtn = document.getElementById('resetCardBtn');
const modeSelect = document.getElementById('scanModeSelect');
const keyboardToggle = document.getElementById('keyboardScannerToggle');
const physicalScannerBtn = document.getElementById('physicalScannerBtn');
if (toggleBtn) {
toggleBtn.addEventListener('click', async () => {
@@ -1670,6 +1777,9 @@
if (modeSelect) {
modeSelect.addEventListener('change', () => {
if (physicalScannerModalOpen) {
updatePhysicalScannerModal('Warte auf Scan...', 'warn');
}
if (modeSelect.value === 'quick_toggle' && !activeStudentCardId) {
setScanStatus('Schnellmodus: zuerst Bibliotheksausweis scannen.', 'warn');
} else if (modeSelect.value === 'card_only') {
@@ -1678,18 +1788,24 @@
});
}
if (keyboardToggle) {
keyboardToggle.addEventListener('change', () => {
keyboardScannerEnabled = !!keyboardToggle.checked;
if (keyboardScannerEnabled) {
document.addEventListener('keydown', keyboardScanKeydownHandler);
setScanStatus('Physischer Scanner aktiv (Schnellmodus empfohlen).', 'ok');
} else {
document.removeEventListener('keydown', keyboardScanKeydownHandler);
setScanStatus('Physischer Scanner deaktiviert.', 'warn');
if (physicalScannerBtn) {
physicalScannerBtn.addEventListener('click', openPhysicalScannerModal);
}
const closePhysicalScannerBtn = document.getElementById('closePhysicalScannerBtn');
const stopPhysicalScannerBtn = document.getElementById('stopPhysicalScannerBtn');
if (closePhysicalScannerBtn) {
closePhysicalScannerBtn.addEventListener('click', closePhysicalScannerModal);
closePhysicalScannerBtn.addEventListener('keydown', (event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
closePhysicalScannerModal();
}
});
}
if (stopPhysicalScannerBtn) {
stopPhysicalScannerBtn.addEventListener('click', closePhysicalScannerModal);
}
}
// Run when DOM structure is entirely ready
+18 -8
View File
@@ -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
View File
@@ -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)