import html import os from urllib.parse import quote from fastapi import APIRouter, Depends, Form, HTTPException, Request from fastapi.responses import HTMLResponse, RedirectResponse from app.auth import require_user from app.db.audit import log_audit_event from app.env_file import ENV_PATH, KEY_RE, read_entries, save_entries from app.routes.runtime import ENV_ACTION_KEYS, render_action_buttons from app.templates.layout import page router = APIRouter() def require_admin(user: dict) -> None: if (user.get("role") or "").lower() != "admin": raise HTTPException(status_code=403, detail="Environment je dostupný pouze administrátorům") def _render_rows(entries: list[tuple[str, str]]) -> str: rows = "" for key, value in entries: key_html = html.escape(key, quote=True) value_html = html.escape(value, quote=True) rows += f""" """ return rows def _render_page(user: dict, entries: list[tuple[str, str]], message: str = "", error: str = "") -> str: notice = "" if message: notice = f'

{html.escape(message)}

' if error: notice = f'

{html.escape(error)}

' action_buttons = render_action_buttons(ENV_ACTION_KEYS, "/portal/admin/environment") return page( "Environment", f"""

Environment

Úprava hlavní AppFactory konfigurace {html.escape(ENV_PATH)}. Zdroj pravdy zůstává tento soubor. Při uložení se vytvoří časově označený backup. Dostupné pouze administrátorům.

{notice}

Proměnné

{_render_rows(entries)}
Klíč Hodnota Akce

Klíč musí odpovídat ^[A-Z_][A-Z0-9_]*$. Prázdné hodnoty jsou povolené, duplicitní klíče ne. Hodnoty s mezerami se uloží v uvozovkách.

Jak se změny projeví

  1. Proměnné používané shell skripty: stačí znovu spustit příslušný skript.
  2. Portal / Worker / Webhook / Monitor: je potřeba restart kontejnerů (tlačítka níže).
  3. Domény, HTTPS a Caddy hodnoty: spusťte generate-caddyfile.sh (Regenerate Caddy).
  4. UID/GID proměnné: je potřeba opatrný restart/redeploy celého stacku.

Navazující akce

Akce nevolají docker přímo — vytvoří úlohu do fronty (zpracuje worker). Historii najdete v Runtime Management a Úlohách.

{action_buttons}
""", user=user, ) @router.get("/admin/environment", response_class=HTMLResponse) def environment_page(request: Request, message: str = "", error: str = "", user=Depends(require_user)): require_admin(user) return _render_page(user, read_entries(), message=message, error=error) @router.post("/admin/environment", response_class=HTMLResponse) def environment_save( request: Request, key: list[str] = Form(default=[]), value: list[str] = Form(default=[]), user=Depends(require_user), ): require_admin(user) # Sestav uspořádaný seznam dvojic; prázdné řádky (bez klíče i hodnoty) ignoruj. pairs: list[tuple[str, str]] = [] for raw_key, raw_value in zip(key, value): k = (raw_key or "").strip() v = (raw_value or "").replace("\r", "").replace("\n", " ") if not k and not v: continue pairs.append((k, v)) # Validace: klíč podle regexu, žádné duplicity. invalid_keys = sorted({k for k, _ in pairs if not KEY_RE.match(k)}) seen: set[str] = set() duplicate_keys = sorted({k for k, _ in pairs if k in seen or seen.add(k)}) if invalid_keys or duplicate_keys: problems = [] if invalid_keys: problems.append("neplatné klíče: " + ", ".join(invalid_keys)) if duplicate_keys: problems.append("duplicitní klíče: " + ", ".join(duplicate_keys)) error = "Změny nebyly uloženy — " + "; ".join(problems) + "." # Re-render z odeslaných dat, ať admin nepřijde o rozdělanou editaci. return _render_page(user, pairs, error=error) # Spočítej změněné klíče (pro audit – bez hodnot). original = dict(read_entries()) new_map = dict(pairs) added = set(new_map) - set(original) removed = set(original) - set(new_map) modified = {k for k in set(new_map) & set(original) if new_map[k] != original[k]} changed_keys = sorted(added | removed | modified) if not changed_keys: return RedirectResponse( url="/portal/admin/environment?message=" + quote("Žádné změny k uložení."), status_code=303, ) try: backup_path = save_entries(pairs) except OSError as exc: return _render_page(user, pairs, error=f"Soubor se nepodařilo uložit: {exc.strerror or exc}") log_audit_event( user, action="environment.updated", target_type="environment", target_id=os.path.basename(ENV_PATH), metadata={"changed_keys": changed_keys, "backup": os.path.basename(backup_path) if backup_path else None}, ) backup_note = f" Backup: {os.path.basename(backup_path)}." if backup_path else "" message = ( f"Uloženo {len(changed_keys)} změněných klíčů.{backup_note} " "Změny se projeví podle typu proměnné různě – viz doporučené akce níže." ) return RedirectResponse( url="/portal/admin/environment?message=" + quote(message), status_code=303, )