diff --git a/Website/main.py b/Website/main.py index 3581428..de44222 100644 --- a/Website/main.py +++ b/Website/main.py @@ -1878,6 +1878,17 @@ def login(): stored_user = _normalize_user_doc(stored_user_raw) if stored_user: + if stored_user.get("verified", True) is False: + if request.is_json: + return jsonify({ + "error": "Account not verified", + "username": stored_user.get("username"), + "needs_verification": True + }, 403) + + flash('Bitte verifizieren Sie Ihre E-Mail-Adresse, bevor Sie sich einloggen.', 'error') + return redirect(url_for('login')) + session['username'] = stored_user.get("username") session['display_name'] = stored_user.get("display_name") or stored_user.get("username") session['is_admin'] = stored_user.get("is_admin", False) @@ -1889,8 +1900,8 @@ def login(): if request.is_json: return jsonify({"error": "Invalid credentials"}), 401 + flash('Login fehlgeschlagen. Bitte prüfen Sie Ihre Eingaben.', 'error') - get_flashed_messages() return render_template('login.html') @@ -1934,26 +1945,73 @@ def register(): contact_person = _sanitize_text(contact_person, 120) email = _sanitize_text(email, 254) + from modules.emailservice.email import send_register_token + import secrets + + token = secrets.token_hex(3).upper() + try: existing_users = user_store.get_all_users() or [] is_first_user = len(existing_users) == 0 - if not user_store.add_user(username, password, school_name, contact_person, email, marketing_opt_in=marketing_opt_in): + if not user_store.add_user( + username, password, school_name, contact_person, email, + marketing_opt_in=marketing_opt_in, + verified=False, + verification_token=token + ): flash("Benutzer konnte nicht erstellt werden.", "error") return redirect(url_for("register")) if is_first_user: user_store.make_admin(username) + + email_sent = send_register_token(email, token) + if not email_sent: + flash("Konto erstellt, aber E-Mail konnte nicht gesendet werden. Bitte Support kontaktieren.", "error") + else: + flash("Schulregistrierung erfolgreich. Bitte überprüfen Sie Ihre E-Mails für den Bestätigungscode.", "success") + except Exception: flash("MongoDB ist derzeit nicht erreichbar.", "error") return redirect(url_for("register")) - flash("Schulregistrierung erfolgreich. Bitte jetzt einloggen.", "success") - return redirect(url_for("login")) + return redirect(url_for("verify_account", username=username)) return render_template("register.html") +@app.route("/verify", methods=["GET", "POST"]) +def verify_account(): + if request.method == "POST": + username = (request.form.get("username") or "").strip() + token = (request.form.get("token") or "").strip().upper() + + if not username or not token: + flash("Bitte Benutzername und Token eingeben.", "error") + return redirect(url_for("verify_account")) + + user = _find_user(username) + if not user: + flash("Benutzer nicht gefunden.", "error") + return redirect(url_for("verify_account")) + + if user.get("verified"): + flash("Konto ist bereits verifiziert. Bitte einloggen.", "success") + return redirect(url_for("login")) + + if user.get("verification_token") == token: + user_store.verify_user(username) + flash("Konto erfolgreich verifiziert! Sie können sich jetzt einloggen.", "success") + return redirect(url_for("login")) + else: + flash("Ungültiger Bestätigungscode. Bitte erneut versuchen.", "error") + return redirect(url_for("verify_account", username=username)) + + prefilled_username = request.args.get("username", "") + return render_template("verify.html", username=prefilled_username) + + @app.route('/download-sample') def download_sample(): # Ensure the file is only served from the specific directory @@ -1969,11 +2027,6 @@ def preise(): return render_template("preise.html") -from datetime import datetime -from flask import request, jsonify, session, flash, redirect, url_for, render_template -from pymongo.errors import PyMongoError - - @app.route('/preise/book-option', methods=['POST']) @login_required def book_option_package(): diff --git a/Website/static/modules/emailservice/__init__.py b/Website/modules/emailservice/__init__.py similarity index 100% rename from Website/static/modules/emailservice/__init__.py rename to Website/modules/emailservice/__init__.py diff --git a/Website/modules/emailservice/email.py b/Website/modules/emailservice/email.py new file mode 100644 index 0000000..f9f086f --- /dev/null +++ b/Website/modules/emailservice/email.py @@ -0,0 +1,136 @@ +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +import smtplib +import time + + +def _build_smtp_client(): + smtp = smtplib.SMTP( + "mail.invario-software.de", + 587, + timeout=10, + ) + smtp.ehlo() + if True: # Always use TLS for security + smtp.starttls() + smtp.ehlo() + smtp.login("no-reply@invario-software.de", "#,EATwIn,68") + return smtp + + +def send(email: list | str, subject: str, text_body: str, html_body: str = None) -> bool: + """Sends the email with both Plain Text and HTML support.""" + + if isinstance(email, str): + email = [email] + + TEXT_SIGNATURE = ( + "\n\n--\n" + "Mit freundlichen Grüßen\n" + "Ihr Invario Team\n\n" + "Invario UG\n" + "Am Sportplatz 10\n" + "83052 Bruckmühl" + ) + + HTML_SIGNATURE = """ +

