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 @@
@@ -23,7 +23,43 @@
+
+
-
{% endblock %}
\ No newline at end of file