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.
This commit is contained in:
37
control-panel/src/pairing.ts
Normal file
37
control-panel/src/pairing.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Operator-facing pairing artifacts derived from a freshly-issued pairing code:
|
||||
* - the ready-to-run `web-terminal-agent pair <code> --install --zone <zone>` command line, and
|
||||
* - a QR PNG data: URL of the code (rendered with the `qrcode` dep) for phone scanning.
|
||||
* Pure/deterministic given the code + zone (except the QR, which is a stable render of the code).
|
||||
*/
|
||||
import QRCode from 'qrcode'
|
||||
|
||||
/** Build the exact command an operator runs on the new host to enroll it into the tunnel. */
|
||||
export function buildPairCommand(code: string, zone: string): string {
|
||||
return `web-terminal-agent pair ${code} --install --zone ${zone}`
|
||||
}
|
||||
|
||||
/** Render a QR PNG data: URL encoding the pairing code (for scanning on the phone client). */
|
||||
export async function buildQrDataUrl(code: string): Promise<string> {
|
||||
return QRCode.toDataURL(code, { errorCorrectionLevel: 'M', margin: 1, width: 256 })
|
||||
}
|
||||
|
||||
export interface PairingArtifacts {
|
||||
readonly code: string
|
||||
readonly expiresAt: string
|
||||
readonly pairCommand: string
|
||||
readonly qrDataUrl: string
|
||||
}
|
||||
|
||||
/** Combine an issued code with its operator-facing command + QR into the /api/pairing-codes payload. */
|
||||
export async function buildPairingArtifacts(
|
||||
issued: { readonly code: string; readonly expiresAt: string },
|
||||
zone: string,
|
||||
): Promise<PairingArtifacts> {
|
||||
return {
|
||||
code: issued.code,
|
||||
expiresAt: issued.expiresAt,
|
||||
pairCommand: buildPairCommand(issued.code, zone),
|
||||
qrDataUrl: await buildQrDataUrl(issued.code),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user