Adds accounting-service PATCH/archive, fiscal helpers, COA templates and setup status, plus SuperApp Accounting UI (DS, scoreboard, masters, vouchers, ledger, ops modules) with session refresh and HTTPS public API URLs. Co-authored-by: Cursor <cursoragent@cursor.com>
141 lines
4.2 KiB
Python
141 lines
4.2 KiB
Python
#!/usr/bin/env python3
|
|
"""صدور/گسترش گواهی Let's Encrypt برای یک دامنه tenant و فعالسازی redirect HTTPS.
|
|
|
|
اجرا روی سرور nginx (با root/sudo):
|
|
python3 /opt/torbatyar/bin/provision_ssl.py example.torbatyar.ir
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import fcntl
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
CERT_NAME = os.environ.get("TORBATYAR_CERT_NAME", "torbatyar.ir")
|
|
CERT_PATH = Path(f"/etc/letsencrypt/live/{CERT_NAME}/fullchain.pem")
|
|
WEBROOT = os.environ.get("TORBATYAR_WEBROOT", "/var/www/html")
|
|
EMAIL = os.environ.get("TORBATYAR_CERTBOT_EMAIL", "support@torbatyar.ir")
|
|
MAP_FILE = Path(
|
|
os.environ.get("TORBATYAR_SSL_MAP", "/etc/nginx/conf.d/torbatyar-tenant-ssl.map")
|
|
)
|
|
LOCK_FILE = Path("/var/lock/torbatyar-ssl-provision.lock")
|
|
PLATFORM_HOSTS = {
|
|
"torbatyar.ir",
|
|
"www.torbatyar.ir",
|
|
"api.torbatyar.ir",
|
|
"identity.torbatyar.ir",
|
|
"auth.torbatyar.ir",
|
|
"accounting.torbatyar.ir",
|
|
}
|
|
|
|
|
|
def die(msg: str, code: int = 1) -> None:
|
|
print(f"ERROR: {msg}", file=sys.stderr)
|
|
raise SystemExit(code)
|
|
|
|
|
|
def normalize_domain(raw: str) -> str:
|
|
host = raw.strip().lower().split("/")[0].split(":")[0]
|
|
if not re.fullmatch(r"[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+", host):
|
|
die(f"invalid domain: {raw}")
|
|
return host
|
|
|
|
|
|
def current_sans() -> set[str]:
|
|
if not CERT_PATH.exists():
|
|
return set(PLATFORM_HOSTS)
|
|
out = subprocess.check_output(
|
|
["openssl", "x509", "-in", str(CERT_PATH), "-noout", "-ext", "subjectAltName"],
|
|
text=True,
|
|
)
|
|
found = set(re.findall(r"DNS:([^,\s]+)", out))
|
|
return found or set(PLATFORM_HOSTS)
|
|
|
|
|
|
def ensure_map_entry(domain: str) -> None:
|
|
MAP_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
if MAP_FILE.exists():
|
|
text = MAP_FILE.read_text(encoding="utf-8")
|
|
else:
|
|
text = (
|
|
"# Auto-managed by provision_ssl.py — do not edit by hand\n"
|
|
"map $host $torbatyar_tenant_ssl {\n"
|
|
" default 0;\n"
|
|
"}\n"
|
|
)
|
|
if re.search(rf"(?m)^\s*{re.escape(domain)}\s+1\s*;", text):
|
|
return
|
|
if "map $host $torbatyar_tenant_ssl" not in text:
|
|
die(f"unexpected map file format: {MAP_FILE}")
|
|
# درج قبل از بستن }
|
|
text = re.sub(
|
|
r"(map \$host \$torbatyar_tenant_ssl \{.*?)(\n\})",
|
|
rf"\1\n {domain} 1;\2",
|
|
text,
|
|
count=1,
|
|
flags=re.S,
|
|
)
|
|
MAP_FILE.write_text(text, encoding="utf-8")
|
|
print(f"map_updated={domain}")
|
|
|
|
|
|
def run_certbot(domains: list[str]) -> None:
|
|
cmd = [
|
|
"certbot",
|
|
"certonly",
|
|
"--webroot",
|
|
"-w",
|
|
WEBROOT,
|
|
"--expand",
|
|
"--non-interactive",
|
|
"--agree-tos",
|
|
"--email",
|
|
EMAIL,
|
|
"--cert-name",
|
|
CERT_NAME,
|
|
]
|
|
for d in domains:
|
|
cmd.extend(["-d", d])
|
|
print("certbot:", " ".join(cmd))
|
|
subprocess.check_call(cmd)
|
|
|
|
|
|
def reload_nginx() -> None:
|
|
subprocess.check_call(["nginx", "-t"])
|
|
subprocess.check_call(["systemctl", "reload", "nginx"])
|
|
print("nginx_reloaded")
|
|
|
|
|
|
def provision(domain: str) -> None:
|
|
domain = normalize_domain(domain)
|
|
LOCK_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
with LOCK_FILE.open("a+", encoding="utf-8") as lock:
|
|
fcntl.flock(lock.fileno(), fcntl.LOCK_EX)
|
|
sans = current_sans()
|
|
if domain not in sans:
|
|
ordered = sorted(PLATFORM_HOSTS | sans | {domain})
|
|
# platform hosts اول برای پایداری cert-name
|
|
preferred = [h for h in sorted(PLATFORM_HOSTS) if h in ordered]
|
|
rest = [h for h in ordered if h not in preferred]
|
|
run_certbot(preferred + rest)
|
|
else:
|
|
print(f"cert_already_has={domain}")
|
|
if domain not in PLATFORM_HOSTS:
|
|
ensure_map_entry(domain)
|
|
reload_nginx()
|
|
print(f"OK {domain}")
|
|
|
|
|
|
def main() -> None:
|
|
if len(sys.argv) != 2:
|
|
die(f"usage: {sys.argv[0]} <domain>")
|
|
if os.geteuid() != 0:
|
|
die("must run as root")
|
|
provision(sys.argv[1])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|