feat: Add admin functionality to anonymize stored names and implement name aliasing
This commit is contained in:
+88
-7
@@ -206,6 +206,7 @@ PERMISSION_ACTION_ENDPOINTS = {
|
|||||||
'register': 'can_manage_users',
|
'register': 'can_manage_users',
|
||||||
'admin_reset_user_password': 'can_manage_users',
|
'admin_reset_user_password': 'can_manage_users',
|
||||||
'admin_update_user_permissions': 'can_manage_users',
|
'admin_update_user_permissions': '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_manage_settings',
|
||||||
'library_admin': 'can_manage_settings',
|
'library_admin': 'can_manage_settings',
|
||||||
@@ -1588,14 +1589,25 @@ def _student_card_id_slug(value):
|
|||||||
return re.sub(r'[^a-z0-9]+', '', normalized).upper()
|
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):
|
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."""
|
"""Create a stable student-card ID without embedding personal names."""
|
||||||
name_slug = _student_card_id_slug(student_name)
|
|
||||||
class_slug = _student_card_id_slug(class_name)
|
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:
|
if base_parts:
|
||||||
base_id = f"SC-{'-'.join(base_parts[:2])}"
|
base_id = f"SC-{'-'.join(base_parts[:1])}-ROW-{row_number}"
|
||||||
else:
|
else:
|
||||||
base_id = f"SC-ROW-{row_number}"
|
base_id = f"SC-ROW-{row_number}"
|
||||||
|
|
||||||
@@ -1715,6 +1727,8 @@ def _upload_student_cards_excel():
|
|||||||
student_name = f'{first_name} {last_name}'.strip()
|
student_name = f'{first_name} {last_name}'.strip()
|
||||||
validation_warnings.append((row_number, 'Schülername wurde aus Vorname und Nachname zusammengesetzt'))
|
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:
|
if not ausweis_id and not student_name and not class_name:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -1739,7 +1753,7 @@ def _upload_student_cards_excel():
|
|||||||
planned_rows.append({
|
planned_rows.append({
|
||||||
'row_number': row_number,
|
'row_number': row_number,
|
||||||
'ausweis_id': ausweis_id,
|
'ausweis_id': ausweis_id,
|
||||||
'student_name': student_name,
|
'student_name': student_name_alias,
|
||||||
'class_name': class_name,
|
'class_name': class_name,
|
||||||
'notes': notes,
|
'notes': notes,
|
||||||
'default_borrow_days': default_borrow_days,
|
'default_borrow_days': default_borrow_days,
|
||||||
@@ -3172,6 +3186,7 @@ def student_cards_admin():
|
|||||||
action = request.form.get('action', 'add')
|
action = request.form.get('action', 'add')
|
||||||
ausweis_id = request.form.get('ausweis_id', '').strip().upper()
|
ausweis_id = request.form.get('ausweis_id', '').strip().upper()
|
||||||
student_name = request.form.get('student_name', '').strip()
|
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)
|
default_borrow_days = request.form.get('default_borrow_days', 14)
|
||||||
class_name = request.form.get('class_name', '').strip()
|
class_name = request.form.get('class_name', '').strip()
|
||||||
notes = request.form.get('notes', '').strip()
|
notes = request.form.get('notes', '').strip()
|
||||||
@@ -3198,7 +3213,7 @@ def student_cards_admin():
|
|||||||
else:
|
else:
|
||||||
encrypted_payload = encrypt_document_fields(
|
encrypted_payload = encrypt_document_fields(
|
||||||
{
|
{
|
||||||
'SchülerName': student_name,
|
'SchülerName': student_name_alias,
|
||||||
'Klasse': class_name,
|
'Klasse': class_name,
|
||||||
'Notizen': notes,
|
'Notizen': notes,
|
||||||
},
|
},
|
||||||
@@ -3231,7 +3246,7 @@ def student_cards_admin():
|
|||||||
try:
|
try:
|
||||||
encrypted_payload = encrypt_document_fields(
|
encrypted_payload = encrypt_document_fields(
|
||||||
{
|
{
|
||||||
'SchülerName': student_name,
|
'SchülerName': student_name_alias,
|
||||||
'Klasse': class_name,
|
'Klasse': class_name,
|
||||||
'Notizen': notes,
|
'Notizen': notes,
|
||||||
},
|
},
|
||||||
@@ -7811,6 +7826,72 @@ def admin_update_user_permissions():
|
|||||||
return redirect(url_for('user_del'))
|
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')
|
@app.route('/logs')
|
||||||
def logs():
|
def logs():
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -17,6 +17,16 @@
|
|||||||
<div class="user-management-container">
|
<div class="user-management-container">
|
||||||
<h2>Benutzer</h2>
|
<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="filter-bar mb-3">
|
||||||
<div class="row g-2 align-items-end">
|
<div class="row g-2 align-items-end">
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
|
|||||||
+39
-3
@@ -12,6 +12,7 @@ Provides methods for creating, validating, and retrieving user information.
|
|||||||
'''
|
'''
|
||||||
import hashlib
|
import hashlib
|
||||||
import copy
|
import copy
|
||||||
|
import re
|
||||||
from bson.objectid import ObjectId
|
from bson.objectid import ObjectId
|
||||||
import settings as cfg
|
import settings as cfg
|
||||||
from settings import MongoClient
|
from settings import MongoClient
|
||||||
@@ -24,6 +25,38 @@ def normalize_student_card_id(card_id):
|
|||||||
return str(card_id).strip().upper()
|
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 = (
|
ACTION_PERMISSION_KEYS = (
|
||||||
'can_borrow',
|
'can_borrow',
|
||||||
'can_insert',
|
'can_insert',
|
||||||
@@ -361,13 +394,15 @@ def add_user(username, password, name, last_name, is_student=False, student_card
|
|||||||
return False
|
return False
|
||||||
permission_defaults = build_default_permission_payload('standard_user')
|
permission_defaults = build_default_permission_payload('standard_user')
|
||||||
|
|
||||||
|
name_alias = build_name_synonym(name, last_name)
|
||||||
|
|
||||||
user_doc = {
|
user_doc = {
|
||||||
'Username': username,
|
'Username': username,
|
||||||
'Password': hashing(password),
|
'Password': hashing(password),
|
||||||
'Admin': False,
|
'Admin': False,
|
||||||
'active_ausleihung': None,
|
'active_ausleihung': None,
|
||||||
'name': name,
|
'name': name_alias,
|
||||||
'last_name': last_name,
|
'last_name': '',
|
||||||
'IsStudent': bool(is_student),
|
'IsStudent': bool(is_student),
|
||||||
'PermissionPreset': permission_defaults['preset'],
|
'PermissionPreset': permission_defaults['preset'],
|
||||||
'ActionPermissions': permission_defaults['actions'],
|
'ActionPermissions': permission_defaults['actions'],
|
||||||
@@ -714,13 +749,14 @@ def update_user_name(username, name, last_name):
|
|||||||
bool: True if updated successfully, False otherwise
|
bool: True if updated successfully, False otherwise
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
name_alias = build_name_synonym(name, last_name)
|
||||||
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||||
db = client[cfg.MONGODB_DB]
|
db = client[cfg.MONGODB_DB]
|
||||||
users = db['users']
|
users = db['users']
|
||||||
|
|
||||||
result = users.update_one(
|
result = users.update_one(
|
||||||
{'Username': username},
|
{'Username': username},
|
||||||
{'$set': {'name': name, 'last_name': last_name}}
|
{'$set': {'name': name_alias, 'last_name': ''}}
|
||||||
)
|
)
|
||||||
|
|
||||||
client.close()
|
client.close()
|
||||||
|
|||||||
Reference in New Issue
Block a user