+ + + + +
+

Mit freundlichen Grüßen,

+

Ihr Invario Team


+

+ Invario UG
+ Am Sportplatz 10
+ 83052 Bruckmühl +

+
+ """ + + text_content = f"{text_body}{TEXT_SIGNATURE}" + + if html_body: + html_content = f"
{html_body}{HTML_SIGNATURE}
" + else: + # Fallback if only text is provided + html_safe_text = text_body.replace('\n', '
') + html_content = f"

{html_safe_text}

{HTML_SIGNATURE}" + + mails_per_second = 10 + interval = 1.0 / mails_per_second + smtp = None + + try: + smtp = _build_smtp_client() + + for i, recipient in enumerate(email): + start_time = time.time() + + msg = MIMEMultipart("alternative") + msg["Subject"] = subject + msg["From"] = "Invario Team " + msg["To"] = str(recipient) + + msg.attach(MIMEText(text_content, "plain")) + msg.attach(MIMEText(html_content, "html")) + + smtp.sendmail( + from_addr="no-reply@invario-software.de", + to_addrs=[recipient], + msg=msg.as_string() + ) + + elapsed_time = time.time() - start_time + sleep_time = interval - elapsed_time + + if sleep_time > 0 and i < len(email) - 1: + time.sleep(sleep_time) + + return True + except Exception as e: + print(f"Debug: Fehler beim Senden der E-Mail: {e}") + return False + finally: + if smtp: + try: + smtp.quit() + except Exception: + pass + + +def send_register_token(email: str, token: str) -> bool: + """Sends a professionally styled registration token to the user.""" + + subject = "Aktion erforderlich: Ihr Bestätigungscode für Invario" + + text_note = ( + "Herzlich willkommen bei Invario!\n\n" + "Um die Einrichtung Ihres Kontos abzuschließen und Ihre E-Mail-Adresse zu bestätigen, " + "geben Sie bitte den folgenden Sicherheitscode ein:\n\n" + f"Code: {token}\n\n" + "Dieser Code ist aus Sicherheitsgründen nur für begrenzte Zeit gültig. " + "Falls Sie diese Registrierung nicht angefordert haben, können Sie diese E-Mail einfach ignorieren." + ) + + html_note = f""" +
+

Willkommen bei Invario!

+

+ Vielen Dank für Ihre Registrierung. Um die Einrichtung Ihres Kontos abzuschließen und Ihre E-Mail-Adresse zu verifizieren, verwenden Sie bitte den folgenden Bestätigungscode: +

+ +
+ {token} +
+ +

+ Dieser Code ist aus Sicherheitsgründen nur für eine begrenzte Zeit gültig.
+ Falls Sie kein Konto bei uns erstellt haben, können Sie diese E-Mail sicher ignorieren. +

+
+ """ + + return send(email, subject, text_body=text_note, html_body=html_note) \ No newline at end of file diff --git a/Website/requirements.txt b/Website/requirements.txt index 76a46a0..49b801c 100644 --- a/Website/requirements.txt +++ b/Website/requirements.txt @@ -3,4 +3,5 @@ Flask-JWT-Extended>=4.6,<5.0 bleach>=6.1,<7.0 pymongo>=4.8,<5.0 gunicorn>=22.0,<23.0 -requests \ No newline at end of file +requests +email \ No newline at end of file diff --git a/Website/static/modules/emailservice/email.py b/Website/static/modules/emailservice/email.py deleted file mode 100644 index 70854d9..0000000 --- a/Website/static/modules/emailservice/email.py +++ /dev/null @@ -1,98 +0,0 @@ -from email.mime.multipart import MIMEMultipart -from email.mime.text import MIMEText -import smtplib -import time - -import Web.modules.database.settings as cfg - - -def _build_smtp_client(): - smtp = smtplib.SMTP( - cfg.EMAIL_SMTP_HOST, - cfg.EMAIL_SMTP_PORT, - timeout=cfg.EMAIL_TIMEOUT_SECONDS, - ) - smtp.ehlo() - if cfg.EMAIL_USE_TLS: - smtp.starttls() - smtp.ehlo() - if cfg.EMAIL_USERNAME: - smtp.login(cfg.EMAIL_USERNAME, cfg.EMAIL_PASSWORD or "") - return smtp - - -def send(email: list | str, subject: str, note: str, sender: str) -> bool: - """Sends the email with the link to the Clients.""" - if not cfg.MODULES.is_enabled("mail"): - print("Debug: Module not enabled") - return False - - if isinstance(email, str): - email = [email] - - body_message = note - - HTML_SIGNATURE = f""" - - - - -
-

