Compare commits

...

24 Commits

Author SHA1 Message Date
Aiirondev_dev 6fcabc8638 changes to the open Damage Invoice Modal 2026-07-27 21:13:18 +02:00
Aiirondev_dev 3068f44563 implementet automatic including of the price 2026-07-27 20:16:53 +02:00
Aiirondev_dev c4dd18a2e4 change to have automatic clearing of the tmp directory 2026-07-27 15:30:34 +02:00
Aiirondev_dev d0ac21e7dd changes to the Library Type 2026-07-27 15:20:27 +02:00
Aiirondev_dev b4a505c20b changes to the borrow_record creation 2026-07-27 15:09:26 +02:00
Aiirondev_dev 39302d4d1f CHanges to the In and out 2026-07-27 14:53:09 +02:00
Aiirondev_dev e46f8b0c66 finisch of the borrower funktion 2026-07-27 14:43:39 +02:00
Aiirondev_dev 06486f039f changes for the loading of the recent borrowers that only the latest 3 get displayed 2026-07-27 14:34:59 +02:00
Aiirondev_dev feac00f0df changes to the permission development 2026-07-27 14:19:01 +02:00
Aiirondev_dev 0b0169ef96 made the Cookie Banner accoding to the ePrivacy-Richtlinie/TTDSG only a visual informative banner 2026-07-27 11:35:12 +02:00
Aiirondev_dev a6db3a001f changes to maybe have the right passtword 2026-07-27 00:32:21 +02:00
Aiirondev_dev 8fa495a23c changes to allow a easier register process 2026-07-26 23:52:08 +02:00
Aiirondev_dev 9df7a73db1 change to the CRFS 2026-07-26 23:45:49 +02:00
Aiirondev_dev dcfd23b412 changes to work 2026-07-26 23:21:56 +02:00
Aiirondev_dev d523dd0a68 change to allow for the wtf tocken to be read in correctly 2026-07-26 23:08:47 +02:00
Aiirondev_dev d7d96d5567 changes to the register Funktion 2026-07-26 22:56:39 +02:00
Aiirondev_dev a0018eebd5 slight mistake because of disregard for the new package that needs to be in the requirements 2026-07-26 22:37:45 +02:00
Aiirondev_dev eebaaef9ea changes to the register process to make it more streamlined 2026-07-26 22:30:26 +02:00
Aiirondev_dev 86112ff295 changes to the username generation to have a more brother view 2026-07-26 22:10:00 +02:00
Aiirondev_dev 048900058f Fiy for a wrong endpoint 2026-07-26 22:00:53 +02:00
Aiirondev_dev b8923b4417 Debug changes 2026-07-26 20:48:09 +02:00
Aiirondev_dev a864bab713 Fix of issues with the student user getting from the database users to student_cards 2026-07-26 20:31:16 +02:00
Aiirondev_dev 14322e11c0 ahhhhhhhh 2026-07-26 15:56:15 +02:00
Aiirondev_dev 93bb901f45 changes to make home_admmin the only home and also the main home so the style is the same and the authorisations are properly displayed 2026-07-26 15:41:31 +02:00
10 changed files with 365 additions and 280 deletions
+103 -120
View File
@@ -556,7 +556,7 @@ def _enforce_module_access():
flash(msg, 'info')
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'):
return redirect('/library')
@@ -630,7 +630,7 @@ def handle_build_error(e):
return redirect('/library')
if 'username' in session:
return redirect(url_for('home'))
return redirect(url_for('home_admin'))
return redirect(url_for('login'))
@@ -708,10 +708,10 @@ def _permission_denied_fallback_endpoint(permissions, current_endpoint=None):
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')
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:
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
if _page_access_allowed(permissions, candidate):
return candidate
@@ -1984,7 +1984,7 @@ def _upload_student_cards_excel():
if not current_permissions['actions'].get('can_manage_user', False):
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'):
flash('Schülerausweis-Modul ist deaktiviert.', 'error')
@@ -2178,7 +2178,7 @@ def _upload_excel_items(scope='inventory'):
if is_library_scope:
if not cfg.MODULES.is_enabled('library'):
flash('Bibliotheks-Modul ist deaktiviert.', 'error')
return redirect(url_for('home'))
return redirect(url_for('home_admin'))
excel_file = request.files.get(file_field)
if not excel_file or not excel_file.filename:
@@ -2194,7 +2194,7 @@ def _upload_excel_items(scope='inventory'):
# 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):
flash('Einfüge-Rechte erforderlich.', 'error')
return redirect(url_for('home'))
return redirect(url_for('home_admin'))
filename_lower = excel_file.filename.lower()
if not filename_lower.endswith(('.xlsx', '.csv')):
@@ -2882,6 +2882,7 @@ def user_status():
##################################################### changes to be made to account for the new account permison managment system ##############################
@app.route('/')
def home():
"""
@@ -2895,33 +2896,7 @@ def home():
flash('Bitte mit registriertem Konto anmelden!', 'error')
return redirect(url_for('login'))
if not cfg.MODULES.is_enabled('inventory'):
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))
return redirect(url_for('home_admin'))
@app.route('/home_admin')
def home_admin():
@@ -2942,9 +2917,12 @@ def home_admin():
else:
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')
return redirect(url_for('login'))
return redirect(url_for('library_view'))
return render_template(
'main_admin.html',
username=session['username'],
@@ -3052,7 +3030,7 @@ def library_view():
return redirect(url_for('login'))
if not cfg.MODULES.is_enabled('library'):
flash('Bibliotheks-Modul ist deaktiviert.', 'error')
return redirect(url_for('home'))
return redirect(url_for('home_admin'))
return render_template(
'library_table.html',
@@ -3080,7 +3058,7 @@ def library_loans_admin():
if not cfg.MODULES.is_enabled('library'):
flash('Bibliotheks-Modul ist deaktiviert.', 'error')
return redirect(url_for('home'))
return redirect(url_for('home_admin'))
_ensure_audit_indexes_once()
@@ -3179,6 +3157,7 @@ def library_loans_admin():
'damage_text': (damage_reports[0].get('description', '') if damage_reports else ''),
'available': bool(item_doc.get('Verfuegbar', False)),
'last_updated': fmt_dt(item_doc.get('LastUpdated')),
'acquisition_costs': item_doc.get('Anschaffungskosten', "")
})
return render_template(
@@ -3627,7 +3606,8 @@ def api_item_detail(item_id):
borrows_html = ''
if borrow_records:
rows = []
for rec in borrow_records:
for rec in borrow_records[:3]:
user_raw = rec.get('User')
try:
user = decrypt_text(user_raw) if user_raw is not None else ''
@@ -3637,7 +3617,6 @@ def api_item_detail(item_id):
start = fmt_dt(rec.get('Start'))
end = fmt_dt(rec.get('End'))
notes = html.escape(str(rec.get('Notes') or ''))
rows.append(
f"<li><strong>{html.escape(str(user or '-'))}</strong> — "
f"{html.escape(str(status))}"
@@ -3675,7 +3654,7 @@ def api_library_item_update(item_id):
if not current_permissions['actions'].get('can_edit', False):
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'):
return jsonify({'ok': False, 'message': 'Bibliotheks-Modul ist deaktiviert.'}), 403
@@ -3908,7 +3887,7 @@ def student_cards_admin():
if not current_permissions['actions'].get('can_manage_users', False):
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'):
flash('Schülerausweis-Modul ist deaktiviert.', 'error')
return redirect(url_for('home_admin'))
@@ -4053,7 +4032,7 @@ def student_cards_print():
if not current_permissions['actions'].get('can_manage_users', False):
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'):
flash('Schülerausweis-Modul ist deaktiviert.', 'error')
return redirect(url_for('home_admin'))
@@ -4087,7 +4066,7 @@ def student_card_barcode_print():
if not current_permissions['actions'].get('can_manage_users', False):
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'):
flash('Schülerausweis-Modul ist deaktiviert.', 'error')
return redirect(url_for('home_admin'))
@@ -4120,7 +4099,7 @@ def student_card_barcode_download():
if not current_permissions['actions'].get('can_manage_users', False):
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'):
flash('Schülerausweis-Modul ist deaktiviert.', 'error')
return redirect(url_for('home_admin'))
@@ -4304,7 +4283,7 @@ def student_card_single_barcode_download(card_id):
if not current_permissions['actions'].get('can_manage_users', False):
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'):
flash('Schülerausweis-Modul ist deaktiviert.', 'error')
return redirect(url_for('home_admin'))
@@ -4478,7 +4457,7 @@ def login():
flask.Response: Rendered template or redirect
"""
if 'username' in session:
return redirect(url_for('home'))
return redirect(url_for('home_admin'))
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
@@ -4514,7 +4493,7 @@ def login():
fallback_endpoint = _permission_denied_fallback_endpoint(permissions, current_endpoint='login')
return redirect(url_for(fallback_endpoint))
else:
return redirect(url_for('home'))
return redirect(url_for('home_admin'))
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)}")
flash('Ungültige Anmeldedaten', 'error')
@@ -4584,7 +4563,7 @@ def change_password():
# Update the password
if us.update_password(session['username'], new_password):
flash('Ihr Passwort wurde erfolgreich geändert.', 'success')
return redirect(url_for('home'))
return redirect(url_for('home_admin'))
else:
flash('Fehler beim Ändern des Passworts. Bitte versuchen Sie es später erneut.', 'error')
@@ -5178,7 +5157,7 @@ def upload_item():
elif cfg.MODULES.is_enabled('library') and _page_access_allowed(permissions, 'home_library'):
success_redirect_endpoint = 'home_library'
else:
success_redirect_endpoint = 'home'
success_redirect_endpoint = 'home_admin'
# Detect if request is from mobile device
is_mobile = 'Mobile' in request.headers.get('User-Agent', '')
@@ -6384,11 +6363,11 @@ def delete_item(id):
if not current_permissions['actions'].get('can_delete', False):
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'):
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
group_item_ids = it.get_group_item_ids(id)
@@ -6448,11 +6427,11 @@ def delete_library_item(id):
if not current_permissions['actions'].get('can_delete', False):
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'):
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
try:
@@ -6522,11 +6501,11 @@ def bulk_delete_items():
if not current_permissions['actions'].get('can_delete', False):
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'):
flash('Bibliotheks-Modul ist deaktiviert.', 'error')
return redirect(url_for('library'))
return redirect(url_for('library_view'))
payload = request.get_json(silent=True) or {}
item_ids = payload.get('item_ids') or request.form.getlist('item_ids')
@@ -6587,11 +6566,11 @@ def edit_item(id):
if not current_permissions['actions'].get('can_edit', False):
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'):
flash('Bibliotheks-Modul ist deaktiviert.', 'error')
return redirect(url_for('library'))
return redirect(url_for('library_view'))
# Strip whitespace from all text fields
name = sanitize_form_value(request.form.get('name'))
@@ -6721,7 +6700,7 @@ def update_group():
if not current_permissions['actions'].get('can_edit', False):
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()
series_group_id = data.get('series_group_id')
@@ -7005,7 +6984,7 @@ def ausleihen(id):
if requested_return_to == 'library' and cfg.MODULES.is_enabled('library'):
redirect_target = 'library_view'
else:
redirect_target = 'home_admin' if us.check_admin(username) else 'home' # check for plausability
redirect_target = 'home_admin'
item = it.get_item(id)
if not item:
@@ -7069,9 +7048,10 @@ def ausleihen(id):
if current_permissions['actions'].get('can_borrow') and not is_library_item:
if 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:
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
if borrow_duration_days is None:
try:
@@ -7321,7 +7301,7 @@ def zurueckgeben(id):
item = it.get_item(id)
if not item:
flash('Element nicht gefunden', 'error')
return redirect(url_for('home'))
return redirect(url_for('home_admin'))
username = session['username']
@@ -7903,20 +7883,20 @@ def terminplan():
if not cfg.MODULES.is_enabled('terminplan'):
flash('Der Terminplaner ist deaktiviert.', 'info')
return redirect(url_for('home'))
return redirect(url_for('home_admin'))
# Make sure the template exists
template_path = os.path.join(BASE_DIR, 'templates', 'terminplan.html')
if not os.path.exists(template_path):
print(f"Template file not found: {template_path}")
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)
except Exception as e:
app.logger.error(f"Error rendering terminplan: {e}")
flash('Ein Fehler ist beim Anzeigen des Kalenders aufgetreten.', 'error')
return redirect(url_for('home'))
return redirect(url_for('home_admin'))
'''-------------------------------------------------------------------------------------------------------------ADMIN ROUTES------------------------------------------------------------------------------------------------------------------'''
@@ -7924,66 +7904,69 @@ def terminplan():
@app.route('/register', methods=['GET', 'POST'])
def register():
"""
User registration route.false
User registration route.
Returns:
flask.Response: Rendered template or redirect
"""
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'))
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()
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.
username = us.build_unique_username_from_name(name, last_name)
username = us.build_unique_username_from_name(name, last_name)
permission_preset = (request.form.get('permission_preset') or 'standard_user').strip()
use_custom_permissions = request.form.get('use_custom_permissions') == 'on'
if not username or not password or not name or not last_name:
flash('Bitte füllen Sie alle Felder aus', 'error')
return redirect(url_for('register'))
permission_preset = (request.form.get('permission_preset') or 'standard_user').strip()
use_custom_permissions = request.form.get('use_custom_permissions') == 'on'
if not us.check_password_strength(password):
flash('Passwort ist zu schwach oder entspricht nicht den Richtlinien', 'error')
return redirect(url_for('register'))
action_permissions = None
page_permissions = None
if use_custom_permissions:
action_permissions = {}
for action_key, _ in PERMISSION_ACTION_OPTIONS:
action_permissions[action_key] = request.form.get(f'action_{action_key}') == 'on'
page_permissions = {}
for endpoint_name, _ in PERMISSION_PAGE_OPTIONS:
page_permissions[endpoint_name] = request.form.get(f'page_{endpoint_name}') == 'on'
if not username or not password or not name or not last_name:
flash('Bitte füllen Sie alle Felder aus', 'error')
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
page_permissions = None
if use_custom_permissions:
action_permissions = {}
for action_key, _ in PERMISSION_ACTION_OPTIONS:
action_permissions[action_key] = request.form.get(f'action_{action_key}') == 'on'
page_permissions = {}
for endpoint_name, _ in PERMISSION_PAGE_OPTIONS:
page_permissions[endpoint_name] = request.form.get(f'page_{endpoint_name}') == 'on'
us.add_user(
username,
password,
name,
last_name,
is_student=False,
student_card_id=None,
max_borrow_days=None,
permission_preset=permission_preset,
action_permissions=action_permissions,
page_permissions=page_permissions,
)
return redirect(url_for('home'))
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
us.add_user(
username,
password,
name,
last_name,
is_student=False,
student_card_id=None,
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'])
def user_del():
@@ -8658,7 +8641,7 @@ def admin_create_invoice(borrow_id):
'damage_reason': damage_reason,
'created_at': now,
'created_by': session.get('username', ''),
'borrower': borrower,
'borrower': decrypt_text(borrower),
'item_id': str(item_doc['_id']),
'item_name': item_name,
'item_code': item_code,
@@ -9108,7 +9091,7 @@ def library_item_invoices(item_id):
if not current_permissions['pages'].get('library_loans_admin', False):
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
try:
@@ -10015,7 +9998,7 @@ def fetch_book_info(isbn):
if not current_permissions['actions'].get('can_insert', False):
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'):
return jsonify({"error": "Bibliotheks-Modul ist deaktiviert."}), 403
@@ -10066,7 +10049,7 @@ def download_book_cover():
if not current_permissions['actions'].get('can_insert', False):
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'):
return jsonify({"error": "Bibliotheks-Modul ist deaktiviert."}), 403
@@ -10438,7 +10421,7 @@ def notifications_view():
except Exception as exc:
app.logger.error(f"Error loading notifications: {exc}")
flash('Fehler beim Laden der Benachrichtigungen.', 'error')
return redirect(url_for('home'))
return redirect(url_for('home_admin'))
finally:
if client:
client.close()
+10 -10
View File
@@ -97,18 +97,18 @@ def build_name_synonym(first_name, last_name=''):
last = _clean_name_fragment(last_name)
if first and last:
return (first[:2] + last[:2]).title()
return (first[:3] + last[:3]).title()
combined = (first + last)
if not combined:
return 'User'
return combined[:4].title()
return combined[:6].title()
def build_username_from_name(first_name, 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:
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=''):
"""
Build a unique username from the first 2 letters of the first name and
the first 2 letters of the last name.
Build a unique username from the first 3 letters of the first name and
the first 3 letters of the last name.
"""
first = _clean_name_fragment(first_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:
base_username = 'user'
@@ -594,8 +594,8 @@ def student_card_exists(student_card_id):
return False
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client)
users = db['users']
exists = users.find_one({'StudentCardId': normalized}) is not None
users = db['student_cards']
exists = users.find_one({'SchülerName': normalized}) is not None
client.close()
return exists
@@ -607,8 +607,8 @@ def get_user_by_student_card(student_card_id):
return None
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client)
users = db['users']
found_user = users.find_one({'StudentCardId': normalized})
users = db['student_cards']
found_user = users.find_one({'SchülerName': normalized})
client.close()
return found_user
+1
View File
@@ -1,4 +1,5 @@
flask
flask-wtf
werkzeug
gunicorn
pymongo
+24 -15
View File
@@ -1375,8 +1375,10 @@
<li><a class="dropdown-item" href="{{ url_for('library_loans_admin') }}">Ausleihen / Defekte Items</a></li>
{% endif %}
{% if student_cards_module_enabled %}
{% if current_permissions.actions.get('can_manage_users', False) %}
<li><a class="dropdown-item" href="{{ url_for('student_cards_admin') }}">Bibliotheksausweis</a></li>
{% endif %}
{% endif %}
{% if current_permissions.pages.get('admin_school_settings', False) %}
<li><a class="dropdown-item" href="{{ url_for('admin_school_settings') }}">Schulstammdaten</a></li>
{% endif %}
@@ -1619,11 +1621,10 @@
<div id="cookie-banner" role="dialog" aria-live="polite" aria-label="Cookie-Hinweis">
<div class="cb-inner">
<div class="cb-text">
Wir verwenden technisch notwendige Cookies, um Ihre Sitzung zu verwalten und die Anwendung bereitzustellen. Bitte akzeptieren Sie Cookies, um fortzufahren.
Wir verwenden ausschließlich technisch notwendige Cookies, um Ihre Sitzung zu verwalten und die Anwendung bereitzustellen. Bitte bestätigen Sie dies, um fortzufahren.
</div>
<div class="cb-actions">
<button class="btn-decline" id="cookie-decline">Ablehnen</button>
<button class="btn-accept" id="cookie-accept">Akzeptieren</button>
<button class="btn-accept" id="cookie-accept">Notwendige Cookies akzeptieren</button>
</div>
</div>
</div>
@@ -1728,31 +1729,39 @@
(function(){
function getCookie(name){
const v = document.cookie.split(';').map(s=>s.trim());
for(const c of v){ if(c.startsWith(name+'=')) return decodeURIComponent(c.split('=')[1]); }
for(const c of v){
if(c.startsWith(name+'=')) return decodeURIComponent(c.split('=')[1]);
}
return null;
}
function setCookie(name, value, days){
const d = new Date(); d.setTime(d.getTime() + (days*24*60*60*1000));
const d = new Date();
d.setTime(d.getTime() + (days*24*60*60*1000));
document.cookie = name + '=' + encodeURIComponent(value) + ';expires=' + d.toUTCString() + ';path=/;SameSite=Lax';
}
function showBanner(){ var el = document.getElementById('cookie-banner'); if(el) el.style.display = 'block'; }
function hideBanner(){ var el = document.getElementById('cookie-banner'); if(el) el.style.display = 'none'; }
// If not decided yet, show banner and block app until decision
function showBanner(){
var el = document.getElementById('cookie-banner');
if(el) el.style.display = 'block';
}
function hideBanner(){
var el = document.getElementById('cookie-banner');
if(el) el.style.display = 'none';
}
// Prüfen, ob der Nutzer bereits zugestimmt hat
const consent = getCookie('cookie_consent');
if(!consent){
showBanner();
// Optionally blur content until consent
document.body.style.filter = 'none';
}
// Nur noch der Akzeptieren-Button für vitale Cookies ist vorhanden
document.getElementById('cookie-accept')?.addEventListener('click', function(){
setCookie('cookie_consent','accepted',365);
setCookie('cookie_consent', 'vital_accepted', 365);
hideBanner();
});
document.getElementById('cookie-decline')?.addEventListener('click', function(){
setCookie('cookie_consent','declined',365);
window.location.href = 'https://www.ecosia.org/';
});
const username = {{ (session['username'] if 'username' in session else '')|tojson }};
const isTutorialPage = window.location.pathname === {{ url_for('tutorial_page')|tojson }};
+30 -15
View File
@@ -520,7 +520,16 @@
window.openDamageReportPrompt = openDamageReportPrompt;
function openDamageInvoiceModal(row, description) {
if (!damageInvoiceModal || !damageInvoiceForm) {
const modal = document.getElementById('damage-invoice-modal');
const form = document.getElementById('damage-invoice-form');
const inputItem = document.getElementById('damage-invoice-item');
const inputBorrower = document.getElementById('damage-invoice-borrower');
const inputCode = document.getElementById('damage-invoice-code');
const inputAmount = document.getElementById('damage-invoice-amount');
const inputReason = document.getElementById('damage-invoice-reason');
if (!modal || !form) {
console.error("Modal oder Formular nicht gefunden.");
return;
}
@@ -530,23 +539,29 @@
const itemCode = row.dataset.itemCode || '';
const itemCost = row.dataset.itemCost || '';
damageInvoiceForm.action = "{{ url_for('admin_create_invoice', borrow_id='__BORROW_ID__') }}".replace('__BORROW_ID__', borrowId);
damageInvoiceItem.value = itemName;
damageInvoiceBorrower.value = borrower;
damageInvoiceCode.value = itemCode;
damageInvoiceAmount.value = String(itemCost).replace(' EUR', '').trim();
damageInvoiceReason.value = description || `Schaden gemeldet für ${itemName}`;
damageInvoiceModal.style.display = 'block';
damageInvoiceAmount.focus();
form.action = "{{ url_for('admin_create_invoice', borrow_id='__BORROW_ID__') }}".replace('__BORROW_ID__', borrowId);
inputItem.value = itemName;
inputBorrower.value = borrower;
inputCode.value = itemCode;
inputAmount.value = String(itemCost).replace(' EUR', '').trim();
inputReason.value = description || `Schaden gemeldet für ${itemName}`;
modal.style.display = 'block';
inputAmount.focus();
}
function closeDamageInvoiceModal() {
const modal = document.getElementById('damage-invoice-modal');
if (modal) {
modal.style.display = 'none';
}
}
window.openDamageInvoiceModal = openDamageInvoiceModal;
function closeDamageInvoiceModal() {
if (damageInvoiceModal) {
damageInvoiceModal.style.display = 'none';
}
}
window.closeDamageInvoiceModal = closeDamageInvoiceModal;
if (damageInvoiceModal) {
damageInvoiceModal.addEventListener('click', function(event) {
+2 -2
View File
@@ -1416,8 +1416,8 @@
<div>
<label for="editLibraryType">Medientyp</label>
<select id="editLibraryType" style="width: 100%;">
<option value="book">Buch</option>
<option value="schoolbook">Schulbuch</option>
<option value="Buch">Buch</option>
<option value="Schulbuch">Schulbuch</option>
<option value="cd">CD</option>
<option value="dvd">DVD</option>
<option value="other">Sonstige Medien</option>
+59 -35
View File
@@ -2309,6 +2309,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
</script>
<div class="admin-content-container">
{% if current_permissions.actions.get('can_edit', False) %}
<!-- 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;">
<strong>⚠ Buchungskonflikte erkannt:</strong>
@@ -2335,6 +2336,8 @@ document.addEventListener('DOMContentLoaded', ()=>{
.catch(() => {});
});
</script>
{% endif %}
<div class="content">
<h1 style="position:relative;">Inventar Objekte
<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">
<span id="favorites-view-icon">🔖</span>
</button>
{% if current_permissions.actions.get('can_delete', False) %}
<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()">
<span class="drawer-icon"></span>
</button>
</div>
{% endif %}
</div>
</h1>
<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 class="bulk-delete-content">
<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>
</div>
</div>
{% endif %}
<div class="filter-container">
<div class="filter-group">
<div class="filter-header">
@@ -2469,6 +2476,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
</div>
<!-- Add the edit modal form -->
{% if current_permissions.actions.get('can_edit', False) %}
<div id="edit-modal" class="item-modal">
<div class="modal-content">
<span class="close-modal" onclick="closeEditModal()">&times;</span>
@@ -2646,6 +2654,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
</form>
</div>
</div>
{% endif %}
</div>
<!-- Schedule Appointment Modal -->
@@ -2724,7 +2733,6 @@ document.addEventListener('DOMContentLoaded', ()=>{
</div>
</div>
<!-- Initialize template variables before any JavaScript code -->
<script>
// Create a global object to hold server-side template values
window.serverVars = {};
@@ -3239,7 +3247,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
return;
}
fetch('{{ url_for('bulk_delete_items') }}', {
fetch("{{ url_for('bulk_delete_items') }}", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -3703,6 +3711,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
Für Löschung markieren
</label>
</div>
{% if current_permissions.actions.get('can_borrow', False) %}
${isAvailableForBorrow && !item.BlockedNow ?
`<form method="POST" action="{{ url_for('ausleihen', id='') }}${item._id}">
${isGroupedItem ? `
@@ -3724,12 +3733,21 @@ document.addEventListener('DOMContentLoaded', ()=>{
:
`<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?')">
<button class="delete-button" type="submit">Löschen</button>
</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>
{% endif %}
{% if current_permissions.actions.get('can_insert', False) %}
<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>` : ''}
{% endif %}
</div>
`;
itemsContainer.appendChild(card);
@@ -4506,10 +4524,10 @@ document.addEventListener('DOMContentLoaded', ()=>{
: '<div style="font-size:0.92rem;color:#64748b;">Keine Beschädigungs-Historie vorhanden.</div>';
modalContent.innerHTML = `
<h2>${item.Name}</h2>
<h2>${escapeHtml(item.Name || '')}</h2>
${borrowerInfoHtml}
${appointmentInfoHtml}
<div class="modal-image-container">
${imagesHtml}
${item.Images && item.Images.length > 1 ? `
@@ -4518,66 +4536,67 @@ document.addEventListener('DOMContentLoaded', ()=>{
<div class="image-counter">1/${item.Images.length}</div>
` : ''}
</div>
<div class="modal-details">
<div class="detail-group">
<div class="detail-label">Ort:</div>
<div class="detail-value">${item.Ort || '-'}</div>
<div class="detail-value">${escapeHtml(item.Ort || '-')}</div>
</div>
<div class="detail-group">
<div class="detail-label">Status:</div>
<div class="detail-value ${isBorrowed ? 'status-borrowed' : ''}">
${isBorrowed ? 'Ausgeliehen' : 'Verfügbar'}
</div>
</div>
<div class="detail-group">
<div class="detail-label">Unterrichtsfach:</div>
<div class="detail-value">${filter1Array.join(', ') || '-'}</div>
<div class="detail-value">${escapeHtml(filter1Array.join(', ') || '-')}</div>
</div>
<div class="detail-group">
<div class="detail-label">Jahrgangsstufe:</div>
<div class="detail-value">${filter2Array.join(', ') || '-'}</div>
<div class="detail-value">${escapeHtml(filter2Array.join(', ') || '-')}</div>
</div>
<div class="detail-group">
<div class="detail-label">Schlagwort:</div>
<div class="detail-value">${filter3Array.join(', ') || '-'}</div>
<div class="detail-value">${escapeHtml(filter3Array.join(', ') || '-')}</div>
</div>
<div class="detail-group">
<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 class="detail-group">
<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>
${isGroupedItem ? `
<div class="detail-group">
<div class="detail-label">Verfügbar:</div>
<div class="detail-value">${availableGroupedCount}</div>
<div class="detail-value">${escapeHtml(String(availableGroupedCount))}</div>
</div>` : ''}
<div class="detail-group">
<div class="detail-label">Anschaffungsjahr:</div>
<div class="detail-value">${item.Anschaffungsjahr || '-'}</div>
</div>
<div class="detail-group">
<div class="detail-label">Anschaffungskosten:</div>
<div class="detail-value">${item.Anschaffungskosten ? item.Anschaffungskosten + ' €' : '-'}</div>
</div>
<div class="detail-group full-width">
<div class="detail-label">Beschreibung:</div>
<div class="detail-value">${item.Beschreibung || '-'}</div>
<div class="detail-value">${escapeHtml(String(item.Anschaffungsjahr || '-'))}</div>
</div>
<div class="detail-group">
<div class="detail-label">Anschaffungskosten:</div>
<div class="detail-value">${item.Anschaffungskosten ? escapeHtml(String(item.Anschaffungskosten)) + ' €' : '-'}</div>
</div>
<div class="detail-group full-width">
<div class="detail-label">Beschreibung:</div>
<div class="detail-value">${escapeHtml(item.Beschreibung || '-')}</div>
</div>
{% if current_permissions.actions.get('can_view_logs', False) %}
<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-value">
@@ -4590,7 +4609,8 @@ document.addEventListener('DOMContentLoaded', ()=>{
</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-label">Verfügbarkeit prüfen:</div>
<div class="detail-value">
@@ -4611,7 +4631,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
</div>
</div>
</div>
<div class="detail-group full-width" style="margin-top:15px;">
<div class="detail-label">Geplante Ausleihen:</div>
<div class="detail-value">
@@ -4635,9 +4655,10 @@ document.addEventListener('DOMContentLoaded', ()=>{
</div>
</div>
</div>
<div class="modal-actions">
${!isBorrowed ?
{% if current_permissions.actions.get('can_borrow', False) %}
`<form method="POST" action="/ausleihen/${item._id}">
${isGroupedItem ? `
<div style="display:flex; gap:8px; flex-wrap:wrap; margin-bottom:8px; align-items:center;">
@@ -4651,6 +4672,9 @@ document.addEventListener('DOMContentLoaded', ()=>{
</div>` : ''}
<button class="ausleihen" type="submit">Ausleihen</button>
</form>`
{% else %}
`<button class="ausleihen disabled-button" disabled title="Keine Berechtigung">Ausleihen nicht möglich</button>`
{% endif %}
: (!isGroupedItem && item.User === currentUsername) ?
`<form method="POST" action="/zurueckgeben/${item._id}">
<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="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>` : ''}
<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>
+133 -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" %}
{% block title %}Register{% endblock %}
@@ -33,6 +25,7 @@
<div class="content">
<div class="form-card">
<form method="POST" action="{{ url_for('register') }}">
<div class="form-group">
<label for="name">Vorname</label>
<div class="input-container">
@@ -44,7 +37,7 @@
<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()">
</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">
<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;">
@@ -64,9 +57,21 @@
<li id="pw-rule-symbol" class="pw-rule">Mindestens ein Sonderzeichen</li>
</ul>
</div>
<div class="input-container">
<span class="input-icon">🔒</span>
<input type="password" id="password" name="password" placeholder="Geben Sie ein sicheres Passwort ein" required>
<div class="input-wrapper">
<div class="input-container">
<span class="input-icon">🔒</span>
<!-- HTML5 Pattern blockiert unsichere Passwörter vor dem Absenden -->
<input
id="password"
name="password"
placeholder="Geben Sie ein sicheres Passwort ein"
required
pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9]).{12,}">
</div>
</div>
<div class="pw-actions">
<button type="button" class="btn-secondary" onclick="generateSecurePassword()">Passwort generieren</button>
</div>
</div>
@@ -258,31 +263,6 @@ input::placeholder {
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 {
margin-bottom: 1.5rem;
}
@@ -422,49 +402,147 @@ input::placeholder {
margin: 4px 0;
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>
<script>
// Function to generate username from first and last name (helper function)
// Hilfsfunktion: Umlaute auflösen und Sonderzeichen entfernen
function cleanNameForUsername(text) {
if (!text) return '';
// Remove special characters, convert umlauts, lowercase
let cleaned = text
.replace(/[^a-zA-Zäöüß\s-]/g, '')
.trim()
.toLowerCase();
// Convert German umlauts to ASCII
cleaned = cleaned
let cleaned = text.trim().toLowerCase()
.replace(/ä/g, 'ae')
.replace(/ö/g, 'oe')
.replace(/ü/g, 'ue')
.replace(/ß/g, 'ss');
.replace(/ß/g, 'ss')
.replace(/[^a-z]/g, '');
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() {
const firstName = cleanNameForUsername(document.getElementById('name').value || '');
const lastName = cleanNameForUsername(document.getElementById('last-name').value || '');
const firstName = cleanNameForUsername(document.getElementById('name').value);
const lastName = cleanNameForUsername(document.getElementById('last-name').value);
let username = '';
if (firstName && lastName) {
username = (firstName.slice(0, 3) + lastName.slice(0, 3));
username = formatPart(firstName, 3) + formatPart(lastName, 3);
} else if (firstName) {
username = firstName.slice(0, 6);
username = formatPart(firstName, 6);
} else if (lastName) {
username = lastName.slice(0, 6);
username = formatPart(lastName, 6);
}
// Set the username field
const usernameField = document.getElementById('username');
if (usernameField) {
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 () {
const permissionPresets = {{ permission_presets | tojson }};
const presetSelect = document.getElementById('permission-preset');
@@ -507,34 +585,6 @@ document.addEventListener('DOMContentLoaded', function () {
}
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) {
passwordInput.addEventListener('input', updatePasswordRules);
passwordInput.addEventListener('blur', updatePasswordRules);
+1
View File
@@ -1,4 +1,5 @@
flask
flask-wtf
werkzeug
gunicorn
pymongo==4.6.3
+2
View File
@@ -1,6 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
sudo find /tmp -maxdepth 1 -name "tmp.*" -exec rm -rf {} +
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
cd "$SCRIPT_DIR"