Compare commits

...

3 Commits

Author SHA1 Message Date
Aiirondev_dev c3db2d9c6b Fix for the delete user function
Release Inventarsystem / release-docker (push) Successful in 2m16s
2026-08-12 16:31:17 +02:00
Aiirondev_dev edc7b72a8f Fix for the delete user function
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-12 16:26:08 +02:00
Aiirondev_dev cdf7b9c45d slight correction to also account for name and last_name
Release Inventarsystem / release-docker (push) Successful in 2m16s
2026-08-12 16:09:52 +02:00
2 changed files with 358 additions and 215 deletions
+157 -101
View File
@@ -7865,6 +7865,82 @@ def register():
permission_page_options=PERMISSION_PAGE_OPTIONS
)
def parse_csv_users(file_bytes):
"""
Liest CSV-Dateien extrem robust ein:
- Erkennt automatisch UTF-8, UTF-8-SIG und Latin-1 (für Excel-Umlaute).
- Erkennt automatisch das Trennzeichen (; oder , oder Tab).
- Erkennt Vor- und Nachname unabhängig von der Spaltenreihenfolge und Headernamen.
"""
# 1. Dekodierung mit Fallback für deutsche Excel-Dateien
try:
content = file_bytes.decode('utf-8-sig')
except UnicodeDecodeError:
content = file_bytes.decode('latin-1')
lines = [line.strip() for line in content.splitlines() if line.strip()]
if not lines:
return []
# 2. Trennzeichen ermitteln (; , oder \t)
first_line = lines[0]
if first_line.count(';') >= first_line.count(','):
delimiter = ';'
elif first_line.count('\t') > first_line.count(','):
delimiter = '\t'
else:
delimiter = ','
stream = io.StringIO(content, newline=None)
reader = csv.DictReader(stream, delimiter=delimiter)
parsed_users = []
# 3. Auswertung mit DictReader (wenn Header vorhanden sind)
if reader.fieldnames:
for row in reader:
# Keys normalisieren (kleingeschrieben, ohne Leerzeichen)
cleaned_row = {str(k).strip().lower(): str(v).strip() for k, v in row.items() if k and v}
# Dynamisches Mapping für Vornamen
name = (
cleaned_row.get('vorname') or
cleaned_row.get('first_name') or
cleaned_row.get('firstname') or
cleaned_row.get('name') or ''
)
# Dynamisches Mapping für Nachnamen
last_name = (
cleaned_row.get('nachname') or
cleaned_row.get('last_name') or
cleaned_row.get('lastname') or
cleaned_row.get('surname') or
cleaned_row.get('familienname') or ''
)
# Falls Header nicht erkannt wurden, aber Werte vorhanden sind (z.B. CSV ohne Header)
if not name and not last_name:
vals = [str(v).strip() for v in row.values() if v]
if len(vals) >= 2:
name, last_name = vals[0], vals[1]
if name or last_name:
parsed_users.append({'name': name, 'last_name': last_name})
else:
# Fallback für dateien ohne Header
stream.seek(0)
raw_reader = csv.reader(stream, delimiter=delimiter)
for row in raw_reader:
clean_row = [str(cell).strip() for cell in row if str(cell).strip()]
if len(clean_row) >= 2:
# Header-Zeilen überspringen
if clean_row[0].lower() in ['vorname', 'first_name', 'name'] and clean_row[1].lower() in ['nachname', 'last_name']:
continue
parsed_users.append({'name': clean_row[0], 'last_name': clean_row[1]})
return parsed_users
def generate_compliant_password(length=16):
lowers = string.ascii_lowercase
uppers = string.ascii_uppercase
@@ -7983,77 +8059,54 @@ def register_csv():
permission_preset = (request.form.get('permission_preset') or 'standard_user').strip()
# 1. CSV über die robuste Funktion einlesen
file_bytes = file.stream.read()
raw_users = parse_csv_users(file_bytes)
if not raw_users:
flash('Keine gültigen Benutzer in der CSV-Datei gefunden. Bitte prüfen Sie das Format.', 'error')
return redirect(url_for('register'))
created_users = []
try:
# Read file stream handling UTF-8 and BOM (Excel exports)
file_stream = io.StringIO(file.stream.read().decode("utf-8-sig"), newline=None)
for entry in raw_users:
name = entry['name']
last_name = entry['last_name']
# Sniff delimiter (comma or semicolon)
sample = file_stream.read(2048)
file_stream.seek(0)
delimiter = ';' if ';' in sample else ','
if not name or not last_name:
continue
reader = csv.reader(file_stream, delimiter=delimiter)
# Benutzernamen & Passwort generieren
username = us.build_unique_username_from_name(name, last_name)
password = generate_compliant_password(16)
for row_num, row in enumerate(reader, start=1):
if not row or all(field.strip() == '' for field in row):
continue # Skip empty rows
# In DB speichern
success = us.add_user(
username=username,
password=password,
name=name,
last_name=last_name,
is_student=False,
student_card_id=None,
max_borrow_days=None,
permission_preset=permission_preset,
)
# Clean entries
row = [field.strip() for field in row]
# Header row detection (Skip row if it looks like "Name, Nachname")
if row_num == 1 and ('name' in row[0].lower() or 'vorname' in row[0].lower()):
continue
if len(row) < 2:
continue
name = row[0]
last_name = row[1]
if not name or not last_name:
continue
# 1. Generate Username via backend logic
username = us.build_unique_username_from_name(name, last_name)
# 2. Generate Random Secure Password
password = generate_compliant_password(16)
# 3. Add User to DB
success = us.add_user(
username=username,
password=password,
name=name,
last_name=last_name,
is_student=False,
student_card_id=None,
max_borrow_days=None,
permission_preset=permission_preset,
)
if success:
created_users.append({
'name': name,
'last_name': last_name,
'username': username,
'password': password
})
except Exception as e:
flash(f'Fehler beim Verarbeiten der CSV-Datei: {str(e)}', 'error')
return redirect(url_for('register'))
if success:
created_users.append({
'name': name,
'last_name': last_name,
'username': username,
'password': password
})
if not created_users:
flash('Keine gültigen Benutzer in der CSV-Datei gefunden.', 'error')
flash('Fehler beim Erstellen der Benutzer aus der CSV.', 'error')
return redirect(url_for('register'))
# Generate PDF
# PDF mit Zugangsdaten generieren
pdf_buffer = generate_credentials_pdf(created_users)
# Return downloadable PDF file
return send_file(
pdf_buffer,
as_attachment=True,
@@ -8061,35 +8114,33 @@ def register_csv():
mimetype='application/pdf'
)
@app.route('/user_del', methods=['GET'])
def user_del():
"""
User deletion interface.
Displays a list of users that can be deleted by an administrator.
Prevents self-deletion by hiding the current user from the list.
Returns:
flask.Response: Rendered template with user list 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. Bitte melden Sie sich an!',
'error')
return redirect(url_for('login'))
# Abruf aller User (falls get_all_users tenant_id unterstützt, kann diese hier übergeben werden)
all_users = us.get_all_users()
users_list = []
for user in all_users:
username = None
for field in ['Username']:
if field in user:
username = user[field]
break
if username and username != session['username']:
username = user.get('Username')
if username and username != session.get('username'):
try:
permissions_payload = us.get_effective_permissions(username)
except Exception:
permissions_payload = us.build_default_permission_payload('standard_user')
try:
name = us.get_name(username)
last_name = us.get_last_name(username)
@@ -8101,12 +8152,13 @@ def user_del():
fullname = last_name
else:
fullname = None
except:
except Exception:
name = ""
last_name = ""
fullname = None
users_list.append({
'username': decrypt_text(username),
'username': username, # Username ist plain in DB, kein decrypt_text() notwendig
'admin': user.get('Admin', False),
'fullname': fullname,
'name': name,
@@ -8115,7 +8167,7 @@ def user_del():
'action_permissions': permissions_payload.get('actions', {}),
'page_permissions': permissions_payload.get('pages', {}),
})
return render_template(
'user_del.html',
users=users_list,
@@ -8128,68 +8180,72 @@ def user_del():
def delete_user():
"""
Process user deletion request.
Deletes a specified user from the system.
Deletes a specified user from the system directly via the tenant db.
Includes safety checks to prevent self-deletion.
Returns:
flask.Response: Redirect to the user deletion interface with status
"""
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, diese Aktion auszuführen. Bitte melden Sie sich an!', 'error')
return redirect(url_for('login'))
username = request.form.get('username')
if not username:
flash('Kein Benutzer ausgewählt', 'error')
return redirect(url_for('user_del'))
# Prevent self-deletion
if username == session['username']:
if username == session.get('username'):
flash('Sie können Ihr eigenes Konto nicht löschen', 'error')
return redirect(url_for('user_del'))
# Reset this user's borrowings and free items before deleting the user
try:
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
db = client[MONGODB_DB]
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
tenant_id = session.get('tenant_id')
db = _get_tenant_db(client, tenant_id)
ausleihungen = db['ausleihungen']
items_col = db['items']
users_col = db['users'] # Direkter Zugriff auf die User-Collection
now = datetime.datetime.now()
# Complete all active borrowings of this user
# 1. Aktive Ausleihen abschließen
ausleihungen.update_many(
{'User': username, 'Status': 'active'},
{'$set': {'Status': 'completed', 'End': now, 'LastUpdated': now}}
)
# Cancel all planned borrowings of this user
# 2. Geplante Ausleihen stornieren
ausleihungen.update_many(
{'User': username, 'Status': 'planned'},
{'$set': {'Status': 'cancelled', 'LastUpdated': now}}
)
# Free all items currently associated with this user
# 3. Inventar wieder verfügbar machen
items_col.update_many(
{'User': username},
{'$set': {'Verfuegbar': True, 'LastUpdated': now}, '$unset': {'User': ""}}
)
client.close()
except Exception as e:
app.logger.error(f"Error resetting borrowings for user {encrypt_text(username)}: {e}")
flash(f'Warnung: Ausleihungen/Reservierungen für {username} konnten nicht vollständig zurückgesetzt werden', 'warning')
# 4. Den Benutzer direkt in der überprüften DB-Verbindung löschen
# Achte auf die exakte Großschreibung 'Username' (so wie in add_user definiert)
delete_result = users_col.delete_one({'Username': username})
# 5. Explizite Erfolgskontrolle
if delete_result.deleted_count > 0:
flash(f'Benutzer {username} erfolgreich gelöscht', 'success')
else:
# Fallback, falls der Nutzer nicht gefunden wurde (Fehlervermeidung)
flash(f'Löschen fehlgeschlagen: Benutzer {username} wurde in der Datenbank nicht gefunden.', 'error')
# Delete the user
try:
us.delete_user(username)
flash(f'Benutzer {username} erfolgreich gelöscht', 'success')
except Exception as e:
app.logger.error(f"Error deleting user {encrypt_text(username)}: {e}")
flash('Fehler beim Löschen des Benutzers', 'error')
app.logger.error(f"Error resetting borrowings or deleting user {username}: {e}")
flash('Kritischer Fehler beim Löschen des Benutzers', 'error')
finally:
# Garantiert, dass die DB-Verbindung geschlossen wird
if 'client' in locals():
client.close()
return redirect(url_for('user_del'))
@app.route('/admin/borrowings')
def admin_borrowings():
"""
+201 -114
View File
@@ -321,11 +321,14 @@ document.addEventListener('DOMContentLoaded', function () {
--primary-color: #3498db;
--primary-dark: #2980b9;
--success-color: #2ecc71;
--success-dark: #27ae60;
--csv-color: #059669;
--csv-dark: #047857;
--error-color: #e74c3c;
--text-color: var(--ui-text);
--text-color: var(--ui-text, #1f2937);
--light-bg: #f9f9f9;
--border-radius: 8px;
--shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
--shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
--transition: all 0.3s ease;
}
@@ -358,37 +361,55 @@ body {
}
.subtitle {
font-size: 1.1rem;
color: #777;
font-size: 1.05rem;
color: #6b7280;
margin-top: 0;
}
.content {
background-color: var(--ui-surface);
background-color: var(--ui-surface, #ffffff);
padding: 2rem;
border-radius: var(--border-radius);
box-shadow: var(--shadow);
margin-bottom: 2rem;
}
/* Karten-Layout für Einzel- & Massenregistrierung */
.form-card {
max-width: 500px;
margin: 0 auto;
max-width: 620px;
margin: 0 auto 2.5rem auto;
padding: 1.5rem;
background: var(--ui-surface-soft, #f9fafb);
border: 1px solid #e5e7eb;
border-radius: var(--border-radius);
}
.form-card:last-child {
margin-bottom: 0;
}
.form-card h2 {
font-size: 1.4rem;
margin-top: 0;
margin-bottom: 0.5rem;
color: #111827;
}
.form-group {
margin-bottom: 1.5rem;
margin-bottom: 1.25rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: var(--primary-dark);
margin-bottom: 0.4rem;
font-weight: 600;
font-size: 0.95rem;
color: #374151;
}
.input-container {
position: relative;
width: 100%;
}
.input-icon {
@@ -396,24 +417,30 @@ body {
left: 1rem;
top: 50%;
transform: translateY(-50%);
color: #aaa;
font-size: 1.2rem;
color: #9ca3af;
font-size: 1.1rem;
pointer-events: none;
z-index: 1;
}
/* Eingabefelder & Selects */
input[type="text"],
input[type="password"],
input[type="file"],
.form-select {
width: 100%;
padding: 0.8rem 1rem 0.8rem 3rem;
border: 1px solid #ddd;
padding: 0.75rem 1rem 0.75rem 2.8rem;
border: 1px solid #d1d5db;
border-radius: var(--border-radius);
font-size: 1rem;
font-size: 0.95rem;
background-color: #ffffff;
transition: var(--transition);
box-sizing: border-box;
}
input[type="text"]:focus,
input[type="password"]:focus,
input[type="file"]:focus,
.form-select:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2);
@@ -422,14 +449,47 @@ input[type="password"]:focus,
.form-select {
padding-left: 1rem;
cursor: pointer;
}
/* Stylings für den CSV-Datei-Upload */
input[type="file"] {
padding-left: 2.8rem;
cursor: pointer;
}
input[type="file"]::file-selector-button {
padding: 0.35rem 0.75rem;
border-radius: 4px;
border: 1px solid #d1d5db;
background: #f3f4f6;
color: #374151;
cursor: pointer;
margin-right: 12px;
font-weight: 500;
transition: var(--transition);
}
input[type="file"]::file-selector-button:hover {
background: #e5e7eb;
}
input::placeholder {
color: #aaa;
color: #9ca3af;
}
code {
background: #e0f2fe;
color: #0369a1;
padding: 2px 6px;
border-radius: 4px;
font-family: monospace;
font-size: 0.9em;
}
/* Buttons */
.form-actions {
margin-top: 2rem;
margin-top: 1.5rem;
}
.action-button {
@@ -437,17 +497,21 @@ input::placeholder {
color: white;
border: none;
padding: 0.8rem 1.5rem;
border-radius: 30px;
border-radius: var(--border-radius);
cursor: pointer;
font-weight: 600;
transition: var(--transition);
width: 100%;
font-size: 1rem;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.action-button:hover {
background-color: var(--primary-dark);
transform: translateY(-2px);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
@@ -456,79 +520,48 @@ input::placeholder {
}
.register-button:hover {
background-color: #27ae60;
background-color: var(--success-dark);
}
.flash-container {
margin-bottom: 1.5rem;
/* Toggle-Button für Passwort-Auge */
.input-wrapper {
position: relative;
width: 100%;
}
.flash {
display: flex;
align-items: center;
padding: 1rem;
margin-bottom: 1rem;
border-radius: var(--border-radius);
animation: fadeIn 0.3s ease-in-out;
.toggle-pw-btn {
position: absolute;
right: 0.75rem;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
cursor: pointer;
font-size: 1.1rem;
padding: 4px;
z-index: 2;
opacity: 0.7;
transition: opacity 0.2s;
}
.flash-icon {
margin-right: 0.8rem;
font-size: 1.2rem;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.3);
}
.flash.success {
background-color: #d4edda;
color: #155724;
border-left: 4px solid var(--success-color);
}
.flash.error {
background-color: #f8d7da;
color: #721c24;
border-left: 4px solid var(--error-color);
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
@media (max-width: 768px) {
.container {
padding: 0 1rem;
margin: 1rem auto;
}
.content {
padding: 1.5rem;
}
.header-section h1 {
font-size: 2rem;
}
.toggle-pw-btn:hover {
opacity: 1;
}
/* Passwort-Regeln Box */
.password-rules {
margin-bottom: 10px;
padding: 10px 12px;
border: 1px solid #e5e7eb;
border-radius: 8px;
background: var(--ui-surface-soft);
background: #ffffff;
}
.password-rules-title {
margin: 0 0 8px;
margin: 0 0 6px;
font-weight: 700;
color: #1f2937;
font-size: 0.92rem;
font-size: 0.88rem;
}
.password-rules ul {
@@ -539,10 +572,10 @@ input::placeholder {
.pw-rule {
position: relative;
padding-left: 22px;
margin: 5px 0;
color: #b91c1c;
font-size: 0.9rem;
padding-left: 20px;
margin: 4px 0;
color: #dc2626;
font-size: 0.85rem;
}
.pw-rule::before {
@@ -561,15 +594,39 @@ input::placeholder {
content: '✓';
}
.pw-actions {
display: flex;
gap: 8px;
margin-top: 8px;
}
.btn-secondary {
padding: 6px 12px;
border: 1px solid #d1d5db;
background-color: #ffffff;
color: #374151;
border-radius: 6px;
cursor: pointer;
font-size: 0.88rem;
font-weight: 500;
transition: var(--transition);
}
.btn-secondary:hover {
background-color: #f3f4f6;
border-color: #9ca3af;
}
/* Hinweise & Rechte-Panels */
.anonymize-hint {
margin-top: 10px;
margin-top: 8px;
margin-bottom: 0;
background: #f0f9ff;
border: 1px solid #bae6fd;
border-radius: 8px;
padding: 10px 12px;
color: #0c4a6e;
font-size: 0.92rem;
border-radius: 6px;
padding: 8px 12px;
color: #0369a1;
font-size: 0.88rem;
}
.permission-panels {
@@ -581,13 +638,13 @@ input::placeholder {
.permission-panel {
border: 1px solid #d1d5db;
border-radius: 8px;
padding: 10px;
background: var(--ui-surface-soft);
padding: 12px;
background: #ffffff;
}
.permission-panel h4 {
margin: 0 0 8px;
font-size: 1rem;
font-size: 0.95rem;
color: #1f2937;
}
@@ -595,42 +652,72 @@ input::placeholder {
display: flex;
align-items: center;
gap: 8px;
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;
margin: 6px 0;
color: #374151;
font-size: 0.9rem;
cursor: pointer;
font-size: 0.9em;
transition: background-color 0.2s;
}
.btn-secondary:hover {
background-color: #e5e7eb;
/* Flash Nachrichten */
.flash-container {
margin-bottom: 1.5rem;
}
.input-wrapper {
.flash {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
padding: 0.9rem 1rem;
margin-bottom: 1rem;
border-radius: var(--border-radius);
animation: fadeIn 0.3s ease-in-out;
}
.input-wrapper .input-container {
flex-grow: 1;
margin: 0;
.flash-icon {
margin-right: 0.8rem;
font-size: 1rem;
display: flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.4);
}
.flash.success {
background-color: #d1fae5;
color: #065f46;
border-left: 4px solid var(--csv-color);
}
.flash.error {
background-color: #fee2e2;
color: #991b1b;
border-left: 4px solid var(--error-color);
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-8px); }
to { opacity: 1; transform: translateY(0); }
}
@media (max-width: 768px) {
.container {
padding: 0 0.75rem;
margin: 1rem auto;
}
.content {
padding: 1rem;
}
.form-card {
padding: 1rem;
}
.header-section h1 {
font-size: 1.8rem;
}
}
</style>
{% endblock %}