Mit freundlichen Grüßen

-

Automatisierter Email Verteiler für die Schule: {cfg.get_school_info().get("name")}


-

Invario UG
Am Sportplatz 10
83052 Bruckmühl

-
- """ - - text_content = f"{body_message}\n\nMit freundlichen Grüßen\n{sender}\n" - - html_content = f""" - - -

{body_message}

-
- {HTML_SIGNATURE} - - - """ - - mails_per_second = 10 - interval = 1.0 / mails_per_second - - smtp = None - try: - smtp = _build_smtp_client() - - for i, recipient in enumerate(email): - start_time = time.time() - - msg = MIMEMultipart("alternative") - msg["Subject"] = str(subject) - msg["From"] = f"{sender} <{cfg.EMAIL_USERNAME}>" - msg["To"] = str(recipient) - - msg.attach(MIMEText(text_content, "plain")) - msg.attach(MIMEText(html_content, "html")) - - smtp.sendmail( - from_addr=cfg.EMAIL_USERNAME, - to_addrs=[recipient], - msg=msg.as_string() - ) - - elapsed_time = time.time() - start_time - sleep_time = interval - elapsed_time - - if sleep_time > 0 and i < len(email) - 1: - time.sleep(sleep_time) - - return True - except Exception as e: - print(f"Debug: Fehler beim Senden der E-Mail: {e}") - return False - finally: - if smtp: - try: - smtp.quit() - except Exception: - pass \ No newline at end of file diff --git a/Website/templates/login.html b/Website/templates/login.html index ae10928..89f73ff 100644 --- a/Website/templates/login.html +++ b/Website/templates/login.html @@ -87,6 +87,82 @@ color: #0a5f8f; font-weight: 700; } + + /* Custom Modal Overlay Styles */ + .custom-modal-overlay { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(14, 32, 48, 0.5); + backdrop-filter: blur(3px); + z-index: 1000; + justify-content: center; + align-items: center; + } + + .custom-modal-overlay.active { + display: flex; + } + + .custom-modal { + background: #ffffff; + padding: 2rem; + border-radius: 16px; + max-width: 440px; + width: 90%; + box-shadow: 0 20px 44px rgba(14, 32, 48, 0.15); + border: 1px solid #d4e0e9; + text-align: center; + } + + .custom-modal h3 { + color: #0f344d; + font-size: 1.4rem; + margin-bottom: 0.5rem; + } + + .custom-modal p { + color: #547184; + font-size: 0.95rem; + margin-bottom: 1.5rem; + } + + .modal-actions { + display: flex; + flex-direction: column; + gap: 0.75rem; + } + + .modal-btn-primary { + background: #0a5f8f; + color: #ffffff; + border: none; + border-radius: 999px; + padding: 0.65rem; + font-weight: 700; + cursor: pointer; + text-decoration: none; + display: block; + } + + .modal-btn-secondary { + background: #f1f5f9; + color: #19445f; + border: 1px solid #c4d5e2; + border-radius: 999px; + padding: 0.65rem; + font-weight: 700; + cursor: pointer; + text-decoration: none; + display: block; + } + + .modal-btn-secondary:hover { + background: #e2e8f0; + } {% endblock %} @@ -107,4 +183,45 @@

Noch kein Konto? Jetzt registrieren

+ + +
+
+

Konto nicht verifiziert

+

Ihr Konto wurde noch nicht aktiviert. Bitte geben Sie Ihren Bestätigungscode ein oder fordern Sie einen neuen an.

+ +
+
+ + {% endblock %} \ No newline at end of file diff --git a/Website/templates/verify.html b/Website/templates/verify.html new file mode 100644 index 0000000..a3d51bd --- /dev/null +++ b/Website/templates/verify.html @@ -0,0 +1,48 @@ +{% extends "base.html" %} + +{% block title %}Konto verifizieren{% endblock %} + +{% block head %} +{{ super() }} + +{% endblock %} + +{% block content %} +
+

E-Mail bestätigen

+

Wir haben Ihnen einen Bestätigungscode per E-Mail gesendet. Bitte geben Sie diesen ein, um Ihr Konto zu aktivieren.

+ +
+
+ + +
+ +
+ + +
+ + +
+
+{% endblock %} \ No newline at end of file