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:
+
+ - Domain: {domain}
+ - Benutzername: {username}
+ - Passwort: {password}
+
+
+
+ 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:
"""