Files
web-terminal/control-panel/public/dom.ts
Yaojia Wang 675de771c7 feat(control-panel): web admin UI for the zero-touch tunnel
Loopback Fastify auth-broker + esbuild SPA. Operator password login (constant-time,
signed HttpOnly session cookie, per-forwarded-IP rate-limit) → session-gated proxy
that mints a fresh 60s manage capability token per call to the control-plane admin
API: list hosts, mint pairing codes (with QR + pair command), revoke hosts. Security
headers + CSP, CP_URL pinned loopback (anti-SSRF), hostId dot-segment guard. 55 tests
pass; security-reviewed. Deployed behind nginx panel.terminal.yaojia.wang.
2026-07-19 19:47:51 +02:00

61 lines
2.3 KiB
TypeScript

/**
* Tiny DOM builder. ALL text and attribute values are set via `textContent` / `setAttribute` — this
* module NEVER assigns `innerHTML`, so untrusted server/host data (host subdomains, etc.) can never
* inject markup. Children passed as strings become text nodes (also safe).
*/
export type Child = Node | string
export interface ElProps {
class?: string
text?: string
type?: string
name?: string
placeholder?: string
value?: string
disabled?: boolean
autocomplete?: string
title?: string
src?: string
alt?: string
role?: string
ariaLabel?: string
onClick?: (e: MouseEvent) => void
onSubmit?: (e: SubmitEvent) => void
}
export function el<K extends keyof HTMLElementTagNameMap>(
tag: K,
props: ElProps = {},
children: Child[] = [],
): HTMLElementTagNameMap[K] {
const node = document.createElement(tag)
if (props.class !== undefined) node.className = props.class
if (props.text !== undefined) node.textContent = props.text
if (props.type !== undefined) node.setAttribute('type', props.type)
if (props.name !== undefined) node.setAttribute('name', props.name)
if (props.placeholder !== undefined) node.setAttribute('placeholder', props.placeholder)
if (props.value !== undefined) (node as HTMLInputElement).value = props.value
if (props.disabled) node.setAttribute('disabled', 'true')
if (props.autocomplete !== undefined) node.setAttribute('autocomplete', props.autocomplete)
if (props.title !== undefined) node.setAttribute('title', props.title)
if (props.src !== undefined) node.setAttribute('src', props.src)
if (props.alt !== undefined) node.setAttribute('alt', props.alt)
if (props.role !== undefined) node.setAttribute('role', props.role)
if (props.ariaLabel !== undefined) node.setAttribute('aria-label', props.ariaLabel)
if (props.onClick !== undefined) node.addEventListener('click', props.onClick as EventListener)
if (props.onSubmit !== undefined) node.addEventListener('submit', props.onSubmit as EventListener)
for (const child of children) node.append(child)
return node
}
/** Remove all children of a node. */
export function clear(node: Element): void {
while (node.firstChild) node.removeChild(node.firstChild)
}
/** Replace a node's content with a single new child. */
export function mount(root: Element, child: Node): void {
clear(root)
root.append(child)
}