# FastAPI backend for bash script generator from fastapi import FastAPI, Request, Form from fastapi.responses import HTMLResponse, Response from fastapi.templating import Jinja2Templates from jinja2 import Environment, FileSystemLoader, StrictUndefined from datetime import datetime from pathlib import Path # Initialize FastAPI app app = FastAPI(title="Bash Script Generator") templates = Jinja2Templates(directory="templates") # Jinja2 environment for script template rendering jinja_env = Environment( loader=FileSystemLoader("templates"), undefined=StrictUndefined, autoescape=False, trim_blocks=True, lstrip_blocks=True, ) def _bool(v: str | None) -> bool: # HTML checkbox sends value only when checked return v is not None @app.get("/", response_class=HTMLResponse) def index(request: Request): """Serve the main HTML form page""" return templates.TemplateResponse("index.html", {"request": request}) @app.post("/generate") def generate( # feature flags - System Setup system_update: str | None = Form(default=None), auto_security_updates: str | None = Form(default=None), setup_timezone: str | None = Form(default=None), setup_hostname: str | None = Form(default=None), setup_ntp: str | None = Form(default=None), setup_swap: str | None = Form(default=None), # feature flags - Security ssh_harden: str | None = Form(default=None), install_fail2ban: str | None = Form(default=None), prelogin_banner: str | None = Form(default=None), postlogin_banner: str | None = Form(default=None), ssh_2fa: str | None = Form(default=None), # feature flags - Docker & Services install_docker: str | None = Form(default=None), docker_admin_user: str | None = Form(default=None), open_ports: str | None = Form(default=None), combine_lan: str | None = Form(default=None), # feature flags - User Management create_admin_user: str | None = Form(default=None), # feature flags - Monitoring install_monitoring_tools: str | None = Form(default=None), install_build_tools: str | None = Form(default=None), # params admin_username: str = Form(default="datamng"), ssh_port: int = Form(default=22), hostname: str = Form(default=""), timezone: str = Form(default="UTC"), new_admin_username: str = Form(default="admin"), swap_size_gb: int = Form(default=2), docker_data_dir: str = Form(default="/opt/docker"), # firewall ports (comma-separated) ports_csv: str = Form(default="22,80,81,443"), # netplan params lan_interfaces_csv: str = Form(default="eth0,eth1"), static_ip_cidr: str = Form(default="192.168.1.9/24"), gateway_ip: str = Form(default="192.168.1.1"), dns_csv: str = Form(default="1.1.1.1,8.8.8.8"), # owner information owner_name: str = Form(default="Scardus"), owner_website: str = Form(default="https://scardustech.com"), owner_email: str = Form(default="info@scardustech.com"), # SSH keys (multiline) ssh_public_keys: str = Form(default=""), ): """Generate bash script based on form inputs""" # Load banner templates from markdown files # Try multiple paths to support both local development and Docker container app_path = Path(__file__).parent base_path = app_path.parent # Priority order: 1) workingscope directory, 2) templates directory, 3) Docker workingscope prelogin_banner_path = base_path / "workingscope" / "loginbanner.md" postlogin_banner_path = base_path / "workingscope" / "postloginbanner.md" # Fallback to templates directory (for repository templates) if not prelogin_banner_path.exists(): prelogin_banner_path = app_path / "templates" / "loginbanner.md.template" if not postlogin_banner_path.exists(): postlogin_banner_path = app_path / "templates" / "postloginbanner.md.template" # Final fallback: Docker container workingscope directory if not prelogin_banner_path.exists(): prelogin_banner_path = Path("workingscope") / "loginbanner.md" if not postlogin_banner_path.exists(): postlogin_banner_path = Path("workingscope") / "postloginbanner.md" # Read banner content from markdown files prelogin_text = "" postlogin_text = "" if prelogin_banner_path.exists(): with open(prelogin_banner_path, "r", encoding="utf-8") as f: prelogin_text = f.read() if postlogin_banner_path.exists(): with open(postlogin_banner_path, "r", encoding="utf-8") as f: postlogin_text = f.read() # Build context for Jinja2 template ctx = { "meta": { "generated_at": datetime.utcnow().isoformat() + "Z", }, "flags": { # System Setup "system_update": _bool(system_update), "auto_security_updates": _bool(auto_security_updates), "setup_timezone": _bool(setup_timezone), "setup_hostname": _bool(setup_hostname), "setup_ntp": _bool(setup_ntp), "setup_swap": _bool(setup_swap), # Security "ssh_harden": _bool(ssh_harden), "install_fail2ban": _bool(install_fail2ban), "prelogin_banner": _bool(prelogin_banner), "postlogin_banner": _bool(postlogin_banner), "ssh_2fa": _bool(ssh_2fa), # Docker & Services "install_docker": _bool(install_docker), "docker_admin_user": _bool(docker_admin_user), "open_ports": _bool(open_ports), "combine_lan": _bool(combine_lan), # User Management "create_admin_user": _bool(create_admin_user), # Monitoring "install_monitoring_tools": _bool(install_monitoring_tools), "install_build_tools": _bool(install_build_tools), }, "params": { "admin_username": admin_username.strip(), "ssh_port": int(ssh_port), "hostname": hostname.strip(), "timezone": timezone.strip(), "new_admin_username": new_admin_username.strip(), "swap_size_gb": int(swap_size_gb), "docker_data_dir": docker_data_dir.strip(), "ports": [p.strip() for p in ports_csv.split(",") if p.strip()], "lan_ifaces": [i.strip() for i in lan_interfaces_csv.split(",") if i.strip()], "static_ip_cidr": static_ip_cidr.strip(), "gateway_ip": gateway_ip.strip(), "dns": [d.strip() for d in dns_csv.split(",") if d.strip()], "owner_name": owner_name.strip(), "owner_website": owner_website.strip(), "owner_email": owner_email.strip(), "ssh_public_keys": [k.strip() for k in ssh_public_keys.split("\n") if k.strip()], "prelogin_text": prelogin_text, "postlogin_text": postlogin_text, }, } # Render bash script from template tpl = jinja_env.get_template("script.sh.j2") script = tpl.render(**ctx) # Return as downloadable file filename = "setup-server.sh" return Response( content=script, media_type="application/x-sh", headers={"Content-Disposition": f'attachment; filename="{filename}"'}, )