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>
148 lines
5.3 KiB
Python
148 lines
5.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Finish nginx routing + ensure core/frontend env for accounting."""
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
from pathlib import Path
|
|
|
|
import paramiko
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
APP_HOST = "192.168.10.162"
|
|
APP_USER = "morteza"
|
|
APP_PASS = "Moli@5404"
|
|
NGX_HOST = "192.168.10.156"
|
|
NGX_USER = "torbatyaruser"
|
|
NGX_PASS = "Moli@5404"
|
|
REMOTE_DIR = "/home/morteza/torbatyar"
|
|
|
|
|
|
def connect(host: str, user: str, password: str) -> paramiko.SSHClient:
|
|
c = paramiko.SSHClient()
|
|
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
c.connect(host, username=user, password=password, timeout=30, allow_agent=False, look_for_keys=False)
|
|
return c
|
|
|
|
|
|
def run(client, cmd: str, *, sudo=False, password="", timeout=600):
|
|
cmd = cmd.replace("\r\n", "\n").replace("\r", "\n").strip() + "\n"
|
|
b64 = base64.b64encode(cmd.encode()).decode()
|
|
if sudo:
|
|
full = f"echo {password!r} | sudo -S -p '' bash -c 'echo {b64} | base64 -d | bash'"
|
|
else:
|
|
full = f"bash -lc 'echo {b64} | base64 -d | bash'"
|
|
print("\n>>>", cmd[:200].replace("\n", " | "), "...")
|
|
_i, out, err = client.exec_command(full, timeout=timeout, get_pty=True)
|
|
text = (out.read() + err.read()).decode("utf-8", "replace")
|
|
code = out.channel.recv_exit_status()
|
|
print(text[-5000:].encode("ascii", "replace").decode("ascii"))
|
|
return code, text
|
|
|
|
|
|
def put_text_sudo(client, password: str, remote: str, content: str) -> None:
|
|
b64 = base64.b64encode(content.encode()).decode()
|
|
cmd = f"mkdir -p $(dirname {remote!r}) && echo {b64} | base64 -d > {remote!r} && chmod 644 {remote!r}"
|
|
code, _ = run(client, cmd, sudo=True, password=password, timeout=60)
|
|
if code != 0:
|
|
raise RuntimeError(f"write failed: {remote}")
|
|
|
|
|
|
def main() -> int:
|
|
app = connect(APP_HOST, APP_USER, APP_PASS)
|
|
try:
|
|
code, _ = run(
|
|
app,
|
|
f"""
|
|
set -e
|
|
cd {REMOTE_DIR}
|
|
# ensure env
|
|
python3 - <<'PY'
|
|
from pathlib import Path
|
|
p = Path('.env')
|
|
text = p.read_text(encoding='utf-8') if p.exists() else ''
|
|
keys = {{
|
|
'NEXT_PUBLIC_ACCOUNTING_API_URL': 'https://accounting.torbatyar.ir',
|
|
'ACCOUNTING_DATABASE_URL': 'postgresql+asyncpg://superapp:superapp_password@postgres:5432/accounting_db',
|
|
'ACCOUNTING_DATABASE_URL_SYNC': 'postgresql+psycopg://superapp:superapp_password@postgres:5432/accounting_db',
|
|
'ACCOUNTING_SERVICE_NAME': 'accounting-service',
|
|
'KEYCLOAK_SERVER_URL': 'http://keycloak:8080',
|
|
'KEYCLOAK_REALM': 'superapp',
|
|
}}
|
|
lines = text.splitlines()
|
|
seen=set(); out=[]
|
|
for line in lines:
|
|
if not line.strip() or line.lstrip().startswith('#') or '=' not in line:
|
|
out.append(line); continue
|
|
k=line.split('=',1)[0].strip()
|
|
if k in keys:
|
|
out.append(f"{{k}}={{keys[k]}}"); seen.add(k)
|
|
else:
|
|
out.append(line)
|
|
for k,v in keys.items():
|
|
if k not in seen:
|
|
out.append(f'{{k}}={{v}}')
|
|
p.write_text('\\n'.join(out).rstrip()+'\\n', encoding='utf-8')
|
|
print('env_ok')
|
|
PY
|
|
grep -E 'NEXT_PUBLIC_ACCOUNTING|ACCOUNTING_DATABASE|KEYCLOAK_SERVER_URL|KEYCLOAK_PUBLIC_URL' .env
|
|
# bring core back if missing + recreate frontend to pick env
|
|
docker compose up -d core-service identity-access-service accounting-service
|
|
docker compose up -d --force-recreate frontend
|
|
sleep 8
|
|
docker compose ps
|
|
curl -sf http://127.0.0.1:8000/health || curl -sf http://127.0.0.1:8000/api/v1/health || echo CORE_HEALTH_CHECK
|
|
curl -sf http://127.0.0.1:8002/health
|
|
echo
|
|
docker compose exec -T frontend printenv NEXT_PUBLIC_ACCOUNTING_API_URL || true
|
|
""",
|
|
sudo=True,
|
|
password=APP_PASS,
|
|
timeout=600,
|
|
)
|
|
if code != 0:
|
|
raise RuntimeError("app finalize failed")
|
|
finally:
|
|
app.close()
|
|
|
|
ngx = connect(NGX_HOST, NGX_USER, NGX_PASS)
|
|
try:
|
|
conf = (ROOT / "infrastructure" / "nginx" / "torbatyar.ir.conf").read_text(encoding="utf-8")
|
|
put_text_sudo(ngx, NGX_PASS, "/etc/nginx/conf.d/torbatyar.ir.conf", conf)
|
|
# also update provision script on nginx host if present
|
|
prov = (ROOT / "infrastructure" / "nginx" / "provision_ssl.py").read_text(encoding="utf-8")
|
|
put_text_sudo(ngx, NGX_PASS, "/opt/torbatyar/bin/provision_ssl.py", prov)
|
|
code, _ = run(
|
|
ngx,
|
|
r"""
|
|
set -e
|
|
mkdir -p /var/www/html
|
|
certbot certonly --webroot -w /var/www/html \
|
|
-d torbatyar.ir -d www.torbatyar.ir \
|
|
-d api.torbatyar.ir -d identity.torbatyar.ir -d auth.torbatyar.ir -d accounting.torbatyar.ir \
|
|
--non-interactive --agree-tos --email support@torbatyar.ir --expand || true
|
|
openssl x509 -in /etc/letsencrypt/live/torbatyar.ir/fullchain.pem -noout -ext subjectAltName | tr ',' '\n' | grep -i accounting || echo SAN_ACCOUNTING_MISSING
|
|
nginx -t
|
|
systemctl reload nginx
|
|
echo NGINX_OK
|
|
curl -skI https://accounting.torbatyar.ir/health | head -n 8
|
|
echo ---
|
|
curl -sk https://accounting.torbatyar.ir/health || echo PUBLIC_HEALTH_FAIL
|
|
echo
|
|
curl -skI https://torbatyar.ir/accounting | head -n 8
|
|
""",
|
|
sudo=True,
|
|
password=NGX_PASS,
|
|
timeout=300,
|
|
)
|
|
if code != 0:
|
|
raise RuntimeError("nginx finalize failed")
|
|
finally:
|
|
ngx.close()
|
|
|
|
print("\n=== FINALIZE DONE ===")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|