Compare commits

...

2 Commits

3 changed files with 162 additions and 16 deletions
+113 -13
View File
@@ -206,6 +206,7 @@ PERMISSION_ACTION_ENDPOINTS = {
'register': 'can_manage_users',
'admin_reset_user_password': 'can_manage_users',
'admin_update_user_permissions': 'can_manage_users',
'admin_anonymize_names': 'can_manage_users',
'home_admin': 'can_manage_settings',
'upload_admin': 'can_manage_settings',
'library_admin': 'can_manage_settings',
@@ -297,7 +298,7 @@ def _enforce_user_permissions():
return jsonify({'ok': False, 'message': message}), 403
flash(message, 'error')
fallback_endpoint = _permission_denied_fallback_endpoint(permissions)
fallback_endpoint = _permission_denied_fallback_endpoint(permissions, current_endpoint=endpoint)
return redirect(url_for(fallback_endpoint))
action_key = PERMISSION_ACTION_ENDPOINTS.get(endpoint)
@@ -307,7 +308,7 @@ def _enforce_user_permissions():
return jsonify({'ok': False, 'message': message}), 403
flash(message, 'error')
fallback_endpoint = _permission_denied_fallback_endpoint(permissions)
fallback_endpoint = _permission_denied_fallback_endpoint(permissions, current_endpoint=endpoint)
return redirect(url_for(fallback_endpoint))
return None
@@ -375,8 +376,16 @@ def _action_access_allowed(permissions, action_key):
return bool(action_permissions.get(action_key, True))
def _permission_denied_fallback_endpoint(permissions):
for candidate in ('home', 'my_borrowed_items', 'tutorial_page', 'notifications_view', 'impressum'):
def _permission_denied_fallback_endpoint(permissions, current_endpoint=None):
username = session.get('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')
for candidate in ('my_borrowed_items', 'tutorial_page', 'notifications_view', 'impressum', 'home'):
if current_endpoint and candidate == current_endpoint:
continue
if candidate == 'home' and is_admin_user and not admin_home_allowed:
continue
if _page_access_allowed(permissions, candidate):
return candidate
return 'logout'
@@ -1588,14 +1597,25 @@ def _student_card_id_slug(value):
return re.sub(r'[^a-z0-9]+', '', normalized).upper()
def _name_to_alias(full_name):
"""Convert clear names to deterministic aliases, e.g. Simon Frings -> SimFri."""
text = sanitize_form_value(full_name)
if not text:
return 'User'
parts = [p for p in re.split(r'\s+', text) if p]
if len(parts) >= 2:
return us.build_name_synonym(parts[0], parts[-1])
return us.build_name_synonym(parts[0], '')
def _build_student_card_excel_id(student_name, class_name, row_number, used_ids):
"""Create a stable student-card ID when the spreadsheet does not provide one."""
name_slug = _student_card_id_slug(student_name)
"""Create a stable student-card ID without embedding personal names."""
class_slug = _student_card_id_slug(class_name)
base_parts = [part for part in (class_slug, name_slug) if part]
base_parts = [part for part in (class_slug,) if part]
if base_parts:
base_id = f"SC-{'-'.join(base_parts[:2])}"
base_id = f"SC-{'-'.join(base_parts[:1])}-ROW-{row_number}"
else:
base_id = f"SC-ROW-{row_number}"
@@ -1715,6 +1735,8 @@ def _upload_student_cards_excel():
student_name = f'{first_name} {last_name}'.strip()
validation_warnings.append((row_number, 'Schülername wurde aus Vorname und Nachname zusammengesetzt'))
student_name_alias = _name_to_alias(student_name)
if not ausweis_id and not student_name and not class_name:
continue
@@ -1739,7 +1761,7 @@ def _upload_student_cards_excel():
planned_rows.append({
'row_number': row_number,
'ausweis_id': ausweis_id,
'student_name': student_name,
'student_name': student_name_alias,
'class_name': class_name,
'notes': notes,
'default_borrow_days': default_borrow_days,
@@ -2393,7 +2415,14 @@ def home():
student_max_borrow_days=cfg.STUDENT_MAX_BORROW_DAYS
)
else:
return redirect(url_for('home_admin'))
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')
@@ -3172,6 +3201,7 @@ def student_cards_admin():
action = request.form.get('action', 'add')
ausweis_id = request.form.get('ausweis_id', '').strip().upper()
student_name = request.form.get('student_name', '').strip()
student_name_alias = _name_to_alias(student_name)
default_borrow_days = request.form.get('default_borrow_days', 14)
class_name = request.form.get('class_name', '').strip()
notes = request.form.get('notes', '').strip()
@@ -3198,7 +3228,7 @@ def student_cards_admin():
else:
encrypted_payload = encrypt_document_fields(
{
'SchülerName': student_name,
'SchülerName': student_name_alias,
'Klasse': class_name,
'Notizen': notes,
},
@@ -3231,7 +3261,7 @@ def student_cards_admin():
try:
encrypted_payload = encrypt_document_fields(
{
'SchülerName': student_name,
'SchülerName': student_name_alias,
'Klasse': class_name,
'Notizen': notes,
},
@@ -3701,7 +3731,11 @@ def login():
session['admin'] = is_admin_user
session['is_admin'] = is_admin_user
if is_admin_user:
return redirect(url_for('home_admin'))
permissions = us.get_effective_permissions(username)
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='login')
return redirect(url_for(fallback_endpoint))
else:
return redirect(url_for('home'))
else:
@@ -7811,6 +7845,72 @@ def admin_update_user_permissions():
return redirect(url_for('user_del'))
@app.route('/admin_anonymize_names', methods=['POST'])
def admin_anonymize_names():
"""Anonymize already stored personal names into short aliases."""
if 'username' not in session or not us.check_admin(session['username']):
flash('Nicht autorisierter Zugriff', 'error')
return redirect(url_for('login'))
client = None
try:
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
db = client[MONGODB_DB]
users_col = db['users']
student_cards_col = db['student_cards']
users_updated = 0
cards_updated = 0
for user_doc in users_col.find({}, {'name': 1, 'last_name': 1, 'Username': 1, 'username': 1}):
first = str(user_doc.get('name') or '').strip()
last = str(user_doc.get('last_name') or '').strip()
fallback = str(user_doc.get('Username') or user_doc.get('username') or '').strip()
alias = us.build_name_synonym(first or fallback, last)
result = users_col.update_one(
{'_id': user_doc['_id']},
{'$set': {'name': alias, 'last_name': ''}}
)
if result.modified_count > 0:
users_updated += 1
for card_doc in student_cards_col.find({}, {'SchülerName': 1, 'Klasse': 1, 'Notizen': 1}):
decrypted = _decrypt_student_card_doc(card_doc)
alias = _name_to_alias(decrypted.get('SchülerName', ''))
class_name = sanitize_form_value(decrypted.get('Klasse', ''))
notes = sanitize_form_value(decrypted.get('Notizen', ''))
encrypted_payload = encrypt_document_fields(
{
'SchülerName': alias,
'Klasse': class_name,
'Notizen': notes,
},
STUDENT_CARD_ENCRYPTED_FIELDS,
)
result = student_cards_col.update_one(
{'_id': card_doc['_id']},
{'$set': {'Aktualisiert': datetime.datetime.now(), **encrypted_payload}}
)
if result.modified_count > 0:
cards_updated += 1
flash(
f'Anonymisierung abgeschlossen: {users_updated} Benutzer und {cards_updated} Ausweise aktualisiert.',
'success'
)
except Exception as exc:
app.logger.error(f'Error anonymizing names: {exc}')
flash('Fehler bei der Anonymisierung der Namen.', 'error')
finally:
if client:
client.close()
return redirect(url_for('user_del'))
@app.route('/logs')
def logs():
"""
+10
View File
@@ -17,6 +17,16 @@
<div class="user-management-container">
<h2>Benutzer</h2>
<form method="POST" action="{{ url_for('admin_anonymize_names') }}" class="mb-3">
<button
type="submit"
class="btn btn-outline-danger"
onclick="return confirm('Sollen alle gespeicherten Klarnamen dauerhaft in Synonym-Kuerzel umgewandelt werden?')"
>
Gespeicherte Namen anonymisieren
</button>
</form>
<div class="filter-bar mb-3">
<div class="row g-2 align-items-end">
<div class="col-md-3">
+39 -3
View File
@@ -12,6 +12,7 @@ Provides methods for creating, validating, and retrieving user information.
'''
import hashlib
import copy
import re
from bson.objectid import ObjectId
import settings as cfg
from settings import MongoClient
@@ -24,6 +25,38 @@ def normalize_student_card_id(card_id):
return str(card_id).strip().upper()
def _clean_name_fragment(value):
cleaned = re.sub(r'[^A-Za-zÄÖÜäöüß]', '', str(value or '').strip())
if not cleaned:
return ''
replacements = {
'ä': 'ae',
'ö': 'oe',
'ü': 'ue',
'ß': 'ss',
'Ä': 'Ae',
'Ö': 'Oe',
'Ü': 'Ue',
}
for old_char, new_char in replacements.items():
cleaned = cleaned.replace(old_char, new_char)
return cleaned
def build_name_synonym(first_name, last_name=''):
"""Build a deterministic, non-personalized short alias like 'SimFri'."""
first = _clean_name_fragment(first_name)
last = _clean_name_fragment(last_name)
if first and last:
return (first[:3] + last[:3]).title()
combined = (first + last)
if not combined:
return 'User'
return combined[:6].title()
ACTION_PERMISSION_KEYS = (
'can_borrow',
'can_insert',
@@ -361,13 +394,15 @@ def add_user(username, password, name, last_name, is_student=False, student_card
return False
permission_defaults = build_default_permission_payload('standard_user')
name_alias = build_name_synonym(name, last_name)
user_doc = {
'Username': username,
'Password': hashing(password),
'Admin': False,
'active_ausleihung': None,
'name': name,
'last_name': last_name,
'name': name_alias,
'last_name': '',
'IsStudent': bool(is_student),
'PermissionPreset': permission_defaults['preset'],
'ActionPermissions': permission_defaults['actions'],
@@ -714,13 +749,14 @@ def update_user_name(username, name, last_name):
bool: True if updated successfully, False otherwise
"""
try:
name_alias = build_name_synonym(name, last_name)
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
users = db['users']
result = users.update_one(
{'Username': username},
{'$set': {'name': name, 'last_name': last_name}}
{'$set': {'name': name_alias, 'last_name': ''}}
)
client.close()