From 891d837684680e61bca301fca77ed19fc9de7532 Mon Sep 17 00:00:00 2001 From: AIIrondev Date: Sat, 3 Oct 2026 00:33:57 +0200 Subject: [PATCH] chzanges --- .gitignore | 1 + Website/.dockerignore | 1 + Website/conf.example.json | 9 ++ Website/docker-compose.yml | 5 + Website/launch.sh | 41 ++++++++ Website/main.py | 60 +++++++++++- Website/modules/emailservice/email.py | 136 ++++---------------------- 7 files changed, 137 insertions(+), 116 deletions(-) create mode 100644 Website/conf.example.json diff --git a/.gitignore b/.gitignore index 6b615e8..c960f87 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ Website/.mongo-secrets.env +Website/conf.json .venv __pycache__ Inventarsystem_Lizenz_Verwaltung diff --git a/Website/.dockerignore b/Website/.dockerignore index 36a8ee4..2c14882 100644 --- a/Website/.dockerignore +++ b/Website/.dockerignore @@ -8,6 +8,7 @@ venv/ build/ .git/ .gitignore +conf.json mongodb.log mongodb.log.* *.dist-info/ diff --git a/Website/conf.example.json b/Website/conf.example.json new file mode 100644 index 0000000..fd114f5 --- /dev/null +++ b/Website/conf.example.json @@ -0,0 +1,9 @@ +{ + "smtp": { + "host": "mail.invario-software.de", + "port": 587, + "username": "no-reply@invario-software.de", + "password": "SET_THIS_OUTSIDE_GIT", + "timeout": 10 + } +} diff --git a/Website/docker-compose.yml b/Website/docker-compose.yml index 670a86e..95f88d8 100644 --- a/Website/docker-compose.yml +++ b/Website/docker-compose.yml @@ -45,6 +45,11 @@ services: MONGO_SOCKET_TIMEOUT_MS: ${MONGO_SOCKET_TIMEOUT_MS:-30000} MONGO_WAIT_QUEUE_TIMEOUT_MS: ${MONGO_WAIT_QUEUE_TIMEOUT_MS:-2000} SESSION_COOKIE_SECURE: ${SESSION_COOKIE_SECURE:-0} + SMTP_HOST: ${SMTP_HOST:-mail.invario-software.de} + SMTP_PORT: ${SMTP_PORT:-587} + SMTP_USERNAME: ${SMTP_USERNAME:-no-reply@invario-software.de} + SMTP_PASSWORD: ${SMTP_PASSWORD:-} + SMTP_TIMEOUT: ${SMTP_TIMEOUT:-10} JWT_SECRET_KEY: change-this-in-production INSTANCE_BASE_DIR: /opt/Invario_Website volumes: diff --git a/Website/launch.sh b/Website/launch.sh index eb7c0b9..85dcdc4 100755 --- a/Website/launch.sh +++ b/Website/launch.sh @@ -410,6 +410,46 @@ export INSTANCE_TLS_MODE="development" export INSTANCE_PARENT_DOMAIN="${INSTANCE_PARENT_DOMAIN:-meine-domain}" export DEV_HOSTS_REFRESH_INTERVAL="${DEV_HOSTS_REFRESH_INTERVAL:-30}" +load_runtime_config() { + local config_file="${SMTP_CONFIG_FILE:-$SCRIPT_DIR/conf.json}" + if [ ! -f "$config_file" ]; then + echo "Keine optionale SMTP-Konfiguration gefunden: $config_file" + return 0 + fi + + if ! command -v python3 >/dev/null 2>&1; then + echo "[FEHLER] conf.json gefunden, aber python3 ist zum Lesen der Datei nicht verfügbar." >&2 + exit 2 + fi + + local config_values + mapfile -t config_values < <(python3 - "$config_file" <<'PY' +import json +import sys + +with open(sys.argv[1], encoding="utf-8") as handle: + config = json.load(handle) + +smtp = config.get("smtp") or config.get("email") or config +for key in ("host", "port", "username", "password", "timeout"): + value = smtp.get(key, "") + print(str(value)) +PY + ) + + if [ "${#config_values[@]}" -ne 5 ]; then + echo "[FEHLER] Ungültige SMTP-Konfiguration in $config_file." >&2 + exit 2 + fi + + [ -n "${config_values[0]}" ] && export SMTP_HOST="${config_values[0]}" + [ -n "${config_values[1]}" ] && export SMTP_PORT="${config_values[1]}" + [ -n "${config_values[2]}" ] && export SMTP_USERNAME="${config_values[2]}" + [ -n "${config_values[3]}" ] && export SMTP_PASSWORD="${config_values[3]}" + [ -n "${config_values[4]}" ] && export SMTP_TIMEOUT="${config_values[4]}" + echo "SMTP-Konfiguration aus $config_file geladen." +} + echo "Launching WEBSITE stack" echo " SESSION_COOKIE_SECURE=$SESSION_COOKIE_SECURE" echo " INSTANCE_TLS_MODE=$INSTANCE_TLS_MODE" @@ -417,6 +457,7 @@ echo " INSTANCE_PARENT_DOMAIN=$INSTANCE_PARENT_DOMAIN" echo " DEV_HOSTS_REFRESH_INTERVAL=$DEV_HOSTS_REFRESH_INTERVAL" ensure_mongo_secrets +load_runtime_config if [ ! -f "$SCRIPT_DIR/gunicorn.conf.py" ]; then echo "[FEHLER] gunicorn.conf.py fehlt in $SCRIPT_DIR. Bitte prüfen." >&2 diff --git a/Website/main.py b/Website/main.py index 7ab3088..a3414ff 100644 --- a/Website/main.py +++ b/Website/main.py @@ -1935,6 +1935,7 @@ def 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) + session['email'] = stored_user.get("email") or "" if request.is_json: return jsonify({"status": "ok"}), 200 @@ -2378,7 +2379,7 @@ def background_booking_provisioning( _ensure_legal_pdf_files() formatted_date = datetime.today().strftime('%d.%m.%Y') - send_accreditation_email( + email_sent = send_accreditation_email( recipient=user_email, domain=domain, username="admin", @@ -2390,6 +2391,16 @@ def background_booking_provisioning( software_name=f"{software_name} (Testversion)" if is_trial else software_name, user_id=username ) + req_col.update_one( + {"_id": ObjectId(prov_id)}, + {"$set": { + "email_status": "sent" if email_sent else "failed", + "email_recipient": user_email, + "email_updated_at": _utc_now_iso(), + }}, + ) + if not email_sent: + print(f"[WARNING] E-Mail-Versand ohne Exception fehlgeschlagen für {user_email}") if is_trial: send_trial_notification( recipient=user_email, @@ -2400,6 +2411,15 @@ def background_booking_provisioning( ) except Exception as mail_err: + req_col.update_one( + {"_id": ObjectId(prov_id)}, + {"$set": { + "email_status": "failed", + "email_error": str(mail_err), + "email_recipient": user_email or "", + "email_updated_at": _utc_now_iso(), + }}, + ) print(f"[WARNING] E-Mail-Versand fehlgeschlagen: {mail_err}") # 8. Status in instance_requests auf "ready" setzen @@ -3107,6 +3127,9 @@ def booking_payment(): if reuse_instance: module_config = build_module_config_string(package_list) + user_email = session.get("email") or form_data.get("contact_email") + address_process = f"{form_data['billing_street']}, {form_data['billing_zip']} {form_data['billing_city']}" + price_str = f"{total_price:.2f}".replace('.', ',') billing_update = { "package": package_raw, "invoice_id": invoice_id, @@ -3140,6 +3163,40 @@ def booking_payment(): ) finally: req_client.close() + + email_status = "not_sent" + if user_email: + try: + _ensure_legal_pdf_files() + email_sent = send_accreditation_email( + recipient=user_email, + domain=reuse_instance.get("domain") or f"{subdomain}.invario-software.de", + username="admin", + password=reuse_instance.get("admin_password") or "", + school_name=form_data["school_name"], + address=address_process, + price=price_str, + date=datetime.today().strftime('%d.%m.%Y'), + software_name=selected_package, + user_id=session.get("username") or "", + ) + email_status = "sent" if email_sent else "failed" + except Exception as mail_err: + email_status = "failed" + print(f"[WARNING] E-Mail-Versand für bestehende Instanz fehlgeschlagen: {mail_err}") + + req_client, req_col = _get_collection("instance_requests") + try: + req_col.update_one( + {"_id": ObjectId(prov_id)}, + {"$set": { + "email_status": email_status, + "email_recipient": user_email or "", + "email_updated_at": _utc_now_iso(), + }}, + ) + finally: + req_client.close() if is_json_request: return jsonify({"success": True, "slug": subdomain, "message": "Ihre bestehende Instanz wurde für die Buchung übernommen."}), 200 flash(f"Buchung für {selected_package} wurde erfasst und Ihre bestehende Instanz übernommen.", "success") @@ -4056,6 +4113,7 @@ def logout(): session.pop('username', None) session.pop('display_name', None) session.pop('is_admin', None) + session.pop('email', None) flash('Logged out successfully', 'info') return redirect(url_for('login')) diff --git a/Website/modules/emailservice/email.py b/Website/modules/emailservice/email.py index 6a6873e..c09b2d1 100644 --- a/Website/modules/emailservice/email.py +++ b/Website/modules/emailservice/email.py @@ -7,10 +7,7 @@ import os import tempfile from fpdf import FPDF from flask import current_app -from pymongo import MongoClient -import threading -from bson import Binary, ObjectId -from datetime import datetime, timezone, timedelta +from datetime import datetime SELLER_INFO = { @@ -29,16 +26,26 @@ SELLER_INFO = { } def _build_smtp_client(): + host = os.environ.get("SMTP_HOST", "mail.invario-software.de") + username = os.environ.get("SMTP_USERNAME", "no-reply@invario-software.de") + password = os.environ.get("SMTP_PASSWORD", "") + try: + port = int(os.environ.get("SMTP_PORT", "587")) + except ValueError: + port = 587 + + if not password: + raise RuntimeError("SMTP_PASSWORD ist nicht gesetzt.") + smtp = smtplib.SMTP( - "mail.invario-software.de", - 587, - timeout=10, + host, + port, + timeout=float(os.environ.get("SMTP_TIMEOUT", "10")), ) smtp.ehlo() - if True: # Always use TLS for security - smtp.starttls() - smtp.ehlo() - smtp.login("no-reply@invario-software.de", "eSpage,65,{") + smtp.starttls() + smtp.ehlo() + smtp.login(username, password) return smtp @@ -105,7 +112,7 @@ def send( # Root message structure for email with attachments msg = MIMEMultipart("mixed") msg["Subject"] = subject - msg["From"] = "Invario Team " + msg["From"] = f"Invario Team <{os.environ.get('SMTP_USERNAME', 'no-reply@invario-software.de')}>" msg["To"] = str(recipient) # Sub-container for Plain Text and HTML bodies @@ -124,7 +131,7 @@ def send( msg.attach(part) smtp.sendmail( - from_addr="no-reply@invario-software.de", + from_addr=os.environ.get("SMTP_USERNAME", "no-reply@invario-software.de"), to_addrs=[recipient], msg=msg.as_string() ) @@ -420,8 +427,6 @@ def send_accreditation_email( """Generiert Hauptvertrag und Rechnung, speichert die Rechnung dauerhaft und versendet die E-Mail.""" safe_school_name = "".join([c for c in school_name if c.isalnum() or c in (' ', '_', '-')]).rstrip() - current_invoice_number = f"RE-{datetime.now().strftime('%Y%m%d-%H%M')}" - # 1. Hauptvertrag dynamisch erzeugen (bleibt temporär) contract_filename = f"Hauptvertrag_Invario_{safe_school_name}.pdf" contract_pdf_path = os.path.join(tempfile.gettempdir(), contract_filename) @@ -444,9 +449,6 @@ def send_accreditation_email( invoices_dir = os.path.join(static_dir, "invoices") os.makedirs(invoices_dir, exist_ok=True) # Erstellt den Ordner 'invoices', falls nicht vorhanden - # 3. Dauerhafte Rechnung im static-Ordner erzeugen - invoice_filename = f"Rechnung_{current_invoice_number}_{safe_school_name}.pdf" - def get_valid_pdf_path(filename): possible_paths = [ os.path.join(static_dir, "downloads", filename), @@ -507,103 +509,7 @@ def send_accreditation_email( attachments.append(avv_pdf_path) - # ---------- MONGO DB SETUP ---------- - def _env_int(name: str, default: int) -> int: - value = os.environ.get(name) - if value is None: - return default - try: - return int(value) - except (TypeError, ValueError): - return default - - MONGO_URI = os.environ.get("MONGO_URI", "mongodb://localhost:27017") - MONGO_DB_NAME = os.environ.get("MONGO_DB_NAME", "Invario_Website") - MONGO_MAX_POOL_SIZE = max(_env_int("MONGO_MAX_POOL_SIZE", 12), 1) - MONGO_MIN_POOL_SIZE = max(_env_int("MONGO_MIN_POOL_SIZE", 0), 0) - MONGO_MAX_IDLE_MS = max(_env_int("MONGO_MAX_IDLE_MS", 60000), 1000) - MONGO_CONNECT_TIMEOUT_MS = max(_env_int("MONGO_CONNECT_TIMEOUT_MS", 1500), 500) - MONGO_SOCKET_TIMEOUT_MS = max(_env_int("MONGO_SOCKET_TIMEOUT_MS", 30000), 1000) - MONGO_WAIT_QUEUE_TIMEOUT_MS = max(_env_int("MONGO_WAIT_QUEUE_TIMEOUT_MS", 2000), 500) - - global _MONGO_CLIENT, _MONGO_LOCK - if '_MONGO_CLIENT' not in globals(): - _MONGO_CLIENT = None - _MONGO_LOCK = threading.Lock() - - class _NoopMongoClientHandle: - def close(self): - return None - - def _get_mongo_client(): - global _MONGO_CLIENT - if _MONGO_CLIENT is not None: - return _MONGO_CLIENT - - with _MONGO_LOCK: - if _MONGO_CLIENT is None: - _MONGO_CLIENT = MongoClient( - MONGO_URI, - serverSelectionTimeoutMS=MONGO_CONNECT_TIMEOUT_MS, - connectTimeoutMS=MONGO_CONNECT_TIMEOUT_MS, - socketTimeoutMS=MONGO_SOCKET_TIMEOUT_MS, - maxPoolSize=MONGO_MAX_POOL_SIZE, - minPoolSize=MONGO_MIN_POOL_SIZE, - maxIdleTimeMS=MONGO_MAX_IDLE_MS, - waitQueueTimeoutMS=MONGO_WAIT_QUEUE_TIMEOUT_MS, - ) - return _MONGO_CLIENT - - def _get_mongo_db(): - client = _get_mongo_client() - return _NoopMongoClientHandle(), client[MONGO_DB_NAME] - - def _get_collection(name: str): - client, db = _get_mongo_db() - return client, db[name] - - def _utc_now_iso(): - return datetime.now(timezone.utc).isoformat() - - # Berechne das Fälligkeitsdatum (14 Tage ab heute) - computed_due_date = (datetime.now(timezone.utc) + timedelta(days=14)).strftime("%d %B %Y") - - # Collections abrufen - req_client, req_col = _get_collection("instance_requests") - inv_client, col = _get_collection("invoices") - - # 7. Die neue Rechnung in die Datenbank (invoices-Collection) schreiben - invoice_document = { - "username": user_id, - "school_name": school_name, - "address": address, - "invoice_number": current_invoice_number, - "period": date, - "amount_eur": price, - "status": "Zu prüfen", - "due_date": computed_due_date, - "pdf_path": "", - "created_at": _utc_now_iso(), - "source": "booking_payment", - "software_name": software_name, - "domain": domain - } - - send("info@invario-software.de", f"Neue Rechnung erstellt: {current_invoice_number}", text_body=f"Eine neue Rechnung wurde erstellt für {school_name} ({user_id}).\n\nRechnungsnummer: {current_invoice_number}\nBetrag: {price} EUR\nFälligkeitsdatum: {computed_due_date} \n\n Schuldaten:\n- Schule: {school_name}\n- Adresse: {address}\n- Domain: {domain}\nGebuchte Software: {software_name}\n\nBitte prüfen Sie die Rechnung und aktualisieren Sie den Status in der Datenbank.") - - # In MongoDB einfügen - col.insert_one(invoice_document) - - # Anfrage-Status aktualisieren - req_col.update_one( - {"username": user_id}, - {"$set": { - "provision_status": "invoice_created", - "updated_at": _utc_now_iso() - }} - ) - - # 8. E-Mail versenden & NUR temporäre Dateien löschen + # E-Mail versenden und nur den temporären Hauptvertrag löschen. try: success = send(recipient, subject, text_body=text_note, html_body=html_note, attachments=attachments) return success