Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3923939387 | |||
| eb9c655398 | |||
| 4c2657218d | |||
| 875cfa01a3 | |||
| c0c61a41a3 | |||
| 9e74d0c16d | |||
| c8818edf0b | |||
| b270bc9367 |
@@ -556,6 +556,14 @@ sudo chmod 644 certs/inventarsystem.crt
|
||||
|
||||
## Fehlerbehebung
|
||||
|
||||
### Logs Auslesen
|
||||
|
||||
```bash
|
||||
docker logs inventarsystem-app-1
|
||||
docker exec inventarsystem-app-1 cat /data/logs/application.log | tail -n 100
|
||||
docker compose exec mongodb mongosh
|
||||
```
|
||||
|
||||
### Webserver startet nicht
|
||||
|
||||
```bash
|
||||
|
||||
+27
-21
@@ -731,6 +731,7 @@ AUDIT_INDEXES_READY = False
|
||||
|
||||
def _ensure_audit_indexes_once():
|
||||
"""Ensure audit indexes exist once per process."""
|
||||
global AUDIT_INDEXES_READY
|
||||
if AUDIT_INDEXES_READY:
|
||||
return
|
||||
|
||||
@@ -3301,7 +3302,13 @@ def api_library_items():
|
||||
borrower = doc.get('User', '')
|
||||
# Decrypt if value is encrypted (decrypt_text returns original if not encrypted)
|
||||
try:
|
||||
borrower = decrypt_text(borrower) if borrower else ''
|
||||
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||
db = client[cfg.MONGODB_DB]
|
||||
student_cards = db['student_cards']
|
||||
card = student_cards.find_one({'_id': ObjectId(borrow_id)})
|
||||
card = _decrypt_student_card_doc(card)
|
||||
client.close()
|
||||
borrower = card['SchülerName']
|
||||
except Exception:
|
||||
# Fallback: keep original string if decryption fails
|
||||
borrower = borrower or ''
|
||||
@@ -3820,7 +3827,7 @@ def library_admin():
|
||||
show_library_features=True,
|
||||
upload_mode='library',
|
||||
page_title='Bücher hochladen',
|
||||
back_target='home_admin'
|
||||
back_target='library'
|
||||
)
|
||||
|
||||
|
||||
@@ -5073,7 +5080,7 @@ def upload_item():
|
||||
Enhanced for mobile browser compatibility.
|
||||
|
||||
Returns:
|
||||
flask.Response: Redirect to admin homepage or JSON response
|
||||
flask.Response: Redirect to admin homepage
|
||||
"""
|
||||
# Check if the user is authenticated
|
||||
if 'username' not in session:
|
||||
@@ -5086,7 +5093,12 @@ def upload_item():
|
||||
return jsonify({'success': False, 'message': 'Einfüge-Rechte erforderlich'}), 403
|
||||
|
||||
can_access_admin_home = _page_access_allowed(permissions, 'home_admin') and _action_access_allowed(permissions, 'can_manage_settings')
|
||||
success_redirect_endpoint = 'home_admin' if can_access_admin_home else 'home'
|
||||
if can_access_admin_home:
|
||||
success_redirect_endpoint = 'home_admin'
|
||||
elif cfg.MODULES.is_enabled('library') and _page_access_allowed(permissions, 'home_library'):
|
||||
success_redirect_endpoint = 'home_library'
|
||||
else:
|
||||
success_redirect_endpoint = 'home'
|
||||
|
||||
# Detect if request is from mobile device
|
||||
is_mobile = 'Mobile' in request.headers.get('User-Agent', '')
|
||||
@@ -6043,22 +6055,9 @@ def upload_item():
|
||||
except Exception:
|
||||
app.logger.warning('Audit write failed for library_item_created')
|
||||
|
||||
if is_mobile:
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': success_msg,
|
||||
'itemId': str(item_id),
|
||||
'stats': {
|
||||
'processed': processed_count,
|
||||
'errors': error_count,
|
||||
'skipped': skipped_count,
|
||||
'duplicates': len(duplicate_images) if duplicate_images else 0,
|
||||
'totalImages': len(image_filenames)
|
||||
}
|
||||
})
|
||||
else:
|
||||
flash(success_msg, 'success')
|
||||
return redirect(url_for(success_redirect_endpoint, highlight_item=str(item_id)))
|
||||
|
||||
flash(success_msg, 'success')
|
||||
return redirect(url_for(success_redirect_endpoint, highlight_item=str(item_id)))
|
||||
else:
|
||||
error_msg = 'Fehler beim Hinzufügen des Elements'
|
||||
if is_mobile:
|
||||
@@ -8375,8 +8374,15 @@ def admin_create_invoice(borrow_id):
|
||||
flash('Für diese Ausleihe existiert bereits eine Rechnung. Bitte Korrekturbuchung verwenden.', 'warning')
|
||||
return redirect(url_for('admin_borrowings'))
|
||||
|
||||
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||
db = client[cfg.MONGODB_DB]
|
||||
student_cards = db['student_cards']
|
||||
card = student_cards.find_one({'_id': ObjectId(borrow_id)})
|
||||
card = _decrypt_student_card_doc(card)
|
||||
client.close()
|
||||
borrower = card['SchülerName']
|
||||
|
||||
invoice_number = existing_invoice.get('invoice_number') or _build_invoice_number(borrow_doc['_id'], now)
|
||||
borrower = borrow_doc.get('User', '')
|
||||
item_name = item_doc.get('Name', '')
|
||||
item_code = item_doc.get('Code_4', '')
|
||||
|
||||
|
||||
@@ -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, title: str=""):
|
||||
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="", custom_fields: list = ()):
|
||||
try:
|
||||
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||
db = _get_tenant_db(client)
|
||||
@@ -54,8 +54,9 @@ def add(date_start: str, date_end: str, time_span: list, slots: int, slot_lenght
|
||||
'mail': mail,
|
||||
'note': note,
|
||||
'title': title,
|
||||
'custom_fields': custom_fields,
|
||||
'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
|
||||
'slots_booked': [], # -> [(start_time, name,(custom1, custom2,...)), ...]the list gets there indexes as the slot 1-defined so is can be counted without an extra variable
|
||||
'Created': datetime.datetime.now(),
|
||||
'LastUpdated': datetime.datetime.now()
|
||||
}
|
||||
|
||||
+90
-106
@@ -20,6 +20,8 @@ import string
|
||||
from bson.objectid import ObjectId
|
||||
import Web.modules.database.settings as cfg
|
||||
from Web.modules.database.settings import MongoClient
|
||||
import hmac
|
||||
import os
|
||||
|
||||
logger = logging.getLogger('app')
|
||||
logger.setLevel(logging.DEBUG)
|
||||
@@ -430,104 +432,84 @@ def check_password_strength(password):
|
||||
return True
|
||||
|
||||
|
||||
def hashing(password):
|
||||
def hashing(password, salt=None):
|
||||
"""
|
||||
Hash a password using scrypt.
|
||||
Hasht ein Passwort mit scrypt.
|
||||
- Wenn kein Salt übergeben wird, wird ein sicherer, zufälliger Salt generiert (für neue Passwörter).
|
||||
- Format für neue Hashes: v1$<salt_hex>$<hash_hex>
|
||||
"""
|
||||
password_bytes = password.encode('utf-8') # Explizit UTF-8 für Plattformunabhängigkeit
|
||||
|
||||
Args:
|
||||
password (str): Password to hash
|
||||
|
||||
Returns:
|
||||
str: Hexadecimal digest of the hashed password
|
||||
if salt is None:
|
||||
# Neuer Benutzer / Passwortänderung -> Dynamischer Salt
|
||||
random_salt = os.urandom(16)
|
||||
hashed = hashlib.scrypt(password_bytes, salt=random_salt, n=16384, r=8, p=1)
|
||||
return f"v1${random_salt.hex()}${hashed.hex()}"
|
||||
else:
|
||||
# Bestehender Benutzer (wird zur Verifizierung aufgerufen)
|
||||
hashed = hashlib.scrypt(password_bytes, salt=salt, n=16384, r=8, p=1)
|
||||
return hashed.hex()
|
||||
|
||||
|
||||
def verify_password(provided_password, stored_password_string):
|
||||
"""
|
||||
hashed = hashlib.scrypt(password.encode(), salt=b'some_salt', n=16384, r=8, p=1)
|
||||
return hashed.hex()
|
||||
Verifiziert ein Passwort gegen einen gespeicherten Hash-String.
|
||||
Unterstützt das alte Format (statischer Salt) und das neue Format (v1$...).
|
||||
"""
|
||||
if not stored_password_string:
|
||||
return False
|
||||
|
||||
# Überprüfung für das neue, sichere Format
|
||||
if stored_password_string.startswith("v1$"):
|
||||
try:
|
||||
_, salt_hex, hash_hex = stored_password_string.split("$")
|
||||
salt_bytes = bytes.fromhex(salt_hex)
|
||||
# Berechne den Hash des eingegebenen Passworts mit dem extrahierten Salt
|
||||
calculated_hash = hashing(provided_password, salt=salt_bytes)
|
||||
# Timing-Attack-sicherer Vergleich
|
||||
return hmac.compare_digest(calculated_hash, hash_hex)
|
||||
except (ValueError, TypeError):
|
||||
logger.error("Ungültiges Hash-Format in der Datenbank entdeckt.")
|
||||
return False
|
||||
else:
|
||||
# Abwärtskompatibilität: Altes Format mit statischem Salt b'some_salt'
|
||||
old_static_salt = b'some_salt'
|
||||
calculated_hash = hashing(provided_password, salt=old_static_salt)
|
||||
return hmac.compare_digest(calculated_hash, stored_password_string)
|
||||
|
||||
|
||||
def check_nm_pwd(username, password):
|
||||
"""
|
||||
Verify username and password combination.
|
||||
|
||||
Args:
|
||||
username (str): Username to check
|
||||
password (str): Password to verify
|
||||
|
||||
Returns:
|
||||
dict: User document if credentials are valid, None otherwise
|
||||
Überprüft die Kombination aus Benutzername und Passwort (optimiert).
|
||||
"""
|
||||
db_name, tenant_id = _resolve_request_tenant_db()
|
||||
ctx = None
|
||||
try:
|
||||
from tenant import get_tenant_context
|
||||
ctx = get_tenant_context()
|
||||
except Exception:
|
||||
ctx = None
|
||||
|
||||
if not db_name:
|
||||
if _has_tenant_configs():
|
||||
logger.warning(
|
||||
"Refusing default DB fallback for login because tenant configs exist and no tenant was resolved."
|
||||
)
|
||||
logger.warning("Default DB fallback verweigert, da Tenant-Konfigurationen existieren.")
|
||||
return None
|
||||
db_name = cfg.MONGODB_DB
|
||||
|
||||
logger.info(
|
||||
"check_nm_pwd start: username=%r tenant=%r db=%r host=%r port=%r uri=%r",
|
||||
username,
|
||||
tenant_id,
|
||||
db_name,
|
||||
cfg.MONGODB_HOST,
|
||||
cfg.MONGODB_PORT,
|
||||
getattr(cfg, 'MONGODB_URI', None),
|
||||
)
|
||||
|
||||
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||
try:
|
||||
hashed_password = hashing(password)
|
||||
logger.info("check_nm_pwd password hash for username=%r: %s", username, hashed_password)
|
||||
available_dbs = []
|
||||
try:
|
||||
available_dbs = client.list_database_names()
|
||||
logger.debug("MongoDB connected. Available databases=%s", available_dbs)
|
||||
except Exception as exc:
|
||||
logger.exception("Unable to list MongoDB databases: %s", exc)
|
||||
|
||||
db = client[db_name]
|
||||
try:
|
||||
existing_collections = db.list_collection_names()
|
||||
except Exception as exc:
|
||||
logger.exception("Unable to list collections for db=%r: %s", db_name, exc)
|
||||
existing_collections = []
|
||||
logger.debug("Tenant db=%r collections=%s", db_name, existing_collections)
|
||||
|
||||
users = db['users']
|
||||
|
||||
query = {'$or': [{'Username': username}, {'username': username}]}
|
||||
logger.debug("Running user lookup on %r: %s", db_name, query)
|
||||
user_record = users.find_one(query)
|
||||
|
||||
if user_record is None:
|
||||
logger.warning("No user document found in db=%r for username=%r", db_name, username)
|
||||
if db_name not in available_dbs:
|
||||
logger.warning("Tenant database %r is missing from available MongoDB databases", db_name)
|
||||
if 'users' not in existing_collections:
|
||||
logger.warning("Tenant database %r has no users collection", db_name)
|
||||
logger.warning("Kein Benutzer für %r in DB %r gefunden.", username, db_name)
|
||||
return None
|
||||
|
||||
logger.info("Found user document for username=%r in db=%r: %s", username, db_name, user_record)
|
||||
stored_password = user_record.get('Password') or user_record.get('password')
|
||||
if stored_password is None:
|
||||
logger.warning("User document for username=%r in db=%r has no password field", username, db_name)
|
||||
|
||||
if not verify_password(password, stored_password):
|
||||
logger.warning("Falsches Passwort für Benutzer %r in DB %r.", username, db_name)
|
||||
return None
|
||||
|
||||
if stored_password != hashed_password:
|
||||
logger.warning(
|
||||
"Password mismatch for username=%r in db=%r: provided_hash=%s stored_hash=%s",
|
||||
username,
|
||||
db_name,
|
||||
hashed_password,
|
||||
stored_password,
|
||||
)
|
||||
return None
|
||||
# Automatische Migration alter Hashes auf das neue Format
|
||||
if not stored_password.startswith("v1$"):
|
||||
users.update_one({'_id': user_record['_id']}, {'$set': {'Password': hashing(password)}})
|
||||
|
||||
return user_record
|
||||
finally:
|
||||
@@ -557,44 +539,46 @@ def add_user(
|
||||
bool: True if user was added successfully, False if password was too weak
|
||||
"""
|
||||
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||
db = _get_tenant_db(client)
|
||||
users = db['users']
|
||||
if not check_password_strength(password):
|
||||
return False
|
||||
permission_defaults = build_default_permission_payload(permission_preset)
|
||||
if isinstance(action_permissions, dict):
|
||||
for key, value in action_permissions.items():
|
||||
permission_defaults['actions'][str(key)] = bool(value)
|
||||
if isinstance(page_permissions, dict):
|
||||
for key, value in page_permissions.items():
|
||||
permission_defaults['pages'][str(key)] = bool(value)
|
||||
try:
|
||||
db = _get_tenant_db(client)
|
||||
users = db['users']
|
||||
if not check_password_strength(password):
|
||||
return False
|
||||
permission_defaults = build_default_permission_payload(permission_preset)
|
||||
if isinstance(action_permissions, dict):
|
||||
for key, value in action_permissions.items():
|
||||
permission_defaults['actions'][str(key)] = bool(value)
|
||||
if isinstance(page_permissions, dict):
|
||||
for key, value in page_permissions.items():
|
||||
permission_defaults['pages'][str(key)] = bool(value)
|
||||
|
||||
user_doc = {
|
||||
'Username': username,
|
||||
'Password': hashing(password),
|
||||
'Admin': False,
|
||||
'active_ausleihung': None,
|
||||
'name': name.strip() if name else '',
|
||||
'last_name': last_name.strip() if last_name else '',
|
||||
'IsStudent': bool(is_student),
|
||||
'PermissionPreset': permission_defaults['preset'],
|
||||
'ActionPermissions': permission_defaults['actions'],
|
||||
'PagePermissions': permission_defaults['pages'],
|
||||
}
|
||||
user_doc = {
|
||||
'Username': username,
|
||||
'Password': hashing(password),
|
||||
'Admin': False,
|
||||
'active_ausleihung': None,
|
||||
'name': name.strip() if name else '',
|
||||
'last_name': last_name.strip() if last_name else '',
|
||||
'IsStudent': bool(is_student),
|
||||
'PermissionPreset': permission_defaults['preset'],
|
||||
'ActionPermissions': permission_defaults['actions'],
|
||||
'PagePermissions': permission_defaults['pages'],
|
||||
}
|
||||
|
||||
normalized_card = normalize_student_card_id(student_card_id)
|
||||
if bool(is_student):
|
||||
if normalized_card:
|
||||
user_doc['StudentCardId'] = normalized_card
|
||||
if max_borrow_days is not None:
|
||||
try:
|
||||
user_doc['MaxBorrowDays'] = int(max_borrow_days)
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
normalized_card = normalize_student_card_id(student_card_id)
|
||||
if bool(is_student):
|
||||
if normalized_card:
|
||||
user_doc['StudentCardId'] = normalized_card
|
||||
if max_borrow_days is not None:
|
||||
try:
|
||||
user_doc['MaxBorrowDays'] = int(max_borrow_days)
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
|
||||
users.insert_one(user_doc)
|
||||
client.close()
|
||||
return True
|
||||
users.insert_one(user_doc)
|
||||
return True
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
|
||||
def student_card_exists(student_card_id):
|
||||
|
||||
@@ -198,7 +198,7 @@ def build_client_slot_ics(appointment_id: str, slot_start: str, client_name: str
|
||||
return '\r\n'.join(ics_lines)
|
||||
|
||||
|
||||
def new(date_start: str, date_end: str, time_span: list, slots, slot_length, user: str, mail: list=None, note:str="", calendar_enabled: bool=False, title: str="") -> dict:
|
||||
def new(date_start: str, date_end: str, time_span: list, slots, slot_length, user: str, mail: list=None, note:str="", calendar_enabled: bool=False, title: str="", custom_fields: list = ()) -> dict:
|
||||
"""
|
||||
Generates a link for the executive to send to his clients to book a time Slot
|
||||
"""
|
||||
@@ -207,6 +207,11 @@ def new(date_start: str, date_end: str, time_span: list, slots, slot_length, use
|
||||
except (ValueError, TypeError):
|
||||
slots_int = 0
|
||||
|
||||
try:
|
||||
custom_fields.pop(-1)
|
||||
except :
|
||||
pass
|
||||
|
||||
try:
|
||||
slot_length_int = int(slot_length)
|
||||
except (ValueError, TypeError):
|
||||
@@ -215,7 +220,7 @@ def new(date_start: str, date_end: str, time_span: list, slots, slot_length, use
|
||||
normalized_time_span = _normalize_time_span(time_span)
|
||||
normalized_mail = _normalize_mail_list(mail or [])
|
||||
|
||||
id = termin.add(date_start, date_end, normalized_time_span, slots_int, slot_length_int, user, normalized_mail, note, calendar_enabled=calendar_enabled, title=title)
|
||||
id = termin.add(date_start, date_end, normalized_time_span, slots_int, slot_length_int, user, normalized_mail, note, calendar_enabled=calendar_enabled, title=title, custom_fields=custom_fields)
|
||||
id_str = str(id)
|
||||
tenant_id = _current_tenant_id()
|
||||
|
||||
@@ -254,7 +259,7 @@ def new(date_start: str, date_end: str, time_span: list, slots, slot_length, use
|
||||
}
|
||||
|
||||
|
||||
def book_slot(id, date_start_time, name):
|
||||
def book_slot(id, date_start_time, name, custom:list=()):
|
||||
try:
|
||||
item = termin.get_item(id)
|
||||
if not item:
|
||||
@@ -273,7 +278,7 @@ def book_slot(id, date_start_time, name):
|
||||
if existing[0] == date_start_time and existing[1] == name:
|
||||
return False
|
||||
|
||||
slots.append((date_start_time, name))
|
||||
slots.append((date_start_time, name, custom))
|
||||
success = termin.update(id, slots)
|
||||
return bool(success)
|
||||
except Exception as e:
|
||||
@@ -418,6 +423,7 @@ def get_user_upcoming_events(user: str, limit: int = 25) -> list[dict]:
|
||||
'link': link,
|
||||
'calendar_link': calendar_link if item.get('calendar_enabled') else None,
|
||||
'title': str(item.get('title') or ''),
|
||||
'custom_fields': list(item.get('custom_fields')),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -50,6 +50,10 @@ def client(appointment_id):
|
||||
current_user = str(session.get('username', '') or '').strip()
|
||||
appointment_item = termin.get_item(appointment_id) or {}
|
||||
appointment_owner = str(appointment_item.get('user', '') or '').strip()
|
||||
|
||||
# Extract the custom fields defined by the appointment owner
|
||||
custom_fields = appointment_item.get('custom_fields', [])
|
||||
|
||||
can_view_booking_names = False
|
||||
if current_user:
|
||||
try:
|
||||
@@ -72,6 +76,9 @@ def client(appointment_id):
|
||||
if request.method == 'POST':
|
||||
start_daytime = request.form.get('start_day_time')
|
||||
username = request.form.get('client_name')
|
||||
|
||||
custom_answers = request.form.getlist('custom_answers')
|
||||
|
||||
if not start_daytime or not username:
|
||||
flash('Bitte Name und gewünschte Uhrzeit angeben.', 'error')
|
||||
return render_template(
|
||||
@@ -81,9 +88,10 @@ def client(appointment_id):
|
||||
current_user=session.get('username', ''),
|
||||
tenant_id=_current_tenant_id(),
|
||||
can_view_booking_names=can_view_booking_names,
|
||||
custom_fields=custom_fields
|
||||
)
|
||||
|
||||
if appointment_service.book_slot(appointment_id, start_daytime, username):
|
||||
if appointment_service.book_slot(appointment_id, start_daytime, username, custom=custom_answers):
|
||||
return redirect(
|
||||
url_for(
|
||||
'terminplaner.client_success',
|
||||
@@ -103,6 +111,7 @@ def client(appointment_id):
|
||||
current_user=session.get('username', ''),
|
||||
tenant_id=_current_tenant_id(),
|
||||
can_view_booking_names=can_view_booking_names,
|
||||
custom_fields=custom_fields
|
||||
)
|
||||
|
||||
|
||||
@@ -169,13 +178,13 @@ def configure():
|
||||
end = request.form.get('end_date')
|
||||
time = request.form.get('time_frame')
|
||||
slots_amount = request.form.get('slots_amounts')
|
||||
slot_length = request.form.get('slot_length') # Korrigiert: slot_length
|
||||
slot_length = request.form.get('slot_length')
|
||||
mail = request.form.get('mail', '')
|
||||
note = request.form.get('note', '')
|
||||
add_to_calendar = request.form.get('add_to_calendar') == 'on'
|
||||
title = request.form.get('title', '').strip() # Korrigiert: title statt titel
|
||||
title = request.form.get('title', '').strip()
|
||||
custom = request.form.getlist('custom_fields')
|
||||
|
||||
# Abfrage ebenfalls auf title und slot_length angepasst
|
||||
if not start or not end or not time or not slots_amount or not slot_length or not title:
|
||||
flash('Bitte alle Pflichtfelder ausfüllen.', 'error')
|
||||
return render_template(
|
||||
@@ -186,7 +195,7 @@ def configure():
|
||||
)
|
||||
|
||||
# Variablen im Funktionsaufruf aktualisiert
|
||||
result = appointment_service.new(start, end, time, slots_amount, slot_length, session["username"], mail, note, calendar_enabled=add_to_calendar, title=title)
|
||||
result = appointment_service.new(start, end, time, slots_amount, slot_length, session["username"], mail, note, calendar_enabled=add_to_calendar, title=title, custom_fields=custom)
|
||||
flash('Der Terminplan wurde angelegt.', 'success')
|
||||
return render_template(
|
||||
'termin_configure.html',
|
||||
|
||||
@@ -1321,10 +1321,7 @@
|
||||
{% if 'username' in session and (session.get('admin', False) or is_admin) and current_permissions.actions.get('can_manage_settings', True) %}
|
||||
<li><h6 class="dropdown-header">Bibliotheks-Verwaltung</h6></li>
|
||||
{% if current_permissions.pages.get('library_loans_admin', True) %}
|
||||
<li><a class="dropdown-item" href="{{ url_for('library_loans_admin') }}">Ausleihen</a></li>
|
||||
{% endif %}
|
||||
{% if current_permissions.pages.get('admin_damaged_items', True) %}
|
||||
<li><a class="dropdown-item" href="{{ url_for('admin_damaged_items') }}">Defekte Items</a></li>
|
||||
<li><a class="dropdown-item" href="{{ url_for('library_loans_admin') }}">Ausleihen / Defekte Items</a></li>
|
||||
{% endif %}
|
||||
{% if student_cards_module_enabled %}
|
||||
<li><a class="dropdown-item" href="{{ url_for('student_cards_admin') }}">Bibliotheksausweis</a></li>
|
||||
|
||||
@@ -109,6 +109,16 @@
|
||||
<label for="client_name" class="form-label fw-semibold">Ihr Name</label>
|
||||
<input type="text" id="client_name" name="client_name" class="form-control form-control-lg" placeholder="Vor- und Nachname" required>
|
||||
</div>
|
||||
|
||||
{% if custom_fields %}
|
||||
{% for field_label in custom_fields %}
|
||||
<div>
|
||||
<label class="form-label fw-semibold">{{ field_label }}</label>
|
||||
<input type="text" name="custom_answers" class="form-control form-control-lg" placeholder="{{ field_label }} eingeben" required>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<div class="d-flex flex-column flex-sm-row gap-2 pt-2">
|
||||
<button type="submit" class="btn btn-primary btn-lg">Termin buchen</button>
|
||||
<a class="btn btn-outline-secondary btn-lg" href="{{ url_for('terminplaner.main', tenant=tenant_id) }}">Zur Übersicht</a>
|
||||
|
||||
@@ -77,6 +77,16 @@
|
||||
<div class="form-text">Wird aus der gebuchten Zeit (abzüglich Pausen) und der Slot-Länge berechnet.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="custom-fields-container" class="mt-4">
|
||||
<h3 class="mb-3">Custom Fields</h3>
|
||||
|
||||
<div class="mb-3 custom-field-row">
|
||||
<input type="text"
|
||||
name="custom_fields"
|
||||
class="form-control form-control-lg custom-dynamic-input"
|
||||
placeholder="Zusatzfeld hinzufügen (z.B. Firma, Kundennummer...)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if mail_service_enabled %}
|
||||
<div>
|
||||
@@ -351,6 +361,39 @@
|
||||
syncTextarea();
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const container = document.getElementById('custom-fields-container');
|
||||
|
||||
// Monitor inputs inside the container using event delegation
|
||||
container.addEventListener('input', function (event) {
|
||||
// Only trigger if the user is typing inside our dynamic inputs
|
||||
if (event.target.classList.contains('custom-dynamic-input')) {
|
||||
const allInputs = container.querySelectorAll('.custom-dynamic-input');
|
||||
const lastInput = allInputs[allInputs.length - 1];
|
||||
|
||||
// If the user typed something into the absolute last input field, spawn a new one
|
||||
if (lastInput.value.trim() !== '') {
|
||||
createNewFieldRow(container);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Helper function to generate and append a clean new input row
|
||||
function createNewFieldRow(targetContainer) {
|
||||
const rowDiv = document.createElement('div');
|
||||
rowDiv.className = 'mb-3 custom-field-row';
|
||||
|
||||
rowDiv.innerHTML = `
|
||||
<input type="text"
|
||||
name="custom_fields"
|
||||
class="form-control form-control-lg custom-dynamic-input"
|
||||
placeholder="Nächstes Zusatzfeld...">
|
||||
`;
|
||||
|
||||
targetContainer.appendChild(rowDiv);
|
||||
}
|
||||
});
|
||||
|
||||
// Event Listeners Registration
|
||||
buildButton.addEventListener('click', renderRows);
|
||||
startDateInput.addEventListener('change', renderRows);
|
||||
|
||||
@@ -954,7 +954,7 @@
|
||||
<div class="isbn-input-group">
|
||||
<input type="text" id="isbn" name="isbn" placeholder="ISBN oder Barcode eingeben..." required>
|
||||
<button type="button" id="scan-isbn-btn" class="fetch-isbn-button">Barcode scannen</button>
|
||||
<button type="button" class="fetch-isbn-button" onclick="fetchBookInfo('upload')">Bild abrufen</button>
|
||||
<button type="button" class="fetch-isbn-button" onclick="fetchBookInfo('upload')">Informationen abrufen</button>
|
||||
</div>
|
||||
<div id="isbn-scanner" style="width:100%; max-width:520px; display:none; margin-top:10px;"></div>
|
||||
<small id="isbn-scan-status" style="display:block; color:#666; margin-top:6px;">Scannen oder manuell eingeben. Gültige ISBNs helfen beim Abruf von Buchdaten, andere Codes werden trotzdem akzeptiert.</small>
|
||||
|
||||
+9
-5
@@ -194,8 +194,8 @@ initialize_tenant_database() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
docker exec "$APP_CONTAINER" python3 -c '
|
||||
import sys, re, datetime, hashlib
|
||||
docker exec -i "$APP_CONTAINER" python3 - "$tenant_id" "$mode" <<'PY'
|
||||
import sys, os, re, datetime, hashlib
|
||||
sys.path.insert(0, "/app")
|
||||
sys.path.insert(0, "/app/Web")
|
||||
from Web.modules.database import settings
|
||||
@@ -207,7 +207,11 @@ 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.scrypt("admin123", salt=b"some_salt", n=16384, r=8, p=1).hex()
|
||||
|
||||
pw_bytes = "admin123".encode("utf-8")
|
||||
random_salt = os.urandom(16)
|
||||
hashed = hashlib.scrypt(pw_bytes, salt=random_salt, n=16384, r=8, p=1)
|
||||
hashed_pw_string = f"v1${random_salt.hex()}${hashed.hex()}"
|
||||
|
||||
action_permissions = {
|
||||
"can_borrow": True,
|
||||
@@ -243,7 +247,7 @@ page_permissions = {
|
||||
if db.users.count_documents({"Username": "admin"}) == 0:
|
||||
db.users.insert_one({
|
||||
"Username": "admin",
|
||||
"Password": hashed_pw,
|
||||
"Password": hashed_pw_string,
|
||||
"Admin": True,
|
||||
"active_ausleihung": None,
|
||||
"name": "Admin",
|
||||
@@ -268,7 +272,7 @@ if mode == "trial":
|
||||
)
|
||||
|
||||
print(f"Tenant {sys.argv[1]} database initialized. Default admin: admin / admin123")
|
||||
' "$tenant_id" "$mode"
|
||||
PY
|
||||
}
|
||||
|
||||
update_runtime_ports() {
|
||||
|
||||
Reference in New Issue
Block a user