nazev u sluzby + kontrola u formulare nove
This commit is contained in:
+51
-2
@@ -1,5 +1,6 @@
|
||||
import html
|
||||
import math
|
||||
import re
|
||||
from urllib.parse import quote, urlencode
|
||||
|
||||
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:
|
||||
return (value or "").strip()
|
||||
|
||||
@@ -330,6 +357,10 @@ def apps_page(
|
||||
for index, item in enumerate(apps, start=1):
|
||||
app_id = html.escape(item.get("id", ""))
|
||||
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}"
|
||||
status = html.escape(item.get("status", ""))
|
||||
health = latest_health.get(item.get("id", "")) or {}
|
||||
@@ -422,7 +453,7 @@ def apps_page(
|
||||
<div class="service-title">
|
||||
{health_dot}
|
||||
<div>
|
||||
<strong>{app_id}</strong><br>
|
||||
<strong>{service_label}</strong><br>
|
||||
<span class="muted">/apps/{app_id}</span>
|
||||
</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">
|
||||
<p>
|
||||
<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>
|
||||
@@ -1376,6 +1411,20 @@ def create_app(
|
||||
cpus: str = Form(""),
|
||||
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)
|
||||
if not selected_template:
|
||||
return HTMLResponse("Nepodporovaná šablona", status_code=400)
|
||||
|
||||
@@ -149,6 +149,20 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
const status = document.getElementById("new-app-status");
|
||||
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) => {
|
||||
event.preventDefault();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user