chzanges
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
Website/.mongo-secrets.env
|
||||
Website/conf.json
|
||||
.venv
|
||||
__pycache__
|
||||
Inventarsystem_Lizenz_Verwaltung
|
||||
|
||||
@@ -8,6 +8,7 @@ venv/
|
||||
build/
|
||||
.git/
|
||||
.gitignore
|
||||
conf.json
|
||||
mongodb.log
|
||||
mongodb.log.*
|
||||
*.dist-info/
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
+59
-1
@@ -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'))
|
||||
|
||||
|
||||
@@ -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 <no-reply@invario-software.de>"
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user