This commit is contained in:
2026-10-03 00:33:57 +02:00
parent f9544b5f5a
commit 891d837684
7 changed files with 137 additions and 116 deletions
+1
View File
@@ -1,4 +1,5 @@
Website/.mongo-secrets.env Website/.mongo-secrets.env
Website/conf.json
.venv .venv
__pycache__ __pycache__
Inventarsystem_Lizenz_Verwaltung Inventarsystem_Lizenz_Verwaltung
+1
View File
@@ -8,6 +8,7 @@ venv/
build/ build/
.git/ .git/
.gitignore .gitignore
conf.json
mongodb.log mongodb.log
mongodb.log.* mongodb.log.*
*.dist-info/ *.dist-info/
+9
View File
@@ -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
}
}
+5
View File
@@ -45,6 +45,11 @@ services:
MONGO_SOCKET_TIMEOUT_MS: ${MONGO_SOCKET_TIMEOUT_MS:-30000} MONGO_SOCKET_TIMEOUT_MS: ${MONGO_SOCKET_TIMEOUT_MS:-30000}
MONGO_WAIT_QUEUE_TIMEOUT_MS: ${MONGO_WAIT_QUEUE_TIMEOUT_MS:-2000} MONGO_WAIT_QUEUE_TIMEOUT_MS: ${MONGO_WAIT_QUEUE_TIMEOUT_MS:-2000}
SESSION_COOKIE_SECURE: ${SESSION_COOKIE_SECURE:-0} 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 JWT_SECRET_KEY: change-this-in-production
INSTANCE_BASE_DIR: /opt/Invario_Website INSTANCE_BASE_DIR: /opt/Invario_Website
volumes: volumes:
+41
View File
@@ -410,6 +410,46 @@ export INSTANCE_TLS_MODE="development"
export INSTANCE_PARENT_DOMAIN="${INSTANCE_PARENT_DOMAIN:-meine-domain}" export INSTANCE_PARENT_DOMAIN="${INSTANCE_PARENT_DOMAIN:-meine-domain}"
export DEV_HOSTS_REFRESH_INTERVAL="${DEV_HOSTS_REFRESH_INTERVAL:-30}" 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 "Launching WEBSITE stack"
echo " SESSION_COOKIE_SECURE=$SESSION_COOKIE_SECURE" echo " SESSION_COOKIE_SECURE=$SESSION_COOKIE_SECURE"
echo " INSTANCE_TLS_MODE=$INSTANCE_TLS_MODE" 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" echo " DEV_HOSTS_REFRESH_INTERVAL=$DEV_HOSTS_REFRESH_INTERVAL"
ensure_mongo_secrets ensure_mongo_secrets
load_runtime_config
if [ ! -f "$SCRIPT_DIR/gunicorn.conf.py" ]; then if [ ! -f "$SCRIPT_DIR/gunicorn.conf.py" ]; then
echo "[FEHLER] gunicorn.conf.py fehlt in $SCRIPT_DIR. Bitte prüfen." >&2 echo "[FEHLER] gunicorn.conf.py fehlt in $SCRIPT_DIR. Bitte prüfen." >&2
+59 -1
View File
@@ -1935,6 +1935,7 @@ def login():
session['username'] = stored_user.get("username") session['username'] = stored_user.get("username")
session['display_name'] = stored_user.get("display_name") or 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['is_admin'] = stored_user.get("is_admin", False)
session['email'] = stored_user.get("email") or ""
if request.is_json: if request.is_json:
return jsonify({"status": "ok"}), 200 return jsonify({"status": "ok"}), 200
@@ -2378,7 +2379,7 @@ def background_booking_provisioning(
_ensure_legal_pdf_files() _ensure_legal_pdf_files()
formatted_date = datetime.today().strftime('%d.%m.%Y') formatted_date = datetime.today().strftime('%d.%m.%Y')
send_accreditation_email( email_sent = send_accreditation_email(
recipient=user_email, recipient=user_email,
domain=domain, domain=domain,
username="admin", username="admin",
@@ -2390,6 +2391,16 @@ def background_booking_provisioning(
software_name=f"{software_name} (Testversion)" if is_trial else software_name, software_name=f"{software_name} (Testversion)" if is_trial else software_name,
user_id=username 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: if is_trial:
send_trial_notification( send_trial_notification(
recipient=user_email, recipient=user_email,
@@ -2400,6 +2411,15 @@ def background_booking_provisioning(
) )
except Exception as mail_err: 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}") print(f"[WARNING] E-Mail-Versand fehlgeschlagen: {mail_err}")
# 8. Status in instance_requests auf "ready" setzen # 8. Status in instance_requests auf "ready" setzen
@@ -3107,6 +3127,9 @@ def booking_payment():
if reuse_instance: if reuse_instance:
module_config = build_module_config_string(package_list) 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 = { billing_update = {
"package": package_raw, "package": package_raw,
"invoice_id": invoice_id, "invoice_id": invoice_id,
@@ -3140,6 +3163,40 @@ def booking_payment():
) )
finally: finally:
req_client.close() 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: if is_json_request:
return jsonify({"success": True, "slug": subdomain, "message": "Ihre bestehende Instanz wurde für die Buchung übernommen."}), 200 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") 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('username', None)
session.pop('display_name', None) session.pop('display_name', None)
session.pop('is_admin', None) session.pop('is_admin', None)
session.pop('email', None)
flash('Logged out successfully', 'info') flash('Logged out successfully', 'info')
return redirect(url_for('login')) return redirect(url_for('login'))
+21 -115
View File
@@ -7,10 +7,7 @@ import os
import tempfile import tempfile
from fpdf import FPDF from fpdf import FPDF
from flask import current_app from flask import current_app
from pymongo import MongoClient from datetime import datetime
import threading
from bson import Binary, ObjectId
from datetime import datetime, timezone, timedelta
SELLER_INFO = { SELLER_INFO = {
@@ -29,16 +26,26 @@ SELLER_INFO = {
} }
def _build_smtp_client(): 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( smtp = smtplib.SMTP(
"mail.invario-software.de", host,
587, port,
timeout=10, timeout=float(os.environ.get("SMTP_TIMEOUT", "10")),
) )
smtp.ehlo() smtp.ehlo()
if True: # Always use TLS for security smtp.starttls()
smtp.starttls() smtp.ehlo()
smtp.ehlo() smtp.login(username, password)
smtp.login("no-reply@invario-software.de", "eSpage,65,{")
return smtp return smtp
@@ -105,7 +112,7 @@ def send(
# Root message structure for email with attachments # Root message structure for email with attachments
msg = MIMEMultipart("mixed") msg = MIMEMultipart("mixed")
msg["Subject"] = subject msg["Subject"] = subject
msg["From"] = "Invario Team <no-reply@invario-software.de>" msg["From"] = f"Invario Team <{os.environ.get('SMTP_USERNAME', 'no-reply@invario-software.de')}>"
msg["To"] = str(recipient) msg["To"] = str(recipient)
# Sub-container for Plain Text and HTML bodies # Sub-container for Plain Text and HTML bodies
@@ -124,7 +131,7 @@ def send(
msg.attach(part) msg.attach(part)
smtp.sendmail( smtp.sendmail(
from_addr="no-reply@invario-software.de", from_addr=os.environ.get("SMTP_USERNAME", "no-reply@invario-software.de"),
to_addrs=[recipient], to_addrs=[recipient],
msg=msg.as_string() 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.""" """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() 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) # 1. Hauptvertrag dynamisch erzeugen (bleibt temporär)
contract_filename = f"Hauptvertrag_Invario_{safe_school_name}.pdf" contract_filename = f"Hauptvertrag_Invario_{safe_school_name}.pdf"
contract_pdf_path = os.path.join(tempfile.gettempdir(), contract_filename) 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") invoices_dir = os.path.join(static_dir, "invoices")
os.makedirs(invoices_dir, exist_ok=True) # Erstellt den Ordner 'invoices', falls nicht vorhanden 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): def get_valid_pdf_path(filename):
possible_paths = [ possible_paths = [
os.path.join(static_dir, "downloads", filename), os.path.join(static_dir, "downloads", filename),
@@ -507,103 +509,7 @@ def send_accreditation_email(
attachments.append(avv_pdf_path) attachments.append(avv_pdf_path)
# ---------- MONGO DB SETUP ---------- # E-Mail versenden und nur den temporären Hauptvertrag löschen.
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
try: try:
success = send(recipient, subject, text_body=text_note, html_body=html_note, attachments=attachments) success = send(recipient, subject, text_body=text_note, html_body=html_note, attachments=attachments)
return success return success