Compare commits

...

10 Commits

6 changed files with 310 additions and 246 deletions
+99 -115
View File
@@ -18,6 +18,7 @@ Features:
""" """
from flask import Flask, render_template, request, redirect, url_for, session, flash, send_from_directory, get_flashed_messages, jsonify, Response, make_response, send_file, abort from flask import Flask, render_template, request, redirect, url_for, session, flash, send_from_directory, get_flashed_messages, jsonify, Response, make_response, send_file, abort
from flask_wtf.csrf import CSRFProtect
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from werkzeug.middleware.proxy_fix import ProxyFix from werkzeug.middleware.proxy_fix import ProxyFix
from werkzeug.exceptions import HTTPException from werkzeug.exceptions import HTTPException
@@ -115,6 +116,7 @@ app.config['PREFERRED_URL_SCHEME'] = 'https' if app.config['SESSION_COOKIE_SECUR
# app.config['QR_CODE_FOLDER'] = cfg.QR_CODE_FOLDER # QR Code storage deactivated # app.config['QR_CODE_FOLDER'] = cfg.QR_CODE_FOLDER # QR Code storage deactivated
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1) app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1)
app.register_blueprint(terminplaner_bp, url_prefix='/terminplaner') app.register_blueprint(terminplaner_bp, url_prefix='/terminplaner')
csrf = CSRFProtect(app)
"""--------------------------------------------------------------Path Init-------------------------------------------------------""" """--------------------------------------------------------------Path Init-------------------------------------------------------"""
@@ -556,7 +558,7 @@ def _enforce_module_access():
flash(msg, 'info') flash(msg, 'info')
if name != 'inventory' and cfg.MODULES.is_enabled('inventory'): if name != 'inventory' and cfg.MODULES.is_enabled('inventory'):
return redirect(url_for('home')) return redirect(url_for('home_admin'))
elif name != 'library' and cfg.MODULES.is_enabled('library'): elif name != 'library' and cfg.MODULES.is_enabled('library'):
return redirect('/library') return redirect('/library')
@@ -630,7 +632,7 @@ def handle_build_error(e):
return redirect('/library') return redirect('/library')
if 'username' in session: if 'username' in session:
return redirect(url_for('home')) return redirect(url_for('home_admin'))
return redirect(url_for('login')) return redirect(url_for('login'))
@@ -708,10 +710,10 @@ def _permission_denied_fallback_endpoint(permissions, current_endpoint=None):
is_admin_user = bool(username and us.check_admin(username)) is_admin_user = bool(username and us.check_admin(username))
admin_home_allowed = _page_access_allowed(permissions, 'home_admin') and _action_access_allowed(permissions, 'can_manage_settings') admin_home_allowed = _page_access_allowed(permissions, 'home_admin') and _action_access_allowed(permissions, 'can_manage_settings')
for candidate in ('my_borrowed_items', 'tutorial_page', 'notifications_view', 'impressum', 'home'): for candidate in ('my_borrowed_items', 'tutorial_page', 'notifications_view', 'impressum', 'home_admin'):
if current_endpoint and candidate == current_endpoint: if current_endpoint and candidate == current_endpoint:
continue continue
if candidate == 'home' and is_admin_user and not admin_home_allowed: if candidate == 'home_admin' and is_admin_user and not admin_home_allowed:
continue continue
if _page_access_allowed(permissions, candidate): if _page_access_allowed(permissions, candidate):
return candidate return candidate
@@ -1984,7 +1986,7 @@ def _upload_student_cards_excel():
if not current_permissions['actions'].get('can_manage_user', False): if not current_permissions['actions'].get('can_manage_user', False):
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error') flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error')
return redirect(url_for('library')) return redirect(url_for('library_view'))
if not cfg.MODULES.is_enabled('student_cards'): if not cfg.MODULES.is_enabled('student_cards'):
flash('Schülerausweis-Modul ist deaktiviert.', 'error') flash('Schülerausweis-Modul ist deaktiviert.', 'error')
@@ -2178,7 +2180,7 @@ def _upload_excel_items(scope='inventory'):
if is_library_scope: if is_library_scope:
if not cfg.MODULES.is_enabled('library'): if not cfg.MODULES.is_enabled('library'):
flash('Bibliotheks-Modul ist deaktiviert.', 'error') flash('Bibliotheks-Modul ist deaktiviert.', 'error')
return redirect(url_for('home')) return redirect(url_for('home_admin'))
excel_file = request.files.get(file_field) excel_file = request.files.get(file_field)
if not excel_file or not excel_file.filename: if not excel_file or not excel_file.filename:
@@ -2194,7 +2196,7 @@ def _upload_excel_items(scope='inventory'):
# Allow CSV imports for authenticated non-admin users only for inventory (non-library) # Allow CSV imports for authenticated non-admin users only for inventory (non-library)
if not (is_csv and not is_library_scope and 'username' in session): if not (is_csv and not is_library_scope and 'username' in session):
flash('Einfüge-Rechte erforderlich.', 'error') flash('Einfüge-Rechte erforderlich.', 'error')
return redirect(url_for('home')) return redirect(url_for('home_admin'))
filename_lower = excel_file.filename.lower() filename_lower = excel_file.filename.lower()
if not filename_lower.endswith(('.xlsx', '.csv')): if not filename_lower.endswith(('.xlsx', '.csv')):
@@ -2882,6 +2884,7 @@ def user_status():
##################################################### changes to be made to account for the new account permison managment system ############################## ##################################################### changes to be made to account for the new account permison managment system ##############################
@app.route('/') @app.route('/')
def home(): def home():
""" """
@@ -2895,33 +2898,7 @@ def home():
flash('Bitte mit registriertem Konto anmelden!', 'error') flash('Bitte mit registriertem Konto anmelden!', 'error')
return redirect(url_for('login')) return redirect(url_for('login'))
if not cfg.MODULES.is_enabled('inventory'): return redirect(url_for('home_admin'))
if cfg.MODULES.is_enabled('library'):
return redirect('/library')
else:
return "Weder Inventar- noch Bibliotheks-Modul sind aktiviert.", 403
elif not us.check_admin(session['username']):
return render_template(
'main.html',
username=session['username'],
library_module_enabled=cfg.MODULES.is_enabled('library'),
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
mail_module_enabled=cfg.MODULES.is_enabled('mail'),
student_default_borrow_days=cfg.STUDENT_DEFAULT_BORROW_DAYS,
student_max_borrow_days=cfg.STUDENT_MAX_BORROW_DAYS,
open_item=request.args.get('open_item')
)
else:
permissions = _get_current_user_permissions() or us.build_default_permission_payload('standard_user')
if _page_access_allowed(permissions, 'home_admin') and _action_access_allowed(permissions, 'can_manage_settings'):
return redirect(url_for('home_admin'))
fallback_endpoint = _permission_denied_fallback_endpoint(permissions, current_endpoint='home')
if fallback_endpoint == 'logout':
flash('Für diesen Benutzer sind aktuell keine Seiten freigegeben.', 'error')
return redirect(url_for(fallback_endpoint))
@app.route('/home_admin') @app.route('/home_admin')
def home_admin(): def home_admin():
@@ -2942,9 +2919,12 @@ def home_admin():
else: else:
return "Weder Inventar- noch Bibliotheks-Modul sind aktiviert.", 403 return "Weder Inventar- noch Bibliotheks-Modul sind aktiviert.", 403
if not us.check_admin(session['username']): current_permissions = us.get_effective_permissions(session['username'])
if not current_permissions['pages'].get('home', False):
flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error') flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error')
return redirect(url_for('login')) return redirect(url_for('library_view'))
return render_template( return render_template(
'main_admin.html', 'main_admin.html',
username=session['username'], username=session['username'],
@@ -3052,7 +3032,7 @@ def library_view():
return redirect(url_for('login')) return redirect(url_for('login'))
if not cfg.MODULES.is_enabled('library'): if not cfg.MODULES.is_enabled('library'):
flash('Bibliotheks-Modul ist deaktiviert.', 'error') flash('Bibliotheks-Modul ist deaktiviert.', 'error')
return redirect(url_for('home')) return redirect(url_for('home_admin'))
return render_template( return render_template(
'library_table.html', 'library_table.html',
@@ -3080,7 +3060,7 @@ def library_loans_admin():
if not cfg.MODULES.is_enabled('library'): if not cfg.MODULES.is_enabled('library'):
flash('Bibliotheks-Modul ist deaktiviert.', 'error') flash('Bibliotheks-Modul ist deaktiviert.', 'error')
return redirect(url_for('home')) return redirect(url_for('home_admin'))
_ensure_audit_indexes_once() _ensure_audit_indexes_once()
@@ -3675,7 +3655,7 @@ def api_library_item_update(item_id):
if not current_permissions['actions'].get('can_edit', False): if not current_permissions['actions'].get('can_edit', False):
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error') flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error')
return redirect(url_for('library')) return redirect(url_for('library_view'))
if not cfg.MODULES.is_enabled('library'): if not cfg.MODULES.is_enabled('library'):
return jsonify({'ok': False, 'message': 'Bibliotheks-Modul ist deaktiviert.'}), 403 return jsonify({'ok': False, 'message': 'Bibliotheks-Modul ist deaktiviert.'}), 403
@@ -3908,7 +3888,7 @@ def student_cards_admin():
if not current_permissions['actions'].get('can_manage_users', False): if not current_permissions['actions'].get('can_manage_users', False):
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error') flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error')
return redirect(url_for('library')) return redirect(url_for('library_view'))
if not cfg.MODULES.is_enabled('student_cards'): if not cfg.MODULES.is_enabled('student_cards'):
flash('Schülerausweis-Modul ist deaktiviert.', 'error') flash('Schülerausweis-Modul ist deaktiviert.', 'error')
return redirect(url_for('home_admin')) return redirect(url_for('home_admin'))
@@ -4053,7 +4033,7 @@ def student_cards_print():
if not current_permissions['actions'].get('can_manage_users', False): if not current_permissions['actions'].get('can_manage_users', False):
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error') flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error')
return redirect(url_for('library')) return redirect(url_for('library_view'))
if not cfg.MODULES.is_enabled('student_cards'): if not cfg.MODULES.is_enabled('student_cards'):
flash('Schülerausweis-Modul ist deaktiviert.', 'error') flash('Schülerausweis-Modul ist deaktiviert.', 'error')
return redirect(url_for('home_admin')) return redirect(url_for('home_admin'))
@@ -4087,7 +4067,7 @@ def student_card_barcode_print():
if not current_permissions['actions'].get('can_manage_users', False): if not current_permissions['actions'].get('can_manage_users', False):
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error') flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error')
return redirect(url_for('library')) return redirect(url_for('library_view'))
if not cfg.MODULES.is_enabled('student_cards'): if not cfg.MODULES.is_enabled('student_cards'):
flash('Schülerausweis-Modul ist deaktiviert.', 'error') flash('Schülerausweis-Modul ist deaktiviert.', 'error')
return redirect(url_for('home_admin')) return redirect(url_for('home_admin'))
@@ -4120,7 +4100,7 @@ def student_card_barcode_download():
if not current_permissions['actions'].get('can_manage_users', False): if not current_permissions['actions'].get('can_manage_users', False):
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error') flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error')
return redirect(url_for('library')) return redirect(url_for('library_view'))
if not cfg.MODULES.is_enabled('student_cards'): if not cfg.MODULES.is_enabled('student_cards'):
flash('Schülerausweis-Modul ist deaktiviert.', 'error') flash('Schülerausweis-Modul ist deaktiviert.', 'error')
return redirect(url_for('home_admin')) return redirect(url_for('home_admin'))
@@ -4304,7 +4284,7 @@ def student_card_single_barcode_download(card_id):
if not current_permissions['actions'].get('can_manage_users', False): if not current_permissions['actions'].get('can_manage_users', False):
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error') flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error')
return redirect(url_for('library')) return redirect(url_for('library_view'))
if not cfg.MODULES.is_enabled('student_cards'): if not cfg.MODULES.is_enabled('student_cards'):
flash('Schülerausweis-Modul ist deaktiviert.', 'error') flash('Schülerausweis-Modul ist deaktiviert.', 'error')
return redirect(url_for('home_admin')) return redirect(url_for('home_admin'))
@@ -4478,7 +4458,7 @@ def login():
flask.Response: Rendered template or redirect flask.Response: Rendered template or redirect
""" """
if 'username' in session: if 'username' in session:
return redirect(url_for('home')) return redirect(url_for('home_admin'))
if request.method == 'POST': if request.method == 'POST':
username = request.form['username'] username = request.form['username']
password = request.form['password'] password = request.form['password']
@@ -4514,7 +4494,7 @@ def login():
fallback_endpoint = _permission_denied_fallback_endpoint(permissions, current_endpoint='login') fallback_endpoint = _permission_denied_fallback_endpoint(permissions, current_endpoint='login')
return redirect(url_for(fallback_endpoint)) return redirect(url_for(fallback_endpoint))
else: else:
return redirect(url_for('home')) return redirect(url_for('home_admin'))
else: else:
app.logger.warning(f"Login failed: username={encrypt_text(username)!r} tenant={current_tenant_id or 'default'} db={current_tenant_db} host={request.host} ip={encrypt_text(request.remote_addr)}") app.logger.warning(f"Login failed: username={encrypt_text(username)!r} tenant={current_tenant_id or 'default'} db={current_tenant_db} host={request.host} ip={encrypt_text(request.remote_addr)}")
flash('Ungültige Anmeldedaten', 'error') flash('Ungültige Anmeldedaten', 'error')
@@ -4584,7 +4564,7 @@ def change_password():
# Update the password # Update the password
if us.update_password(session['username'], new_password): if us.update_password(session['username'], new_password):
flash('Ihr Passwort wurde erfolgreich geändert.', 'success') flash('Ihr Passwort wurde erfolgreich geändert.', 'success')
return redirect(url_for('home')) return redirect(url_for('home_admin'))
else: else:
flash('Fehler beim Ändern des Passworts. Bitte versuchen Sie es später erneut.', 'error') flash('Fehler beim Ändern des Passworts. Bitte versuchen Sie es später erneut.', 'error')
@@ -5178,7 +5158,7 @@ def upload_item():
elif cfg.MODULES.is_enabled('library') and _page_access_allowed(permissions, 'home_library'): elif cfg.MODULES.is_enabled('library') and _page_access_allowed(permissions, 'home_library'):
success_redirect_endpoint = 'home_library' success_redirect_endpoint = 'home_library'
else: else:
success_redirect_endpoint = 'home' success_redirect_endpoint = 'home_admin'
# Detect if request is from mobile device # Detect if request is from mobile device
is_mobile = 'Mobile' in request.headers.get('User-Agent', '') is_mobile = 'Mobile' in request.headers.get('User-Agent', '')
@@ -6384,11 +6364,11 @@ def delete_item(id):
if not current_permissions['actions'].get('can_delete', False): if not current_permissions['actions'].get('can_delete', False):
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion (Löschen) auszuführen.', 'error') flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion (Löschen) auszuführen.', 'error')
return redirect(url_for('home')) return redirect(url_for('home_admin'))
if not cfg.MODULES.is_enabled('inventory'): if not cfg.MODULES.is_enabled('inventory'):
flash('Bibliotheks-Modul ist deaktiviert.', 'error') flash('Bibliotheks-Modul ist deaktiviert.', 'error')
return redirect(url_for('library')) return redirect(url_for('library_view'))
# Resolve all related item ids (grouped variants) and load their data # Resolve all related item ids (grouped variants) and load their data
group_item_ids = it.get_group_item_ids(id) group_item_ids = it.get_group_item_ids(id)
@@ -6448,11 +6428,11 @@ def delete_library_item(id):
if not current_permissions['actions'].get('can_delete', False): if not current_permissions['actions'].get('can_delete', False):
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion (Löschen) auszuführen.', 'error') flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion (Löschen) auszuführen.', 'error')
return redirect(url_for('home')) return redirect(url_for('home_admin'))
if not cfg.MODULES.is_enabled('library'): if not cfg.MODULES.is_enabled('library'):
flash('Bibliotheks-Modul ist deaktiviert.', 'error') flash('Bibliotheks-Modul ist deaktiviert.', 'error')
return redirect(url_for('home')) return redirect(url_for('home_admin'))
# Verify item exists and is a library item # Verify item exists and is a library item
try: try:
@@ -6522,11 +6502,11 @@ def bulk_delete_items():
if not current_permissions['actions'].get('can_delete', False): if not current_permissions['actions'].get('can_delete', False):
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion (Löschen) auszuführen.', 'error') flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion (Löschen) auszuführen.', 'error')
return redirect(url_for('home')) return redirect(url_for('home_admin'))
if not cfg.MODULES.is_enabled('inventory'): if not cfg.MODULES.is_enabled('inventory'):
flash('Bibliotheks-Modul ist deaktiviert.', 'error') flash('Bibliotheks-Modul ist deaktiviert.', 'error')
return redirect(url_for('library')) return redirect(url_for('library_view'))
payload = request.get_json(silent=True) or {} payload = request.get_json(silent=True) or {}
item_ids = payload.get('item_ids') or request.form.getlist('item_ids') item_ids = payload.get('item_ids') or request.form.getlist('item_ids')
@@ -6587,11 +6567,11 @@ def edit_item(id):
if not current_permissions['actions'].get('can_edit', False): if not current_permissions['actions'].get('can_edit', False):
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion (Löschen) auszuführen.', 'error') flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion (Löschen) auszuführen.', 'error')
return redirect(url_for('home')) return redirect(url_for('home_admin'))
if not cfg.MODULES.is_enabled('inventory'): if not cfg.MODULES.is_enabled('inventory'):
flash('Bibliotheks-Modul ist deaktiviert.', 'error') flash('Bibliotheks-Modul ist deaktiviert.', 'error')
return redirect(url_for('library')) return redirect(url_for('library_view'))
# Strip whitespace from all text fields # Strip whitespace from all text fields
name = sanitize_form_value(request.form.get('name')) name = sanitize_form_value(request.form.get('name'))
@@ -6721,7 +6701,7 @@ def update_group():
if not current_permissions['actions'].get('can_edit', False): if not current_permissions['actions'].get('can_edit', False):
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion (Löschen) auszuführen.', 'error') flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion (Löschen) auszuführen.', 'error')
return redirect(url_for('library')) return redirect(url_for('library_view'))
data = request.get_json() data = request.get_json()
series_group_id = data.get('series_group_id') series_group_id = data.get('series_group_id')
@@ -7005,7 +6985,7 @@ def ausleihen(id):
if requested_return_to == 'library' and cfg.MODULES.is_enabled('library'): if requested_return_to == 'library' and cfg.MODULES.is_enabled('library'):
redirect_target = 'library_view' redirect_target = 'library_view'
else: else:
redirect_target = 'home_admin' if us.check_admin(username) else 'home' # check for plausability redirect_target = 'home_admin'
item = it.get_item(id) item = it.get_item(id)
if not item: if not item:
@@ -7069,9 +7049,10 @@ def ausleihen(id):
if current_permissions['actions'].get('can_borrow') and not is_library_item: if current_permissions['actions'].get('can_borrow') and not is_library_item:
if student_card_id: if student_card_id:
student_user = us.get_user_by_student_card(student_card_id) student_user = us.get_user_by_student_card(student_card_id)
app.logger.debug(f"Borrowing on behalf of student card {student_card_id}: found user {student_user}")
if not student_user: if not student_user:
flash('Keine Schülerin/kein Schüler mit dieser Ausweis-ID gefunden.', 'error') flash('Keine Schülerin/kein Schüler mit dieser Ausweis-ID gefunden.', 'error')
return redirect(url_for('home_admin')) return redirect(url_for('library_view'))
effective_borrower = student_user.get('Username') or student_user.get('username') or username effective_borrower = student_user.get('Username') or student_user.get('username') or username
if borrow_duration_days is None: if borrow_duration_days is None:
try: try:
@@ -7321,7 +7302,7 @@ def zurueckgeben(id):
item = it.get_item(id) item = it.get_item(id)
if not item: if not item:
flash('Element nicht gefunden', 'error') flash('Element nicht gefunden', 'error')
return redirect(url_for('home')) return redirect(url_for('home_admin'))
username = session['username'] username = session['username']
@@ -7903,20 +7884,20 @@ def terminplan():
if not cfg.MODULES.is_enabled('terminplan'): if not cfg.MODULES.is_enabled('terminplan'):
flash('Der Terminplaner ist deaktiviert.', 'info') flash('Der Terminplaner ist deaktiviert.', 'info')
return redirect(url_for('home')) return redirect(url_for('home_admin'))
# Make sure the template exists # Make sure the template exists
template_path = os.path.join(BASE_DIR, 'templates', 'terminplan.html') template_path = os.path.join(BASE_DIR, 'templates', 'terminplan.html')
if not os.path.exists(template_path): if not os.path.exists(template_path):
print(f"Template file not found: {template_path}") print(f"Template file not found: {template_path}")
flash('Vorlage nicht gefunden. Bitte kontaktieren Sie den Administrator.', 'error') flash('Vorlage nicht gefunden. Bitte kontaktieren Sie den Administrator.', 'error')
return redirect(url_for('home')) return redirect(url_for('home_admin'))
return render_template('terminplan.html', school_periods=SCHOOL_PERIODS) return render_template('terminplan.html', school_periods=SCHOOL_PERIODS)
except Exception as e: except Exception as e:
app.logger.error(f"Error rendering terminplan: {e}") app.logger.error(f"Error rendering terminplan: {e}")
flash('Ein Fehler ist beim Anzeigen des Kalenders aufgetreten.', 'error') flash('Ein Fehler ist beim Anzeigen des Kalenders aufgetreten.', 'error')
return redirect(url_for('home')) return redirect(url_for('home_admin'))
'''-------------------------------------------------------------------------------------------------------------ADMIN ROUTES------------------------------------------------------------------------------------------------------------------''' '''-------------------------------------------------------------------------------------------------------------ADMIN ROUTES------------------------------------------------------------------------------------------------------------------'''
@@ -7924,66 +7905,69 @@ def terminplan():
@app.route('/register', methods=['GET', 'POST']) @app.route('/register', methods=['GET', 'POST'])
def register(): def register():
""" """
User registration route.false User registration route.
Returns: Returns:
flask.Response: Rendered template or redirect flask.Response: Rendered template or redirect
""" """
if 'username' not in session: if 'username' not in session:
flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error') flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adresse zu nutzen, versuchen Sie es erneut, nachdem Sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error')
return redirect(url_for('login')) return redirect(url_for('login'))
if 'username' in session:
if request.method == 'POST':
password = request.form['password']
name = (request.form.get('name') or '').strip()
last_name = (request.form.get('last-name') or '').strip()
# Generate a username from the first 2 letters of first and last name. if request.method == 'POST':
username = us.build_unique_username_from_name(name, last_name) password = request.form.get('password', '')
name = (request.form.get('name') or '').strip()
last_name = (request.form.get('last-name') or '').strip()
permission_preset = (request.form.get('permission_preset') or 'standard_user').strip() username = us.build_unique_username_from_name(name, last_name)
use_custom_permissions = request.form.get('use_custom_permissions') == 'on'
if not username or not password or not name or not last_name: permission_preset = (request.form.get('permission_preset') or 'standard_user').strip()
flash('Bitte füllen Sie alle Felder aus', 'error') use_custom_permissions = request.form.get('use_custom_permissions') == 'on'
return redirect(url_for('register'))
if not us.check_password_strength(password):
flash('Passwort ist zu schwach', 'error')
return redirect(url_for('register'))
action_permissions = None if not username or not password or not name or not last_name:
page_permissions = None flash('Bitte füllen Sie alle Felder aus', 'error')
if use_custom_permissions: return redirect(url_for('register'))
action_permissions = {}
for action_key, _ in PERMISSION_ACTION_OPTIONS:
action_permissions[action_key] = request.form.get(f'action_{action_key}') == 'on'
page_permissions = {} if not us.check_password_strength(password):
for endpoint_name, _ in PERMISSION_PAGE_OPTIONS: flash('Passwort ist zu schwach oder entspricht nicht den Richtlinien', 'error')
page_permissions[endpoint_name] = request.form.get(f'page_{endpoint_name}') == 'on' return redirect(url_for('register'))
us.add_user( action_permissions = None
username, page_permissions = None
password, if use_custom_permissions:
name, action_permissions = {}
last_name, for action_key, _ in PERMISSION_ACTION_OPTIONS:
is_student=False, action_permissions[action_key] = request.form.get(f'action_{action_key}') == 'on'
student_card_id=None,
max_borrow_days=None, page_permissions = {}
permission_preset=permission_preset, for endpoint_name, _ in PERMISSION_PAGE_OPTIONS:
action_permissions=action_permissions, page_permissions[endpoint_name] = request.form.get(f'page_{endpoint_name}') == 'on'
page_permissions=page_permissions,
) us.add_user(
return redirect(url_for('home')) username,
return render_template( password,
'register.html', name,
library_module_enabled=cfg.MODULES.is_enabled('library'), last_name,
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'), is_student=False,
student_default_borrow_days=cfg.STUDENT_DEFAULT_BORROW_DAYS, student_card_id=None,
student_max_borrow_days=cfg.STUDENT_MAX_BORROW_DAYS max_borrow_days=None,
permission_preset=permission_preset,
action_permissions=action_permissions,
page_permissions=page_permissions,
) )
flash('Sie sind nicht berechtigt, diese Seite anzuzeigen', 'error')
return redirect(url_for('login'))
flash(f'Benutzer "{username}" wurde erfolgreich registriert!', 'success')
return redirect(url_for('home_admin'))
return render_template(
'register.html',
library_module_enabled=cfg.MODULES.is_enabled('library'),
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
student_default_borrow_days=cfg.STUDENT_DEFAULT_BORROW_DAYS,
student_max_borrow_days=cfg.STUDENT_MAX_BORROW_DAYS,
permission_presets=getattr(us, 'PERMISSION_PRESETS', {}),
permission_action_options=PERMISSION_ACTION_OPTIONS,
permission_page_options=PERMISSION_PAGE_OPTIONS
)
@app.route('/user_del', methods=['GET']) @app.route('/user_del', methods=['GET'])
def user_del(): def user_del():
@@ -8658,7 +8642,7 @@ def admin_create_invoice(borrow_id):
'damage_reason': damage_reason, 'damage_reason': damage_reason,
'created_at': now, 'created_at': now,
'created_by': session.get('username', ''), 'created_by': session.get('username', ''),
'borrower': borrower, 'borrower': decrypt_text(borrower),
'item_id': str(item_doc['_id']), 'item_id': str(item_doc['_id']),
'item_name': item_name, 'item_name': item_name,
'item_code': item_code, 'item_code': item_code,
@@ -9108,7 +9092,7 @@ def library_item_invoices(item_id):
if not current_permissions['pages'].get('library_loans_admin', False): if not current_permissions['pages'].get('library_loans_admin', False):
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error') flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error')
return redirect(url_for('library')) return redirect(url_for('library_view'))
client = None client = None
try: try:
@@ -10015,7 +9999,7 @@ def fetch_book_info(isbn):
if not current_permissions['actions'].get('can_insert', False): if not current_permissions['actions'].get('can_insert', False):
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error') flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error')
return redirect(url_for('library')) return redirect(url_for('library_view'))
if not cfg.MODULES.is_enabled('library'): if not cfg.MODULES.is_enabled('library'):
return jsonify({"error": "Bibliotheks-Modul ist deaktiviert."}), 403 return jsonify({"error": "Bibliotheks-Modul ist deaktiviert."}), 403
@@ -10066,7 +10050,7 @@ def download_book_cover():
if not current_permissions['actions'].get('can_insert', False): if not current_permissions['actions'].get('can_insert', False):
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error') flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error')
return redirect(url_for('library')) return redirect(url_for('library_view'))
if not cfg.MODULES.is_enabled('library'): if not cfg.MODULES.is_enabled('library'):
return jsonify({"error": "Bibliotheks-Modul ist deaktiviert."}), 403 return jsonify({"error": "Bibliotheks-Modul ist deaktiviert."}), 403
@@ -10438,7 +10422,7 @@ def notifications_view():
except Exception as exc: except Exception as exc:
app.logger.error(f"Error loading notifications: {exc}") app.logger.error(f"Error loading notifications: {exc}")
flash('Fehler beim Laden der Benachrichtigungen.', 'error') flash('Fehler beim Laden der Benachrichtigungen.', 'error')
return redirect(url_for('home')) return redirect(url_for('home_admin'))
finally: finally:
if client: if client:
client.close() client.close()
+10 -10
View File
@@ -97,18 +97,18 @@ def build_name_synonym(first_name, last_name=''):
last = _clean_name_fragment(last_name) last = _clean_name_fragment(last_name)
if first and last: if first and last:
return (first[:2] + last[:2]).title() return (first[:3] + last[:3]).title()
combined = (first + last) combined = (first + last)
if not combined: if not combined:
return 'User' return 'User'
return combined[:4].title() return combined[:6].title()
def build_username_from_name(first_name, last_name=''): def build_username_from_name(first_name, last_name=''):
""" """
Build a deterministic username abbreviation from first and last name. Build a deterministic username abbreviation from first and last name.
Uses 2 letters from each name and stores it lowercase. Uses 3 letters from each name and stores it lowercase.
Args: Args:
first_name (str): First name first_name (str): First name
@@ -123,12 +123,12 @@ def build_username_from_name(first_name, last_name=''):
def build_unique_username_from_name(first_name, last_name=''): def build_unique_username_from_name(first_name, last_name=''):
""" """
Build a unique username from the first 2 letters of the first name and Build a unique username from the first 3 letters of the first name and
the first 2 letters of the last name. the first 3 letters of the last name.
""" """
first = _clean_name_fragment(first_name) first = _clean_name_fragment(first_name)
last = _clean_name_fragment(last_name) last = _clean_name_fragment(last_name)
base_username = (first[:2] + last[:2]).lower() base_username = (first[:3] + last[:3]).lower()
if not base_username: if not base_username:
base_username = 'user' base_username = 'user'
@@ -594,8 +594,8 @@ def student_card_exists(student_card_id):
return False return False
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client) db = _get_tenant_db(client)
users = db['users'] users = db['student_cards']
exists = users.find_one({'StudentCardId': normalized}) is not None exists = users.find_one({'SchülerName': normalized}) is not None
client.close() client.close()
return exists return exists
@@ -607,8 +607,8 @@ def get_user_by_student_card(student_card_id):
return None return None
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client) db = _get_tenant_db(client)
users = db['users'] users = db['student_cards']
found_user = users.find_one({'StudentCardId': normalized}) found_user = users.find_one({'SchülerName': normalized})
client.close() client.close()
return found_user return found_user
+1
View File
@@ -1,4 +1,5 @@
flask flask
flask-wtf
werkzeug werkzeug
gunicorn gunicorn
pymongo pymongo
+38 -14
View File
@@ -2309,6 +2309,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
</script> </script>
<div class="admin-content-container"> <div class="admin-content-container">
{% if current_permissions.actions.get('can_edit', False) %}
<!-- Admin conflict warning banner (populated by JS) --> <!-- Admin conflict warning banner (populated by JS) -->
<div id="conflict-banner" style="display:none; background:#fff3cd; border:1px solid #ffc107; border-left:4px solid #fd7e14; color:#856404; padding:12px 16px; border-radius:4px; margin-bottom:16px; position:relative;"> <div id="conflict-banner" style="display:none; background:#fff3cd; border:1px solid #ffc107; border-left:4px solid #fd7e14; color:#856404; padding:12px 16px; border-radius:4px; margin-bottom:16px; position:relative;">
<strong>⚠ Buchungskonflikte erkannt:</strong> <strong>⚠ Buchungskonflikte erkannt:</strong>
@@ -2335,6 +2336,8 @@ document.addEventListener('DOMContentLoaded', ()=>{
.catch(() => {}); .catch(() => {});
}); });
</script> </script>
{% endif %}
<div class="content"> <div class="content">
<h1 style="position:relative;">Inventar Objekte <h1 style="position:relative;">Inventar Objekte
<div class="view-switch-group"> <div class="view-switch-group">
@@ -2344,14 +2347,17 @@ document.addEventListener('DOMContentLoaded', ()=>{
<button id="toggle-favorites-view" class="view-toggle-btn" title="Nur Merkliste anzeigen"> <button id="toggle-favorites-view" class="view-toggle-btn" title="Nur Merkliste anzeigen">
<span id="favorites-view-icon">🔖</span> <span id="favorites-view-icon">🔖</span>
</button> </button>
{% if current_permissions.actions.get('can_delete', False) %}
<div class="bulk-delete-inline-wrap"> <div class="bulk-delete-inline-wrap">
<button type="button" id="bulk-delete-drawer-toggle" class="view-toggle-btn bulk-delete-drawer-toggle" aria-expanded="false" title="Massenlöschung" onclick="toggleBulkDeleteDrawer()"> <button type="button" id="bulk-delete-drawer-toggle" class="view-toggle-btn bulk-delete-drawer-toggle" aria-expanded="false" title="Massenlöschung" onclick="toggleBulkDeleteDrawer()">
<span class="drawer-icon"></span> <span class="drawer-icon"></span>
</button> </button>
</div> </div>
{% endif %}
</div> </div>
</h1> </h1>
<div id="items-indicator" class="items-indicator">Objekte im System: 0</div> <div id="items-indicator" class="items-indicator">Objekte im System: 0</div>
{% if current_permissions.actions.get('can_delete', False) %}
<div id="bulk-delete-drawer" class="bulk-delete-drawer" aria-hidden="true"> <div id="bulk-delete-drawer" class="bulk-delete-drawer" aria-hidden="true">
<div class="bulk-delete-content"> <div class="bulk-delete-content">
<strong>Massenlöschung nach Kriterien</strong> <strong>Massenlöschung nach Kriterien</strong>
@@ -2364,6 +2370,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
<button type="button" id="bulk-delete-button" class="danger" onclick="deleteSelectedItems()" disabled>Auswahl löschen</button> <button type="button" id="bulk-delete-button" class="danger" onclick="deleteSelectedItems()" disabled>Auswahl löschen</button>
</div> </div>
</div> </div>
{% endif %}
<div class="filter-container"> <div class="filter-container">
<div class="filter-group"> <div class="filter-group">
<div class="filter-header"> <div class="filter-header">
@@ -2469,6 +2476,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
</div> </div>
<!-- Add the edit modal form --> <!-- Add the edit modal form -->
{% if current_permissions.actions.get('can_edit', False) %}
<div id="edit-modal" class="item-modal"> <div id="edit-modal" class="item-modal">
<div class="modal-content"> <div class="modal-content">
<span class="close-modal" onclick="closeEditModal()">&times;</span> <span class="close-modal" onclick="closeEditModal()">&times;</span>
@@ -2646,6 +2654,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
</form> </form>
</div> </div>
</div> </div>
{% endif %}
</div> </div>
<!-- Schedule Appointment Modal --> <!-- Schedule Appointment Modal -->
@@ -2724,7 +2733,6 @@ document.addEventListener('DOMContentLoaded', ()=>{
</div> </div>
</div> </div>
<!-- Initialize template variables before any JavaScript code -->
<script> <script>
// Create a global object to hold server-side template values // Create a global object to hold server-side template values
window.serverVars = {}; window.serverVars = {};
@@ -3239,7 +3247,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
return; return;
} }
fetch('{{ url_for('bulk_delete_items') }}', { fetch("{{ url_for('bulk_delete_items') }}", {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -3703,6 +3711,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
Für Löschung markieren Für Löschung markieren
</label> </label>
</div> </div>
{% if current_permissions.actions.get('can_borrow', False) %}
${isAvailableForBorrow && !item.BlockedNow ? ${isAvailableForBorrow && !item.BlockedNow ?
`<form method="POST" action="{{ url_for('ausleihen', id='') }}${item._id}"> `<form method="POST" action="{{ url_for('ausleihen', id='') }}${item._id}">
${isGroupedItem ? ` ${isGroupedItem ? `
@@ -3724,12 +3733,21 @@ document.addEventListener('DOMContentLoaded', ()=>{
: :
`<button class="ausleihen disabled-button" disabled>${item.BlockedNow ? 'Reserviert' : 'Ausgeliehen'}</button>` `<button class="ausleihen disabled-button" disabled>${item.BlockedNow ? 'Reserviert' : 'Ausgeliehen'}</button>`
} }
{% endif %}
{% if current_permissions.actions.get('can_delete', False) %}
<form method="POST" action="{{ url_for('delete_item', id='') }}${item._id}" style="display:inline;" onsubmit="return confirm('Sind Sie sicher, dass Sie dieses Objekt löschen möchten?')"> <form method="POST" action="{{ url_for('delete_item', id='') }}${item._id}" style="display:inline;" onsubmit="return confirm('Sind Sie sicher, dass Sie dieses Objekt löschen möchten?')">
<button class="delete-button" type="submit">Löschen</button> <button class="delete-button" type="submit">Löschen</button>
</form> </form>
{% endif %}
{% if current_permissions.actions.get('can_edit', False) %}
<button class="edit-button" onclick="openEditModalForSelectedUnit('${item._id}', 'specific-item-card-${item._id}')">Bearbeiten</button> <button class="edit-button" onclick="openEditModalForSelectedUnit('${item._id}', 'specific-item-card-${item._id}')">Bearbeiten</button>
{% endif %}
{% if current_permissions.actions.get('can_insert', False) %}
<button class="duplicate-button" onclick="duplicateItem('${item._id}')">Duplizieren</button> <button class="duplicate-button" onclick="duplicateItem('${item._id}')">Duplizieren</button>
{% endif %}
{% if current_permissions.pages.get('admin_school_settings', False) %}
${canScheduleItem ? `<button class="schedule-button" onclick="openScheduleModal('${item._id}')">Reservieren</button>` : ''} ${canScheduleItem ? `<button class="schedule-button" onclick="openScheduleModal('${item._id}')">Reservieren</button>` : ''}
{% endif %}
</div> </div>
`; `;
itemsContainer.appendChild(card); itemsContainer.appendChild(card);
@@ -4506,7 +4524,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
: '<div style="font-size:0.92rem;color:#64748b;">Keine Beschädigungs-Historie vorhanden.</div>'; : '<div style="font-size:0.92rem;color:#64748b;">Keine Beschädigungs-Historie vorhanden.</div>';
modalContent.innerHTML = ` modalContent.innerHTML = `
<h2>${item.Name}</h2> <h2>${escapeHtml(item.Name || '')}</h2>
${borrowerInfoHtml} ${borrowerInfoHtml}
${appointmentInfoHtml} ${appointmentInfoHtml}
@@ -4522,7 +4540,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
<div class="modal-details"> <div class="modal-details">
<div class="detail-group"> <div class="detail-group">
<div class="detail-label">Ort:</div> <div class="detail-label">Ort:</div>
<div class="detail-value">${item.Ort || '-'}</div> <div class="detail-value">${escapeHtml(item.Ort || '-')}</div>
</div> </div>
<div class="detail-group"> <div class="detail-group">
@@ -4534,50 +4552,51 @@ document.addEventListener('DOMContentLoaded', ()=>{
<div class="detail-group"> <div class="detail-group">
<div class="detail-label">Unterrichtsfach:</div> <div class="detail-label">Unterrichtsfach:</div>
<div class="detail-value">${filter1Array.join(', ') || '-'}</div> <div class="detail-value">${escapeHtml(filter1Array.join(', ') || '-')}</div>
</div> </div>
<div class="detail-group"> <div class="detail-group">
<div class="detail-label">Jahrgangsstufe:</div> <div class="detail-label">Jahrgangsstufe:</div>
<div class="detail-value">${filter2Array.join(', ') || '-'}</div> <div class="detail-value">${escapeHtml(filter2Array.join(', ') || '-')}</div>
</div> </div>
<div class="detail-group"> <div class="detail-group">
<div class="detail-label">Schlagwort:</div> <div class="detail-label">Schlagwort:</div>
<div class="detail-value">${filter3Array.join(', ') || '-'}</div> <div class="detail-value">${escapeHtml(filter3Array.join(', ') || '-')}</div>
</div> </div>
<div class="detail-group"> <div class="detail-group">
<div class="detail-label">Code:</div> <div class="detail-label">Code:</div>
<div class="detail-value">${item.Code_4 || '-'}</div> <div class="detail-value">${escapeHtml(item.Code_4 || '-')}</div>
</div> </div>
<div class="detail-group"> <div class="detail-group">
<div class="detail-label">Anzahl:</div> <div class="detail-label">Anzahl:</div>
<div class="detail-value">${item.GroupedDisplayCount || 1}</div> <div class="detail-value">${escapeHtml(String(item.GroupedDisplayCount || 1))}</div>
</div> </div>
${isGroupedItem ? ` ${isGroupedItem ? `
<div class="detail-group"> <div class="detail-group">
<div class="detail-label">Verfügbar:</div> <div class="detail-label">Verfügbar:</div>
<div class="detail-value">${availableGroupedCount}</div> <div class="detail-value">${escapeHtml(String(availableGroupedCount))}</div>
</div>` : ''} </div>` : ''}
<div class="detail-group"> <div class="detail-group">
<div class="detail-label">Anschaffungsjahr:</div> <div class="detail-label">Anschaffungsjahr:</div>
<div class="detail-value">${item.Anschaffungsjahr || '-'}</div> <div class="detail-value">${escapeHtml(String(item.Anschaffungsjahr || '-'))}</div>
</div> </div>
<div class="detail-group"> <div class="detail-group">
<div class="detail-label">Anschaffungskosten:</div> <div class="detail-label">Anschaffungskosten:</div>
<div class="detail-value">${item.Anschaffungskosten ? item.Anschaffungskosten + ' €' : '-'}</div> <div class="detail-value">${item.Anschaffungskosten ? escapeHtml(String(item.Anschaffungskosten)) + ' €' : '-'}</div>
</div> </div>
<div class="detail-group full-width"> <div class="detail-group full-width">
<div class="detail-label">Beschreibung:</div> <div class="detail-label">Beschreibung:</div>
<div class="detail-value">${item.Beschreibung || '-'}</div> <div class="detail-value">${escapeHtml(item.Beschreibung || '-')}</div>
</div> </div>
{% if current_permissions.actions.get('can_view_logs', False) %}
<div class="detail-group full-width" style="margin-top:12px;"> <div class="detail-group full-width" style="margin-top:12px;">
<div class="detail-label" style="font-weight:600; color:#374151;">Beschädigungs-Historie</div> <div class="detail-label" style="font-weight:600; color:#374151;">Beschädigungs-Historie</div>
<div class="detail-value"> <div class="detail-value">
@@ -4590,6 +4609,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
</div> </div>
</div> </div>
</div> </div>
{% endif %}
<div class="detail-group full-width" style="margin-top:12px; padding:10px; border:1px solid #e3e3e3; border-radius:8px;"> <div class="detail-group full-width" style="margin-top:12px; padding:10px; border:1px solid #e3e3e3; border-radius:8px;">
<div class="detail-label">Verfügbarkeit prüfen:</div> <div class="detail-label">Verfügbarkeit prüfen:</div>
@@ -4638,6 +4658,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
<div class="modal-actions"> <div class="modal-actions">
${!isBorrowed ? ${!isBorrowed ?
{% if current_permissions.actions.get('can_borrow', False) %}
`<form method="POST" action="/ausleihen/${item._id}"> `<form method="POST" action="/ausleihen/${item._id}">
${isGroupedItem ? ` ${isGroupedItem ? `
<div style="display:flex; gap:8px; flex-wrap:wrap; margin-bottom:8px; align-items:center;"> <div style="display:flex; gap:8px; flex-wrap:wrap; margin-bottom:8px; align-items:center;">
@@ -4651,6 +4672,9 @@ document.addEventListener('DOMContentLoaded', ()=>{
</div>` : ''} </div>` : ''}
<button class="ausleihen" type="submit">Ausleihen</button> <button class="ausleihen" type="submit">Ausleihen</button>
</form>` </form>`
{% else %}
`<button class="ausleihen disabled-button" disabled title="Keine Berechtigung">Ausleihen nicht möglich</button>`
{% endif %}
: (!isGroupedItem && item.User === currentUsername) ? : (!isGroupedItem && item.User === currentUsername) ?
`<form method="POST" action="/zurueckgeben/${item._id}"> `<form method="POST" action="/zurueckgeben/${item._id}">
<button class="ausleihen" type="submit">Zurückgeben</button> <button class="ausleihen" type="submit">Zurückgeben</button>
@@ -4659,7 +4683,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
} }
<button class="edit-button" onclick="openEditModalForSelectedUnit('${item._id}', 'specific-item-modal-${item._id}')">Bearbeiten</button> <button class="edit-button" onclick="openEditModalForSelectedUnit('${item._id}', 'specific-item-modal-${item._id}')">Bearbeiten</button>
<button class="duplicate-button" onclick="duplicateItem('${item._id}')">Duplizieren</button> <button class="duplicate-button" onclick="duplicateItem('${item._id}')">Duplizieren</button>
${damageReports.length > 0 ? `<button class="damage-button" onclick="markDamageAsRepaired('${item._id}')">Repariert</button>` : `<button class="damage-button" onclick="registerDamage('${item._id}')">Schaden melden</button>`} ${damageReports && damageReports.length > 0 ? `<button class="damage-button" onclick="markDamageAsRepaired('${item._id}')">Repariert</button>` : `<button class="damage-button" onclick="registerDamage('${item._id}')">Schaden melden</button>`}
${canScheduleItem ? `<button class="schedule-button" onclick="openScheduleModal('${item._id}')">Reservieren</button>` : ''} ${canScheduleItem ? `<button class="schedule-button" onclick="openScheduleModal('${item._id}')">Reservieren</button>` : ''}
<form method="POST" action="/delete_item/${item._id}" style="display:inline;" onsubmit="return confirm('Sind Sie sicher?')"> <form method="POST" action="/delete_item/${item._id}" style="display:inline;" onsubmit="return confirm('Sind Sie sicher?')">
<button class="delete-button" type="submit">Löschen</button> <button class="delete-button" type="submit">Löschen</button>
+137 -83
View File
@@ -1,11 +1,3 @@
<!--
Copyright 2025-2026 AIIrondev
Licensed under the Inventarsystem EULA (Endbenutzer-Lizenzvertrag).
See Legal/LICENSE for the full license text.
Unauthorized commercial use, SaaS hosting, or removal of branding is prohibited.
For commercial licensing inquiries: https://github.com/AIIrondev
-->
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Register{% endblock %} {% block title %}Register{% endblock %}
@@ -33,6 +25,10 @@
<div class="content"> <div class="content">
<div class="form-card"> <div class="form-card">
<form method="POST" action="{{ url_for('register') }}"> <form method="POST" action="{{ url_for('register') }}">
<!-- CSRF-Schutz (Zwingend erforderlich für POST) -->
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="form-group"> <div class="form-group">
<label for="name">Vorname</label> <label for="name">Vorname</label>
<div class="input-container"> <div class="input-container">
@@ -44,7 +40,7 @@
<span class="input-icon">👤</span> <span class="input-icon">👤</span>
<input type="text" id="last-name" name="last-name" placeholder="Geben Sie den Nachnamen ein" required onchange="generateUsername()" oninput="generateUsername()"> <input type="text" id="last-name" name="last-name" placeholder="Geben Sie den Nachnamen ein" required onchange="generateUsername()" oninput="generateUsername()">
</div> </div>
<label for="username">Benutzername <span style="color: #9ca3af;">(wird automatisch generiert)</span></label> <label for="username">Benutzername <span style="color: #9ca3af;">(Vorschau - wird serverseitig finalisiert)</span></label>
<div class="input-container"> <div class="input-container">
<span class="input-icon">👤</span> <span class="input-icon">👤</span>
<input type="text" id="username" name="username" placeholder="Automatisch aus Name und Nachname" readonly style="background-color: #f3f4f6; cursor: not-allowed;"> <input type="text" id="username" name="username" placeholder="Automatisch aus Name und Nachname" readonly style="background-color: #f3f4f6; cursor: not-allowed;">
@@ -64,9 +60,22 @@
<li id="pw-rule-symbol" class="pw-rule">Mindestens ein Sonderzeichen</li> <li id="pw-rule-symbol" class="pw-rule">Mindestens ein Sonderzeichen</li>
</ul> </ul>
</div> </div>
<div class="input-container">
<span class="input-icon">🔒</span> <div class="input-wrapper">
<input type="password" id="password" name="password" placeholder="Geben Sie ein sicheres Passwort ein" required> <div class="input-container">
<span class="input-icon">🔒</span>
<!-- HTML5 Pattern blockiert unsichere Passwörter vor dem Absenden -->
<input type="password"
id="password"
name="password"
placeholder="Geben Sie ein sicheres Passwort ein"
required
pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9]).{12,}">
</div>
<button type="button" class="btn-secondary" id="toggle-pw-btn" onclick="togglePasswordVisibility()" title="Passwort anzeigen/verbergen">👁️</button>
</div>
<div class="pw-actions">
<button type="button" class="btn-secondary" onclick="generateSecurePassword()">🎲 Automatisches Passwort generieren</button>
</div> </div>
</div> </div>
@@ -258,31 +267,6 @@ input::placeholder {
background-color: #27ae60; background-color: #27ae60;
} }
.navigation-buttons {
text-align: center;
margin: 1.5rem 0;
}
.back-button {
display: inline-flex;
align-items: center;
color: var(--primary-color);
text-decoration: none;
font-weight: 500;
transition: var(--transition);
padding: 0.5rem 1rem;
border-radius: var(--border-radius);
}
.back-button:hover {
background-color: rgba(52, 152, 219, 0.1);
}
.back-icon {
margin-right: 0.5rem;
font-size: 1.2rem;
}
.flash-container { .flash-container {
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
} }
@@ -422,49 +406,147 @@ input::placeholder {
margin: 4px 0; margin: 4px 0;
color: #1f2937; color: #1f2937;
} }
/* Neue Styles für die Passwort-Erweiterungen */
.pw-actions {
display: flex;
gap: 8px;
margin-top: 8px;
}
.btn-secondary {
padding: 8px 12px;
border: 1px solid #d1d5db;
background-color: #f3f4f6;
border-radius: 4px;
cursor: pointer;
font-size: 0.9em;
transition: background-color 0.2s;
}
.btn-secondary:hover {
background-color: #e5e7eb;
}
.input-wrapper {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
}
.input-wrapper .input-container {
flex-grow: 1;
margin: 0;
}
</style> </style>
<script> <script>
// Function to generate username from first and last name (helper function) // Hilfsfunktion: Umlaute auflösen und Sonderzeichen entfernen
function cleanNameForUsername(text) { function cleanNameForUsername(text) {
if (!text) return ''; if (!text) return '';
// Remove special characters, convert umlauts, lowercase let cleaned = text.trim().toLowerCase()
let cleaned = text
.replace(/[^a-zA-Zäöüß\s-]/g, '')
.trim()
.toLowerCase();
// Convert German umlauts to ASCII
cleaned = cleaned
.replace(/ä/g, 'ae') .replace(/ä/g, 'ae')
.replace(/ö/g, 'oe') .replace(/ö/g, 'oe')
.replace(/ü/g, 'ue') .replace(/ü/g, 'ue')
.replace(/ß/g, 'ss'); .replace(/ß/g, 'ss')
.replace(/[^a-z]/g, '');
return cleaned; return cleaned;
} }
// Generate username from name and last_name fields // Hilfsfunktion: Erster Buchstabe groß
function formatPart(str, len) {
if (!str) return '';
const part = str.slice(0, len);
return part.charAt(0).toUpperCase() + part.slice(1);
}
// Generiert die Vorschau des Benutzernamens (z.B. SimFri)
function generateUsername() { function generateUsername() {
const firstName = cleanNameForUsername(document.getElementById('name').value || ''); const firstName = cleanNameForUsername(document.getElementById('name').value);
const lastName = cleanNameForUsername(document.getElementById('last-name').value || ''); const lastName = cleanNameForUsername(document.getElementById('last-name').value);
let username = ''; let username = '';
if (firstName && lastName) { if (firstName && lastName) {
username = (firstName.slice(0, 3) + lastName.slice(0, 3)); username = formatPart(firstName, 3) + formatPart(lastName, 3);
} else if (firstName) { } else if (firstName) {
username = firstName.slice(0, 6); username = formatPart(firstName, 6);
} else if (lastName) { } else if (lastName) {
username = lastName.slice(0, 6); username = formatPart(lastName, 6);
} }
// Set the username field
const usernameField = document.getElementById('username'); const usernameField = document.getElementById('username');
if (usernameField) { if (usernameField) {
usernameField.value = username || ''; usernameField.value = username || '';
} }
} }
// PASSWORT GENERATOR
function generateSecurePassword() {
const length = 16;
const charsetLower = "abcdefghijklmnopqrstuvwxyz";
const charsetUpper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const charsetNum = "0123456789";
const charsetSym = "!@#$%^&*()_+~|}{[]:;?><,.-=";
let password = "";
// Garantiert mindestens 1 Zeichen aus jeder Kategorie
password += charsetLower[Math.floor(Math.random() * charsetLower.length)];
password += charsetUpper[Math.floor(Math.random() * charsetUpper.length)];
password += charsetNum[Math.floor(Math.random() * charsetNum.length)];
password += charsetSym[Math.floor(Math.random() * charsetSym.length)];
const allChars = charsetLower + charsetUpper + charsetNum + charsetSym;
// Restliche Zeichen auffüllen
for (let i = 4; i < length; i++) {
password += allChars[Math.floor(Math.random() * allChars.length)];
}
// Passwort durchmischen
password = password.split('').sort(() => 0.5 - Math.random()).join('');
const pwField = document.getElementById('password');
pwField.value = password;
// Automatisch sichtbar machen, damit der User es kopieren kann
pwField.type = 'text';
document.getElementById('toggle-pw-btn').textContent = '🙈';
updatePasswordRules();
}
// PASSWORT SICHTBARKEIT UMSCHALTEN
function togglePasswordVisibility() {
const pwField = document.getElementById('password');
const toggleBtn = document.getElementById('toggle-pw-btn');
if (pwField.type === "password") {
pwField.type = "text";
toggleBtn.textContent = '🙈';
} else {
pwField.type = "password";
toggleBtn.textContent = '👁️';
}
}
// Globale Funktion für Passwort-Update
function updatePasswordRules() {
const passwordInput = document.getElementById('password');
if (!passwordInput) return;
const value = String(passwordInput.value || '');
const setRuleState = (id, ok) => {
const node = document.getElementById(id);
if (node) node.classList.toggle('ok', !!ok);
};
setRuleState('pw-rule-length', value.length >= 12);
setRuleState('pw-rule-lower', /[a-z]/.test(value));
setRuleState('pw-rule-upper', /[A-Z]/.test(value));
setRuleState('pw-rule-digit', /[0-9]/.test(value));
setRuleState('pw-rule-symbol', /[^A-Za-z0-9]/.test(value));
}
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
const permissionPresets = {{ permission_presets | tojson }}; const permissionPresets = {{ permission_presets | tojson }};
const presetSelect = document.getElementById('permission-preset'); const presetSelect = document.getElementById('permission-preset');
@@ -507,34 +589,6 @@ document.addEventListener('DOMContentLoaded', function () {
} }
const passwordInput = document.getElementById('password'); const passwordInput = document.getElementById('password');
const passwordRules = {
length: document.getElementById('pw-rule-length'),
lower: document.getElementById('pw-rule-lower'),
upper: document.getElementById('pw-rule-upper'),
digit: document.getElementById('pw-rule-digit'),
symbol: document.getElementById('pw-rule-symbol')
};
function setRuleState(node, ok) {
if (!node) {
return;
}
node.classList.toggle('ok', !!ok);
}
function updatePasswordRules() {
if (!passwordInput) {
return;
}
const value = String(passwordInput.value || '');
setRuleState(passwordRules.length, value.length >= 12);
setRuleState(passwordRules.lower, /[a-z]/.test(value));
setRuleState(passwordRules.upper, /[A-Z]/.test(value));
setRuleState(passwordRules.digit, /[0-9]/.test(value));
setRuleState(passwordRules.symbol, /[^A-Za-z0-9]/.test(value));
}
if (passwordInput) { if (passwordInput) {
passwordInput.addEventListener('input', updatePasswordRules); passwordInput.addEventListener('input', updatePasswordRules);
passwordInput.addEventListener('blur', updatePasswordRules); passwordInput.addEventListener('blur', updatePasswordRules);
+1
View File
@@ -1,4 +1,5 @@
flask flask
flask-wtf
werkzeug werkzeug
gunicorn gunicorn
pymongo==4.6.3 pymongo==4.6.3