feat: Update user registration to include first and last name fields and enhance permission checks for upload actions

This commit is contained in:
2026-04-17 23:22:10 +02:00
parent 3b637de188
commit 9b7ba39702
5 changed files with 151 additions and 38 deletions
+39 -30
View File
@@ -208,8 +208,8 @@ PERMISSION_ACTION_ENDPOINTS = {
'admin_update_user_permissions': 'can_manage_users', 'admin_update_user_permissions': 'can_manage_users',
'admin_anonymize_names': 'can_manage_users', 'admin_anonymize_names': 'can_manage_users',
'home_admin': 'can_manage_settings', 'home_admin': 'can_manage_settings',
'upload_admin': 'can_manage_settings', 'upload_admin': 'can_insert',
'library_admin': 'can_manage_settings', 'library_admin': 'can_insert',
'admin_borrowings': 'can_manage_settings', 'admin_borrowings': 'can_manage_settings',
'library_loans_admin': 'can_manage_settings', 'library_loans_admin': 'can_manage_settings',
'admin_damaged_items': 'can_manage_settings', 'admin_damaged_items': 'can_manage_settings',
@@ -1826,8 +1826,9 @@ def _upload_excel_items(scope='inventory'):
flash('Nicht angemeldet.', 'error') flash('Nicht angemeldet.', 'error')
return redirect(url_for('login')) return redirect(url_for('login'))
if not us.check_admin(session['username']): permissions = _get_current_user_permissions() or us.build_default_permission_payload('standard_user')
flash('Administratorrechte erforderlich.', 'error') if not _action_access_allowed(permissions, 'can_insert'):
flash('Einfüge-Rechte erforderlich.', 'error')
return redirect(url_for('home')) return redirect(url_for('home'))
is_library_scope = scope == 'library' is_library_scope = scope == 'library'
@@ -1837,7 +1838,7 @@ def _upload_excel_items(scope='inventory'):
if is_library_scope: if is_library_scope:
if not cfg.LIBRARY_MODULE_ENABLED: if not cfg.LIBRARY_MODULE_ENABLED:
flash('Bibliotheks-Modul ist deaktiviert.', 'error') flash('Bibliotheks-Modul ist deaktiviert.', 'error')
return redirect(url_for('home_admin')) return redirect(url_for('home'))
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:
@@ -3017,8 +3018,8 @@ def api_library_item_update(item_id):
@app.route('/upload_admin') @app.route('/upload_admin')
def upload_admin(): def upload_admin():
""" """
Admin upload page route. Upload page route for inventory items.
Only accessible by users with admin privileges. Accessible to users with insert permission.
Supports duplication by passing duplicate_from parameter. Supports duplication by passing duplicate_from parameter.
Returns: Returns:
@@ -3027,7 +3028,8 @@ def upload_admin():
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 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('login'))
if not us.check_admin(session['username']): permissions = _get_current_user_permissions() or us.build_default_permission_payload('standard_user')
if not _action_access_allowed(permissions, 'can_insert'):
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('login'))
@@ -3091,13 +3093,14 @@ def upload_admin():
@app.route('/library_admin') @app.route('/library_admin')
def library_admin(): def library_admin():
""" """
Dedicated admin page for library/book uploads with ISBN scanning. Dedicated page for library/book uploads with ISBN scanning.
Only accessible by admins and only when the library module is enabled. Accessible to users with insert permission when the library module is enabled.
""" """
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 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('login'))
if not us.check_admin(session['username']): permissions = _get_current_user_permissions() or us.build_default_permission_payload('standard_user')
if not _action_access_allowed(permissions, 'can_insert'):
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('login'))
if not cfg.LIBRARY_MODULE_ENABLED: if not cfg.LIBRARY_MODULE_ENABLED:
@@ -4192,10 +4195,14 @@ def upload_item():
if 'username' not in session: if 'username' not in session:
return jsonify({'success': False, 'message': 'Nicht angemeldet'}), 401 return jsonify({'success': False, 'message': 'Nicht angemeldet'}), 401
# Check if user is an admin # Check if user may insert items
username = session['username'] username = session['username']
if not us.check_admin(username): permissions = _get_current_user_permissions() or us.build_default_permission_payload('standard_user')
return jsonify({'success': False, 'message': 'Administratorrechte erforderlich'}), 403 if not _action_access_allowed(permissions, 'can_insert'):
return jsonify({'success': False, 'message': 'Einfüge-Rechte erforderlich'}), 403
can_access_admin_home = _page_access_allowed(permissions, 'home_admin') and _action_access_allowed(permissions, 'can_manage_settings')
success_redirect_endpoint = 'home_admin' if can_access_admin_home else 'home'
# 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', '')
@@ -4277,7 +4284,7 @@ def upload_item():
return jsonify({'success': False, 'message': error_msg}), 400 return jsonify({'success': False, 'message': error_msg}), 400
else: else:
flash('Fehler beim Verarbeiten der Formulardaten. Bitte versuchen Sie es erneut.', 'error') flash('Fehler beim Verarbeiten der Formulardaten. Bitte versuchen Sie es erneut.', 'error')
return redirect(url_for('home_admin')) return redirect(url_for(success_redirect_endpoint))
# Expand special "all values" selections for predefined filters. # Expand special "all values" selections for predefined filters.
filter_upload = expand_filter_selection(filter_upload, 1) filter_upload = expand_filter_selection(filter_upload, 1)
@@ -4290,7 +4297,7 @@ def upload_item():
return jsonify({'success': False, 'message': error_msg}), 400 return jsonify({'success': False, 'message': error_msg}), 400
else: else:
flash(error_msg, 'error') flash(error_msg, 'error')
return redirect(url_for('home_admin')) return redirect(url_for(success_redirect_endpoint))
item_isbn = '' item_isbn = ''
item_type = 'general' item_type = 'general'
@@ -4301,7 +4308,7 @@ def upload_item():
if is_mobile: if is_mobile:
return jsonify({'success': False, 'message': error_msg}), 400 return jsonify({'success': False, 'message': error_msg}), 400
flash(error_msg, 'error') flash(error_msg, 'error')
return redirect(url_for('home_admin')) return redirect(url_for(success_redirect_endpoint))
if item_isbn: if item_isbn:
item_type = 'book' item_type = 'book'
@@ -4311,7 +4318,7 @@ def upload_item():
if is_mobile: if is_mobile:
return jsonify({'success': False, 'message': error_msg}), 400 return jsonify({'success': False, 'message': error_msg}), 400
flash(error_msg, 'error') flash(error_msg, 'error')
return redirect(url_for('home_admin')) return redirect(url_for(success_redirect_endpoint))
if not item_isbn: if not item_isbn:
error_msg = 'Für Bücher ist eine gültige ISBN erforderlich.' error_msg = 'Für Bücher ist eine gültige ISBN erforderlich.'
if is_mobile: if is_mobile:
@@ -4328,7 +4335,7 @@ def upload_item():
return jsonify({'success': False, 'message': error_msg}), 400 return jsonify({'success': False, 'message': error_msg}), 400
else: else:
flash(error_msg, 'error') flash(error_msg, 'error')
return redirect(url_for('home_admin')) return redirect(url_for(success_redirect_endpoint))
# Check if base code is unique for single-item uploads # Check if base code is unique for single-item uploads
if code_4 and item_count == 1 and not it.is_code_unique(code_4[0]): if code_4 and item_count == 1 and not it.is_code_unique(code_4[0]):
@@ -4337,7 +4344,7 @@ def upload_item():
return jsonify({'success': False, 'message': error_msg}), 400 return jsonify({'success': False, 'message': error_msg}), 400
else: else:
flash(error_msg, 'error') flash(error_msg, 'error')
return redirect(url_for('home_admin')) return redirect(url_for(success_redirect_endpoint))
# Validate optional per-item codes # Validate optional per-item codes
if individual_codes: if individual_codes:
@@ -4346,14 +4353,14 @@ def upload_item():
if is_mobile: if is_mobile:
return jsonify({'success': False, 'message': error_msg}), 400 return jsonify({'success': False, 'message': error_msg}), 400
flash(error_msg, 'error') flash(error_msg, 'error')
return redirect(url_for('home_admin')) return redirect(url_for(success_redirect_endpoint))
if len(set(individual_codes)) != len(individual_codes): if len(set(individual_codes)) != len(individual_codes):
error_msg = 'Doppelte Einzelcodes erkannt. Bitte alle Codes eindeutig eintragen.' error_msg = 'Doppelte Einzelcodes erkannt. Bitte alle Codes eindeutig eintragen.'
if is_mobile: if is_mobile:
return jsonify({'success': False, 'message': error_msg}), 400 return jsonify({'success': False, 'message': error_msg}), 400
flash(error_msg, 'error') flash(error_msg, 'error')
return redirect(url_for('home_admin')) return redirect(url_for(success_redirect_endpoint))
for specific_code in individual_codes: for specific_code in individual_codes:
if not it.is_code_unique(specific_code): if not it.is_code_unique(specific_code):
@@ -4361,7 +4368,7 @@ def upload_item():
if is_mobile: if is_mobile:
return jsonify({'success': False, 'message': error_msg}), 400 return jsonify({'success': False, 'message': error_msg}), 400
flash(error_msg, 'error') flash(error_msg, 'error')
return redirect(url_for('home_admin')) return redirect(url_for(success_redirect_endpoint))
def generate_unique_batch_code(base_code, position): def generate_unique_batch_code(base_code, position):
"""Generate a unique code for every item in a batch.""" """Generate a unique code for every item in a batch."""
@@ -5054,7 +5061,7 @@ def upload_item():
if is_mobile: if is_mobile:
return jsonify({'success': False, 'message': error_msg}), 400 return jsonify({'success': False, 'message': error_msg}), 400
flash(error_msg, 'error') flash(error_msg, 'error')
return redirect(url_for('home_admin')) return redirect(url_for(success_redirect_endpoint))
parent_item_id = str(created_item_ids[0]) if created_item_ids else None parent_item_id = str(created_item_ids[0]) if created_item_ids else None
item_id = it.add_item( item_id = it.add_item(
@@ -5081,7 +5088,7 @@ def upload_item():
if is_mobile: if is_mobile:
return jsonify({'success': False, 'message': error_msg}), 500 return jsonify({'success': False, 'message': error_msg}), 500
flash(error_msg, 'error') flash(error_msg, 'error')
return redirect(url_for('home_admin')) return redirect(url_for(success_redirect_endpoint))
item_id = created_item_ids[0] if created_item_ids else None item_id = created_item_ids[0] if created_item_ids else None
@@ -5105,14 +5112,14 @@ def upload_item():
}) })
else: else:
flash(success_msg, 'success') flash(success_msg, 'success')
return redirect(url_for('home_admin', highlight_item=str(item_id))) return redirect(url_for(success_redirect_endpoint, highlight_item=str(item_id)))
else: else:
error_msg = 'Fehler beim Hinzufügen des Elements' error_msg = 'Fehler beim Hinzufügen des Elements'
if is_mobile: if is_mobile:
return jsonify({'success': False, 'message': error_msg}), 500 return jsonify({'success': False, 'message': error_msg}), 500
else: else:
flash(error_msg, 'error') flash(error_msg, 'error')
return redirect(url_for('home_admin')) return redirect(url_for(success_redirect_endpoint))
@app.route('/duplicate_item', methods=['POST']) @app.route('/duplicate_item', methods=['POST'])
@@ -6688,12 +6695,14 @@ def register():
if request.method == 'POST': if request.method == 'POST':
username = request.form['username'] username = request.form['username']
password = request.form['password'] password = request.form['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() permission_preset = (request.form.get('permission_preset') or 'standard_user').strip()
use_custom_permissions = request.form.get('use_custom_permissions') == 'on' use_custom_permissions = request.form.get('use_custom_permissions') == 'on'
is_student = bool(request.form.get('is_student')) if cfg.STUDENT_CARDS_MODULE_ENABLED else False is_student = bool(request.form.get('is_student')) if cfg.STUDENT_CARDS_MODULE_ENABLED else False
student_card_id = us.normalize_student_card_id(request.form.get('student_card_id')) if cfg.STUDENT_CARDS_MODULE_ENABLED else '' student_card_id = us.normalize_student_card_id(request.form.get('student_card_id')) if cfg.STUDENT_CARDS_MODULE_ENABLED else ''
max_borrow_days_raw = request.form.get('max_borrow_days') if cfg.STUDENT_CARDS_MODULE_ENABLED else None max_borrow_days_raw = request.form.get('max_borrow_days') if cfg.STUDENT_CARDS_MODULE_ENABLED else None
if not username or not password: if not username or not password or not name or not last_name:
flash('Bitte füllen Sie alle Felder aus', 'error') flash('Bitte füllen Sie alle Felder aus', 'error')
return redirect(url_for('register')) return redirect(url_for('register'))
if us.get_user(username): if us.get_user(username):
@@ -6731,8 +6740,8 @@ def register():
us.add_user( us.add_user(
username, username,
password, password,
username, name,
'', last_name,
is_student=is_student, is_student=is_student,
student_card_id=student_card_id if is_student else None, student_card_id=student_card_id if is_student else None,
max_borrow_days=max_borrow_days, max_borrow_days=max_borrow_days,
+2 -2
View File
@@ -786,7 +786,7 @@
</li> </li>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if 'username' in session and (session.get('admin', False) or is_admin) and current_permissions.pages.get('upload_admin', True) and current_permissions.actions.get('can_manage_settings', True) %} {% if 'username' in session and current_permissions.pages.get('upload_admin', True) and current_permissions.actions.get('can_insert', True) %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link nav-priority-link {% if current_path == url_for('upload_admin') %}nav-active{% endif %}" href="{{ url_for('upload_admin') }}"> Hochladen</a> <a class="nav-link nav-priority-link {% if current_path == url_for('upload_admin') %}nav-active{% endif %}" href="{{ url_for('upload_admin') }}"> Hochladen</a>
</li> </li>
@@ -907,7 +907,7 @@
</li> </li>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if 'username' in session and (session.get('admin', False) or is_admin) and current_permissions.actions.get('can_manage_settings', True) and current_permissions.pages.get('library_admin', True) %} {% if 'username' in session and current_permissions.actions.get('can_insert', True) and current_permissions.pages.get('library_admin', True) %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link nav-priority-link {% if current_path == url_for('library_admin') %}nav-active{% endif %}" href="{{ url_for('library_admin') }}">📖 Hochladen</a> <a class="nav-link nav-priority-link {% if current_path == url_for('library_admin') %}nav-active{% endif %}" href="{{ url_for('library_admin') }}">📖 Hochladen</a>
</li> </li>
+99 -4
View File
@@ -39,12 +39,31 @@
<span class="input-icon">👤</span> <span class="input-icon">👤</span>
<input type="text" id="username" name="username" placeholder="Geben Sie einen Benutzernamen ein" required> <input type="text" id="username" name="username" placeholder="Geben Sie einen Benutzernamen ein" required>
</div> </div>
<p class="anonymize-hint">Klarnamen werden nicht gespeichert. Ein anonymes Kürzel wird automatisch aus dem Benutzernamen erzeugt.</p> <label for="name">Vorname</label>
<div class="input-container">
<span class="input-icon">👤</span>
<input type="text" id="name" name="name" placeholder="Geben Sie den Vornamen ein" required>
</div>
<label for="last-name">Nachname</label>
<div class="input-container">
<span class="input-icon">👤</span>
<input type="text" id="last-name" name="last-name" placeholder="Geben Sie den Nachnamen ein" required>
</div>
<p class="anonymize-hint">Klarnamen werden nur zur Erzeugung eines Kuerzels (z.B. SimFri) verwendet und nicht als Klarname gespeichert.</p>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="password">Passwort</label> <label for="password">Passwort</label>
<a class="richtlinen">Das Password muss mindestens 6 Zeichen beinhalten mit Sonderzeichen, groß und klein Buchstaben sowie Zahlen!</a> <div class="password-rules" id="password-rules" aria-live="polite">
<p class="password-rules-title">Passwort-Anforderungen (live):</p>
<ul>
<li id="pw-rule-length" class="pw-rule">Mindestens 12 Zeichen</li>
<li id="pw-rule-lower" class="pw-rule">Mindestens ein Kleinbuchstabe</li>
<li id="pw-rule-upper" class="pw-rule">Mindestens ein Grossbuchstabe</li>
<li id="pw-rule-digit" class="pw-rule">Mindestens eine Zahl</li>
<li id="pw-rule-symbol" class="pw-rule">Mindestens ein Sonderzeichen</li>
</ul>
</div>
<div class="input-container"> <div class="input-container">
<span class="input-icon">🔒</span> <span class="input-icon">🔒</span>
<input type="password" id="password" name="password" placeholder="Geben Sie ein sicheres Passwort ein" required> <input type="password" id="password" name="password" placeholder="Geben Sie ein sicheres Passwort ein" required>
@@ -342,8 +361,49 @@ input::placeholder {
} }
} }
.richtlinen{ .password-rules {
color: #ec0920; margin-bottom: 10px;
padding: 10px 12px;
border: 1px solid #e5e7eb;
border-radius: 8px;
background: #f8fafc;
}
.password-rules-title {
margin: 0 0 8px;
font-weight: 700;
color: #1f2937;
font-size: 0.92rem;
}
.password-rules ul {
list-style: none;
margin: 0;
padding: 0;
}
.pw-rule {
position: relative;
padding-left: 22px;
margin: 5px 0;
color: #b91c1c;
font-size: 0.9rem;
}
.pw-rule::before {
content: '✗';
position: absolute;
left: 0;
top: 0;
font-weight: 700;
}
.pw-rule.ok {
color: #166534;
}
.pw-rule.ok::before {
content: '✓';
} }
.anonymize-hint { .anonymize-hint {
@@ -427,6 +487,41 @@ document.addEventListener('DOMContentLoaded', function () {
toggleCustomPermissions(); toggleCustomPermissions();
} }
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);
updatePasswordRules();
}
{% if student_cards_module_enabled %} {% if student_cards_module_enabled %}
const studentCheckbox = document.getElementById('is-student'); const studentCheckbox = document.getElementById('is-student');
const studentFields = document.getElementById('student-fields'); const studentFields = document.getElementById('student-fields');
+8
View File
@@ -446,5 +446,13 @@
font-weight: 700; font-weight: 700;
margin-bottom: 10px; margin-bottom: 10px;
} }
.modal-backdrop {
z-index: 1998 !important;
}
.modal {
z-index: 1999 !important;
}
</style> </style>
{% endblock %} {% endblock %}
+3 -2
View File
@@ -415,8 +415,9 @@ def add_user(
for key, value in page_permissions.items(): for key, value in page_permissions.items():
permission_defaults['pages'][str(key)] = bool(value) permission_defaults['pages'][str(key)] = bool(value)
alias_source = name if str(name or '').strip() else username alias_first = name if str(name or '').strip() else username
name_alias = build_name_synonym(alias_source, '') alias_last = last_name if str(last_name or '').strip() else ''
name_alias = build_name_synonym(alias_first, alias_last)
user_doc = { user_doc = {
'Username': username, 'Username': username,