nazev u sluzby + kontrola u formulare nove

This commit is contained in:
JiriUhlir
2026-07-16 12:05:02 +02:00
parent 3dc0c5b146
commit ec9a643291
2 changed files with 65 additions and 2 deletions
+51 -2
View File
@@ -1,5 +1,6 @@
import html import html
import math import math
import re
from urllib.parse import quote, urlencode from urllib.parse import quote, urlencode
from fastapi import APIRouter, Depends, Form, HTTPException, Query, Request from fastapi import APIRouter, Depends, Form, HTTPException, Query, Request
@@ -78,6 +79,32 @@ METADATA_FIELDS = (
) )
# ID služby se používá jako Docker container/image name, Compose service name, Gitea repo,
# adresář workspace a část URL/domény. Nejpřísnější je Docker image name (jen lowercase)
# a DNS label (max 63 znaků, alfanumerické na začátku i konci). Proto povolujeme pouze
# malá písmena a-z, číslice a pomlčky uvnitř, bez diakritiky, podtržítek a teček.
APP_ID_MAX_LENGTH = 63
APP_ID_RE = re.compile(r"^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$")
def validate_new_app_id(app_id: str) -> str | None:
"""Vrátí chybovou hlášku, pokud ID služby nesplňuje omezení, jinak None."""
if not app_id:
return "ID služby nesmí být prázdné."
if app_id != app_id.lower():
return "ID služby musí být malými písmeny (lowercase)."
if len(app_id) > APP_ID_MAX_LENGTH:
return f"ID služby může mít nejvýše {APP_ID_MAX_LENGTH} znaků (limit DNS/Docker)."
if not APP_ID_RE.match(app_id):
return (
"Neplatné ID služby. Povolena jsou jen malá písmena a-z, číslice a pomlčky; "
"musí začínat i končit písmenem nebo číslicí (např. gmail-service). "
"Bez diakritiky, mezer, podtržítek a teček — ID se používá jako název "
"Docker kontejneru, image a Gitea repozitáře."
)
return None
def clean_optional(value: str | None) -> str: def clean_optional(value: str | None) -> str:
return (value or "").strip() return (value or "").strip()
@@ -330,6 +357,10 @@ def apps_page(
for index, item in enumerate(apps, start=1): for index, item in enumerate(apps, start=1):
app_id = html.escape(item.get("id", "")) app_id = html.escape(item.get("id", ""))
app_url_id = quote(item.get("id", ""), safe="") app_url_id = quote(item.get("id", ""), safe="")
app_name = html.escape(item.get("name", "") or "")
# Ve sloupci Služba zobrazujeme "Název (id)"; když název chybí nebo je shodný s ID,
# zůstane jen ID bez zdvojení.
service_label = f"{app_name} ({app_id})" if app_name and app_name != app_id else app_id
resource_modal_id = f"resources-{page_number}-{index}" resource_modal_id = f"resources-{page_number}-{index}"
status = html.escape(item.get("status", "")) status = html.escape(item.get("status", ""))
health = latest_health.get(item.get("id", "")) or {} health = latest_health.get(item.get("id", "")) or {}
@@ -422,7 +453,7 @@ def apps_page(
<div class="service-title"> <div class="service-title">
{health_dot} {health_dot}
<div> <div>
<strong>{app_id}</strong><br> <strong>{service_label}</strong><br>
<span class="muted">/apps/{app_id}</span> <span class="muted">/apps/{app_id}</span>
</div> </div>
</div> </div>
@@ -1334,7 +1365,11 @@ def new_app_form(request: Request, user=Depends(require_developer)):
<form method="post" action="/portal/new-app" id="new-app-form" data-ajax-form="new-app"> <form method="post" action="/portal/new-app" id="new-app-form" data-ajax-form="new-app">
<p> <p>
<label>ID služby</label><br> <label>ID služby</label><br>
<input name="app_id" placeholder="gmail-service" required> <input name="app_id" placeholder="gmail-service" required
pattern="[a-z0-9]([a-z0-9\\-]*[a-z0-9])?" maxlength="{APP_ID_MAX_LENGTH}"
autocomplete="off" spellcheck="false"
title="Jen malá písmena a-z, číslice a pomlčky; musí začínat i končit písmenem nebo číslicí."><br>
<span class="muted">Jen malá písmena, číslice a pomlčky (např. gmail-service). ID se použije jako název Docker kontejneru, Gitea repozitáře a v URL — později ho nelze změnit.</span>
</p> </p>
<p> <p>
@@ -1376,6 +1411,20 @@ def create_app(
cpus: str = Form(""), cpus: str = Form(""),
user=Depends(require_developer), user=Depends(require_developer),
): ):
# Normalizace + validace ID: ID musí být vždy lowercase a splňovat omezení Dockeru,
# DNS a Gitea (viz validate_new_app_id). Bez toho by create script vytvořil kontejner
# nebo repozitář, který později nejde nasadit či adresovat.
app_id = clean_optional(app_id).lower()
app_name = clean_optional(app_name)
id_error = validate_new_app_id(app_id)
if id_error:
return HTMLResponse(id_error, status_code=400)
if not app_name:
return HTMLResponse("Název služby nesmí být prázdný.", status_code=400)
if get_app(app_id):
return HTMLResponse(f"Služba s ID '{app_id}' už existuje.", status_code=400)
selected_template = get_app_template(template, create_enabled=True) selected_template = get_app_template(template, create_enabled=True)
if not selected_template: if not selected_template:
return HTMLResponse("Nepodporovaná šablona", status_code=400) return HTMLResponse("Nepodporovaná šablona", status_code=400)
+14
View File
@@ -149,6 +149,20 @@ document.addEventListener("DOMContentLoaded", () => {
const status = document.getElementById("new-app-status"); const status = document.getElementById("new-app-status");
const result = document.getElementById("new-app-result"); const result = document.getElementById("new-app-result");
// ID služby musí být vždy lowercase (Docker image/container, Gitea repo, URL).
// Přepisujeme rovnou při psaní, ať uživatel vidí skutečnou hodnotu, která se odešle.
const appIdInput = form.querySelector('input[name="app_id"]');
if (appIdInput) {
appIdInput.addEventListener("input", () => {
const lowered = appIdInput.value.toLowerCase();
if (appIdInput.value !== lowered) {
const cursor = appIdInput.selectionStart;
appIdInput.value = lowered;
appIdInput.setSelectionRange(cursor, cursor);
}
});
}
form.addEventListener("submit", async (event) => { form.addEventListener("submit", async (event) => {
event.preventDefault(); event.preventDefault();