diff --git a/Website/main.py b/Website/main.py index f291ce4..f63eeb5 100644 --- a/Website/main.py +++ b/Website/main.py @@ -2166,84 +2166,89 @@ def book_option_package(): return redirect(url_for("booking_payment", packages=packages_raw, flow=booking_flow)) -def background_booking_provisioning(app_instance, prov_id, subdomain, modules_to_provision, user_id, school_name): - """Hintergrund-Worker für die klassische Buchung""" +def background_booking_provisioning(app_instance, prov_id, subdomain, modules_to_provision, user_id, school_name, user_email): + """Hintergrund-Worker that actually deploys the instance using the correct API""" with app_instance.app_context(): req_client, req_col = _get_collection("instance_requests") try: - # Status auf In Progress setzen + # 1. Set Status to In Progress req_col.update_one({"_id": ObjectId(prov_id)}, {"$set": {"provision_status": "in_progress"}}) - provision_error = "" - provision_config_ok = True - provision_port = None + # 2. Dynamically determine next Port and Password (Proven working logic) + existing_tenants = Instance.list() + used_ports = [t['port'] for t in existing_tenants if 'port' in t] + next_port = max(used_ports) + 1 if used_ports else 10002 admin_password = generate_secure_password() - # 1. Shell Provisioning + # 3. Create Tenant Shell + success = Instance.new(subdomain, next_port, admin_password) + if not success: + raise Exception("Tenant shell creation failed (Instance.new returned False)") + + # 4. Enable selected Modules + # Always enable "starter" as a baseline if needed, then the booked ones + if "starter" not in modules_to_provision: + Instance.edit(subdomain, "starter") + + for mod in modules_to_provision: + Instance.edit(subdomain, mod) + + # 5. Register Instance in Database + domain = f"{subdomain}.invario-software.de" + instance_doc = { + "owner_id": user_id, + "school_name": school_name, + "subdomain": subdomain, + "domain": domain, + "https_port": next_port, + "status": "ready", + "nginx_status": "active", + "admin_username": "admin", + "admin_password": admin_password + } + + inst_client, inst_col = _get_collection("instances") try: - port = steering.instance.new(subdomain) - if port and port is not False: - try: - provision_port = int(port) if isinstance(port, (int, str)) and str(port).isdigit() else port - except Exception: - provision_port = port + created_inst_id = str(inst_col.insert_one(instance_doc).inserted_id) + finally: + inst_client.close() + + # 6. Send Email Notification + try: + if user_email: + send_accreditation_email( + recipient=user_email, + domain=domain, + username="admin", + password=admin_password, + invoice_pdf_path=f"/var/invoices/{subdomain}_invoice.pdf", + contract_pdf_path=f"/var/contracts/{subdomain}_contract.pdf" + ) + except Exception as mail_err: + print(f"[WARNING] Email sending failed: {mail_err}") - # 2. Gewählte Module aktivieren - for mod in modules_to_provision: - try: - edit_ok = steering.instance.edit(subdomain, mod) - if not edit_ok: - provision_config_ok = False - provision_error += f" | Edit failed for {mod}" - except Exception as e: - provision_config_ok = False - provision_error += f" | {mod}: {str(e)}" - else: - provision_error = "Provisioning script returned failure" - provision_config_ok = False - except Exception as e: - provision_error = str(e) - provision_config_ok = False - - # 3. Wenn erfolgreich, Instanz in die 'instances' Collection eintragen! (WICHTIG für Health-Check) - if provision_config_ok and not provision_error: - domain = f"{subdomain}.invario-software.de" - instance_doc = { - "owner_id": user_id, - "school_name": school_name, - "subdomain": subdomain, - "domain": domain, - "https_port": provision_port, - "status": "ready", - "nginx_status": "active", - "admin_username": "admin", - "admin_password": admin_password - } - inst_client, inst_col = _get_collection("instances") - try: - inst_col.insert_one(instance_doc) - finally: - inst_client.close() - - # 4. Finalen Status setzen ("ready" löst den Redirect im Frontend aus) - final_status = "ready" if provision_config_ok and not provision_error else "failed" + # 7. Mark Request as Ready for the Frontend req_col.update_one( {"_id": ObjectId(prov_id)}, {"$set": { - "provision_status": final_status, - "provision_port": provision_port or "", - "provision_config_ok": provision_config_ok, - "provision_error": provision_error.strip(" | "), + "provision_status": "ready", + "provision_port": next_port, + "instance_id": created_inst_id, + "url": f"https://{domain}", + "username": "admin", + "password": admin_password, "updated_at": _utc_now_iso() }} ) + print(f"[SUCCESS] Provisioning completed for {subdomain}") + except Exception as e: - print(f"[ERROR] Async Provisioning failed: {e}") - req_col.update_one({"_id": ObjectId(prov_id)}, {"$set": {"provision_status": "failed", "provision_error": str(e)}}) + print(f"[ERROR] Async Provisioning failed for {subdomain}:") + traceback.print_exc() + req_col.update_one({"_id": ObjectId(prov_id)}, {"$set": {"provision_status": "failed", "last_error": str(e)}}) finally: req_client.close() - @app.route('/booking/payment', methods=['GET', 'POST']) @login_required def booking_payment(): @@ -2344,41 +2349,55 @@ def booking_payment(): f"Notizen: {form_data['notes'] or '-'}" ) - client = None try: + # FLOW 1: Beratung / Gesprächs-Buchung if booking_flow == "consultation": client, col = _get_collection("chat_messages") - col.insert_one( - { - "username": session.get("username"), - "sender": session.get("display_name") or session.get("username"), - "sender_role": "user", - "message": message, - "created_at": _utc_now_iso(), - "booking_flow": booking_flow, - "booking_package": package_raw, - "booking_data": form_data, - } - ) + try: + col.insert_one( + { + "username": session.get("username"), + "sender": session.get("display_name") or session.get("username"), + "sender_role": "user", + "message": message, + "created_at": _utc_now_iso(), + "booking_flow": booking_flow, + "booking_package": package_raw, + "booking_data": form_data, + } + ) + finally: + client.close() + + flash(f"Gesprächstermin für {selected_package} wurde an das Team gesendet.", "success") + return redirect(url_for("user_chat")) + # FLOW 2: Kostenpflichtige Direkt-Buchung mit Provisioning if booking_flow == "payment": booking_number = f"BOOK-{datetime.utcnow().strftime('%Y%m%d%H%M%S')}" raw_base = form_data.get("school_name") or session.get("username") or booking_number base = _slugify_subdomain(raw_base).lower()[:51] + # Subdomain-Prüfung über das korrekte Instance Model try: - existing = steering.instance.list() or [] - except Exception: - existing = [] + existing_tenants = Instance.list() or [] + existing_subdomains = [ + t.get("subdomain") or t.get("tenant_slug") + for t in existing_tenants if isinstance(t, dict) + ] + except Exception as e: + print(f"[WARNING] Konnte bestehende Instanzen nicht abfragen: {e}") + existing_subdomains = [] - if base in existing: + if base in existing_subdomains: flash("Die gewünschte Subdomain (aus dem Schulnamen) existiert bereits. Bitte wähle einen anderen Schulnamen.", "error") return render_template("schnell_buchung.html", **review_data) subdomain = base[:63] provision_payload = { + "user_id": session.get("user_id"), "username": session.get("username"), "booking_number": booking_number, "package": package_raw, @@ -2405,30 +2424,28 @@ def booking_payment(): valid_modules = set(BOOKING_PACKAGE_MAP.keys()) modules_to_provision = {pkg for pkg in package_list if pkg in valid_modules} - thread = threading.Thread( - target=background_booking_provisioning, - args=(app, prov_id, subdomain, modules_to_provision, session.get("user_id"), form_data["school_name"]) - ) - thread.start() + # Flask App Context für den Thread sicher abrufen + app_obj = app + user_id = session.get("user_id") + user_email = session.get("email") or form_data.get("contact_email") + + try: + thread = threading.Thread( + target=background_booking_provisioning, + args=(app_obj, prov_id, subdomain, modules_to_provision, user_id, form_data["school_name"], user_email) + ) + thread.start() + print(f"[INFO] Background provisioning gestartet für Subdomain: {subdomain}") + except Exception as thread_err: + print(f"[ERROR] Fehler beim Starten des Background Threads: {thread_err}") - except PyMongoError: + flash(f"Buchung für {selected_package} wurde erfasst. Die Instanz wird jetzt eingerichtet.", "success") + return redirect(url_for("my_instance_management")) + + except PyMongoError as pe: + print(f"[ERROR] Database error in booking_payment: {pe}") flash("Buchungsanfrage konnte nicht gesendet werden. Bitte versuchen Sie es erneut.", "error") - return redirect(url_for("booking_payment", packages=package_raw, flow=booking_flow)) - finally: - if client: - client.close() - - success_text = ( - f"Gesprächstermin für {selected_package} wurde an das Team gesendet." - if booking_flow == "consultation" - else f"Buchung für {selected_package} wird eingerichtet. Sie werden gleich weitergeleitet." - ) - flash(success_text, "success") - - if booking_flow == "payment": - return redirect(url_for("my_invoices")) - else: - return redirect(url_for("user_chat")) + return render_template("schnell_buchung.html", **review_data) @app.route('/instance_request/delete', methods=['POST'])