diff --git a/Website/main.py b/Website/main.py index 817e6a3..5a9543b 100644 --- a/Website/main.py +++ b/Website/main.py @@ -26,8 +26,10 @@ from bson.objectid import ObjectId import user as user_store import buchungen import server_steering as steering -from modules.emailservice.email import send_register_token +from modules.emailservice.email import send_register_token, send_accreditation_email import secrets +import requests +from server_steering import Instance app = Flask(__name__) app.secret_key = "ASDfhbsdfseiufhgildsrfrjg874368546987s6e8468f4!?FAUS/&s" @@ -3607,26 +3609,171 @@ def logout(): flash('Logged out successfully', 'info') return redirect(url_for('login')) -@app.route('/booking_payment_async', methods=['POST']) -def booking_payment_async(): - # ... Speichere Daten in DB ... - # ... Starte das manage-tenants.sh Skript ASYNCHRON (z.B. Celery) ... - return jsonify({"success": True, "tenant_slug": request.form['tenant_slug']}), 200 +def generate_secure_password(length=14): + alphabet = string.ascii_letters + string.digits + return ''.join(secrets.choice(alphabet) for _ in range(length)) +def _utc_now_iso(): + return datetime.now(timezone.utc).isoformat() + +def run_tenant_provisioning_async(app, user_id, tenant_slug, school_name, user_email, prov_id): + with app.app_context(): + # Helper to update provisioning status in 'instance_requests' collection + def _update_prov_status(status, extra_fields=None): + req_client, req_col = _get_collection("instance_requests") + try: + update_data = { + "provision_status": status, + "updated_at": _utc_now_iso() + } + if extra_fields: + update_data.update(extra_fields) + + req_col.update_one( + {"_id": ObjectId(prov_id)}, + {"$set": update_data} + ) + finally: + req_client.close() + + # Set status to processing + _update_prov_status("in_progress") + + # 1. Determine next available port dynamically + 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() + + # 2. Execute script to create tenant + success = Instance.new(tenant_slug, next_port, admin_password) + if not success: + _update_prov_status("failed", {"last_error": "Tenant shell creation failed"}) + return + + # Enable default modules + Instance.edit(tenant_slug, "starter") + + # 3. Create instance document & link to user + instance_doc = { + "owner_id": user_id, + "school_name": school_name, + "subdomain": tenant_slug, + "domain": f"{tenant_slug}.invario-software.de", + "https_port": next_port, + "status": "ready", + "nginx_status": "active", + "admin_username": "admin", + "admin_password": admin_password + } + + inst_client, inst_col = _get_collection("instances") + try: + inst_result = inst_col.insert_one(instance_doc) + created_inst_id = str(inst_result.inserted_id) + finally: + inst_client.close() + + # 4. Mark request as completed in instance_requests + _update_prov_status("completed", { + "instance_id": created_inst_id, + "domain": instance_doc["domain"] + }) + + # 5. Send Email with documents and access credentials + send_accreditation_email( + recipient=user_email, + domain=instance_doc["domain"], + username="admin", + password=admin_password, + invoice_pdf_path=f"/var/invoices/{tenant_slug}_invoice.pdf", + contract_pdf_path=f"/var/contracts/{tenant_slug}_contract.pdf" + ) + +@app.route('/booking_payment_async', methods=['POST']) +@login_required +def booking_payment_async(): + user_id = session.get("user_id") + user_email = session.get("email") + tenant_slug = _slugify_subdomain(request.form.get('tenant_slug')) + school_name = request.form.get('school_name', tenant_slug) + + # Insert request record into instance_requests + req_client, req_col = _get_collection("instance_requests") + try: + prov_doc = { + "user_id": user_id, + "tenant_slug": tenant_slug, + "school_name": school_name, + "provision_status": "pending", + "created_at": _utc_now_iso(), + "updated_at": _utc_now_iso() + } + prov_id = str(req_col.insert_one(prov_doc).inserted_id) + finally: + req_client.close() + + # Start async worker passing prov_id + thread = threading.Thread( + target=run_tenant_provisioning_async, + args=(app._get_current_object(), user_id, tenant_slug, school_name, user_email, prov_id) + ) + thread.start() + + return jsonify({ + "success": True, + "tenant_slug": tenant_slug, + "request_id": prov_id + }), 200 @app.route('/instance_request/status') def tenant_status(): slug = request.args.get('slug') - # Prüfe in der Datenbank, ob das Skript für diesen Slug schon fertig ist - if ist_fertig(slug): - return jsonify({ - "status": "ready", - "url": f"https://{slug}.invario-software.de", - "username": "admin", - "password": generiertes_passwort - }) - else: - return jsonify({"status": "pending"}) + req_id = request.args.get('request_id') + + req_client, req_col = _get_collection("instance_requests") + try: + query = {} + if req_id: + query["_id"] = ObjectId(req_id) + elif slug: + query["tenant_slug"] = slug + else: + return jsonify({"status": "error", "message": "Missing slug or request_id"}), 400 + + prov_req = req_col.find_one(query) + finally: + req_client.close() + + if not prov_req or prov_req.get("provision_status") != "completed": + return jsonify({"status": "pending"}), 200 + + # Retrieve created instance details for health check + inst_client, inst_col = _get_collection("instances") + try: + instance = inst_col.find_one({"subdomain": prov_req.get("tenant_slug")}) + finally: + inst_client.close() + + if not instance: + return jsonify({"status": "pending"}), 200 + + # Perform health check ping + target_url = f"https://{instance['domain']}/health" + try: + response = requests.get(target_url, timeout=3) + if response.status_code == 200: + return jsonify({ + "status": "ready", + "url": f"https://{instance['domain']}", + "username": instance.get("admin_username", "admin"), + "password": instance.get("admin_password") + }), 200 + except requests.RequestException: + pass + + return jsonify({"status": "pending"}), 200 def main(): app.run(host="0.0.0.0", port=4999, debug=False) diff --git a/Website/modules/emailservice/email.py b/Website/modules/emailservice/email.py index 4526678..45312a3 100644 --- a/Website/modules/emailservice/email.py +++ b/Website/modules/emailservice/email.py @@ -166,4 +166,42 @@ def send_password_reset_token(email: str, token: str) -> bool: """ - return send(email, subject, text_body=text_note, html_body=html_note) \ No newline at end of file + return send(email, subject, text_body=text_note, html_body=html_note) + +def send_accreditation_email(recipient: str, domain: str, username: str, password: str, invoice_pdf_path: str, contract_pdf_path: str) -> bool: + """Sends an accreditation email with login credentials and attached documents.""" + + subject = "Ihre Zugangsdaten und Unterlagen für Invario" + + text_note = ( + f"Sehr geehrte/r {username},\n\n" + "Vielen Dank für Ihre Akkreditierung bei Invario. " + "Im Anhang finden Sie Ihre Rechnung und den Vertrag.\n\n" + f"Ihre Zugangsdaten:\n" + f"Domain: {domain}\n" + f"Benutzername: {username}\n" + f"Passwort: {password}\n\n" + "Bitte bewahren Sie diese Informationen sicher auf." + ) + + html_note = f""" +
+

