Compare commits

...

22 Commits

Author SHA1 Message Date
Aiirondev_dev da7b075cc4 addition of thew help for tenant managment 2026-06-23 23:57:35 +02:00
Aiirondev_dev 347f258b8f adjustment of the encryption for user password 2026-06-23 22:41:53 +02:00
Aiirondev_dev 2421f867ed Smal changes for Module access 2026-06-23 14:40:27 +02:00
Aiirondev_dev 921915e92f new slight adjust of some namings in thze dropdown 2026-06-23 14:26:39 +02:00
Aiirondev_dev a4b0866293 permision changes for testing 2026-06-23 13:03:51 +02:00
Aiirondev_dev e10d86da4b manage te damaged Item ffield 2026-06-23 12:26:09 +02:00
Aiirondev_dev f160f23e9c sdmal changes 2026-06-23 12:01:23 +02:00
Aiirondev_dev ca272542de Merge remote-tracking branch 'refs/remotes/origin/main' 2026-06-22 00:21:26 +02:00
Aiirondev_dev 7df9b65389 Changes from sha512 to scrypt to fit with DS-GVO 2026-06-22 00:21:15 +02:00
Aiirondev_dev e4af76c9c1 smal change of the upload layout and text, changes of the Student Barcode download to alow further scoolyears 2026-06-20 13:54:42 +02:00
Aiirondev_dev f5944bf090 implementation of the Item Media Type 2026-06-20 12:01:21 +02:00
Aiirondev_dev 4e68da2368 Second Changes to include the Scool name into the Bibliotheksausweis 2026-06-18 13:09:57 +02:00
Aiirondev_dev 0d6aca4e8c added the school_name to the Schülerausweis 2026-06-18 12:56:10 +02:00
Aiirondev_dev 2bb0cbe599 Implmentation of a autonmatic integration of the price of a book 2026-06-17 16:27:02 +02:00
Aiirondev_dev 339487b4d4 Implementierung des Preses bei Fallback 4 2026-06-17 16:14:38 +02:00
Aiirondev_dev 3f90e2d4f1 Fixes for the secondary information getting with isbn.de 2026-06-17 16:07:37 +02:00
Aiirondev_dev b238b52fbf impoved request handeling for ISBN 2026-06-17 15:55:21 +02:00
Aiirondev_dev 545dd7783c New implementation of a nother scanning methode for ISBN 2026-06-17 11:10:53 +02:00
Aiirondev_dev f4ce1cf3b4 license Changes 2026-06-12 23:45:49 +02:00
Aiirondev_dev 7bd12bddaa Titel adding to the client version oif the termin 2026-06-12 22:14:19 +02:00
Aiirondev_dev b8e23b94d2 final changes for the changes to add the Titel to the Terminplanes 2026-06-12 22:00:20 +02:00
Aiirondev_dev 06b32b4a3d temporary debug mode 2026-06-12 21:45:06 +02:00
12 changed files with 246 additions and 28 deletions
+1 -2
View File
@@ -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
+2
View File
@@ -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:
+155 -6
View File
@@ -25,6 +25,7 @@ from werkzeug.routing import BuildError
from jinja2 import TemplateNotFound
import os
import sys
from bs4 import BeautifulSoup
# Ensure imports work regardless of whether gunicorn starts in /app or /app/Web.
_CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
@@ -1735,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)
@@ -1995,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'],
@@ -4123,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)
@@ -4150,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)
@@ -4290,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)
@@ -5106,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
@@ -5185,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'):
@@ -9539,6 +9551,134 @@ def _fetch_from_open_library(clean_isbn):
print(f"OpenLibrary Search error: {e}")
return None
def _fetch_from_isbn_de(clean_isbn):
"""
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}"
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
response = requests.get(url, headers=headers, timeout=5)
if response.status_code != 200:
return None
soup = BeautifulSoup(response.text, 'html.parser')
# 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"
# Falls Fehler- oder Suchseite
if "nicht gefunden" in title.lower() or "suche" in title.lower():
return None
# --- 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
# 2. Verlag holen
publisher = get_sidebar_value('verlag')
if not publisher:
publisher = "Unknown Publisher"
# 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"
# 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"
# 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_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": author,
"publisher": publisher,
"publishedDate": published_date,
"description": description,
"pageCount": page_count,
"price": price, # Gibt nun z.B. 12.25 als float zurück
"thumbnail": thumbnail,
"source": "isbn-de"
}
except Exception as e:
print(f"isbn.de Scraping error: {e}")
return None
@app.route('/fetch_book_info/<isbn>')
def fetch_book_info(isbn):
"""
@@ -9562,6 +9702,7 @@ def fetch_book_info(isbn):
providers = [
_fetch_from_google_books,
_fetch_from_lobid_germany,
_fetch_from_isbn_de,
_fetch_from_open_library
]
@@ -10130,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)
+1 -2
View File
@@ -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
+2 -1
View File
@@ -38,7 +38,7 @@ def _active_record_query(extra_query=None):
return base_query
def add(date_start: str, date_end: str, time_span: list, slots: int, slot_lenght: int, user: str, mail: list=[], note:str="", calendar_enabled: bool=False):
def add(date_start: str, date_end: str, time_span: list, slots: int, slot_lenght: int, user: str, mail: list=[], note:str="", calendar_enabled: bool=False, title: str=""):
try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client)
@@ -53,6 +53,7 @@ def add(date_start: str, date_end: str, time_span: list, slots: int, slot_lenght
'user': user,
'mail': mail,
'note': note,
'title': title,
'calendar_enabled': bool(calendar_enabled),
'slots_booked': [], # -> [(start_time, name), ...]the list gets there indexes as the slot 1-defined so is can be counted without an extra variable
'Created': datetime.datetime.now(),
+3 -2
View File
@@ -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):
+1
View File
@@ -15,3 +15,4 @@ openpyxl
cryptography>=42.0.0
pywebpush
py-vapid>=1.9.0
beautifulsoup4
+6 -10
View File
@@ -28,11 +28,10 @@
<li class="nav-item" role="presentation">
<button class="nav-link" id="tab-legal" data-bs-toggle="tab" data-bs-target="#pane-legal"
type="button" role="tab">Rechtsgrundlage</button>
</li>
</ul>
<!-- ── NOTICE ────────────────────────────────────────────────────── -->
<div class="tab-pane fade" id="pane-notice" role="tabpanel">
<div class="tab-pane" id="pane-notice" role="tabpanel">
<div class="license-content">
<h2>NOTICE Urheberrechtliche Hinweise</h2>
<p><strong>Invario Modul Suite</strong><br>Copyright © 2026 Invario UG</p>
@@ -59,14 +58,14 @@
<h4>Frontend / Laufzeit-Assets</h4>
<ul>
<li>html5-qrcode (MIT) © 2020-2025 Manoj Brahmbhatt (mebjas)</li>
<li>quagga2 (MIT) © 2014 Christoph Oberhofer, © 2019 Eric Blade and contributors</li>
</ul>
<hr>
</div>
</div><!-- /#pane-notice -->
<!-- ── DATENSCHUTZ ───────────────────────────────────────────────── -->
<div class="tab-pane fade" id="pane-privacy" role="tabpanel">
<div class="tab-pane" id="pane-privacy" role="tabpanel">
<div class="license-content">
<h2>Datenschutzerklärung (Privacy Policy)</h2>
@@ -108,7 +107,7 @@
</div><!-- /#pane-privacy -->
<!-- ── SICHERHEIT ────────────────────────────────────────────────── -->
<div class="tab-pane fade" id="pane-security" role="tabpanel">
<div class="tab-pane" id="pane-security" role="tabpanel">
<div class="license-content">
<h2>Sicherheitsrichtlinie (Security Policy)</h2>
@@ -129,7 +128,7 @@
</div><!-- /#pane-security -->
<!-- ── DATENVERARBEITUNG ─────────────────────────────────────────── -->
<div class="tab-pane fade" id="pane-data" role="tabpanel">
<div class="tab-pane" id="pane-data" role="tabpanel">
<div class="license-content">
<h2>Dokumentation der Datenverarbeitung (VVT)</h2>
@@ -157,7 +156,7 @@
</div><!-- /#pane-data -->
<!-- ── RECHTSGRUNDLAGE ───────────────────────────────────────────── -->
<div class="tab-pane fade" id="pane-legal" role="tabpanel">
<div class="tab-pane" id="pane-legal" role="tabpanel">
<div class="license-content">
<h2>Rechtsgrundlage der Nutzung</h2>
<p>Um das Inventarsystem DSGVO-konform zu betreiben, muss der Betreiber eine der folgenden Grundlagen festlegen:</p>
@@ -170,9 +169,6 @@
Einwilligung vorliegen (Art. 6 Abs. 1 lit. a DSGVO).</li>
</ol>
</div>
</div><!-- /#pane-legal -->
</div><!-- /.tab-content -->
</div>
</div>
+1 -1
View File
@@ -43,7 +43,7 @@
<div class="card border-0 shadow-lg rounded-4 h-100">
<div class="card-body p-4 p-md-5">
<p class="text-uppercase text-muted fw-semibold mb-2">Terminplaner</p>
<h1 class="h3 fw-bold mb-3">Termin buchen</h1>
<h1 class="h3 fw-bold mb-3">Termin buchen - {{ available.title }}</h1>
<p class="text-muted mb-4">Wählen Sie im Kalender einen freien Slot aus. Den gewählten Termin können Sie danach direkt wie in einem Kalender-Block verschieben.</p>
<div class="p-3 rounded-3 bg-light mb-3">
+21 -1
View File
@@ -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);
+52 -3
View File
@@ -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,
@@ -566,6 +566,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 +658,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 +739,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:-}"
+1
View File
@@ -15,3 +15,4 @@ openpyxl
cryptography>=42.0.0
pywebpush
py-vapid>=1.9.0
beautifulsoup4