auth/runtime config, URLs centralizados, parsing/hardening dos hooks/charts

This commit is contained in:
litoral05
2026-06-08 16:32:21 +01:00
parent d6daac97c7
commit 19df30326b
32 changed files with 899 additions and 267 deletions
+71 -39
View File
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { appendAccessToken, getVncWebSocketUrl } from "../../../lib/api/gatewayConfig";
export type VncConnectionState =
| "IDLE"
@@ -11,6 +12,7 @@ export type VncConnectionState =
export type UseVncConsoleOptions = {
websocketUrl?: string;
accessToken?: string | null;
defaultHost?: string;
defaultPort?: number;
};
@@ -21,31 +23,35 @@ export type ConnectVncInput = {
password: string;
};
const DEFAULT_WEBSOCKET_URL = "ws://localhost:18450/ws/vnc";
const DEFAULT_HOST = "198.19.0.176";
const DEFAULT_PORT = 5900;
export function useVncConsole(options: UseVncConsoleOptions = {}) {
const websocketUrl = options.websocketUrl ?? DEFAULT_WEBSOCKET_URL;
const websocketUrl = options.websocketUrl ?? getVncWebSocketUrl();
const accessToken = options.accessToken ?? null;
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const wsRef = useRef<WebSocket | null>(null);
const ctxRef = useRef<CanvasRenderingContext2D | null>(null);
const rgbaRef = useRef<Uint8ClampedArray | null>(null);
const framebufferRef = useRef({
width: 0,
height: 0,
});
const framebufferRef = useRef({ width: 0, height: 0 });
const [state, setState] = useState<VncConnectionState>("IDLE");
const [error, setError] = useState<string | null>(null);
const [host, setHost] = useState(options.defaultHost ?? DEFAULT_HOST);
const [host, setHost] = useState(options.defaultHost ?? "");
const [port, setPort] = useState(options.defaultPort ?? DEFAULT_PORT);
const [password, setPassword] = useState("");
const [frameSize, setFrameSize] = useState({ width: 0, height: 0 });
const [lastFrameAt, setLastFrameAt] = useState<string | null>(null);
const buildWebSocketUrl = useCallback(() => {
if (!accessToken) {
return websocketUrl;
}
return appendAccessToken(websocketUrl, accessToken);
}, [websocketUrl, accessToken]);
const clearFrame = useCallback(() => {
const canvas = canvasRef.current;
const ctx = ctxRef.current;
@@ -58,11 +64,7 @@ export function useVncConsole(options: UseVncConsoleOptions = {}) {
ctxRef.current = null;
rgbaRef.current = null;
framebufferRef.current = {
width: 0,
height: 0,
};
framebufferRef.current = { width: 0, height: 0 };
setFrameSize({ width: 0, height: 0 });
setLastFrameAt(null);
@@ -104,14 +106,16 @@ export function useVncConsole(options: UseVncConsoleOptions = {}) {
const width = view.getInt32(0);
const height = view.getInt32(4);
console.log("[VNC] drawFrame", {
byteLength: buffer.byteLength,
width,
height,
});
if (!width || !height || width <= 0 || height <= 0) return;
const pixels = new Uint8ClampedArray(buffer, 8);
framebufferRef.current = {
width,
height,
};
framebufferRef.current = { width, height };
if (!ctxRef.current) {
ctxRef.current = canvas.getContext("2d", {
@@ -156,6 +160,12 @@ export function useVncConsole(options: UseVncConsoleOptions = {}) {
const nextPort = input?.port ?? port;
const nextPassword = input?.password ?? password;
if (!accessToken) {
setError("Token de autenticação em falta.");
setState("ERROR");
return;
}
if (!nextHost.trim()) {
setError("Host VNC em falta.");
setState("ERROR");
@@ -169,7 +179,7 @@ export function useVncConsole(options: UseVncConsoleOptions = {}) {
setState("CONNECTING_WS");
window.setTimeout(() => {
const socket = new WebSocket(websocketUrl);
const socket = new WebSocket(buildWebSocketUrl());
wsRef.current = socket;
socket.binaryType = "arraybuffer";
@@ -236,8 +246,19 @@ export function useVncConsole(options: UseVncConsoleOptions = {}) {
return;
}
if (event.data instanceof ArrayBuffer) {
console.log("[VNC] binary ArrayBuffer", event.data.byteLength);
drawFrame(event.data);
return;
}
drawFrame(event.data);
if (event.data instanceof Blob) {
console.log("[VNC] binary Blob", event.data.size);
event.data.arrayBuffer().then(drawFrame);
return;
}
console.warn("[VNC] unknown binary payload", event.data);
};
socket.onerror = () => {
@@ -261,13 +282,14 @@ export function useVncConsole(options: UseVncConsoleOptions = {}) {
}, 100);
},
[
accessToken,
buildWebSocketUrl,
clearFrame,
closeSocket,
drawFrame,
host,
password,
port,
websocketUrl,
],
);
@@ -288,26 +310,36 @@ export function useVncConsole(options: UseVncConsoleOptions = {}) {
const rect = canvas.getBoundingClientRect();
const relativeX = (clientX - rect.left) / rect.width;
const relativeY = (clientY - rect.top) / rect.height;
const containerAspect = rect.width / rect.height;
const frameAspect = framebuffer.width / framebuffer.height;
const x = Math.max(
0,
Math.min(framebuffer.width - 1, Math.round(relativeX * framebuffer.width)),
);
let renderedWidth: number;
let renderedHeight: number;
let offsetX: number;
let offsetY: number;
const y = Math.max(
0,
Math.min(framebuffer.height - 1, Math.round(relativeY * framebuffer.height)),
);
if (containerAspect > frameAspect) {
renderedHeight = rect.height;
renderedWidth = rect.height * frameAspect;
offsetX = rect.left + (rect.width - renderedWidth) / 2;
offsetY = rect.top;
} else {
renderedWidth = rect.width;
renderedHeight = rect.width / frameAspect;
offsetX = rect.left;
offsetY = rect.top + (rect.height - renderedHeight) / 2;
}
ws.send(
JSON.stringify({
type: "click",
x,
y,
}),
);
const relativeX = (clientX - offsetX) / renderedWidth;
const relativeY = (clientY - offsetY) / renderedHeight;
const clampedX = Math.max(0, Math.min(1, relativeX));
const clampedY = Math.max(0, Math.min(1, relativeY));
const x = Math.round(clampedX * (framebuffer.width - 1));
const y = Math.round(clampedY * (framebuffer.height - 1));
ws.send(JSON.stringify({ type: "click", x, y }));
}, []);
const handleCanvasPointerDown = useCallback(
@@ -343,4 +375,4 @@ export function useVncConsole(options: UseVncConsoleOptions = {}) {
connected: state === "CONNECTED" || state === "FIRST_FRAME",
connecting: state === "CONNECTING_WS" || state === "CONNECTING_VNC",
};
}
}