From 5ba0944faf03f8d8ca4d38647883fbf13ecbce92 Mon Sep 17 00:00:00 2001 From: AIIrondev Date: Wed, 12 Aug 2026 15:46:31 +0200 Subject: [PATCH] implementation of the user .csv importing --- Web/app.py | 200 +++++++++++++++++++ Web/templates/register.html | 373 ++++++++++++++++++++---------------- 2 files changed, 407 insertions(+), 166 deletions(-) diff --git a/Web/app.py b/Web/app.py index cb1bc23..209a970 100755 --- a/Web/app.py +++ b/Web/app.py @@ -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,6 +7865,202 @@ def register(): permission_page_options=PERMISSION_PAGE_OPTIONS ) +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() + + 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) + + # Sniff delimiter (comma or semicolon) + sample = file_stream.read(2048) + file_stream.seek(0) + delimiter = ';' if ';' in sample else ',' + + reader = csv.reader(file_stream, delimiter=delimiter) + + for row_num, row in enumerate(reader, start=1): + if not row or all(field.strip() == '' for field in row): + continue # Skip empty rows + + # 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 not created_users: + flash('Keine gültigen Benutzer in der CSV-Datei gefunden.', 'error') + return redirect(url_for('register')) + + # Generate PDF + pdf_buffer = generate_credentials_pdf(created_users) + + # Return downloadable PDF file + 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(): """ diff --git a/Web/templates/register.html b/Web/templates/register.html index dcaf06f..ab3ceb8 100755 --- a/Web/templates/register.html +++ b/Web/templates/register.html @@ -6,7 +6,7 @@

Neuen Benutzer registrieren

-

Erstellen Sie ein neues Benutzerkonto und legen Sie Zugriffsrechte fest

+

Erstellen Sie ein neues Benutzerkonto oder importieren Sie mehrere Benutzer per CSV

@@ -23,7 +23,43 @@
+ +
+
+

Massenregistrierung via CSV

+

+ Laden Sie eine CSV-Datei hoch (Format: Vorname, Nachname). + Benutzernamen und sichere Passwörter werden serverseitig generiert. Nach dem Upload erhalten Sie direkt ein PDF mit Zugangsdaten (2 pro Seite zum Ausschneiden). +

+
+ +
+
+ + + + +
+ 📄 + +
+
+ +
+ +
+
+
+ +
+

Einzelnen Benutzer registrieren

@@ -32,11 +68,13 @@ 👤
+
👤
+
👤 @@ -44,7 +82,7 @@

Klarnamen werden nur zur Erzeugung des Benutzernamens als Kürzel (z.B. SimFri) verwendet; bei Kollision wird automatisch ein Buchstabe mehr genommen.

- +
@@ -57,20 +95,21 @@
  • Mindestens ein Sonderzeichen
  • - +
    🔒 - - +
    -
    +
    @@ -120,6 +159,163 @@
    + + - {% endblock %} \ No newline at end of file