Compare commits

...

3 Commits

Author SHA1 Message Date
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
Aiirondev_dev 5ba0944faf implementation of the user .csv importing
Release Inventarsystem / release-docker (push) Successful in 2m16s
2026-08-12 15:46:31 +02:00
2 changed files with 753 additions and 371 deletions
+290 -36
View File
@@ -27,6 +27,10 @@ import os
import sys
from bs4 import BeautifulSoup
from gridfs import GridFS
import string
from reportlab.lib.pagesizes import A4
from reportlab.pdfgen import canvas
from reportlab.lib import colors
# Ensure imports work regardless of whether gunicorn starts in /app or /app/Web.
_CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
@@ -7861,35 +7865,282 @@ 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
digits = string.digits
symbols = "!@#$%^&*()_+~|}{[]:;?><,.-="
# Ensure at least one character from each required category
pwd = [
secrets.choice(lowers),
secrets.choice(uppers),
secrets.choice(digits),
secrets.choice(symbols)
]
all_chars = lowers + uppers + digits + symbols
pwd += [secrets.choice(all_chars) for _ in range(length - 4)]
# Shuffle so guaranteed types aren't always at the start
secrets.SystemRandom().shuffle(pwd)
return "".join(pwd)
def generate_credentials_pdf(created_users):
"""
Creates a PDF in memory with 2 user credential cards per A4 page.
created_users: list of dicts [{'name': ..., 'last_name': ..., 'username': ..., 'password': ...}]
"""
buffer = io.BytesIO()
pdf = canvas.Canvas(buffer, pagesize=A4)
width, height = A4 # 595.27 x 841.89 points
card_height = height / 2.0 # Split page into 2 equal halves
for i, user in enumerate(created_users):
page_slot = i % 2 # 0 = Top half, 1 = Bottom half
# If starting a new page (except for the very first item)
if i > 0 and page_slot == 0:
pdf.showPage()
# Calculate Y offset for card position
y_offset = height - (page_slot + 1) * card_height
# Card Container Box
margin = 35
box_x = margin
box_y = y_offset + margin
box_w = width - (2 * margin)
box_h = card_height - (2 * margin)
# Outer Border
pdf.setStrokeColor(colors.HexColor('#CBD5E1'))
pdf.setLineWidth(1)
pdf.rect(box_x, box_y, box_w, box_h, fill=0)
# Header Banner inside Card
pdf.setFillColor(colors.HexColor('#1E293B'))
pdf.rect(box_x, box_y + box_h - 45, box_w, 45, fill=1, stroke=0)
# Header Title Text
pdf.setFillColor(colors.white)
pdf.setFont("Helvetica-Bold", 14)
pdf.drawString(box_x + 20, box_y + box_h - 28, "Zugangsdaten / Account Credentials")
# User Info Details
content_y = box_y + box_h - 80
# Name
pdf.setFillColor(colors.HexColor('#0F172A'))
pdf.setFont("Helvetica-Bold", 12)
pdf.drawString(box_x + 25, content_y, f"Name: {user['name']} {user['last_name']}")
# Username
content_y -= 35
pdf.setFont("Helvetica", 11)
pdf.setFillColor(colors.HexColor('#475569'))
pdf.drawString(box_x + 25, content_y, "Benutzername:")
pdf.setFont("Helvetica-Bold", 13)
pdf.setFillColor(colors.HexColor('#0F172A'))
pdf.drawString(box_x + 160, content_y, user['username'])
# Password
content_y -= 30
pdf.setFont("Helvetica", 11)
pdf.setFillColor(colors.HexColor('#475569'))
pdf.drawString(box_x + 25, content_y, "Passwort:")
pdf.setFont("Courier-Bold", 13)
pdf.setFillColor(colors.HexColor('#0F172A'))
pdf.drawString(box_x + 160, content_y, user['password'])
# Security Footer Note
content_y -= 45
pdf.setFont("Helvetica-Oblique", 9)
pdf.setFillColor(colors.HexColor('#64748B'))
pdf.drawString(box_x + 25, content_y, "Hinweis: Bitte ändern Sie Ihr Passwort nach der ersten Anmeldung.")
# Dashed Cut Line between top and bottom cards
if page_slot == 0 and i < len(created_users) - 1:
pdf.setDash(4, 4)
pdf.setStrokeColor(colors.HexColor('#94A3B8'))
pdf.line(0, card_height, width, card_height)
pdf.setDash() # Reset dash
pdf.save()
buffer.seek(0)
return buffer
@app.route('/register/csv', methods=['POST'])
def register_csv():
if 'username' not in session:
flash('Ihnen ist es nicht gestattet, diese Aktion auszuführen.', 'error')
return redirect(url_for('login'))
file = request.files.get('csv_file')
if not file or not file.filename.endswith('.csv'):
flash('Bitte laden Sie eine gültige CSV-Datei hoch.', 'error')
return redirect(url_for('register'))
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 = []
for entry in raw_users:
name = entry['name']
last_name = entry['last_name']
if not name or not last_name:
continue
# Benutzernamen & Passwort generieren
username = us.build_unique_username_from_name(name, last_name)
password = generate_compliant_password(16)
# 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,
)
if success:
created_users.append({
'name': name,
'last_name': last_name,
'username': username,
'password': password
})
if not created_users:
flash('Fehler beim Erstellen der Benutzer aus der CSV.', 'error')
return redirect(url_for('register'))
# PDF mit Zugangsdaten generieren
pdf_buffer = generate_credentials_pdf(created_users)
return send_file(
pdf_buffer,
as_attachment=True,
download_name='benutzer_zugangsdaten.pdf',
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)
@@ -7901,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,
@@ -7915,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,
@@ -7930,61 +8182,63 @@ def delete_user():
Process user deletion request.
Deletes a specified user from the system.
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 auf dieser Internetanwendung, die eben besuchte Adresse zu nutzen. 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']
now = datetime.datetime.now()
# Complete all active borrowings of this user
# 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
# Geplante Ausleihen stornieren
ausleihungen.update_many(
{'User': username, 'Status': 'planned'},
{'$set': {'Status': 'cancelled', 'LastUpdated': now}}
)
# Free all items currently associated with this user
# 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')
app.logger.error(f"Error resetting borrowings for user {username}: {e}")
flash(f'Warnung: Ausleihungen/Reservierungen für {username} konnten nicht vollständig zurückgesetzt werden',
'warning')
finally:
# Garantiert, dass die Datenbankverbindung geschlossen wird
if 'client' in locals():
client.close()
# Delete the user
try:
# us.delete_user muss ggf. noch angepasst werden, falls es auch den tenant_id verlangt
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}")
app.logger.error(f"Error deleting user {username}: {e}")
flash('Fehler beim Löschen des Benutzers', 'error')
return redirect(url_for('user_del'))
+463 -335
View File
@@ -6,7 +6,7 @@
<div class="container">
<div class="header-section">
<h1>Neuen Benutzer registrieren</h1>
<p class="subtitle">Erstellen Sie ein neues Benutzerkonto und legen Sie Zugriffsrechte fest</p>
<p class="subtitle">Erstellen Sie ein neues Benutzerkonto oder importieren Sie mehrere Benutzer per CSV</p>
</div>
<div class="flash-container">
@@ -23,7 +23,43 @@
</div>
<div class="content">
<!-- 1. CSV BULK IMPORT CARD -->
<div class="form-card" style="margin-bottom: 2rem; border-top: 4px solid #059669;">
<div class="card-header" style="margin-bottom: 1rem;">
<h2>Massenregistrierung via CSV</h2>
<p class="subtitle" style="color: #4b5563;">
Laden Sie eine CSV-Datei hoch (Format: <code>Vorname, Nachname</code>).
Benutzernamen und sichere Passwörter werden serverseitig generiert. Nach dem Upload erhalten Sie direkt ein PDF mit Zugangsdaten (2 pro Seite zum Ausschneiden).
</p>
</div>
<form method="POST" action="{{ url_for('register_csv') }}" enctype="multipart/form-data">
<div class="form-group">
<label for="permission-preset-csv">Berechtigungs-Preset für alle CSV-Benutzer</label>
<select id="permission-preset-csv" name="permission_preset" class="form-select" style="margin-bottom: 1rem;">
{% for preset_key, preset in permission_presets.items() %}
<option value="{{ preset_key }}">{{ preset.label }}</option>
{% endfor %}
</select>
<label for="csv_file">CSV-Datei auswählen</label>
<div class="input-container">
<span class="input-icon">📄</span>
<input type="file" id="csv_file" name="csv_file" accept=".csv" required style="padding: 10px;">
</div>
</div>
<div class="form-group form-actions" style="margin-top: 1.5rem;">
<button type="submit" class="action-button" style="background-color: #059669; color: white;">
📥 CSV Importieren & Zugangsdaten-PDF Herunterladen
</button>
</div>
</form>
</div>
<!-- 2. SINGLE USER REGISTRATION CARD -->
<div class="form-card">
<h2>Einzelnen Benutzer registrieren</h2>
<form method="POST" action="{{ url_for('register') }}">
<div class="form-group">
@@ -32,11 +68,13 @@
<span class="input-icon">👤</span>
<input type="text" id="name" name="name" placeholder="Geben Sie den Vornamen ein" required onchange="generateUsername()" oninput="generateUsername()">
</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 onchange="generateUsername()" oninput="generateUsername()">
</div>
<label for="username">Benutzername <span style="color: #9ca3af;">(Vorschau - wird serverseitig finalisiert)</span></label>
<div class="input-container">
<span class="input-icon">👤</span>
@@ -44,7 +82,7 @@
</div>
<p class="anonymize-hint">Klarnamen werden nur zur Erzeugung des Benutzernamens als Kürzel (z.B. SimFri) verwendet; bei Kollision wird automatisch ein Buchstabe mehr genommen.</p>
</div>
<div class="form-group">
<label for="password">Passwort</label>
<div class="password-rules" id="password-rules" aria-live="polite">
@@ -57,20 +95,21 @@
<li id="pw-rule-symbol" class="pw-rule">Mindestens ein Sonderzeichen</li>
</ul>
</div>
<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"
<input
type="password"
id="password"
name="password"
placeholder="Geben Sie ein sicheres Passwort ein"
required
pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9]).{12,}">
<button type="button" id="toggle-pw-btn" class="toggle-pw-btn" onclick="togglePasswordVisibility()" style="background:none; border:none; cursor:pointer; padding-right:10px;">👁️</button>
</div>
</div>
<div class="pw-actions">
<div class="pw-actions" style="margin-top: 8px;">
<button type="button" class="btn-secondary" onclick="generateSecurePassword()">Passwort generieren</button>
</div>
</div>
@@ -120,323 +159,6 @@
</div>
</div>
<style>
:root {
--primary-color: #3498db;
--primary-dark: #2980b9;
--success-color: #2ecc71;
--error-color: #e74c3c;
--text-color: var(--ui-text);
--light-bg: #f9f9f9;
--border-radius: 8px;
--shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
--transition: all 0.3s ease;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: var(--light-bg);
margin: 0;
padding: 0;
color: var(--text-color);
line-height: 1.6;
}
.container {
max-width: 800px;
margin: 2rem auto;
padding: 0 1.5rem;
}
.header-section {
text-align: center;
margin-bottom: 2rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid #eee;
}
.header-section h1 {
font-size: 2.5rem;
margin-bottom: 0.5rem;
color: var(--primary-color);
}
.subtitle {
font-size: 1.1rem;
color: #777;
margin-top: 0;
}
.content {
background-color: var(--ui-surface);
padding: 2rem;
border-radius: var(--border-radius);
box-shadow: var(--shadow);
margin-bottom: 2rem;
}
.form-card {
max-width: 500px;
margin: 0 auto;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: var(--primary-dark);
}
.input-container {
position: relative;
}
.input-icon {
position: absolute;
left: 1rem;
top: 50%;
transform: translateY(-50%);
color: #aaa;
font-size: 1.2rem;
}
input[type="text"],
input[type="password"],
.form-select {
width: 100%;
padding: 0.8rem 1rem 0.8rem 3rem;
border: 1px solid #ddd;
border-radius: var(--border-radius);
font-size: 1rem;
transition: var(--transition);
box-sizing: border-box;
}
input[type="text"]:focus,
input[type="password"]:focus,
.form-select:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2);
outline: none;
}
.form-select {
padding-left: 1rem;
}
input::placeholder {
color: #aaa;
}
.form-actions {
margin-top: 2rem;
}
.action-button {
background-color: var(--primary-color);
color: white;
border: none;
padding: 0.8rem 1.5rem;
border-radius: 30px;
cursor: pointer;
font-weight: 600;
transition: var(--transition);
width: 100%;
font-size: 1rem;
}
.action-button:hover {
background-color: var(--primary-dark);
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.register-button {
background-color: var(--success-color);
}
.register-button:hover {
background-color: #27ae60;
}
.flash-container {
margin-bottom: 1.5rem;
}
.flash {
display: flex;
align-items: center;
padding: 1rem;
margin-bottom: 1rem;
border-radius: var(--border-radius);
animation: fadeIn 0.3s ease-in-out;
}
.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;
}
}
.password-rules {
margin-bottom: 10px;
padding: 10px 12px;
border: 1px solid #e5e7eb;
border-radius: 8px;
background: var(--ui-surface-soft);
}
.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 {
margin-top: 10px;
margin-bottom: 0;
background: #f0f9ff;
border: 1px solid #bae6fd;
border-radius: 8px;
padding: 10px 12px;
color: #0c4a6e;
font-size: 0.92rem;
}
.permission-panels {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 12px;
}
.permission-panel {
border: 1px solid #d1d5db;
border-radius: 8px;
padding: 10px;
background: var(--ui-surface-soft);
}
.permission-panel h4 {
margin: 0 0 8px;
font-size: 1rem;
color: #1f2937;
}
.permission-check {
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;
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>
// Hilfsfunktion: Umlaute auflösen und Sonderzeichen entfernen
function cleanNameForUsername(text) {
@@ -462,7 +184,7 @@ function generateUsername() {
const firstName = cleanNameForUsername(document.getElementById('name').value);
const lastName = cleanNameForUsername(document.getElementById('last-name').value);
let username = '';
if (firstName && lastName) {
username = formatPart(firstName, 3) + formatPart(lastName, 3);
} else if (firstName) {
@@ -470,7 +192,7 @@ function generateUsername() {
} else if (lastName) {
username = formatPart(lastName, 6);
}
const usernameField = document.getElementById('username');
if (usernameField) {
usernameField.value = username || '';
@@ -503,11 +225,12 @@ function generateSecurePassword() {
const pwField = document.getElementById('password');
pwField.value = password;
// Automatisch sichtbar machen, damit der User es kopieren kann
// Automatisch sichtbar machen
pwField.type = 'text';
document.getElementById('toggle-pw-btn').textContent = '🙈';
const toggleBtn = document.getElementById('toggle-pw-btn');
if (toggleBtn) toggleBtn.textContent = '🙈';
updatePasswordRules();
}
@@ -517,10 +240,10 @@ function togglePasswordVisibility() {
const toggleBtn = document.getElementById('toggle-pw-btn');
if (pwField.type === "password") {
pwField.type = "text";
toggleBtn.textContent = '🙈';
if (toggleBtn) toggleBtn.textContent = '🙈';
} else {
pwField.type = "password";
toggleBtn.textContent = '👁️';
if (toggleBtn) toggleBtn.textContent = '👁️';
}
}
@@ -530,7 +253,7 @@ function updatePasswordRules() {
if (!passwordInput) return;
const value = String(passwordInput.value || '');
const setRuleState = (id, ok) => {
const node = document.getElementById(id);
if (node) node.classList.toggle('ok', !!ok);
@@ -592,4 +315,409 @@ document.addEventListener('DOMContentLoaded', function () {
}
});
</script>
<style>
:root {
--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, #1f2937);
--light-bg: #f9f9f9;
--border-radius: 8px;
--shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
--transition: all 0.3s ease;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: var(--light-bg);
margin: 0;
padding: 0;
color: var(--text-color);
line-height: 1.6;
}
.container {
max-width: 800px;
margin: 2rem auto;
padding: 0 1.5rem;
}
.header-section {
text-align: center;
margin-bottom: 2rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid #eee;
}
.header-section h1 {
font-size: 2.5rem;
margin-bottom: 0.5rem;
color: var(--primary-color);
}
.subtitle {
font-size: 1.05rem;
color: #6b7280;
margin-top: 0;
}
.content {
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: 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.25rem;
}
.form-group label {
display: block;
margin-bottom: 0.4rem;
font-weight: 600;
font-size: 0.95rem;
color: #374151;
}
.input-container {
position: relative;
width: 100%;
}
.input-icon {
position: absolute;
left: 1rem;
top: 50%;
transform: translateY(-50%);
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.75rem 1rem 0.75rem 2.8rem;
border: 1px solid #d1d5db;
border-radius: var(--border-radius);
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);
outline: none;
}
.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: #9ca3af;
}
code {
background: #e0f2fe;
color: #0369a1;
padding: 2px 6px;
border-radius: 4px;
font-family: monospace;
font-size: 0.9em;
}
/* Buttons */
.form-actions {
margin-top: 1.5rem;
}
.action-button {
background-color: var(--primary-color);
color: white;
border: none;
padding: 0.8rem 1.5rem;
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(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.register-button {
background-color: var(--success-color);
}
.register-button:hover {
background-color: var(--success-dark);
}
/* Toggle-Button für Passwort-Auge */
.input-wrapper {
position: relative;
width: 100%;
}
.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;
}
.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: #ffffff;
}
.password-rules-title {
margin: 0 0 6px;
font-weight: 700;
color: #1f2937;
font-size: 0.88rem;
}
.password-rules ul {
list-style: none;
margin: 0;
padding: 0;
}
.pw-rule {
position: relative;
padding-left: 20px;
margin: 4px 0;
color: #dc2626;
font-size: 0.85rem;
}
.pw-rule::before {
content: '✗';
position: absolute;
left: 0;
top: 0;
font-weight: 700;
}
.pw-rule.ok {
color: #166534;
}
.pw-rule.ok::before {
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: 8px;
margin-bottom: 0;
background: #f0f9ff;
border: 1px solid #bae6fd;
border-radius: 6px;
padding: 8px 12px;
color: #0369a1;
font-size: 0.88rem;
}
.permission-panels {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 12px;
}
.permission-panel {
border: 1px solid #d1d5db;
border-radius: 8px;
padding: 12px;
background: #ffffff;
}
.permission-panel h4 {
margin: 0 0 8px;
font-size: 0.95rem;
color: #1f2937;
}
.permission-check {
display: flex;
align-items: center;
gap: 8px;
margin: 6px 0;
color: #374151;
font-size: 0.9rem;
cursor: pointer;
}
/* Flash Nachrichten */
.flash-container {
margin-bottom: 1.5rem;
}
.flash {
display: flex;
align-items: center;
padding: 0.9rem 1rem;
margin-bottom: 1rem;
border-radius: var(--border-radius);
animation: fadeIn 0.3s ease-in-out;
}
.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 %}