nazev u sluzby + kontrola u formulare nove
This commit is contained in:
+51
-2
@@ -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)
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user