Willkommen bei Invario!

+

+ Vielen Dank für Ihre Akkreditierung. Im Anhang finden Sie Ihre Rechnung und den Vertrag. +

+ +

Ihre Zugangsdaten:

+ + +

+ Bitte bewahren Sie diese Informationen sicher auf. +

+
+ """ + + return send(recipient, subject, text_body=text_note, html_body=html_note) \ No newline at end of file diff --git a/Website/server_steering.py b/Website/server_steering.py index d5aa610..42f3a08 100644 --- a/Website/server_steering.py +++ b/Website/server_steering.py @@ -16,21 +16,6 @@ def _get_users_collection(): db = client[MONGO_DB_NAME] return client, db["packages"] - -def add_dns(name: str, port: int) -> bool: - """ - Adds the subdomain to the DNS Routes - - Input: - - name (Name of the subdomain) -> String - - port (Port number for the subdomain) -> int - - Output: - - bool (True if adding of the Subdomain is active) - """ - # This is a placeholder function, as the actual implementation would depend on the DNS provider and how the DNS records are managed. - return False - def clear_special(var_:str) -> str: """ Clears the variable of any special carakters @@ -59,7 +44,7 @@ def clear_special(var_:str) -> str: except: return False -def execute_script(wd_: str, file_: str, com_: str, com2_: str="None", com3_: str="None"): +def execute_script(wd_: str, file_: str, com_: str, com2_: str="None", com3_: str="None", com4_: str="None") -> str: """ executes a script with the option of to extra inputs @@ -69,7 +54,7 @@ def execute_script(wd_: str, file_: str, com_: str, com2_: str="None", com3_: st - com_ = first option -> String - com2_ = second option (Optional if needet)-> String - com3_ = third option (Optional if needet)-> String - + - com4_ = fourth option (Optional if needet)-> String Output: - ether False if failed -> bool - or result.stdout output of the executed process -> str @@ -82,6 +67,8 @@ def execute_script(wd_: str, file_: str, com_: str, com2_: str="None", com3_: st cmd = f'bash "{update_path}" {com_} {com2_}' elif com3_ != "None": cmd = f'bash "{update_path}" {com_} {com2_} {com3_}' + elif com4_ != "None": + cmd = f'bash "{update_path}" {com_} {com2_} {com3_} {com4_}' else: cmd = f'bash "{update_path}" {com_}' try: @@ -210,225 +197,90 @@ class versions: return int(instance_age) if int(instance_age) is not None and instance_age <= 5 else False -class instace: - """ - This will give access to anything like: - - Instances for Clients - - starting - - stopping - - restarting - - list all Clients - - modules: - - new(name:str) - - remove(name:str) - - status(name:str) - - restart(name:str) - - list() - """ - - def __init__(): - return list() - - def new_trial(name: str) -> bool: - """ - Generates a new trial instance with the subdomain [name].invario.eu - - Input: - - name -> String - - Output: - - bool if the creation of the instance works (True: creation worked; False: didnt work) - - manage-tenant.sh trial [port] [days] - """ - port_starter = 10002 - port = port_starter - for i in instace.list(): - port =+ 1 - - if execute_script(_var, _cmd, "trial", clear_special(name), port): - add_dns(name, port) - return int(port) - else: - return False - - def new(name: str) -> int: - """ - Generates a new instance with the subdomain [name].invario.eu - - Input: - - name -> String - - Output: - - int (Port number for the subdomain) or False if the creation of the instance didnt work - """ - port_starter = 10002 - port = port_starter - for i in instace.list(): - port =+ 1 - - if execute_script(_var, _cmd, "add", clear_special(name), port): - add_dns(name, port) - return int(port) - else: - return False - - def edit(name: str, module:str) -> bool: - """ - Edits the name of an instance with the subdomain [name].invario.eu to [new_name].invario.eu - - Input: - - name -> String - - module -> String (Options: "inventarsystem", "buecherei", "terminverwaltung", "emailversand", "starter", "advanced") - - Output: - - bool if the edit works (True: edit worked; False: didnt work) - """ - if module == "inventarsystem": - if execute_script(_var, _cmd, "module", clear_special(name), 'library=off inventory=on'): - return True - else: - return False - elif module == "buecherei": - if execute_script(_var, _cmd, "module", clear_special(name), 'library=on inventory=off'): - return True - else: - return False - elif module == "terminverwaltung": - if execute_script(_var, _cmd, "module", clear_special(name), 'library=off inventory=off'): # -> has to be changed when the module is ready - return True - else: - return False - elif module == "emailversand": - if execute_script(_var, _cmd, "module", clear_special(name), 'library=off inventory=on'):# -> has to be changed when the module is ready - return True - else: - return False - elif module == "starter": - if execute_script(_var, _cmd, "module", clear_special(name), 'library=on inventory=on'): - return True - else: - return False - elif module == "advanced": - if execute_script(_var, _cmd, "module", clear_special(name), 'library=on inventory=on'):# -> has to be changed when the module is ready - return True - else: - return False - else: - return False - - - def remove(name: str) -> bool: - """ - Removes a instance with the subdomain [name].invario.eu - - Input: - - name -> String - - Output: - - bool if the removal works (True: removal worked; False: didnt work) - """ - safe_name = clear_special(name) - # try to remove via management script - script_ok = execute_script(_var, _cmd, "remove", safe_name) - if not script_ok: - return False - - # best-effort: remove the associated package entry from the database - client = None - try: - client, packages = _get_users_collection() - packages.delete_one({"client_name": name}) - except Exception: - # don't fail the overall removal if DB cleanup fails - pass - finally: - if client: - client.close() - - return True - - - def status(name: str) -> bool: - """ - Returns if a instance with the subdomain [name].invario.eu is up. - - Input: - - name -> String - - Output: - - bool if the page is online (True: Is working; False: Isnt online) - """ - name = clear_special(name) - request = str(requests.get(f"{name}.invario.eu/test_connection")) - if request == '{"message":"Connection successful","status":"success","status_code":200}': - return True - else: - return False - - def restart(name: str) -> bool: - """ - Restart an instance with the subdomain [name].invario.eu - - Input: - - name -> String - - Output: - - bool if the restart works (True: restart worked; False: didnt work) - """ - if execute_script(_var, _cmd, "restart-tenant", clear_special(name)): - return True - else: - return False - +class Instance: + @staticmethod def list() -> list: """ - List off all tenants. - - Output: - - list with all tenants ("tenant1", "tenant2") - """ + Lists all existing tenants along with their mapped ports. + Returns: list of dicts -> [{'name': 'school1', 'port': 10002}, ...] + """ result = execute_script(_var, _cmd, "list") if not isinstance(result, str): return [] - result = result.splitlines() - if not result: - return [] - return [line.replace("- ", "") for line in result[1:]] + + tenants = [] + pattern = re.compile(r"^-\s+([^\s]+)\s+\(port\s+(\d+)\)") + for line in result.splitlines(): + match = pattern.match(line.strip()) + if match: + tenants.append({ + "name": match.group(1), + "port": int(match.group(2)) + }) + return tenants - def backup(name: str) -> bool: - """ - Creates a backup of an instance with the subdomain [name].invario.eu + @classmethod + def get_next_available_port(cls, start_port=10002) -> int: + tenants = cls.list() + if not tenants: + return start_port + used_ports = [t["port"] for t in tenants] + return max(used_ports) + 1 - Input: - - name -> String + @classmethod + def new(cls, name: str, port: int = None, password: str = "admin123") -> bool: + safe_name = clear_special(name) + if port is None: + port = cls.get_next_available_port() + + return execute_script(_var, _cmd, "add", safe_name, str(port), password) - Output: - - bool if the backup works (True: backup worked; False: didnt work) - """ - import subprocess - try: - subprocess.run( - [ - "docker", - "compose", - "-f", - "docker-compose-multitenant.yml", - "exec", - "-T", - "mongodb", - "mongodump", - f"--archive=/data/backups/inventar_{name}-$(date +%Y%m%d%H%M%S).gz", - "--gzip", - "--db", - f"inventar_{name}" - ], - check=True - ) - return True - except subprocess.CalledProcessError: + @staticmethod + def edit(name: str, module_preset: str) -> bool: + safe_name = clear_special(name) + presets = { + "inventarsystem": "library=off inventory=on student_cards=off terminplan=off", + "buecherei": "library=on inventory=off student_cards=off terminplan=off", + "terminverwaltung": "library=off inventory=off student_cards=off terminplan=on", + "emailversand": "library=off inventory=on student_cards=off terminplan=off", + "starter": "library=on inventory=on student_cards=on terminplan=off", + "advanced": "library=on inventory=on student_cards=on terminplan=on", + } + + config = presets.get(module_preset) + if not config: return False + + return execute_script(_var, _cmd, "module", safe_name, config) + + @staticmethod + def remove(name: str) -> bool: + safe_name = clear_special(name) + if not execute_script(_var, _cmd, "remove", safe_name): + return False + + try: + client, packages = _get_users_collection() + packages.delete_one({"client_name": safe_name}) + except Exception: + pass + finally: + if 'client' in locals() and client: + client.close() + return True + + @staticmethod + def status(name: str) -> bool: + safe_name = clear_special(name) + try: + res = requests.get(f"https://{safe_name}.invario-software.de/health", timeout=5) + return res.status_code == 200 + except requests.RequestException: + return False + + @staticmethod + def restart(name: str) -> bool: + return execute_script(_var, _cmd, "restart-tenant", clear_special(name)) class ussage: """