9916e4d833
Přidaná centrální runtime konfigurace public URL, HTTPS a Google OAuth readiness v config.py (line 37). Google OAuth přidaný jako volitelná další možnost přihlášení, bez nahrazení lokálního/Gitea loginu, v auth.py (line 128). Default Google redirect URI se skládá z APPFACTORY_PORTAL_PUBLIC_URL + /auth/google/callback, bez hardcoded IP/localhost/domény. Readiness JSON endpoint přidán na /portal/migration-readiness/auth-domain-config; vrací jen boolean hodnoty, žádné secrety, v migration_readiness.py (line 112). UI sekce Auth & Domain Readiness přidaná do Migration Readiness v migration_readiness.py (line 41). Audit event auth_domain_readiness.viewed se zapisuje při zobrazení readiness stránky v migration_readiness.py (line 133). APPFACTORY_ENABLE_HTTPS se promítá i do secure session cookies v main.py (line 19).
262 lines
11 KiB
Python
262 lines
11 KiB
Python
import html
|
|
|
|
from fastapi import APIRouter, Depends, Form, HTTPException, Request
|
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
|
|
|
from app.auth import require_user
|
|
from app.config import (
|
|
get_auth_domain_readiness,
|
|
get_google_redirect_uri,
|
|
get_portal_public_url,
|
|
)
|
|
from app.db.audit import log_audit_event
|
|
from app.db.jobs import create_job
|
|
from app.db.migration_readiness import (
|
|
BOOTSTRAP_SCRIPT,
|
|
PREFLIGHT_SCRIPT,
|
|
get_latest_bootstrap_job,
|
|
get_latest_preflight_job,
|
|
summarize_readiness,
|
|
summarize_script_run,
|
|
)
|
|
from app.routes.jobs import render_job_status
|
|
from app.templates.layout import page
|
|
|
|
router = APIRouter()
|
|
BOOTSTRAP_MODES = {"check", "repair", "deploy"}
|
|
|
|
|
|
def render_readiness_pill(ready: bool) -> str:
|
|
if ready:
|
|
return '<span class="pill pill-success">READY</span>'
|
|
return '<span class="pill pill-danger">NOT READY</span>'
|
|
|
|
|
|
def render_boolean_pill(enabled: bool) -> str:
|
|
if enabled:
|
|
return '<span class="pill pill-success">YES</span>'
|
|
return '<span class="pill">NO</span>'
|
|
|
|
|
|
def render_auth_domain_readiness() -> str:
|
|
readiness = get_auth_domain_readiness()
|
|
public_url = get_portal_public_url() or "relative/proxy mode"
|
|
redirect_uri = get_google_redirect_uri() or "not configured"
|
|
warning = ""
|
|
|
|
if readiness["google_oauth_enabled"] and (
|
|
not readiness["portal_public_url_configured"] or not readiness["google_redirect_uri_configured"]
|
|
):
|
|
warning = """
|
|
<div class="alert alert-danger">
|
|
<strong>Google OAuth is enabled but domain readiness is incomplete.</strong>
|
|
<p>Set APPFACTORY_PORTAL_PUBLIC_URL or APPFACTORY_ENABLE_HTTPS=true with APPFACTORY_PORTAL_DOMAIN, and make sure a Google redirect URI is available.</p>
|
|
</div>
|
|
"""
|
|
|
|
return f"""
|
|
<div class="card">
|
|
<h2>Auth & Domain Readiness</h2>
|
|
<table>
|
|
<tr><th>Portal public URL</th><td>{html.escape(public_url)}</td></tr>
|
|
<tr><th>HTTPS enabled</th><td>{render_boolean_pill(readiness["https_enabled"])}</td></tr>
|
|
<tr><th>Google OAuth enabled</th><td>{render_boolean_pill(readiness["google_oauth_enabled"])}</td></tr>
|
|
<tr><th>Google OAuth configured</th><td>{render_boolean_pill(readiness["google_oauth_configured"])}</td></tr>
|
|
<tr><th>Google redirect URI effective value</th><td>{html.escape(redirect_uri)}</td></tr>
|
|
</table>
|
|
{warning}
|
|
</div>
|
|
"""
|
|
|
|
|
|
def render_script_result(title: str, script_name: str, summary: dict) -> str:
|
|
job = summary.get("job")
|
|
job_rows = f'<tr><td colspan="2">Zatím není uložený žádný výsledek skriptu {html.escape(script_name)}.</td></tr>'
|
|
if job:
|
|
job_id = html.escape(str(job.get("id", "")))
|
|
return_code = summary.get("return_code")
|
|
return_code_label = "" if return_code is None else html.escape(str(return_code))
|
|
job_rows = f"""
|
|
<tr><th>Úloha</th><td><a href="/portal/jobs/{job_id}">#{job_id}</a></td></tr>
|
|
<tr><th>Status</th><td>{render_job_status(job.get("status"))}</td></tr>
|
|
<tr><th>Return code</th><td>{return_code_label}</td></tr>
|
|
<tr><th>Started at</th><td>{html.escape(job.get("started_at", "") or "")}</td></tr>
|
|
<tr><th>Finished at</th><td>{html.escape(job.get("finished_at", "") or "")}</td></tr>
|
|
"""
|
|
|
|
stdout = html.escape(summary.get("stdout") or "Zatím není uložený žádný stdout log.")
|
|
stderr = html.escape(summary.get("stderr") or "Zatím není uložený žádný stderr log.")
|
|
|
|
return f"""
|
|
<div class="card">
|
|
<h2>{html.escape(title)}</h2>
|
|
<p class="muted">Poslední výsledek maintenance skriptu {html.escape(script_name)} uložený v databázi.</p>
|
|
<table>
|
|
{job_rows}
|
|
</table>
|
|
</div>
|
|
|
|
<div class="grid">
|
|
<div class="card">
|
|
<h2>Raw stdout log</h2>
|
|
<pre class="log-viewer log-stdout">{stdout}</pre>
|
|
</div>
|
|
<div class="card">
|
|
<h2>Raw stderr log</h2>
|
|
<pre class="log-viewer log-stderr">{stderr}</pre>
|
|
</div>
|
|
</div>
|
|
"""
|
|
|
|
|
|
@router.get("/migration-readiness/auth-domain-config")
|
|
def auth_domain_readiness_config(user=Depends(require_user)):
|
|
return get_auth_domain_readiness()
|
|
|
|
|
|
@router.get("/migration-readiness", response_class=HTMLResponse)
|
|
def migration_readiness_page(request: Request, user=Depends(require_user)):
|
|
latest_job = get_latest_preflight_job()
|
|
summary = summarize_readiness(latest_job)
|
|
bootstrap_summary = summarize_script_run(get_latest_bootstrap_job())
|
|
job = summary["job"]
|
|
|
|
log_audit_event(
|
|
user,
|
|
action="migration_readiness.viewed",
|
|
target_type="migration_readiness",
|
|
target_id=PREFLIGHT_SCRIPT,
|
|
metadata={"job_id": job.get("id") if job else None},
|
|
)
|
|
log_audit_event(
|
|
user,
|
|
action="auth_domain_readiness.viewed",
|
|
target_type="auth_domain_readiness",
|
|
metadata=get_auth_domain_readiness(),
|
|
)
|
|
|
|
job_rows = '<tr><td colspan="2">Zatím není uložený žádný výsledek preflight kontroly.</td></tr>'
|
|
if job:
|
|
job_id = html.escape(str(job.get("id", "")))
|
|
return_code = summary.get("return_code")
|
|
return_code_label = "" if return_code is None else html.escape(str(return_code))
|
|
job_rows = f"""
|
|
<tr><th>Úloha</th><td><a href="/portal/jobs/{job_id}">#{job_id}</a></td></tr>
|
|
<tr><th>Stav úlohy</th><td>{render_job_status(job.get("status"))}</td></tr>
|
|
<tr><th>Return code</th><td>{return_code_label}</td></tr>
|
|
<tr><th>Vytvořeno</th><td>{html.escape(job.get("created_at", "") or "")}</td></tr>
|
|
<tr><th>Spuštěno</th><td>{html.escape(job.get("started_at", "") or "")}</td></tr>
|
|
<tr><th>Dokončeno</th><td>{html.escape(job.get("finished_at", "") or "")}</td></tr>
|
|
<tr><th>Worker</th><td>{html.escape(job.get("worker_id", "") or "")}</td></tr>
|
|
"""
|
|
|
|
stdout = html.escape(summary.get("stdout") or "Zatím není uložený žádný stdout log.")
|
|
stderr = html.escape(summary.get("stderr") or "Zatím není uložený žádný stderr log.")
|
|
|
|
return page(
|
|
"Migration Readiness",
|
|
f"""
|
|
<div class="card">
|
|
<h2>Migration Readiness</h2>
|
|
<p class="muted">Poslední výsledek maintenance skriptu {html.escape(PREFLIGHT_SCRIPT)} uložený v databázi. Stav READY / NOT READY vychází pouze z return code jobu.</p>
|
|
<div class="inline-form">
|
|
<form method="post" action="/portal/migration-readiness/run">
|
|
<button type="submit">Spustit preflight kontrolu</button>
|
|
</form>
|
|
<form method="post" action="/portal/migration-readiness/bootstrap">
|
|
<input type="hidden" name="mode" value="check">
|
|
<button type="submit" class="btn-secondary">Run Bootstrap Check</button>
|
|
</form>
|
|
<form method="post" action="/portal/migration-readiness/bootstrap" onsubmit="return confirm('Spustit Bootstrap v2 repair? Tato akce opravuje práva přes maintenance job.');">
|
|
<input type="hidden" name="mode" value="repair">
|
|
<button type="submit" class="btn-secondary">Run Bootstrap Repair</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="alert">
|
|
<strong>Bootstrap v2 má explicitní režimy.</strong>
|
|
<ul>
|
|
<li><strong>check</strong> bezpečně ověřuje stav bez oprav.</li>
|
|
<li><strong>repair</strong> opravuje ownership /opt/appfactory a nastavuje executable práva pro maintenance a alert skripty.</li>
|
|
<li><strong>deploy</strong> provádí plný bootstrap včetně deploy core services a spuštění preflight-check.sh.</li>
|
|
</ul>
|
|
<p>Portál bootstrap nespouští přímo přes shell. Pouze založí maintenance job pro {html.escape(BOOTSTRAP_SCRIPT)}.</p>
|
|
</div>
|
|
|
|
<div class="alert alert-danger">
|
|
<strong>Plný Bootstrap Deploy není výchozí akce.</strong>
|
|
<p>Režim deploy deployuje core services a může změnit stav systému. Spouštějte ho jen po kontrole dopadu.</p>
|
|
<form method="post" action="/portal/migration-readiness/bootstrap" onsubmit="return confirm('Spustit plný Bootstrap v2 deploy? Tato akce deployuje core services přes maintenance job.');">
|
|
<input type="hidden" name="mode" value="deploy">
|
|
<button type="submit" class="danger">Run Bootstrap Deploy</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="stats-grid">
|
|
<div class="stat-card {"stat-success" if summary["ready"] else "stat-danger"}"><span>Stav</span><strong>{render_readiness_pill(summary["ready"])}</strong></div>
|
|
<div class="stat-card stat-success"><span>OK</span><strong>{html.escape(str(summary["ok_count"]))}</strong></div>
|
|
<div class="stat-card stat-warning"><span>WARN</span><strong>{html.escape(str(summary["warn_count"]))}</strong></div>
|
|
<div class="stat-card stat-danger"><span>FAIL</span><strong>{html.escape(str(summary["fail_count"]))}</strong></div>
|
|
</div>
|
|
|
|
{render_auth_domain_readiness()}
|
|
|
|
<div class="card">
|
|
<h2>Poslední preflight úloha</h2>
|
|
<table>
|
|
{job_rows}
|
|
</table>
|
|
</div>
|
|
|
|
<div class="grid">
|
|
<div class="card">
|
|
<h2>Raw preflight stdout</h2>
|
|
<pre class="log-viewer log-stdout">{stdout}</pre>
|
|
</div>
|
|
<div class="card">
|
|
<h2>Raw preflight stderr</h2>
|
|
<pre class="log-viewer log-stderr">{stderr}</pre>
|
|
</div>
|
|
</div>
|
|
|
|
{render_script_result("Poslední výsledek Bootstrap v2", BOOTSTRAP_SCRIPT, bootstrap_summary)}
|
|
""",
|
|
user=user,
|
|
)
|
|
|
|
|
|
@router.post("/migration-readiness/run")
|
|
def run_migration_readiness(user=Depends(require_user)):
|
|
job_id = create_job(
|
|
job_type="run_script",
|
|
target_type="maintenance_script",
|
|
target_id=PREFLIGHT_SCRIPT,
|
|
payload={"script_name": PREFLIGHT_SCRIPT},
|
|
user=user,
|
|
source="portal_manual",
|
|
)
|
|
return RedirectResponse(url=f"/portal/jobs/{job_id}", status_code=303)
|
|
|
|
|
|
@router.post("/migration-readiness/bootstrap")
|
|
def run_bootstrap_v2(mode: str = Form(...), user=Depends(require_user)):
|
|
mode = (mode or "").strip().lower()
|
|
if mode not in BOOTSTRAP_MODES:
|
|
raise HTTPException(status_code=400, detail="Neplatný režim bootstrapu")
|
|
|
|
job_id = create_job(
|
|
job_type="run_script",
|
|
target_type="maintenance_script",
|
|
target_id=BOOTSTRAP_SCRIPT,
|
|
payload={
|
|
"script_name": BOOTSTRAP_SCRIPT,
|
|
"args": [mode],
|
|
"arguments": [mode],
|
|
"mode": mode,
|
|
},
|
|
user=user,
|
|
source="portal_manual",
|
|
)
|
|
return RedirectResponse(url=f"/portal/jobs/{job_id}", status_code=303)
|