Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9fde0b4b2a | |||
| da7b075cc4 | |||
| 347f258b8f | |||
| 2421f867ed | |||
| 921915e92f | |||
| a4b0866293 | |||
| e10d86da4b | |||
| f160f23e9c | |||
| ca272542de | |||
| 7df9b65389 | |||
| e4af76c9c1 | |||
| f5944bf090 | |||
| 4e68da2368 | |||
| 0d6aca4e8c | |||
| 2bb0cbe599 | |||
| 339487b4d4 | |||
| 3f90e2d4f1 |
+1
-2
@@ -8,5 +8,4 @@ INVENTAR_APP_MEM_SWAP_LIMIT=768m
|
||||
INVENTAR_WORKERS=4
|
||||
INVENTAR_THREADS=2
|
||||
INVENTAR_WORKER_TIMEOUT=30
|
||||
INVENTAR_WORKER_CONNECTIONS=100
|
||||
|
||||
INVENTAR_WORKER_CONNECTIONS=100
|
||||
@@ -1,5 +1,7 @@
|
||||
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", "1000", "--max-requests-jitter", "100", "--log-level", "info", "--access-logfile", "-", "--error-logfile", "-"]
|
||||
image: ghcr.io/aiirondev/legendary-octo-garbanzo:v0.7.42
|
||||
build: null
|
||||
ports:
|
||||
|
||||
+109
-69
@@ -1736,7 +1736,6 @@ def is_valid_isbn13(isbn13):
|
||||
check_digit = (10 - (checksum % 10)) % 10
|
||||
return check_digit == int(isbn13[12])
|
||||
|
||||
|
||||
def normalize_and_validate_isbn(isbn_raw):
|
||||
"""Normalize ISBN and return a valid canonical ISBN-13/10 or empty string."""
|
||||
isbn = normalize_isbn(isbn_raw)
|
||||
@@ -1996,7 +1995,7 @@ def _upload_student_cards_excel():
|
||||
synonyms = {
|
||||
'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'],
|
||||
'first_name': ['vorname', 'first_name', 'firstname'],
|
||||
'first_name': ['vorname', 'first_name', 'firstname', 'rufname'],
|
||||
'last_name': ['nachname', 'last_name', 'lastname'],
|
||||
'class_name': ['klasse', 'class', 'class_name', 'jahrgang', 'jahrgangsstufe', 'stufe', 'gruppe', 'asv_klasse'],
|
||||
'notes': ['notizen', 'notes', 'bemerkungen', 'bemerkung', 'hinweis', 'hinweise'],
|
||||
@@ -4124,10 +4123,13 @@ def student_card_barcode_download():
|
||||
c.setLineWidth(2)
|
||||
c.line(x_pos, y_pos - 10*mm, x_pos + card_width, y_pos - 10*mm)
|
||||
|
||||
|
||||
school_name = cfg.get_school_info()
|
||||
school_name = school_name["name"]
|
||||
# "SCHÜLERAUSWEIS" text in header
|
||||
c.setFont("Helvetica-Bold", 9)
|
||||
c.setFillColor(white)
|
||||
c.drawString(x_pos + 3*mm, y_pos - 6.5*mm, "SCHÜLERAUSWEIS")
|
||||
c.drawString(x_pos + 3*mm, y_pos - 6.5*mm, f"SCHÜLERAUSWEIS - {str(school_name)}")
|
||||
|
||||
# Student name - large and bold
|
||||
c.setFillColor(text_dark)
|
||||
@@ -4151,7 +4153,11 @@ def student_card_barcode_download():
|
||||
c.setFillColor(text_dark)
|
||||
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 - 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
|
||||
barcode_x_start = x_pos + info_width + 1*mm
|
||||
c.setFillColor(accent_color)
|
||||
@@ -4291,10 +4297,12 @@ def student_card_single_barcode_download(card_id):
|
||||
c.setLineWidth(2.5)
|
||||
c.line(x_pos, y_pos - 10*mm, x_pos + card_width, y_pos - 10*mm)
|
||||
|
||||
school_name = cfg.get_school_info()
|
||||
school_name = school_name["name"]
|
||||
# "SCHÜLERAUSWEIS" text in header
|
||||
c.setFont("Helvetica-Bold", 11)
|
||||
c.setFillColor(white)
|
||||
c.drawString(x_pos + 4*mm, y_pos - 6.5*mm, "SCHÜLERAUSWEIS")
|
||||
c.drawString(x_pos + 4*mm, y_pos - 6.5*mm, f"SCHÜLERAUSWEIS - {str(school_name)}")
|
||||
|
||||
# Student name - large and prominent
|
||||
c.setFillColor(text_dark)
|
||||
@@ -5107,6 +5115,7 @@ def upload_item():
|
||||
upload_mode = sanitize_form_value(request.form.get('upload_mode', 'item'))
|
||||
individual_codes_raw = sanitize_form_value(request.form.get('individual_codes', ''))
|
||||
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:
|
||||
item_count = int(item_count_raw) if item_count_raw else 1
|
||||
@@ -5186,6 +5195,8 @@ def upload_item():
|
||||
item_isbn = isbn_raw
|
||||
if upload_mode == 'library':
|
||||
item_type = 'book'
|
||||
if item_type_input != "":
|
||||
item_type = item_type_input
|
||||
|
||||
if upload_mode == 'library':
|
||||
if not cfg.MODULES.is_enabled('library'):
|
||||
@@ -9542,8 +9553,8 @@ def _fetch_from_open_library(clean_isbn):
|
||||
|
||||
def _fetch_from_isbn_de(clean_isbn):
|
||||
"""
|
||||
Source 4: isbn.de (Web Scraping für deutsche Schulbücher)
|
||||
Robuste Version mit Fallbacks für Meta-Tags und Tabellendaten.
|
||||
Source 4: isbn.de (Maßgeschneidertes Scraping basierend auf realem HTML)
|
||||
Extrahiert alle Buchdaten inklusive Preise aus den Meta-Tags und der Sidebar.
|
||||
"""
|
||||
try:
|
||||
url = f"https://www.isbn.de/buch/{clean_isbn}"
|
||||
@@ -9552,94 +9563,115 @@ def _fetch_from_isbn_de(clean_isbn):
|
||||
}
|
||||
|
||||
response = requests.get(url, headers=headers, timeout=5)
|
||||
|
||||
if response.status_code != 200:
|
||||
return None
|
||||
|
||||
soup = BeautifulSoup(response.text, 'html.parser')
|
||||
|
||||
# 1. Titel prüfen
|
||||
title_elem = soup.find('h1')
|
||||
if not title_elem or "nicht gefunden" in title_elem.text.lower() or "Suche" in title_elem.text:
|
||||
return None
|
||||
title = title_elem.text.strip()
|
||||
# 1. Titel aus Meta-Tags extrahieren (extrem zuverlässig)
|
||||
title_meta = soup.find('meta', property='og:title')
|
||||
title = title_meta.get('content', '').strip() if title_meta else None
|
||||
if not title:
|
||||
h1_elem = soup.find('h1')
|
||||
title = h1_elem.text.strip() if h1_elem else "Unknown Title"
|
||||
|
||||
# --- Hilfsfunktion für unstrukturierte HTML-Tabellen ---
|
||||
def get_detail_by_keyword(keywords):
|
||||
"""Sucht nach Schlagwörtern (z.B. 'Verlag') und gibt den angrenzenden Wert zurück."""
|
||||
for tag in soup.find_all(['th', 'td', 'strong', 'b', 'span']):
|
||||
text = tag.text.strip().lower()
|
||||
if any(kw in text for kw in keywords):
|
||||
# Wenn es eine Tabellenzelle (th/td) ist, nimm das nächste Geschwister-Element
|
||||
if tag.name in ['th', 'td']:
|
||||
sibling = tag.find_next_sibling('td')
|
||||
if sibling:
|
||||
return sibling.text.strip()
|
||||
# Wenn es ein Label in einem Listen- oder Absatz-Element ist
|
||||
parent = tag.parent
|
||||
if parent and parent.name in ['li', 'p', 'div']:
|
||||
return parent.text.replace(tag.text, '').strip()
|
||||
# Falls Fehler- oder Suchseite
|
||||
if "nicht gefunden" in title.lower() or "suche" in title.lower():
|
||||
return None
|
||||
|
||||
# --- Hilfsfunktion für Schema.org (itemprop) ---
|
||||
def get_itemprop(prop_name):
|
||||
elem = soup.find(attrs={"itemprop": prop_name})
|
||||
if elem:
|
||||
return elem.get('content') or elem.text.strip()
|
||||
# --- Hilfsfunktion zum Parsen der .infotab-Sidebar-Struktur ---
|
||||
def get_sidebar_value(keyword):
|
||||
infotab = soup.find(class_='infotab')
|
||||
if infotab:
|
||||
for d in infotab.find_all('div'):
|
||||
if d.text.strip().lower() == keyword.lower():
|
||||
parent = d.parent
|
||||
# Label vom Gesamttext abziehen, um nur den Wert zu erhalten
|
||||
return parent.text.replace(d.text, '', 1).strip()
|
||||
return None
|
||||
|
||||
authors = get_itemprop("author")
|
||||
if not authors:
|
||||
authors = get_detail_by_keyword(['autor', 'herausgeber', 'von:'])
|
||||
authors = authors if authors else "Unknown Author"
|
||||
|
||||
publisher = get_itemprop("publisher")
|
||||
# 2. Verlag holen
|
||||
publisher = get_sidebar_value('verlag')
|
||||
if not publisher:
|
||||
publisher = get_detail_by_keyword(['verlag'])
|
||||
publisher = publisher if publisher else "Unknown Publisher"
|
||||
publisher = "Unknown Publisher"
|
||||
|
||||
pub_date = get_itemprop("datePublished")
|
||||
if not pub_date:
|
||||
pub_date = get_detail_by_keyword(['erscheinungsjahr', 'erschienen', 'datum'])
|
||||
pub_date = pub_date if pub_date else "Unknown Date"
|
||||
# 3. Erscheinungsdatum (Aus Meta-Tag oder Sidebar)
|
||||
date_meta = soup.find('meta', property='og:book:release_date')
|
||||
if date_meta and date_meta.get('content'):
|
||||
published_date = date_meta.get('content', '').strip()
|
||||
else:
|
||||
published_date = get_sidebar_value('erschienen am')
|
||||
|
||||
if not published_date:
|
||||
published_date = "Unknown Date"
|
||||
|
||||
page_count = get_itemprop("numberOfPages")
|
||||
if not page_count:
|
||||
page_count = get_detail_by_keyword(['seiten', 'umfang'])
|
||||
|
||||
# 4. Autor (Da Schulbuch-Workbooks oft keinen Einzelautor haben, kluger Fallback)
|
||||
author_meta = soup.find('meta', property='og:book:author')
|
||||
author = author_meta.get('content', '').strip() if author_meta else ""
|
||||
if not author:
|
||||
author = get_sidebar_value('autor') or get_sidebar_value('herausgeber')
|
||||
|
||||
if not author:
|
||||
author = f"{publisher} Redaktion" if publisher != "Unknown Publisher" else "Unknown Author"
|
||||
|
||||
# 5. Seitenanzahl
|
||||
page_count = get_sidebar_value('seiten') or get_sidebar_value('umfang')
|
||||
if page_count:
|
||||
match = re.search(r'\d+', page_count)
|
||||
page_count = match.group(0) if match else "Unknown"
|
||||
else:
|
||||
page_count = "Unknown"
|
||||
|
||||
description = get_itemprop("description")
|
||||
if not description:
|
||||
for class_name in ['description', 'zusammenfassung', 'klappentext', 'buch-beschreibung']:
|
||||
fallback_desc = soup.find('div', class_=re.compile(class_name, re.IGNORECASE))
|
||||
if fallback_desc:
|
||||
description = fallback_desc.text.strip()
|
||||
break
|
||||
description = description if description else "Keine Beschreibung verfügbar"
|
||||
# 6. Beschreibung aus ID 'bookdesc' holen und Whitespace-Fluss säubern
|
||||
description = "Keine Beschreibung verfügbar"
|
||||
desc_div = soup.find(id='bookdesc')
|
||||
if desc_div:
|
||||
# Holt den Text inklusive aller Listenpunkte (li) und formatiert ihn sauber
|
||||
description = " ".join(desc_div.text.split())
|
||||
|
||||
# 7. Cover-Bild (Nutzt hochauflösenden Link aus den Metas)
|
||||
thumbnail = ""
|
||||
img_elem = soup.find('img', itemprop="image")
|
||||
if not img_elem:
|
||||
img_elem = soup.find('img', class_=lambda c: c and 'cover' in str(c).lower())
|
||||
|
||||
if img_elem and 'src' in img_elem.attrs:
|
||||
thumbnail = img_elem['src']
|
||||
if thumbnail.startswith('/'):
|
||||
thumbnail = "https://www.isbn.de" + thumbnail
|
||||
img_meta = soup.find('meta', property='og:image')
|
||||
if img_meta:
|
||||
thumbnail = img_meta.get('content', '').strip()
|
||||
else:
|
||||
img_tag = soup.find('img', id='ISBNcover')
|
||||
if img_tag:
|
||||
thumbnail = img_tag.get('data-big') or img_tag.get('src')
|
||||
|
||||
if thumbnail and thumbnail.startswith('/'):
|
||||
thumbnail = "https://www.isbn.de" + thumbnail
|
||||
|
||||
# 8. PREIS EXTRAHIEREN (Neu integriert)
|
||||
price = None
|
||||
# Option A: Aus dem standardisierten Meta-Tag extrahieren (Ergibt z.B. 12.25)
|
||||
price_meta = soup.find('meta', property='product:price:amount')
|
||||
if price_meta and price_meta.get('content'):
|
||||
try:
|
||||
price = float(price_meta.get('content').strip())
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Option B: Fallback über die Infotab-Sidebar (Falls Meta fehlt, filtert "12,25 €*" zu Float)
|
||||
if price is None:
|
||||
price_str = get_sidebar_value('preis')
|
||||
if price_str:
|
||||
# Findet Zahlen wie "12,25" oder "12.25"
|
||||
match = re.search(r'\d+([.,]\d+)?', price_str)
|
||||
if match:
|
||||
try:
|
||||
price = float(match.group(0).replace(',', '.'))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return {
|
||||
"title": title,
|
||||
"authors": authors,
|
||||
"authors": author,
|
||||
"publisher": publisher,
|
||||
"publishedDate": pub_date,
|
||||
"publishedDate": published_date,
|
||||
"description": description,
|
||||
"pageCount": page_count,
|
||||
"price": None,
|
||||
"price": price, # Gibt nun z.B. 12.25 als float zurück
|
||||
"thumbnail": thumbnail,
|
||||
"source": "isbn-de"
|
||||
}
|
||||
@@ -10239,10 +10271,18 @@ def notifications_unread_status():
|
||||
@app.route('/admin/damaged_items')
|
||||
def admin_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')
|
||||
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
|
||||
try:
|
||||
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
|
||||
|
||||
@@ -284,7 +284,6 @@ def _match_inventory(path):
|
||||
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'))
|
||||
|
||||
|
||||
def _match_terminplan(path):
|
||||
if not path:
|
||||
return False
|
||||
@@ -292,7 +291,7 @@ def _match_terminplan(path):
|
||||
|
||||
def _match_library(path):
|
||||
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):
|
||||
if not path: return False
|
||||
|
||||
@@ -432,7 +432,7 @@ def check_password_strength(password):
|
||||
|
||||
def hashing(password):
|
||||
"""
|
||||
Hash a password using SHA-512.
|
||||
Hash a password using scrypt.
|
||||
|
||||
Args:
|
||||
password (str): Password to hash
|
||||
@@ -440,7 +440,8 @@ def hashing(password):
|
||||
Returns:
|
||||
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):
|
||||
|
||||
@@ -788,7 +788,19 @@
|
||||
{% if show_library_features %}
|
||||
<!-- Library Mode: Single Customizable Filter -->
|
||||
<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">
|
||||
<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>
|
||||
@@ -1563,6 +1575,7 @@
|
||||
// Get form fields
|
||||
const nameField = document.getElementById('name');
|
||||
const descriptionField = document.getElementById('beschreibung');
|
||||
const priceField = document.getElementById('anschaffungskosten'); // <-- NEU
|
||||
|
||||
if (!nameField || !descriptionField) {
|
||||
alert('Fehler: Formularfelder nicht gefunden.');
|
||||
@@ -1600,6 +1613,13 @@
|
||||
nameField.value = bookTitle;
|
||||
descriptionField.value = description;
|
||||
|
||||
// Preis in das Formularfeld eintragen
|
||||
if (priceField && currentBookData.price !== null && currentBookData.price !== undefined) {
|
||||
// Wandelt den Float (z.B. 12.25) in einen String mit Komma (12,25) um
|
||||
let formattedPrice = currentBookData.price.toString().replace('.', ',');
|
||||
priceField.value = formattedPrice;
|
||||
}
|
||||
|
||||
// Download and import book cover image if available
|
||||
if (currentBookData.thumbnail) {
|
||||
downloadBookCover(currentBookData.thumbnail);
|
||||
|
||||
+67
-20
@@ -258,7 +258,7 @@ 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()
|
||||
hashed_pw = hashlib.scrypt('admin123'.encode(), salt=b'some_salt', n=16384, r=8, p=1).hex()
|
||||
|
||||
action_permissions = {
|
||||
"can_borrow": True,
|
||||
@@ -524,6 +524,8 @@ remove_runtime_port() {
|
||||
local port_list=()
|
||||
local new_ports=()
|
||||
local first_port=""
|
||||
local ports_csv=""
|
||||
|
||||
|
||||
if [ ! -f "$env_file" ]; then
|
||||
return 0
|
||||
@@ -551,7 +553,8 @@ remove_runtime_port() {
|
||||
fi
|
||||
|
||||
first_port="${new_ports[0]}"
|
||||
local ports_csv="$(IFS=,; echo "${new_ports[*]}")"
|
||||
# Das 'local' hier entfernen, da wir es oben deklariert haben:
|
||||
ports_csv="$(IFS=,; echo "${new_ports[*]}")"
|
||||
|
||||
if grep -q '^INVENTAR_HTTP_PORTS=' "$env_file" 2>/dev/null; then
|
||||
sed -i "s|^INVENTAR_HTTP_PORTS=.*|INVENTAR_HTTP_PORTS=$ports_csv|" "$env_file"
|
||||
@@ -566,6 +569,55 @@ remove_runtime_port() {
|
||||
fi
|
||||
}
|
||||
|
||||
show_help() {
|
||||
local script_name
|
||||
script_name="$(basename "$0")"
|
||||
|
||||
echo "=== MULTI-TENANT INVENTAR MANAGER ==="
|
||||
echo ""
|
||||
echo "NUTZUNG:"
|
||||
echo " ./$script_name <befehl> [tenant_id] [optionen]"
|
||||
echo ""
|
||||
echo "VERFÜGBARE BEFEHLE:"
|
||||
echo " add <tenant_id> [port]"
|
||||
echo " Legt einen neuen Tenant an, registriert den Port und initialisiert"
|
||||
echo " die MongoDB-Datenbank mit einem Standard-Admin (admin / admin123)."
|
||||
echo ""
|
||||
echo " trial <tenant_id> [port] [tage]"
|
||||
echo " Erstellt einen temporären Test-Tenant (Standardlaufzeit: 7 Tage)."
|
||||
echo " Dieser läuft nach der festgelegten Zeit ab und wird automatisch gelöscht."
|
||||
echo ""
|
||||
echo " remove [-y|--yes] <tenant_id>"
|
||||
echo " Löscht einen Tenant komplett (Konfiguration, Ports und Datenbank)."
|
||||
echo " Nutze -y oder --yes, um die Bestätigungsabfrage zu überspringen."
|
||||
echo ""
|
||||
echo " restart-tenant <tenant_id>"
|
||||
echo " Startet einen spezifischen Tenant neu, indem alle aktiven Sessions"
|
||||
echo " und der Cache in der MongoDB geleert werden (Sitzungs-Reset)."
|
||||
echo ""
|
||||
echo " restart-all"
|
||||
echo " Führt einen Zero-Downtime Rolling-Restart für alle App-Container durch."
|
||||
echo ""
|
||||
echo " list"
|
||||
echo " Listet alle registrierten Tenants (aus der config.json) sowie alle"
|
||||
echo " aktiven Tenant-Datenbanken (aus der MongoDB) übersichtlich auf."
|
||||
echo ""
|
||||
echo " module <tenant_id> <modulname>=<on|off> [...]"
|
||||
echo " Aktiviert oder deaktiviert bestimmte Features/Module für einen Tenant."
|
||||
echo " Es können mehrere Module gleichzeitig konfiguriert werden."
|
||||
echo ""
|
||||
echo "GLOBALE OPTIONEN:"
|
||||
echo " -h, --help"
|
||||
echo " Zeigt dieses Hilfemenü an."
|
||||
echo ""
|
||||
echo "BEISPIELE:"
|
||||
echo " ./$script_name add schule_muenchen 8081"
|
||||
echo " ./$script_name trial test_user 8082 14"
|
||||
echo " ./$script_name module schule_muenchen inventory=on mail=off"
|
||||
echo " ./$script_name remove --yes test_user"
|
||||
echo ""
|
||||
}
|
||||
|
||||
if [ -z "${1:-}" ]; then
|
||||
show_help
|
||||
fi
|
||||
@@ -609,7 +661,7 @@ 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()
|
||||
hashed_pw = hashlib.scrypt('admin123'.encode(), salt=b'some_salt', n=16384, r=8, p=1).hex()
|
||||
if db.users.count_documents({'Username': 'admin'}) == 0:
|
||||
db.users.insert_one({
|
||||
'Username': 'admin',
|
||||
@@ -690,7 +742,7 @@ print(f'Tenant {sys.argv[1]} database initialized. Default admin: admin / admin1
|
||||
;;
|
||||
|
||||
remove)
|
||||
FORCE_REMOVE=false
|
||||
FORCE_REMOVE=true
|
||||
if [ "${2:-}" = "--yes" ] || [ "${2:-}" = "-y" ]; then
|
||||
FORCE_REMOVE=true
|
||||
TENANT_ID="${3:-}"
|
||||
@@ -713,33 +765,27 @@ print(f'Tenant {sys.argv[1]} database initialized. Default admin: admin / admin1
|
||||
echo "Removing tenant '$TENANT_ID'..."
|
||||
APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1)
|
||||
port_to_remove=""
|
||||
|
||||
if port_to_remove="$(remove_tenant_port "$TENANT_ID" 2>/dev/null)"; then
|
||||
:
|
||||
else
|
||||
echo "Warning: tenant '$TENANT_ID' was not found in config.json."
|
||||
fi
|
||||
|
||||
if [ -n "$APP_CONTAINER" ]; then
|
||||
port_to_remove="$(docker exec "$APP_CONTAINER" python3 - "$TENANT_ID" <<'PY'
|
||||
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
|
||||
from tenant import delete_tenant
|
||||
|
||||
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)
|
||||
print(f'Error: failed to delete tenant {tenant_id} database', file=sys.stderr)
|
||||
PY
|
||||
)"
|
||||
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
|
||||
@@ -749,6 +795,7 @@ PY
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user