From bad55a7d417fd018e1931c7f0dc3a7a6778cde57 Mon Sep 17 00:00:00 2001
From: JiriUhlir <149317995+JiriUhlir@users.noreply.github.com>
Date: Tue, 16 Jun 2026 10:23:43 +0200
Subject: [PATCH] fixes + guest
---
app/auth.py | 17 +++-
app/db/users.py | 4 +-
app/routes/alerting.py | 30 +++---
app/routes/apps.py | 104 ++++++++++----------
app/routes/audit.py | 2 +-
app/routes/auth.py | 26 ++++-
app/routes/deployments.py | 16 +--
app/routes/developers.py | 3 +-
app/routes/environment.py | 4 +-
app/routes/incidents.py | 2 +-
app/routes/jobs.py | 18 ++--
app/routes/migration_readiness.py | 10 +-
app/routes/operations.py | 2 +-
app/routes/runtime.py | 10 +-
app/routes/scheduled_scripts.py | 157 ++++++++++++++++--------------
app/routes/users.py | 13 +--
app/routes/workers.py | 6 +-
app/templates/layout.py | 80 +++++++++++----
18 files changed, 295 insertions(+), 209 deletions(-)
diff --git a/app/auth.py b/app/auth.py
index b8b7124..4bb6104 100644
--- a/app/auth.py
+++ b/app/auth.py
@@ -125,7 +125,7 @@ def find_or_create_oauth_user(username: str, display_name: str, email: str) -> d
created_at,
updated_at
)
- VALUES (?, ?, ?, '', 'developer', 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
+ VALUES (?, ?, ?, '', 'guest', 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
""",
(username, display_name, email),
)
@@ -196,7 +196,9 @@ def find_or_create_google_user(email: str, display_name: str, provider_subject:
created = False
user_id = row["id"]
else:
- role = "admin" if con.execute("SELECT COUNT(*) AS count FROM users").fetchone()["count"] == 0 else "viewer"
+ # První účet v prázdné tabulce je admin (bootstrap), každý další nový účet je guest –
+ # bez přístupu, dokud mu admin nepřidělí vyšší roli.
+ role = "admin" if con.execute("SELECT COUNT(*) AS count FROM users").fetchone()["count"] == 0 else "guest"
insert_columns = [
"username",
"display_name",
@@ -292,6 +294,12 @@ def current_user(request: Request) -> dict[str, Any] | None:
def require_user(request: Request) -> dict[str, Any]:
user = current_user(request)
if user:
+ # Guest nemá přístup nikam – přesměruj ho na stránku s výzvou o přidělení práv.
+ if is_guest(user):
+ raise HTTPException(
+ status_code=status.HTTP_303_SEE_OTHER,
+ headers={"Location": "/portal/guest"},
+ )
return user
raise HTTPException(
@@ -307,6 +315,11 @@ def require_user(request: Request) -> dict[str, Any]:
# variables, resources, metadata). Cannot delete services or access admin areas.
# admin – full access, including deleting services and the Administration / Users area.
+def is_guest(user: dict | None) -> bool:
+ """True for the default role assigned to a freshly created account (no access yet)."""
+ return bool(user) and (user.get("role") or "").lower() == "guest"
+
+
def is_admin(user: dict | None) -> bool:
return bool(user) and (user.get("role") or "").lower() == "admin"
diff --git a/app/db/users.py b/app/db/users.py
index c8d2dbd..5e48e75 100644
--- a/app/db/users.py
+++ b/app/db/users.py
@@ -3,7 +3,9 @@ from typing import Any
from app.db.database import get_connection
from app.db.migrations import run_migrations
-ROLES = ("admin", "developer", "viewer")
+# Pořadí od nejvyšších práv po nejnižší. "guest" je výchozí role nového účtu –
+# nemá přístup nikam, jen vidí výzvu k přidělení práv administrátorem.
+ROLES = ("admin", "developer", "viewer", "guest")
def _table_columns(con, table_name: str) -> set[str]:
diff --git a/app/routes/alerting.py b/app/routes/alerting.py
index c5c00e1..afa4d41 100644
--- a/app/routes/alerting.py
+++ b/app/routes/alerting.py
@@ -196,7 +196,7 @@ def render_rule_form(rule: dict | None, action: str) -> str:
Zapnuto
- Uložit
+ Uložit
"""
@@ -230,7 +230,7 @@ def render_event_rows(events: list[dict], empty_colspan: int = 9, include_rule:
{render_status(event.get("status"))}
{job_link}
{html.escape(event.get("created_at", "") or "")}
- Detail
+ Detail
"""
@@ -273,7 +273,7 @@ def render_script_editor(rule: dict, user: dict) -> str:
Obsah souboru
- Uložit skript
+ Uložit skript
@@ -307,13 +307,13 @@ def alert_rules_page(request: Request, user=Depends(require_user)):
{last_event}
{html.escape(rule.get("updated_at", "") or "")}
- Detail
- Upravit
+ Detail
+ Upravit
@@ -329,8 +329,8 @@ def alert_rules_page(request: Request, user=Depends(require_user)):
Alert pravidla
Správa pravidel pro spouštění alert skriptů při událostech incidentů.
- + Nové alert pravidlo
- Alert eventy
+ Nové alert pravidlo
+ Alert eventy
@@ -415,15 +415,15 @@ def alert_rule_detail(rule_id: int, request: Request, user=Depends(require_user)
{html.escape(rule.get("name", "") or "")}
← Zpět na alert pravidla
- Upravit
- Alert eventy
+ Upravit
+ Alert eventy
@@ -580,7 +580,7 @@ def alert_events_page(request: Request, user=Depends(require_user)):
diff --git a/app/routes/apps.py b/app/routes/apps.py
index a302d53..06af123 100644
--- a/app/routes/apps.py
+++ b/app/routes/apps.py
@@ -144,7 +144,7 @@ def ensure_variable_key_allowed(app_id: str, key: str, variable_id: int | None =
def render_template_options(templates: list[dict], selected_template: str, include_blank: bool = True) -> str:
- options = ['Bez šablony '] if include_blank else []
+ options = ['Bez šablony '] if include_blank else []
selected_exists = not selected_template
for template in templates:
template_id = template.get("id", "") or ""
@@ -164,7 +164,7 @@ def render_template_options(templates: list[dict], selected_template: str, inclu
)
if not selected_exists:
options.append(
- f'Neznámá šablona '
+ f'Neznámá šablona '
)
return "".join(options)
@@ -175,7 +175,7 @@ def render_template_label(templates: list[dict], template_id: str) -> str:
for template in templates:
if template.get("id") == template_id:
return html.escape(template.get("name", "") or template_id)
- return "Neznámá šablona"
+ return "Neznámá šablona"
def render_environment_usage_example(language: str, runtime: str, template: str) -> str:
@@ -361,8 +361,8 @@ def apps_page(
CPU
- Zrušit
- Použít
+ Zrušit
+ Použít
"""
@@ -377,13 +377,13 @@ def apps_page(
HTTP
- Kopírovat
+ Kopírovat
SSH
- Kopírovat
+ Kopírovat
"""
else:
@@ -470,12 +470,12 @@ def apps_page(
return f"/portal/apps?{urlencode(params)}"
previous_link = (
- f'Předchozí '
+ f' Předchozí '
if page_number > 1
else ""
)
next_link = (
- f'Další '
+ f'Další '
if page_number < total_pages
else ""
)
@@ -502,7 +502,7 @@ def apps_page(
"""
Nová služba
"""
if can_manage
@@ -709,17 +709,17 @@ def app_detail(app_id: str, request: Request, message: str = "", error: str = ""
Secret
- Uložit
+ Uložit
"""
if not variable_rows:
- variable_rows = 'Zatím nejsou evidované žádné proměnné. '
+ variable_rows = 'Zatím nejsou evidované žádné proměnné. '
def render_method_options(selected: str) -> str:
selected_upper = (selected or "").upper()
@@ -749,10 +749,10 @@ def app_detail(app_id: str, request: Request, message: str = "", error: str = ""
{render_method_options(rule.get("methods", ""))}
- Aktivní
+ Aktivní
- Uložit
+ Uložit
@@ -761,7 +761,7 @@ def app_detail(app_id: str, request: Request, message: str = "", error: str = ""
"""
if not ip_access_rule_rows:
- ip_access_rule_rows = ' Zatím nejsou evidovaná žádná IP access pravidla. '
+ ip_access_rule_rows = 'Zatím nejsou evidovaná žádná IP access pravidla. '
notice = ""
if message:
@@ -774,7 +774,7 @@ def app_detail(app_id: str, request: Request, message: str = "", error: str = ""
redeploy_block = (
f"""
"""
if can_manage
@@ -783,17 +783,17 @@ def app_detail(app_id: str, request: Request, message: str = "", error: str = ""
detail_tabs = (
"""
-
-
Metadata
-
Proměnné
-
Security / IP Access
-
Historie
+
"""
if can_manage
else """
-
-
Historie
+
"""
)
@@ -803,13 +803,13 @@ def app_detail(app_id: str, request: Request, message: str = "", error: str = ""
@@ -852,9 +852,9 @@ def app_detail(app_id: str, request: Request, message: str = "", error: str = ""
variables_card = (
f"""
@@ -889,9 +889,9 @@ def app_detail(app_id: str, request: Request, message: str = "", error: str = ""
Security / IP Access
- Pravidla určují, z jakých IP/CIDR adres lze volat dané HTTP metody.
- WRITE = POST, PUT, PATCH, DELETE; ALL = všechny běžné metody včetně GET.
- Pravidla se zatím pouze ukládají (Caddy je začne vynucovat později).
+ Pravidla určují, z jakých IP/CIDR adres lze volat dané HTTP metody.
+ WRITE = POST, PUT, PATCH, DELETE; ALL = všechny běžné metody včetně GET.
+ Pravidla se zatím pouze ukládají (Caddy je začne vynucovat později).
@@ -905,16 +905,16 @@ def app_detail(app_id: str, request: Request, message: str = "", error: str = ""
- Přidat pravidlo
+ Přidat pravidlo
IP/CIDR
-
+
Methods
{render_method_options("WRITE")}
Description
-
- Aktivní
+
+ Aktivní
- Přidat pravidlo
+ Přidat pravidlo
@@ -932,8 +932,8 @@ def app_detail(app_id: str, request: Request, message: str = "", error: str = ""
{notice}
← Zpět na služby
- Nasazení služby
- Úlohy služby
+ Nasazení služby
+ Úlohy služby
{redeploy_block}
@@ -952,7 +952,7 @@ def app_detail(app_id: str, request: Request, message: str = "", error: str = ""
ID {escaped_app_id}
Název {name}
Popis {description}
-
Vlastník {owner}
+
Vlastník {owner}
Template {template_value}
Runtime {runtime}
Repository URL {repository_url}
@@ -961,8 +961,8 @@ def app_detail(app_id: str, request: Request, message: str = "", error: str = ""
Domain {domain}
Health URL {health_url}
Container Port {container_port}
-
Veřejná služba {"Ano" if is_public else "Ne"}
-
Aktivní služba {"Ano" if is_enabled else "Ne"}
+
Veřejná služba {"Ano" if is_public else "Ne"}
+
Aktivní služba {"Ano" if is_enabled else "Ne"}
Jazyk {language}
Verze {version}
Status {status}
@@ -998,13 +998,13 @@ def app_detail(app_id: str, request: Request, message: str = "", error: str = ""
-
Incidenty služby
+
Incidenty služby
- Název
- Začátek
+ Název
+ Začátek
Konec
- Trvání
+ Trvání
Stav
{render_incident_history_rows(incidents, include_service=False)}
@@ -1364,10 +1364,10 @@ def new_app_form(request: Request, user=Depends(require_developer)):
- Vytvořit službu
+ Vytvořit službu
- Vytvářím službu...
+ Vytvářím službu...
diff --git a/app/routes/audit.py b/app/routes/audit.py
index ceb638b..cbd045a 100644
--- a/app/routes/audit.py
+++ b/app/routes/audit.py
@@ -37,7 +37,7 @@ def audit_page(request: Request, user=Depends(require_user)):
"Audit",
f"""
-
Audit
+
Audit
Přehled interních akcí provedených v portálu.
diff --git a/app/routes/auth.py b/app/routes/auth.py
index 4122bf9..e7a4952 100644
--- a/app/routes/auth.py
+++ b/app/routes/auth.py
@@ -259,6 +259,28 @@ def google_callback(request: Request, code: str = "", state: str = "", error: st
return _render_login("Google login failed.")
+@router.get("/guest", response_class=HTMLResponse)
+def guest_page(request: Request):
+ user = current_user(request)
+ if not user:
+ return RedirectResponse(url="/portal/login", status_code=303)
+ # Účet s přidělenými právy sem nepatří – pošli ho na běžnou úvodní stránku.
+ if (user.get("role") or "").lower() != "guest":
+ return RedirectResponse(url="/portal/apps", status_code=303)
+
+ return page(
+ "Čekání na schválení",
+ """
+
+
Účet zatím nemá přidělená práva
+
Požádejte o přidělení práv administrátora.
+
Jakmile vám administrátor přiřadí roli, obnovte stránku a portál se vám zpřístupní.
+
+ """,
+ user=user,
+ )
+
+
@router.post("/logout")
def logout(request: Request):
user = current_user(request)
@@ -424,7 +446,7 @@ def _render_login(error: str | None = None) -> str:
- Sign in locally
+ Sign in locally
"""
@@ -464,7 +486,7 @@ def _render_login(error: str | None = None) -> str:
- Přihlásit lokálně
+ Přihlásit lokálně
""",
diff --git a/app/routes/deployments.py b/app/routes/deployments.py
index 4baca65..454c3f7 100644
--- a/app/routes/deployments.py
+++ b/app/routes/deployments.py
@@ -211,7 +211,7 @@ async def deployments_page(
{returncode_label}
{finished_at}
- Detail
+ Detail
"""
@@ -226,12 +226,12 @@ async def deployments_page(
first_item = offset + 1 if total_deployments else 0
last_item = min(offset + len(deployments), total_deployments)
previous_link = (
- f'Předchozí '
+ f' Předchozí '
if page_number > 1
else ""
)
next_link = (
- f'Další '
+ f'Další '
if page_number < total_pages
else ""
)
@@ -253,7 +253,7 @@ async def deployments_page(
f"""
{refresh}
-
Historie nasazení
+
Historie nasazení
Přehled posledních běhů nasazení a jejich výsledků.
← Zpět na přehled
@@ -272,8 +272,8 @@ async def deployments_page(
{app_options}
{source_options}
- Filtrovat
- Reset
+ Filtrovat
+ Reset
@@ -361,8 +361,8 @@ async def deployment_detail_page(
{failed_notice}
← Zpět na nasazení
- Detail služby
- Surové logy
+ Detail služby
+ Surové logy
diff --git a/app/routes/developers.py b/app/routes/developers.py
index 9f564a3..2cae5cc 100644
--- a/app/routes/developers.py
+++ b/app/routes/developers.py
@@ -53,6 +53,7 @@ PUBLIC_ENDPOINTS = [
# Roles model.
ROLES = [
+ ("guest", "Výchozí role nového účtu po prvním přihlášení. Bez přístupu – vidí jen výzvu požádat administrátora o přidělení práv."),
("viewer", "Pouze pro čtení: vidí jaké služby běží a jejich dokumentaci (Swagger). Bez Gitu, úprav, nasazení a mazání."),
("developer", "Vše co viewer + provozní workflow služeb: Git/klonování, nasazení, proměnné, prostředky, metadata. Nemůže mazat služby ani do Administrace."),
("admin", "Plný přístup: vše co developer + mazání služeb, správa záloh a jejich obnova, správa uživatelů, audit a admin-only operace."),
@@ -120,7 +121,7 @@ def developers_page(user=Depends(require_user)):
Naklonujte repozitář.
- Kopírovat
+ Kopírovat
Přesný příkaz pro konkrétní službu (HTTP i SSH) najdete také na stránce
Služby .
diff --git a/app/routes/environment.py b/app/routes/environment.py
index da8915c..0e255bd 100644
--- a/app/routes/environment.py
+++ b/app/routes/environment.py
@@ -29,7 +29,7 @@ def _render_rows(entries: list[tuple[str, str]]) -> str:
- Smazat
+ Smazat
"""
@@ -109,7 +109,7 @@ def _render_page(user: dict, entries: list[tuple[str, str]], message: str = "",
const tr = document.createElement("tr");
tr.innerHTML = ' '
+ ' '
- + 'Smazat ';
+ + ' Smazat ';
tbody.appendChild(tr);
}}
diff --git a/app/routes/incidents.py b/app/routes/incidents.py
index 0deb907..47d9b2a 100644
--- a/app/routes/incidents.py
+++ b/app/routes/incidents.py
@@ -111,7 +111,7 @@ def incidents_page(request: Request, user=Depends(require_user)):
"Incidenty",
f"""
-
Incidenty
+
Incidenty
P\u0159ehled incident\u016f vytv\u00e1\u0159en\u00fdch a uzav\u00edran\u00fdch monitorem.
diff --git a/app/routes/jobs.py b/app/routes/jobs.py
index 61e190c..58bf45b 100644
--- a/app/routes/jobs.py
+++ b/app/routes/jobs.py
@@ -198,7 +198,7 @@ def jobs_page(
{created_at}
{started_at}
{finished_at}
- Detail
+ Detail
"""
@@ -222,12 +222,12 @@ def jobs_page(
return f"/portal/jobs?{urlencode(params)}"
previous_link = (
- f'Předchozí '
+ f' Předchozí '
if page_number > 1
else ""
)
next_link = (
- f'Další '
+ f'Další '
if page_number < total_pages
else ""
)
@@ -249,7 +249,7 @@ def jobs_page(
f"""
{refresh}
-
Úlohy
+
Úlohy
Fronta portálových a webhook úloh připravená pro centrální worker.
@@ -267,8 +267,8 @@ def jobs_page(
{type_options}
- Filtrovat
- Reset
+ Filtrovat
+ Reset
@@ -335,7 +335,7 @@ def job_detail_page(job_id: int, request: Request, user=Depends(require_user)):
if status_value == "failed" and can_retry_job(job):
actions += f"""
- Spustit znovu
+ Spustit znovu
"""
elif status_value == "failed" and (job.get("target_id") or "") in IGNORED_RETRY_REPOSITORIES:
@@ -343,7 +343,7 @@ def job_detail_page(job_id: int, request: Request, user=Depends(require_user)):
if status_value in {"queued", "running"}:
actions += f"""
- Zrušit úlohu
+ Zrušit úlohu
"""
if actions:
@@ -441,7 +441,7 @@ def job_detail_page(job_id: int, request: Request, user=Depends(require_user)):
{title}
← Zpět na úlohy
- Detail cíle
+ Detail cíle
{retry_blocked_notice}
{actions}
diff --git a/app/routes/migration_readiness.py b/app/routes/migration_readiness.py
index 3295f30..37c3365 100644
--- a/app/routes/migration_readiness.py
+++ b/app/routes/migration_readiness.py
@@ -157,19 +157,19 @@ def migration_readiness_page(request: Request, user=Depends(require_user)):
"Migration Readiness",
f"""
-
Migration Readiness
+
Migration Readiness
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.
- Spustit preflight kontrolu
+ Spustit preflight kontrolu
- Run Bootstrap Check
+ Run Bootstrap Check
- Run Bootstrap Repair
+ Run Bootstrap Repair
@@ -189,7 +189,7 @@ def migration_readiness_page(request: Request, user=Depends(require_user)):
Režim deploy deployuje core services a může změnit stav systému. Spouštějte ho jen po kontrole dopadu.
- Run Bootstrap Deploy
+ Run Bootstrap Deploy
diff --git a/app/routes/operations.py b/app/routes/operations.py
index 07bb4a5..687522d 100644
--- a/app/routes/operations.py
+++ b/app/routes/operations.py
@@ -182,7 +182,7 @@ def operations_dashboard(request: Request, user=Depends(require_user)):
"Přehled",
f"""
-
Přehled systému
+
Přehled systému
Rychlá odpověď na otázku, zda jsou služby, úlohy a nasazení v pořádku.
diff --git a/app/routes/runtime.py b/app/routes/runtime.py
index b27376a..9f630de 100644
--- a/app/routes/runtime.py
+++ b/app/routes/runtime.py
@@ -14,10 +14,12 @@ from app.templates.layout import page
router = APIRouter()
# Portál NEMÁ vlastní deploy logiku. Všechny provozní akce deleguje do existujících shell skriptů
-# v appfactory-tools přes existující job systém (job_type="run_script"; worker spustí scripts/