mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
120 lines
4.0 KiB
TypeScript
120 lines
4.0 KiB
TypeScript
import type { WebEndpoint } from "../shared/web-endpoint-config";
|
|
|
|
/**
|
|
* Editor-side validation for web endpoint rows.
|
|
*
|
|
* The authority on what a stored endpoint may look like is
|
|
* `normalizeWebEndpoints` in ../shared/web-endpoint-config.ts, and it DROPS
|
|
* any row it refuses rather than reporting it -- so without a check here the
|
|
* user adds a row, saves, sees no error, and finds the endpoint gone on
|
|
* reload.
|
|
*
|
|
* This deliberately does NOT restate that module's full rule set: it covers
|
|
* only the conditions a user can reach by typing, with per-field errors a
|
|
* drop/keep normalizer cannot give. tests/backend/web-endpoint-config.test.ts
|
|
* covers the normalizer; this file's own tests cover the same inputs here.
|
|
*/
|
|
|
|
export const MIN_WEB_ENDPOINT_PORT = 1;
|
|
export const MAX_WEB_ENDPOINT_PORT = 65535;
|
|
|
|
export type WebEndpointFieldError =
|
|
"label" | "duplicateLabel" | "port" | "path";
|
|
|
|
const ERROR_KEYS: Record<WebEndpointFieldError, string> = {
|
|
label: "hosts.webUiErrorLabelRequired",
|
|
duplicateLabel: "hosts.webUiErrorLabelDuplicate",
|
|
port: "hosts.webUiErrorPortRange",
|
|
path: "hosts.webUiErrorPathInvalid",
|
|
};
|
|
|
|
/**
|
|
* The i18n key for a field error. Every key takes a `row` interpolation so the
|
|
* message can say which endpoint is at fault.
|
|
*/
|
|
export function webEndpointErrorKey(error: WebEndpointFieldError): string {
|
|
return ERROR_KEYS[error];
|
|
}
|
|
|
|
export function isWebEndpointPortValid(port: unknown): boolean {
|
|
return (
|
|
typeof port === "number" &&
|
|
Number.isInteger(port) &&
|
|
port >= MIN_WEB_ENDPOINT_PORT &&
|
|
port <= MAX_WEB_ENDPOINT_PORT
|
|
);
|
|
}
|
|
|
|
function hasControlCharacter(value: string): boolean {
|
|
for (let index = 0; index < value.length; index += 1) {
|
|
const code = value.charCodeAt(index);
|
|
if (code <= 0x1f || code === 0x7f) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* True if the normalizer would accept this path. An empty or absent path is
|
|
* fine -- the normalizer defaults it to "/" -- and so is a path with no
|
|
* leading slash, which the normalizer coerces.
|
|
*/
|
|
export function isWebEndpointPathValid(
|
|
raw: string | undefined | null,
|
|
): boolean {
|
|
if (raw === undefined || raw === null || raw === "") return true;
|
|
if (hasControlCharacter(raw)) return false;
|
|
|
|
const path = raw.startsWith("/") ? raw : `/${raw}`;
|
|
|
|
// A protocol-relative or absolute path would send the iframe or the link
|
|
// somewhere other than the endpoint.
|
|
if (path.startsWith("//")) return false;
|
|
if (/^\/[a-z][a-z0-9+.-]*:/i.test(path)) return false;
|
|
if (path.includes("\\")) return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* The first thing wrong with one row, or null if the normalizer would keep it.
|
|
* Label is checked before port so the message points at the field the user
|
|
* most likely just left blank.
|
|
*/
|
|
export function webEndpointRowError(
|
|
endpoint: WebEndpoint,
|
|
): WebEndpointFieldError | null {
|
|
if (typeof endpoint.label !== "string" || endpoint.label.trim() === "") {
|
|
return "label";
|
|
}
|
|
if (!isWebEndpointPortValid(endpoint.port)) return "port";
|
|
if (!isWebEndpointPathValid(endpoint.path)) return "path";
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* The first row the normalizer would drop, so a save can be blocked with a
|
|
* message naming it. Returns null when every row would survive.
|
|
*
|
|
* Duplicate LABELS are reported even though the normalizer tolerates them:
|
|
* the sidebar picker shows labels, so two identical ones are indistinguishable
|
|
* to the user. Duplicate ids are generated, never typed.
|
|
*/
|
|
export function findInvalidWebEndpoint(
|
|
endpoints: WebEndpoint[] | undefined | null,
|
|
): { index: number; error: WebEndpointFieldError } | null {
|
|
if (!Array.isArray(endpoints)) return null;
|
|
|
|
const seen = new Set<string>();
|
|
for (let index = 0; index < endpoints.length; index += 1) {
|
|
const endpoint = endpoints[index];
|
|
const error = webEndpointRowError(endpoint);
|
|
if (error) return { index, error };
|
|
|
|
const label = endpoint.label.trim().toLowerCase();
|
|
if (seen.has(label)) return { index, error: "duplicateLabel" };
|
|
seen.add(label);
|
|
}
|
|
|
|
return null;
|
|
}
|