Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e1c284fc40 | |||
| debd4dceb1 | |||
| db15df57c3 | |||
| bcfe2095d5 | |||
| 69ac6eca63 | |||
| 717e3ce15e | |||
| 007ae04bf3 | |||
| c0947c5cf1 | |||
| c1d2935ad6 | |||
| 9fde0b4b2a | |||
| da7b075cc4 | |||
| 347f258b8f | |||
| 2421f867ed | |||
| 921915e92f | |||
| a4b0866293 | |||
| e10d86da4b | |||
| f160f23e9c | |||
| ca272542de | |||
| 7df9b65389 | |||
| e4af76c9c1 | |||
| f5944bf090 |
@@ -9,4 +9,3 @@ INVENTAR_WORKERS=4
|
|||||||
INVENTAR_THREADS=2
|
INVENTAR_THREADS=2
|
||||||
INVENTAR_WORKER_TIMEOUT=30
|
INVENTAR_WORKER_TIMEOUT=30
|
||||||
INVENTAR_WORKER_CONNECTIONS=100
|
INVENTAR_WORKER_CONNECTIONS=100
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
services:
|
services:
|
||||||
app:
|
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", "1000", "--max-requests-jitter", "100", "--log-level", "info", "--access-logfile", "-", "--error-logfile", "-"]
|
||||||
image: ghcr.io/aiirondev/legendary-octo-garbanzo:v0.7.42
|
image: ghcr.io/aiirondev/legendary-octo-garbanzo:v0.7.42
|
||||||
build: null
|
build: null
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
+17
-3
@@ -1736,7 +1736,6 @@ def is_valid_isbn13(isbn13):
|
|||||||
check_digit = (10 - (checksum % 10)) % 10
|
check_digit = (10 - (checksum % 10)) % 10
|
||||||
return check_digit == int(isbn13[12])
|
return check_digit == int(isbn13[12])
|
||||||
|
|
||||||
|
|
||||||
def normalize_and_validate_isbn(isbn_raw):
|
def normalize_and_validate_isbn(isbn_raw):
|
||||||
"""Normalize ISBN and return a valid canonical ISBN-13/10 or empty string."""
|
"""Normalize ISBN and return a valid canonical ISBN-13/10 or empty string."""
|
||||||
isbn = normalize_isbn(isbn_raw)
|
isbn = normalize_isbn(isbn_raw)
|
||||||
@@ -1996,7 +1995,7 @@ def _upload_student_cards_excel():
|
|||||||
synonyms = {
|
synonyms = {
|
||||||
'ausweis_id': ['ausweis_id', 'ausweisid', 'ausweis-id', 'karte', 'kartennummer', 'card_id', 'id'],
|
'ausweis_id': ['ausweis_id', 'ausweisid', 'ausweis-id', 'karte', 'kartennummer', 'card_id', 'id'],
|
||||||
'student_name': ['student_name', 'schuelername', 'schülername', 'schueler', 'schüler', 'name', 'vollname', 'vorname_nachname', 'nachname_vorname'],
|
'student_name': ['student_name', 'schuelername', 'schülername', 'schueler', 'schüler', 'name', 'vollname', 'vorname_nachname', 'nachname_vorname'],
|
||||||
'first_name': ['vorname', 'first_name', 'firstname'],
|
'first_name': ['vorname', 'first_name', 'firstname', 'rufname'],
|
||||||
'last_name': ['nachname', 'last_name', 'lastname'],
|
'last_name': ['nachname', 'last_name', 'lastname'],
|
||||||
'class_name': ['klasse', 'class', 'class_name', 'jahrgang', 'jahrgangsstufe', 'stufe', 'gruppe', 'asv_klasse'],
|
'class_name': ['klasse', 'class', 'class_name', 'jahrgang', 'jahrgangsstufe', 'stufe', 'gruppe', 'asv_klasse'],
|
||||||
'notes': ['notizen', 'notes', 'bemerkungen', 'bemerkung', 'hinweis', 'hinweise'],
|
'notes': ['notizen', 'notes', 'bemerkungen', 'bemerkung', 'hinweis', 'hinweise'],
|
||||||
@@ -4154,6 +4153,10 @@ def student_card_barcode_download():
|
|||||||
c.setFillColor(text_dark)
|
c.setFillColor(text_dark)
|
||||||
c.setFont("Helvetica-Bold", 9)
|
c.setFont("Helvetica-Bold", 9)
|
||||||
c.drawString(x_pos + 3*mm, y_pos - 28*mm, card['Klasse'])
|
c.drawString(x_pos + 3*mm, y_pos - 28*mm, card['Klasse'])
|
||||||
|
c.drawString(x_pos + 3*mm, y_pos - 31*mm, "-")
|
||||||
|
c.drawString(x_pos + 3*mm, y_pos - 34*mm, "-")
|
||||||
|
c.drawString(x_pos + 3*mm, y_pos - 37*mm, "-")
|
||||||
|
c.drawString(x_pos + 3*mm, y_pos - 40*mm, "-")
|
||||||
|
|
||||||
# Right barcode section with border highlight
|
# Right barcode section with border highlight
|
||||||
barcode_x_start = x_pos + info_width + 1*mm
|
barcode_x_start = x_pos + info_width + 1*mm
|
||||||
@@ -5112,6 +5115,7 @@ def upload_item():
|
|||||||
upload_mode = sanitize_form_value(request.form.get('upload_mode', 'item'))
|
upload_mode = sanitize_form_value(request.form.get('upload_mode', 'item'))
|
||||||
individual_codes_raw = sanitize_form_value(request.form.get('individual_codes', ''))
|
individual_codes_raw = sanitize_form_value(request.form.get('individual_codes', ''))
|
||||||
item_count_raw = sanitize_form_value(request.form.get('item_count', '1'))
|
item_count_raw = sanitize_form_value(request.form.get('item_count', '1'))
|
||||||
|
item_type_input = sanitize_form_value(request.form.get('item_type_input', ''))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
item_count = int(item_count_raw) if item_count_raw else 1
|
item_count = int(item_count_raw) if item_count_raw else 1
|
||||||
@@ -5191,6 +5195,8 @@ def upload_item():
|
|||||||
item_isbn = isbn_raw
|
item_isbn = isbn_raw
|
||||||
if upload_mode == 'library':
|
if upload_mode == 'library':
|
||||||
item_type = 'book'
|
item_type = 'book'
|
||||||
|
if item_type_input != "":
|
||||||
|
item_type = item_type_input
|
||||||
|
|
||||||
if upload_mode == 'library':
|
if upload_mode == 'library':
|
||||||
if not cfg.MODULES.is_enabled('library'):
|
if not cfg.MODULES.is_enabled('library'):
|
||||||
@@ -10265,10 +10271,18 @@ def notifications_unread_status():
|
|||||||
@app.route('/admin/damaged_items')
|
@app.route('/admin/damaged_items')
|
||||||
def admin_damaged_items():
|
def admin_damaged_items():
|
||||||
"""Dedicated admin management window for damaged items."""
|
"""Dedicated admin management window for damaged items."""
|
||||||
if 'username' not in session or not us.check_admin(session['username']):
|
if 'username' not in session:
|
||||||
flash('Administratorrechte erforderlich.', 'error')
|
flash('Administratorrechte erforderlich.', 'error')
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
'''
|
||||||
|
permissions = _get_current_user_permissions()
|
||||||
|
if not _action_access_allowed(permissions, 'can_manage_settings'):
|
||||||
|
flash('Sie haben keine Berechtigung, die Schulstammdaten zu ändern.', 'error')
|
||||||
|
if cfg.MODULES.is_enabled('library'):
|
||||||
|
return redirect(url_for('library_admin'))
|
||||||
|
'''
|
||||||
|
|
||||||
client = None
|
client = None
|
||||||
try:
|
try:
|
||||||
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
|
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
|
||||||
|
|||||||
@@ -284,7 +284,6 @@ def _match_inventory(path):
|
|||||||
if path == '/' or path.startswith('/home'): return True
|
if path == '/' or path.startswith('/home'): return True
|
||||||
return path.startswith(('/scanner', '/inventory', '/upload_admin', '/manage_filters', '/manage_locations', '/admin_borrowings', '/admin_damaged_items', '/admin/borrowings', '/admin/damaged_items'))
|
return path.startswith(('/scanner', '/inventory', '/upload_admin', '/manage_filters', '/manage_locations', '/admin_borrowings', '/admin_damaged_items', '/admin/borrowings', '/admin/damaged_items'))
|
||||||
|
|
||||||
|
|
||||||
def _match_terminplan(path):
|
def _match_terminplan(path):
|
||||||
if not path:
|
if not path:
|
||||||
return False
|
return False
|
||||||
@@ -292,7 +291,7 @@ def _match_terminplan(path):
|
|||||||
|
|
||||||
def _match_library(path):
|
def _match_library(path):
|
||||||
if not path: return False
|
if not path: return False
|
||||||
return path.startswith(('/library', '/library_', '/student_cards'))
|
return path.startswith(('/library', '/library_', '/student_cards', '/admin_damaged_items', '/admin_borrowings', '/admin/library'))
|
||||||
|
|
||||||
def _match_student_cards(path):
|
def _match_student_cards(path):
|
||||||
if not path: return False
|
if not path: return False
|
||||||
|
|||||||
@@ -432,7 +432,7 @@ def check_password_strength(password):
|
|||||||
|
|
||||||
def hashing(password):
|
def hashing(password):
|
||||||
"""
|
"""
|
||||||
Hash a password using SHA-512.
|
Hash a password using scrypt.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
password (str): Password to hash
|
password (str): Password to hash
|
||||||
@@ -440,7 +440,8 @@ def hashing(password):
|
|||||||
Returns:
|
Returns:
|
||||||
str: Hexadecimal digest of the hashed password
|
str: Hexadecimal digest of the hashed password
|
||||||
"""
|
"""
|
||||||
return hashlib.sha512(password.encode()).hexdigest()
|
hashed = hashlib.scrypt(password.encode(), salt=b'some_salt', n=16384, r=8, p=1)
|
||||||
|
return hashed.hex()
|
||||||
|
|
||||||
|
|
||||||
def check_nm_pwd(username, password):
|
def check_nm_pwd(username, password):
|
||||||
|
|||||||
@@ -788,7 +788,19 @@
|
|||||||
{% if show_library_features %}
|
{% if show_library_features %}
|
||||||
<!-- Library Mode: Single Customizable Filter -->
|
<!-- Library Mode: Single Customizable Filter -->
|
||||||
<div class="filter-inputs">
|
<div class="filter-inputs">
|
||||||
<h3>Kategorie/Typ (customisierbar):</h3>
|
<h3>Medientyp</h3>
|
||||||
|
<div class="form-group">
|
||||||
|
<select name="item_type_input" id="item_type_input">
|
||||||
|
<option value="" disabled selected>Medientyp auswählen...</option>
|
||||||
|
<option value="Buch">Buch</option>
|
||||||
|
<option value="Schulbuch">Schulbuch</option>
|
||||||
|
<option value="CD">CD</option>
|
||||||
|
<option value="DVD">DVD</option>
|
||||||
|
<option value="Sonstiges">Sonstiges</option>
|
||||||
|
</select>
|
||||||
|
<small style="display:block; color:#666;">Wählen Sie einen Medientyp aus zur Klassifizierung.</small>
|
||||||
|
</div>
|
||||||
|
<h3>Kategorie/Typ:</h3>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input type="text" name="library_category" id="library_category" placeholder="z.B. Belletristik, Sachbücher, Nachschlagewerke, etc.">
|
<input type="text" name="library_category" id="library_category" placeholder="z.B. Belletristik, Sachbücher, Nachschlagewerke, etc.">
|
||||||
<small style="display:block; color:#666;">Geben Sie hier eine beliebige Kategorie ein zur freien Klassifizierung.</small>
|
<small style="display:block; color:#666;">Geben Sie hier eine beliebige Kategorie ein zur freien Klassifizierung.</small>
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ services:
|
|||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 10
|
retries: 10
|
||||||
|
start_period: 30s
|
||||||
environment:
|
environment:
|
||||||
MONGO_INITDB_DATABASE: inventar_default
|
MONGO_INITDB_DATABASE: inventar_default
|
||||||
|
|
||||||
|
|||||||
+99
-131
@@ -23,70 +23,19 @@ ensure_runtime_config_json() {
|
|||||||
echo "Warning: moved unexpected directory $config_path to $backup_path"
|
echo "Warning: moved unexpected directory $config_path to $backup_path"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
FORCE_REMOVE=false
|
if [ ! -f "$config_path" ]; then
|
||||||
if [ "${2:-}" = "--yes" ] || [ "${2:-}" = "-y" ]; then
|
|
||||||
FORCE_REMOVE=true
|
|
||||||
TENANT_ID="${3:-}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$TENANT_ID" ]; then
|
|
||||||
cat > "$config_path" <<'EOF'
|
cat > "$config_path" <<'EOF'
|
||||||
{
|
{
|
||||||
"ver": "2.6.5",
|
"ver": "2.6.5",
|
||||||
|
"tenants": {}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
echo "Created default config.json"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
if [ "$FORCE_REMOVE" != true ]; then
|
get_tenant_aliases() {
|
||||||
echo -n "WARNING: Are you sure you want to permanently delete all data for tenant '$TENANT_ID'? (y/N) "
|
local tenant_id="$1"
|
||||||
read confirm
|
|
||||||
if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then
|
|
||||||
echo "Removal canceled."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Removing tenant '$TENANT_ID'..."
|
|
||||||
APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1)
|
|
||||||
port_to_remove=""
|
|
||||||
if [ -n "$APP_CONTAINER" ]; then
|
|
||||||
port_to_remove="$({ docker exec "$APP_CONTAINER" python3 - "$TENANT_ID" <<'PY'
|
|
||||||
import sys
|
|
||||||
sys.path.insert(0, '/app')
|
|
||||||
sys.path.insert(0, '/app/Web')
|
|
||||||
from tenant import delete_tenant, get_tenant_config
|
|
||||||
|
|
||||||
tenant_id = sys.argv[1]
|
|
||||||
tenant_cfg = get_tenant_config(tenant_id)
|
|
||||||
port = tenant_cfg.get('port')
|
|
||||||
|
|
||||||
if not delete_tenant(tenant_id):
|
|
||||||
print(f'Error: failed to delete tenant {tenant_id}', file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if port is not None:
|
|
||||||
print(port)
|
|
||||||
PY
|
|
||||||
} 2>/dev/null)"
|
|
||||||
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 "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"
|
|
||||||
fi
|
|
||||||
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."
|
|
||||||
else
|
|
||||||
echo "Removed tenant '$TENANT_ID'. No port mapping was present."
|
|
||||||
fi
|
|
||||||
local normalized alias
|
local normalized alias
|
||||||
normalized="$(printf '%s' "$tenant_id" | tr '[:upper:]' '[:lower:]')"
|
normalized="$(printf '%s' "$tenant_id" | tr '[:upper:]' '[:lower:]')"
|
||||||
printf '%s\n' "$tenant_id"
|
printf '%s\n' "$tenant_id"
|
||||||
@@ -212,7 +161,7 @@ existing['modules'] = {
|
|||||||
'inventory': {'enabled': True},
|
'inventory': {'enabled': True},
|
||||||
'library': {'enabled': True},
|
'library': {'enabled': True},
|
||||||
'student_cards': {'enabled': True, 'default_borrow_days': 14, 'max_borrow_days': 365},
|
'student_cards': {'enabled': True, 'default_borrow_days': 14, 'max_borrow_days': 365},
|
||||||
'terminplan': {'enabled': True},
|
'terminplan': {'enabled': True},
|
||||||
'mail': {'enabled': True},
|
'mail': {'enabled': True},
|
||||||
}
|
}
|
||||||
existing['trial'] = trial_config
|
existing['trial'] = trial_config
|
||||||
@@ -258,7 +207,7 @@ sanitized = "".join(c for c in tenant_id if c.isalnum() or c == "_")
|
|||||||
db_name = f"inventar_{sanitized}"
|
db_name = f"inventar_{sanitized}"
|
||||||
client = MongoClient(settings.MONGODB_HOST, int(settings.MONGODB_PORT))
|
client = MongoClient(settings.MONGODB_HOST, int(settings.MONGODB_PORT))
|
||||||
db = client[db_name]
|
db = client[db_name]
|
||||||
hashed_pw = hashlib.sha512("admin123".encode()).hexdigest()
|
hashed_pw = hashlib.scrypt("admin123", salt=b"some_salt", n=16384, r=8, p=1).hex()
|
||||||
|
|
||||||
action_permissions = {
|
action_permissions = {
|
||||||
"can_borrow": True,
|
"can_borrow": True,
|
||||||
@@ -435,7 +384,6 @@ restart_app_container() {
|
|||||||
|
|
||||||
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 [ -n "${HOST_WORKDIR:-}" ]; then
|
if [ -n "${HOST_WORKDIR:-}" ]; then
|
||||||
workdir="$HOST_WORKDIR"
|
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")" )
|
||||||
@@ -446,7 +394,6 @@ restart_app_container() {
|
|||||||
compose_args+=( --env-file "$(readlink -f "$HOST_WORKDIR/.docker-build.env")" )
|
compose_args+=( --env-file "$(readlink -f "$HOST_WORKDIR/.docker-build.env")" )
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
# Normal case: called directly from host
|
|
||||||
compose_args+=( -f "$workdir/docker-compose-multitenant.yml" )
|
compose_args+=( -f "$workdir/docker-compose-multitenant.yml" )
|
||||||
if [ -f "$workdir/.docker-compose.runtime.override.yml" ]; then
|
if [ -f "$workdir/.docker-compose.runtime.override.yml" ]; then
|
||||||
compose_args+=( -f "$workdir/.docker-compose.runtime.override.yml" )
|
compose_args+=( -f "$workdir/.docker-compose.runtime.override.yml" )
|
||||||
@@ -456,7 +403,6 @@ restart_app_container() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 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
|
||||||
@@ -566,24 +512,83 @@ remove_runtime_port() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
show_help() {
|
||||||
|
local HEADER='\033[95m'
|
||||||
|
local BLUE='\033[94m'
|
||||||
|
local GREEN='\033[92m'
|
||||||
|
local YELLOW='\033[93m'
|
||||||
|
local RED='\033[91m'
|
||||||
|
local BOLD='\033[1m'
|
||||||
|
local RESET='\033[0m'
|
||||||
|
|
||||||
|
cat << EOF
|
||||||
|
|
||||||
|
${HEADER}${BOLD}=== MULTI-TENANT INVENTAR MANAGER ===${RESET}
|
||||||
|
${YELLOW}Nutzung:${RESET} $0 <befehl> [tenant_id] [optionen]
|
||||||
|
|
||||||
|
${BLUE}${BOLD}VERFÜGBARE BEFEHLE:${RESET}
|
||||||
|
${GREEN}add${RESET} <tenant_id> [port]
|
||||||
|
Legt einen neuen Tenant an, registriert den Port und initialisiert
|
||||||
|
die MongoDB-Datenbank mit einem Standard-Admin (${YELLOW}admin / admin123${RESET}).
|
||||||
|
|
||||||
|
${GREEN}trial${RESET} <tenant_id> [port] [tage]
|
||||||
|
Erstellt einen temporären Test-Tenant. Standardlaufzeit: 7 Tage.
|
||||||
|
Läuft automatisch ab und löscht sich selbst.
|
||||||
|
|
||||||
|
${GREEN}remove${RESET} [-y|--yes] <tenant_id>
|
||||||
|
Löscht einen Tenant, seine Konfiguration, Ports und die Datenbank.
|
||||||
|
Nutze ${RED}-y${RESET}, um die Bestätigungsabfrage zu überspringen.
|
||||||
|
|
||||||
|
${GREEN}restart-tenant${RESET} <tenant_id>
|
||||||
|
Startet einen spezifischen Tenant neu, indem alle aktiven Sessions
|
||||||
|
und der Cache in der MongoDB geleert werden (Sitzungs-Reset).
|
||||||
|
|
||||||
|
${GREEN}restart-all${RESET}
|
||||||
|
Führt einen Zero-Downtime Rolling-Restart für alle App-Container durch.
|
||||||
|
|
||||||
|
${GREEN}list${RESET}
|
||||||
|
Listet alle registrierten Tenants aus der 'config.json' sowie alle
|
||||||
|
aktiven Tenant-Datenbanken aus der MongoDB auf.
|
||||||
|
|
||||||
|
${GREEN}module${RESET} <tenant_id> <modulname>=<on|off> ...
|
||||||
|
Aktiviert oder deaktiviert bestimmte Features/Module für einen Tenant.
|
||||||
|
Es können mehrere Module gleichzeitig übergeben werden.
|
||||||
|
|
||||||
|
${BLUE}${BOLD}GLOBALE OPTIONEN:${RESET}
|
||||||
|
${GREEN}-h, --help${RESET}
|
||||||
|
Zeigt diese Hilfe an.
|
||||||
|
|
||||||
|
${YELLOW}Beispiele:${RESET}
|
||||||
|
$0 add schule_muenchen 8081
|
||||||
|
$0 trial test_user 8082 14
|
||||||
|
$0 module schule_muenchen barre_code=on leih_historie=off
|
||||||
|
$0 remove -y test_user
|
||||||
|
|
||||||
|
-----------------------------------------------------------------
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
if [ -z "${1:-}" ]; then
|
if [ -z "${1:-}" ]; then
|
||||||
show_help
|
show_help
|
||||||
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
COMMAND="$1"
|
COMMAND="$1"
|
||||||
TENANT_ID="${2:-}"
|
|
||||||
|
# === MAIN ROUTING BLOCK ===
|
||||||
|
|
||||||
case "$COMMAND" in
|
case "$COMMAND" in
|
||||||
-h|--help)
|
-h|--help)
|
||||||
show_help
|
show_help
|
||||||
;;
|
;;
|
||||||
add)
|
add)
|
||||||
|
TENANT_ID="${2:-}"
|
||||||
if [ -z "$TENANT_ID" ]; then
|
if [ -z "$TENANT_ID" ]; then
|
||||||
echo "Error: Please provide a tenant_id."
|
echo "Error: Please provide a tenant_id."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
PORT_ARG="$3"
|
PORT_ARG="${3:-}"
|
||||||
if [ -n "$PORT_ARG" ]; then
|
if [ -n "$PORT_ARG" ]; then
|
||||||
if ! printf '%s\n' "$PORT_ARG" | grep -qE '^[0-9]+$'; then
|
if ! printf '%s\n' "$PORT_ARG" | grep -qE '^[0-9]+$'; then
|
||||||
echo "Error: Port must be a numeric value."
|
echo "Error: Port must be a numeric value."
|
||||||
@@ -598,68 +603,13 @@ case "$COMMAND" in
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Adding new tenant '$TENANT_ID'..."
|
echo "Adding new tenant '$TENANT_ID'..."
|
||||||
# Initialize tenant database via Python inside container
|
|
||||||
echo "Initializing database for $TENANT_ID..."
|
echo "Initializing database for $TENANT_ID..."
|
||||||
APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1)
|
initialize_tenant_database "$TENANT_ID" "standard"
|
||||||
if [ -n "$APP_CONTAINER" ]; then
|
echo "Tenant '$TENANT_ID' successfully added. Ready to use."
|
||||||
docker exec $APP_CONTAINER python3 -c "
|
|
||||||
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}'
|
|
||||||
client = MongoClient(settings.MONGODB_HOST, int(settings.MONGODB_PORT))
|
|
||||||
db = client[db_name]
|
|
||||||
hashed_pw = hashlib.sha512('admin123'.encode()).hexdigest()
|
|
||||||
if db.users.count_documents({'Username': 'admin'}) == 0:
|
|
||||||
db.users.insert_one({
|
|
||||||
'Username': 'admin',
|
|
||||||
'Password': hashed_pw,
|
|
||||||
'Admin': True,
|
|
||||||
'active_ausleihung': None,
|
|
||||||
'name': 'Admin',
|
|
||||||
'last_name': 'User',
|
|
||||||
'IsStudent': False,
|
|
||||||
'PermissionPreset': 'full_access',
|
|
||||||
'ActionPermissions': {
|
|
||||||
'can_borrow': True,
|
|
||||||
'can_insert': True,
|
|
||||||
'can_edit': True,
|
|
||||||
'can_delete': True,
|
|
||||||
'can_manage_users': True,
|
|
||||||
'can_manage_settings': True,
|
|
||||||
'can_view_logs': True,
|
|
||||||
},
|
|
||||||
'PagePermissions': {
|
|
||||||
'home': True,
|
|
||||||
'tutorial_page': True,
|
|
||||||
'my_borrowed_items': True,
|
|
||||||
'notifications_view': True,
|
|
||||||
'impressum': True,
|
|
||||||
'license': True,
|
|
||||||
'library_view': True,
|
|
||||||
'terminplan': True,
|
|
||||||
'home_admin': True,
|
|
||||||
'upload_admin': True,
|
|
||||||
'library_admin': True,
|
|
||||||
'admin_borrowings': True,
|
|
||||||
'library_loans_admin': True,
|
|
||||||
'admin_damaged_items': True,
|
|
||||||
'admin_audit_dashboard': True,
|
|
||||||
'logs': True,
|
|
||||||
'manage_filters': True,
|
|
||||||
'manage_locations': True,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
print(f'Tenant {sys.argv[1]} database initialized. Default admin: admin / admin123')
|
|
||||||
" "$TENANT_ID"
|
|
||||||
echo "Tenant '$TENANT_ID' successfully added. Ready to use."
|
|
||||||
else
|
|
||||||
echo "Warning: Application container is not running. Please start the multi-tenant system first."
|
|
||||||
echo "Data will be initialized upon first access by the tenant."
|
|
||||||
fi
|
|
||||||
;;
|
;;
|
||||||
|
|
||||||
trial)
|
trial)
|
||||||
|
TENANT_ID="${2:-}"
|
||||||
if [ -z "$TENANT_ID" ]; then
|
if [ -z "$TENANT_ID" ]; then
|
||||||
echo "Error: Please provide a tenant_id."
|
echo "Error: Please provide a tenant_id."
|
||||||
exit 1
|
exit 1
|
||||||
@@ -691,9 +641,13 @@ print(f'Tenant {sys.argv[1]} database initialized. Default admin: admin / admin1
|
|||||||
|
|
||||||
remove)
|
remove)
|
||||||
FORCE_REMOVE=false
|
FORCE_REMOVE=false
|
||||||
if [ "${2:-}" = "--yes" ] || [ "${2:-}" = "-y" ]; then
|
TENANT_ARG="${2:-}"
|
||||||
|
|
||||||
|
if [ "$TENANT_ARG" = "--yes" ] || [ "$TENANT_ARG" = "-y" ]; then
|
||||||
FORCE_REMOVE=true
|
FORCE_REMOVE=true
|
||||||
TENANT_ID="${3:-}"
|
TENANT_ID="${3:-}"
|
||||||
|
else
|
||||||
|
TENANT_ID="$TENANT_ARG"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -z "$TENANT_ID" ]; then
|
if [ -z "$TENANT_ID" ]; then
|
||||||
@@ -713,17 +667,32 @@ print(f'Tenant {sys.argv[1]} database initialized. Default admin: admin / admin1
|
|||||||
echo "Removing tenant '$TENANT_ID'..."
|
echo "Removing tenant '$TENANT_ID'..."
|
||||||
APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1)
|
APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1)
|
||||||
port_to_remove=""
|
port_to_remove=""
|
||||||
|
|
||||||
if [ -n "$APP_CONTAINER" ]; then
|
if [ -n "$APP_CONTAINER" ]; then
|
||||||
|
# Zuerst die Datenbank via PyMongo hart droppen (Erzwungenes Löschen)
|
||||||
port_to_remove="$(docker exec "$APP_CONTAINER" python3 - "$TENANT_ID" <<'PY'
|
port_to_remove="$(docker exec "$APP_CONTAINER" python3 - "$TENANT_ID" <<'PY'
|
||||||
import sys
|
import sys, re
|
||||||
sys.path.insert(0, '/app')
|
sys.path.insert(0, '/app')
|
||||||
sys.path.insert(0, '/app/Web')
|
sys.path.insert(0, '/app/Web')
|
||||||
from tenant import delete_tenant, get_tenant_config
|
from tenant import delete_tenant, get_tenant_config
|
||||||
|
from Web.modules.database import settings
|
||||||
|
from pymongo import MongoClient
|
||||||
|
|
||||||
tenant_id = sys.argv[1]
|
tenant_id = sys.argv[1]
|
||||||
tenant_cfg = get_tenant_config(tenant_id)
|
tenant_cfg = get_tenant_config(tenant_id)
|
||||||
port = tenant_cfg.get('port')
|
port = tenant_cfg.get('port')
|
||||||
|
|
||||||
|
# Datenbanknamen exakt rekonstruieren
|
||||||
|
sanitized = "".join(c for c in tenant_id if c.isalnum() or c == "_")
|
||||||
|
db_name = f"inventar_{sanitized}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
client = MongoClient(settings.MONGODB_HOST, int(settings.MONGODB_PORT))
|
||||||
|
client.drop_database(db_name)
|
||||||
|
print(f"MongoDB database '{db_name}' dropped successfully.", file=sys.stderr)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: Could not drop database '{db_name}': {e}", file=sys.stderr)
|
||||||
|
|
||||||
if not delete_tenant(tenant_id):
|
if not delete_tenant(tenant_id):
|
||||||
print(f'Error: failed to delete tenant {tenant_id}', file=sys.stderr)
|
print(f'Error: failed to delete tenant {tenant_id}', file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@@ -757,20 +726,19 @@ PY
|
|||||||
;;
|
;;
|
||||||
|
|
||||||
restart-tenant)
|
restart-tenant)
|
||||||
|
TENANT_ID="${2:-}"
|
||||||
if [ -z "$TENANT_ID" ]; then
|
if [ -z "$TENANT_ID" ]; then
|
||||||
echo "Error: Please provide a tenant_id."
|
echo "Error: Please provide a tenant_id."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "Restarting tenant '$TENANT_ID' (clearing session/cache)..."
|
echo "Restarting tenant '$TENANT_ID' (clearing session/cache)..."
|
||||||
# To restart a single tenant without restarting the global python processes,
|
|
||||||
# we can invalidate their cache or drop their sessions collection to sign everyone out
|
|
||||||
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'); sys.path.insert(0, '/app/Web'); from Web.modules.database 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()
|
||||||
print(f'Tenant {sys.argv[1]} session cache cleared. Tenant restarted.')
|
print(f'Tenant {sys.argv[1]} session cache cleared. Tenant restarted.')
|
||||||
" "$TENANT_ID"
|
" "$TENANT_ID"
|
||||||
echo "Tenant '$TENANT_ID' has been refreshed without impacting others."
|
echo "Tenant '$TENANT_ID' has been refreshed without impacting others."
|
||||||
@@ -833,12 +801,12 @@ PY
|
|||||||
;;
|
;;
|
||||||
|
|
||||||
module)
|
module)
|
||||||
|
TENANT_ID="${2:-}"
|
||||||
if [ -z "$TENANT_ID" ] || [ -z "${3:-}" ]; then
|
if [ -z "$TENANT_ID" ] || [ -z "${3:-}" ]; then
|
||||||
echo "Error: Usage: manage-tenant.sh module <tenant_id> <module_name>=<on|off> [...]"
|
echo "Error: Usage: manage-tenant.sh module <tenant_id> <module_name>=<on|off> [...]"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Pass all remaining arguments to python script
|
|
||||||
shift 2
|
shift 2
|
||||||
|
|
||||||
if python3 - "$CONFIG_FILE" "$TENANT_ID" "$@" <<'PY'
|
if python3 - "$CONFIG_FILE" "$TENANT_ID" "$@" <<'PY'
|
||||||
|
|||||||
Reference in New Issue
Block a user