mirror of https://github.com/jetkvm/kvm.git
refactor: centralize permissions with context provider and remove redundant code
Improvements: - Centralized permission state management in PermissionsProvider - Eliminates duplicate RPC calls across components - Single source of truth for permission state - Automatic HID re-initialization on permission changes - Split exports into separate files for React Fast Refresh compliance - Created types/permissions.ts for Permission enum - Created hooks/usePermissions.ts for the hook with safe defaults - Created contexts/PermissionsContext.ts for context definition - Updated PermissionsProvider.tsx to only export the provider component - Removed redundant getSessionSettings RPC call (settings already in WebSocket/WebRTC messages) - Added connectionModeChanged event handler for seamless emergency promotions - Fixed approval dialog race condition by checking isLoadingPermissions - Removed all redundant comments and code for leaner implementation - Updated imports across 10+ component files Result: Zero ESLint warnings, cleaner architecture, no duplicate RPC calls, all functionality preserved
This commit is contained in:
parent
f9e190f8b9
commit
335c6ee35e
File diff suppressed because it is too large
Load Diff
|
|
@ -21,7 +21,8 @@ import ExtensionPopover from "@/components/popovers/ExtensionPopover";
|
|||
import SessionPopover from "@/components/popovers/SessionPopover";
|
||||
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
||||
import { useSessionStore } from "@/stores/sessionStore";
|
||||
import { usePermissions, Permission } from "@/hooks/usePermissions";
|
||||
import { usePermissions } from "@/hooks/usePermissions";
|
||||
import { Permission } from "@/types/permissions";
|
||||
|
||||
export default function Actionbar({
|
||||
requestFullscreen,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ import Container from "@components/Container";
|
|||
import { useMacrosStore } from "@/hooks/stores";
|
||||
import useKeyboard from "@/hooks/useKeyboard";
|
||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { usePermissions, Permission } from "@/hooks/usePermissions";
|
||||
import { usePermissions } from "@/hooks/usePermissions";
|
||||
import { Permission } from "@/types/permissions";
|
||||
|
||||
export default function MacroBar() {
|
||||
const { macros, initialized, loadMacros, setSendFn } = useMacrosStore();
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ import clsx from "clsx";
|
|||
import { useSessionStore } from "@/stores/sessionStore";
|
||||
import { sessionApi } from "@/api/sessionApi";
|
||||
import { Button } from "@/components/Button";
|
||||
import { usePermissions, Permission } from "@/hooks/usePermissions";
|
||||
import { usePermissions } from "@/hooks/usePermissions";
|
||||
import { Permission } from "@/types/permissions";
|
||||
|
||||
type RpcSendFunction = (method: string, params: Record<string, unknown>, callback: (response: { result?: unknown; error?: { message: string } }) => void) => void;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ import { PencilIcon, CheckIcon, XMarkIcon } from "@heroicons/react/20/solid";
|
|||
import clsx from "clsx";
|
||||
|
||||
import { formatters } from "@/utils";
|
||||
import { usePermissions, Permission } from "@/hooks/usePermissions";
|
||||
import { usePermissions } from "@/hooks/usePermissions";
|
||||
import { Permission } from "@/types/permissions";
|
||||
|
||||
interface Session {
|
||||
id: string;
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ import {
|
|||
useSettingsStore,
|
||||
useVideoStore,
|
||||
} from "@/hooks/stores";
|
||||
import { usePermissions, Permission } from "@/hooks/usePermissions";
|
||||
import { usePermissions } from "@/hooks/usePermissions";
|
||||
import { Permission } from "@/types/permissions";
|
||||
import useMouse from "@/hooks/useMouse";
|
||||
|
||||
import {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
import { createContext } from "react";
|
||||
|
||||
import { PermissionsContextValue } from "@/hooks/usePermissions";
|
||||
|
||||
export const PermissionsContext = createContext<PermissionsContextValue | undefined>(undefined);
|
||||
|
|
@ -1,171 +1,34 @@
|
|||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
import { useContext } from "react";
|
||||
|
||||
import { useJsonRpc, JsonRpcRequest } from "@/hooks/useJsonRpc";
|
||||
import { useSessionStore } from "@/stores/sessionStore";
|
||||
import { useRTCStore } from "@/hooks/stores";
|
||||
import { PermissionsContext } from "@/contexts/PermissionsContext";
|
||||
import { Permission } from "@/types/permissions";
|
||||
|
||||
type RpcSendFunction = (method: string, params: Record<string, unknown>, callback: (response: { result?: unknown; error?: { message: string } }) => void) => void;
|
||||
|
||||
// Permission types matching backend
|
||||
export enum Permission {
|
||||
// Video/Display permissions
|
||||
VIDEO_VIEW = "video.view",
|
||||
|
||||
// Input permissions
|
||||
KEYBOARD_INPUT = "keyboard.input",
|
||||
MOUSE_INPUT = "mouse.input",
|
||||
PASTE = "clipboard.paste",
|
||||
|
||||
// Session management permissions
|
||||
SESSION_TRANSFER = "session.transfer",
|
||||
SESSION_APPROVE = "session.approve",
|
||||
SESSION_KICK = "session.kick",
|
||||
SESSION_REQUEST_PRIMARY = "session.request_primary",
|
||||
SESSION_RELEASE_PRIMARY = "session.release_primary",
|
||||
SESSION_MANAGE = "session.manage",
|
||||
|
||||
// Mount/Media permissions
|
||||
MOUNT_MEDIA = "mount.media",
|
||||
UNMOUNT_MEDIA = "mount.unmedia",
|
||||
MOUNT_LIST = "mount.list",
|
||||
|
||||
// Extension permissions
|
||||
EXTENSION_MANAGE = "extension.manage",
|
||||
EXTENSION_ATX = "extension.atx",
|
||||
EXTENSION_DC = "extension.dc",
|
||||
EXTENSION_SERIAL = "extension.serial",
|
||||
EXTENSION_WOL = "extension.wol",
|
||||
|
||||
// Settings permissions
|
||||
SETTINGS_READ = "settings.read",
|
||||
SETTINGS_WRITE = "settings.write",
|
||||
SETTINGS_ACCESS = "settings.access",
|
||||
|
||||
// System permissions
|
||||
SYSTEM_REBOOT = "system.reboot",
|
||||
SYSTEM_UPDATE = "system.update",
|
||||
SYSTEM_NETWORK = "system.network",
|
||||
|
||||
// Power/USB control permissions
|
||||
POWER_CONTROL = "power.control",
|
||||
USB_CONTROL = "usb.control",
|
||||
|
||||
// Terminal/Serial permissions
|
||||
TERMINAL_ACCESS = "terminal.access",
|
||||
SERIAL_ACCESS = "serial.access",
|
||||
}
|
||||
|
||||
interface PermissionsResponse {
|
||||
mode: string;
|
||||
export interface PermissionsContextValue {
|
||||
permissions: Record<string, boolean>;
|
||||
isLoading: boolean;
|
||||
hasPermission: (permission: Permission) => boolean;
|
||||
hasAnyPermission: (...perms: Permission[]) => boolean;
|
||||
hasAllPermissions: (...perms: Permission[]) => boolean;
|
||||
isPrimary: () => boolean;
|
||||
isObserver: () => boolean;
|
||||
isPending: () => boolean;
|
||||
}
|
||||
|
||||
export function usePermissions() {
|
||||
const { currentMode } = useSessionStore();
|
||||
const { setRpcHidProtocolVersion, rpcHidChannel, rpcDataChannel } = useRTCStore();
|
||||
const [permissions, setPermissions] = useState<Record<string, boolean>>({});
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const previousCanControl = useRef<boolean>(false);
|
||||
|
||||
// Function to poll permissions
|
||||
const pollPermissions = useCallback((send: RpcSendFunction) => {
|
||||
if (!send) return;
|
||||
|
||||
setIsLoading(true);
|
||||
send("getPermissions", {}, (response: { result?: unknown; error?: { message: string } }) => {
|
||||
if (!response.error && response.result) {
|
||||
const result = response.result as PermissionsResponse;
|
||||
setPermissions(result.permissions);
|
||||
}
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Handle connectionModeChanged events that require WebRTC reconnection
|
||||
const handleRpcRequest = useCallback((request: JsonRpcRequest) => {
|
||||
if (request.method === "connectionModeChanged") {
|
||||
console.info("Connection mode changed, WebRTC reconnection required", request.params);
|
||||
|
||||
// For session promotion that requires reconnection, refresh the page
|
||||
// This ensures WebRTC connection is re-established with proper mode
|
||||
const params = request.params as { action?: string; reason?: string };
|
||||
if (params.action === "reconnect_required" && params.reason === "session_promotion") {
|
||||
console.info("Session promoted, refreshing page to re-establish WebRTC connection");
|
||||
// Small delay to ensure all state updates are processed
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const { send } = useJsonRpc(handleRpcRequest);
|
||||
|
||||
useEffect(() => {
|
||||
if (rpcDataChannel?.readyState !== "open") return;
|
||||
pollPermissions(send);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentMode, rpcDataChannel?.readyState]);
|
||||
|
||||
// Monitor permission changes and re-initialize HID when gaining control
|
||||
useEffect(() => {
|
||||
const currentCanControl = hasPermission(Permission.KEYBOARD_INPUT) && hasPermission(Permission.MOUSE_INPUT);
|
||||
const hadControl = previousCanControl.current;
|
||||
|
||||
// If we just gained control permissions, re-initialize HID
|
||||
if (currentCanControl && !hadControl && rpcHidChannel?.readyState === "open") {
|
||||
console.info("Gained control permissions, re-initializing HID");
|
||||
|
||||
// Reset protocol version to force re-handshake
|
||||
setRpcHidProtocolVersion(null);
|
||||
|
||||
// Import handshake functionality dynamically
|
||||
import("./hidRpc").then(({ HID_RPC_VERSION, HandshakeMessage }) => {
|
||||
// Send handshake after a small delay
|
||||
setTimeout(() => {
|
||||
if (rpcHidChannel?.readyState === "open") {
|
||||
const handshakeMessage = new HandshakeMessage(HID_RPC_VERSION);
|
||||
try {
|
||||
const data = handshakeMessage.marshal();
|
||||
rpcHidChannel.send(data as unknown as ArrayBuffer);
|
||||
console.info("Sent HID handshake after permission change");
|
||||
} catch (e) {
|
||||
console.error("Failed to send HID handshake", e);
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
previousCanControl.current = currentCanControl;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [permissions, rpcHidChannel, setRpcHidProtocolVersion]); // hasPermission depends on permissions which is already in deps
|
||||
|
||||
const hasPermission = (permission: Permission): boolean => {
|
||||
return permissions[permission] === true;
|
||||
};
|
||||
|
||||
const hasAnyPermission = (...perms: Permission[]): boolean => {
|
||||
return perms.some(perm => hasPermission(perm));
|
||||
};
|
||||
|
||||
const hasAllPermissions = (...perms: Permission[]): boolean => {
|
||||
return perms.every(perm => hasPermission(perm));
|
||||
};
|
||||
|
||||
// Session mode helpers
|
||||
const isPrimary = () => currentMode === "primary";
|
||||
const isObserver = () => currentMode === "observer";
|
||||
const isPending = () => currentMode === "pending";
|
||||
export function usePermissions(): PermissionsContextValue {
|
||||
const context = useContext(PermissionsContext);
|
||||
|
||||
if (context === undefined) {
|
||||
return {
|
||||
permissions,
|
||||
isLoading,
|
||||
hasPermission,
|
||||
hasAnyPermission,
|
||||
hasAllPermissions,
|
||||
isPrimary,
|
||||
isObserver,
|
||||
isPending,
|
||||
permissions: {},
|
||||
isLoading: true,
|
||||
hasPermission: () => false,
|
||||
hasAnyPermission: () => false,
|
||||
hasAllPermissions: () => false,
|
||||
isPrimary: () => false,
|
||||
isObserver: () => false,
|
||||
isPending: () => false,
|
||||
};
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
|
@ -16,6 +16,10 @@ interface ModeChangedData {
|
|||
mode: string;
|
||||
}
|
||||
|
||||
interface ConnectionModeChangedData {
|
||||
newMode: string;
|
||||
}
|
||||
|
||||
export function useSessionEvents(sendFn: RpcSendFunction | null) {
|
||||
const {
|
||||
currentMode,
|
||||
|
|
@ -27,7 +31,6 @@ export function useSessionEvents(sendFn: RpcSendFunction | null) {
|
|||
const sendFnRef = useRef(sendFn);
|
||||
sendFnRef.current = sendFn;
|
||||
|
||||
// Handle session-related RPC events
|
||||
const handleSessionEvent = (method: string, params: unknown) => {
|
||||
switch (method) {
|
||||
case "sessionsUpdated":
|
||||
|
|
@ -36,6 +39,9 @@ export function useSessionEvents(sendFn: RpcSendFunction | null) {
|
|||
case "modeChanged":
|
||||
handleModeChanged(params as ModeChangedData);
|
||||
break;
|
||||
case "connectionModeChanged":
|
||||
handleConnectionModeChanged(params as ConnectionModeChangedData);
|
||||
break;
|
||||
case "hidReadyForPrimary":
|
||||
handleHidReadyForPrimary();
|
||||
break;
|
||||
|
|
@ -103,23 +109,25 @@ export function useSessionEvents(sendFn: RpcSendFunction | null) {
|
|||
}
|
||||
};
|
||||
|
||||
const handleConnectionModeChanged = (data: ConnectionModeChangedData) => {
|
||||
if (data.newMode) {
|
||||
handleModeChanged({ mode: data.newMode });
|
||||
}
|
||||
};
|
||||
|
||||
const handleHidReadyForPrimary = () => {
|
||||
// Backend signals that HID system is ready for primary session re-initialization
|
||||
const { rpcHidChannel } = useRTCStore.getState();
|
||||
if (rpcHidChannel?.readyState === "open") {
|
||||
// Trigger HID re-handshake
|
||||
rpcHidChannel.dispatchEvent(new Event("open"));
|
||||
}
|
||||
};
|
||||
|
||||
const handleOtherSessionConnected = () => {
|
||||
// Another session is trying to connect
|
||||
notify.warning("Another session is connecting", {
|
||||
duration: 5000
|
||||
});
|
||||
};
|
||||
|
||||
// Fetch initial sessions when component mounts
|
||||
useEffect(() => {
|
||||
if (!sendFnRef.current) return;
|
||||
|
||||
|
|
@ -136,7 +144,6 @@ export function useSessionEvents(sendFn: RpcSendFunction | null) {
|
|||
fetchSessions();
|
||||
}, [setSessions, setSessionError]);
|
||||
|
||||
// Set up periodic session refresh
|
||||
useEffect(() => {
|
||||
if (!sendFnRef.current) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import { useEffect, useCallback, useState } from "react";
|
|||
import { useSessionStore } from "@/stores/sessionStore";
|
||||
import { useSessionEvents } from "@/hooks/useSessionEvents";
|
||||
import { useSettingsStore } from "@/hooks/stores";
|
||||
import { usePermissions, Permission } from "@/hooks/usePermissions";
|
||||
import { usePermissions } from "@/hooks/usePermissions";
|
||||
import { Permission } from "@/types/permissions";
|
||||
|
||||
type RpcSendFunction = (method: string, params: Record<string, unknown>, callback: (response: { result?: unknown; error?: { message: string } }) => void) => void;
|
||||
|
||||
|
|
@ -32,21 +33,19 @@ export function useSessionManagement(sendFn: RpcSendFunction | null) {
|
|||
clearSession
|
||||
} = useSessionStore();
|
||||
|
||||
const { hasPermission } = usePermissions();
|
||||
const { hasPermission, isLoading: isLoadingPermissions } = usePermissions();
|
||||
|
||||
const { requireSessionApproval } = useSettingsStore();
|
||||
const { handleSessionEvent } = useSessionEvents(sendFn);
|
||||
const [primaryControlRequest, setPrimaryControlRequest] = useState<PrimaryControlRequest | null>(null);
|
||||
const [newSessionRequest, setNewSessionRequest] = useState<NewSessionRequest | null>(null);
|
||||
|
||||
// Handle session info from WebRTC answer
|
||||
const handleSessionResponse = useCallback((response: SessionResponse) => {
|
||||
if (response.sessionId && response.mode) {
|
||||
setCurrentSession(response.sessionId, response.mode as "primary" | "observer" | "queued" | "pending");
|
||||
}
|
||||
}, [setCurrentSession]);
|
||||
|
||||
// Handle approval of primary control request
|
||||
const handleApprovePrimaryRequest = useCallback(async (requestId: string) => {
|
||||
if (!sendFn) return;
|
||||
|
||||
|
|
@ -63,7 +62,6 @@ export function useSessionManagement(sendFn: RpcSendFunction | null) {
|
|||
});
|
||||
}, [sendFn]);
|
||||
|
||||
// Handle denial of primary control request
|
||||
const handleDenyPrimaryRequest = useCallback(async (requestId: string) => {
|
||||
if (!sendFn) return;
|
||||
|
||||
|
|
@ -80,7 +78,6 @@ export function useSessionManagement(sendFn: RpcSendFunction | null) {
|
|||
});
|
||||
}, [sendFn]);
|
||||
|
||||
// Handle approval of new session
|
||||
const handleApproveNewSession = useCallback(async (sessionId: string) => {
|
||||
if (!sendFn) return;
|
||||
|
||||
|
|
@ -97,7 +94,6 @@ export function useSessionManagement(sendFn: RpcSendFunction | null) {
|
|||
});
|
||||
}, [sendFn]);
|
||||
|
||||
// Handle denial of new session
|
||||
const handleDenyNewSession = useCallback(async (sessionId: string) => {
|
||||
if (!sendFn) return;
|
||||
|
||||
|
|
@ -114,34 +110,30 @@ export function useSessionManagement(sendFn: RpcSendFunction | null) {
|
|||
});
|
||||
}, [sendFn]);
|
||||
|
||||
// Handle RPC events
|
||||
const handleRpcEvent = useCallback((method: string, params: unknown) => {
|
||||
// Pass session events to the session event handler
|
||||
if (method === "sessionsUpdated" ||
|
||||
method === "modeChanged" ||
|
||||
method === "connectionModeChanged" ||
|
||||
method === "otherSessionConnected") {
|
||||
handleSessionEvent(method, params);
|
||||
}
|
||||
|
||||
// Handle new session approval request (only if approval is required and user has permission)
|
||||
if (method === "newSessionPending" && requireSessionApproval && hasPermission(Permission.SESSION_APPROVE)) {
|
||||
if (method === "newSessionPending" && requireSessionApproval) {
|
||||
if (isLoadingPermissions || hasPermission(Permission.SESSION_APPROVE)) {
|
||||
setNewSessionRequest(params as NewSessionRequest);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle primary control request
|
||||
if (method === "primaryControlRequested") {
|
||||
setPrimaryControlRequest(params as PrimaryControlRequest);
|
||||
}
|
||||
|
||||
// Handle approval/denial responses
|
||||
if (method === "primaryControlApproved") {
|
||||
// Clear requesting state in store
|
||||
const { setRequestingPrimary } = useSessionStore.getState();
|
||||
setRequestingPrimary(false);
|
||||
}
|
||||
|
||||
if (method === "primaryControlDenied") {
|
||||
// Clear requesting state and show error
|
||||
const { setRequestingPrimary, setSessionError } = useSessionStore.getState();
|
||||
setRequestingPrimary(false);
|
||||
setSessionError("Your primary control request was denied");
|
||||
|
|
@ -152,9 +144,14 @@ export function useSessionManagement(sendFn: RpcSendFunction | null) {
|
|||
const errorParams = params as { message?: string };
|
||||
setSessionError(errorParams.message || "Session access was denied by the primary session");
|
||||
}
|
||||
}, [handleSessionEvent, hasPermission, requireSessionApproval]);
|
||||
}, [handleSessionEvent, hasPermission, isLoadingPermissions, requireSessionApproval]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoadingPermissions && newSessionRequest && !hasPermission(Permission.SESSION_APPROVE)) {
|
||||
setNewSessionRequest(null);
|
||||
}
|
||||
}, [isLoadingPermissions, hasPermission, newSessionRequest]);
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearSession();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,106 @@
|
|||
import { useState, useEffect, useRef, useCallback, ReactNode } from "react";
|
||||
|
||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { useSessionStore } from "@/stores/sessionStore";
|
||||
import { useRTCStore } from "@/hooks/stores";
|
||||
import { Permission } from "@/types/permissions";
|
||||
import { PermissionsContextValue } from "@/hooks/usePermissions";
|
||||
import { PermissionsContext } from "@/contexts/PermissionsContext";
|
||||
|
||||
type RpcSendFunction = (method: string, params: Record<string, unknown>, callback: (response: { result?: unknown; error?: { message: string } }) => void) => void;
|
||||
|
||||
interface PermissionsResponse {
|
||||
mode: string;
|
||||
permissions: Record<string, boolean>;
|
||||
}
|
||||
|
||||
export function PermissionsProvider({ children }: { children: ReactNode }) {
|
||||
const { currentMode } = useSessionStore();
|
||||
const { setRpcHidProtocolVersion, rpcHidChannel, rpcDataChannel } = useRTCStore();
|
||||
const [permissions, setPermissions] = useState<Record<string, boolean>>({});
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const previousCanControl = useRef<boolean>(false);
|
||||
|
||||
const pollPermissions = useCallback((send: RpcSendFunction) => {
|
||||
if (!send) return;
|
||||
|
||||
setIsLoading(true);
|
||||
send("getPermissions", {}, (response: { result?: unknown; error?: { message: string } }) => {
|
||||
if (!response.error && response.result) {
|
||||
const result = response.result as PermissionsResponse;
|
||||
setPermissions(result.permissions);
|
||||
}
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const { send } = useJsonRpc();
|
||||
|
||||
useEffect(() => {
|
||||
if (rpcDataChannel?.readyState !== "open") return;
|
||||
pollPermissions(send);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentMode, rpcDataChannel?.readyState]);
|
||||
|
||||
const hasPermission = useCallback((permission: Permission): boolean => {
|
||||
return permissions[permission] === true;
|
||||
}, [permissions]);
|
||||
|
||||
const hasAnyPermission = useCallback((...perms: Permission[]): boolean => {
|
||||
return perms.some(perm => hasPermission(perm));
|
||||
}, [hasPermission]);
|
||||
|
||||
const hasAllPermissions = useCallback((...perms: Permission[]): boolean => {
|
||||
return perms.every(perm => hasPermission(perm));
|
||||
}, [hasPermission]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentCanControl = hasPermission(Permission.KEYBOARD_INPUT) && hasPermission(Permission.MOUSE_INPUT);
|
||||
const hadControl = previousCanControl.current;
|
||||
|
||||
if (currentCanControl && !hadControl && rpcHidChannel?.readyState === "open") {
|
||||
console.info("Gained control permissions, re-initializing HID");
|
||||
|
||||
setRpcHidProtocolVersion(null);
|
||||
|
||||
import("@/hooks/hidRpc").then(({ HID_RPC_VERSION, HandshakeMessage }) => {
|
||||
setTimeout(() => {
|
||||
if (rpcHidChannel?.readyState === "open") {
|
||||
const handshakeMessage = new HandshakeMessage(HID_RPC_VERSION);
|
||||
try {
|
||||
const data = handshakeMessage.marshal();
|
||||
rpcHidChannel.send(data as unknown as ArrayBuffer);
|
||||
console.info("Sent HID handshake after permission change");
|
||||
} catch (e) {
|
||||
console.error("Failed to send HID handshake", e);
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
previousCanControl.current = currentCanControl;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [permissions, rpcHidChannel, setRpcHidProtocolVersion]);
|
||||
|
||||
const isPrimary = useCallback(() => currentMode === "primary", [currentMode]);
|
||||
const isObserver = useCallback(() => currentMode === "observer", [currentMode]);
|
||||
const isPending = useCallback(() => currentMode === "pending", [currentMode]);
|
||||
|
||||
const value: PermissionsContextValue = {
|
||||
permissions,
|
||||
isLoading,
|
||||
hasPermission,
|
||||
hasAnyPermission,
|
||||
hasAllPermissions,
|
||||
isPrimary,
|
||||
isObserver,
|
||||
isPending,
|
||||
};
|
||||
|
||||
return (
|
||||
<PermissionsContext.Provider value={value}>
|
||||
{children}
|
||||
</PermissionsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
|
@ -6,7 +6,8 @@ import { BacklightSettings, useSettingsStore } from "@/hooks/stores";
|
|||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
||||
import { UsbDeviceSetting } from "@components/UsbDeviceSetting";
|
||||
import { usePermissions, Permission } from "@/hooks/usePermissions";
|
||||
import { usePermissions } from "@/hooks/usePermissions";
|
||||
import { Permission } from "@/types/permissions";
|
||||
|
||||
import notifications from "../notifications";
|
||||
import { UsbInfoSetting } from "../components/UsbInfoSetting";
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ import {
|
|||
} from "@heroicons/react/16/solid";
|
||||
|
||||
import { useJsonRpc, JsonRpcResponse } from "@/hooks/useJsonRpc";
|
||||
import { usePermissions, Permission } from "@/hooks/usePermissions";
|
||||
import { usePermissions } from "@/hooks/usePermissions";
|
||||
import { Permission } from "@/types/permissions";
|
||||
import { useSettingsStore } from "@/hooks/stores";
|
||||
import { notify } from "@/notifications";
|
||||
import Card from "@/components/Card";
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@ import { LinkButton } from "@components/Button";
|
|||
import { FeatureFlag } from "@components/FeatureFlag";
|
||||
import { useUiStore } from "@/hooks/stores";
|
||||
import { useSessionStore } from "@/stores/sessionStore";
|
||||
import { usePermissions, Permission } from "@/hooks/usePermissions";
|
||||
import { usePermissions } from "@/hooks/usePermissions";
|
||||
import { Permission } from "@/types/permissions";
|
||||
|
||||
/* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */
|
||||
export default function SettingsRoute() {
|
||||
|
|
@ -34,7 +35,7 @@ export default function SettingsRoute() {
|
|||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !permissions[Permission.SETTINGS_ACCESS] && currentMode !== null) {
|
||||
navigate("/devices/local", { replace: true });
|
||||
navigate("/", { replace: true });
|
||||
}
|
||||
}, [permissions, isLoading, currentMode, navigate]);
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ import useWebSocket from "react-use-websocket";
|
|||
import { CLOUD_API, DEVICE_API } from "@/ui.config";
|
||||
import api from "@/api";
|
||||
import { checkAuth, isInCloud, isOnDevice } from "@/main";
|
||||
import { usePermissions, Permission } from "@/hooks/usePermissions";
|
||||
import { cx } from "@/cva.config";
|
||||
import {
|
||||
KeyboardLedState,
|
||||
|
|
@ -54,6 +53,7 @@ import {
|
|||
} from "@/components/VideoOverlay";
|
||||
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
||||
import { FeatureFlagProvider } from "@/providers/FeatureFlagProvider";
|
||||
import { PermissionsProvider } from "@/providers/PermissionsProvider";
|
||||
import { DeviceStatus } from "@routes/welcome-local";
|
||||
import { useVersion } from "@/hooks/useVersion";
|
||||
import { useSessionManagement } from "@/hooks/useSessionManagement";
|
||||
|
|
@ -159,7 +159,6 @@ export default function KvmIdRoute() {
|
|||
const { nickname, setNickname } = useSharedSessionStore();
|
||||
const { setRequireSessionApproval, setRequireSessionNickname } = useSettingsStore();
|
||||
const [globalSessionSettings, setGlobalSessionSettings] = useState<{requireApproval: boolean, requireNickname: boolean} | null>(null);
|
||||
const { hasPermission } = usePermissions();
|
||||
|
||||
const [loadingMessage, setLoadingMessage] = useState("Connecting to device...");
|
||||
const cleanupAndStopReconnecting = useCallback(
|
||||
|
|
@ -549,44 +548,6 @@ export default function KvmIdRoute() {
|
|||
const rpcDataChannel = pc.createDataChannel("rpc");
|
||||
rpcDataChannel.onopen = () => {
|
||||
setRpcDataChannel(rpcDataChannel);
|
||||
|
||||
// Fetch global session settings
|
||||
const fetchSettings = () => {
|
||||
// Only fetch settings if user has permission to read settings
|
||||
if (!hasPermission(Permission.SETTINGS_READ)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const id = Math.random().toString(36).substring(2);
|
||||
const message = JSON.stringify({ jsonrpc: "2.0", method: "getSessionSettings", params: {}, id });
|
||||
|
||||
const handler = (event: MessageEvent) => {
|
||||
try {
|
||||
const response = JSON.parse(event.data);
|
||||
if (response.id === id) {
|
||||
rpcDataChannel.removeEventListener("message", handler);
|
||||
if (response.result) {
|
||||
setGlobalSessionSettings(response.result);
|
||||
// Also update the settings store for approval handling
|
||||
setRequireSessionApproval(response.result.requireApproval);
|
||||
setRequireSessionNickname(response.result.requireNickname);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
}
|
||||
};
|
||||
|
||||
rpcDataChannel.addEventListener("message", handler);
|
||||
rpcDataChannel.send(message);
|
||||
|
||||
// Clean up after timeout
|
||||
setTimeout(() => {
|
||||
rpcDataChannel.removeEventListener("message", handler);
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
fetchSettings();
|
||||
};
|
||||
|
||||
const rpcHidChannel = pc.createDataChannel("hidrpc");
|
||||
|
|
@ -627,9 +588,6 @@ export default function KvmIdRoute() {
|
|||
setRpcHidUnreliableNonOrderedChannel,
|
||||
setRpcHidUnreliableChannel,
|
||||
setTransceiver,
|
||||
hasPermission,
|
||||
setRequireSessionApproval,
|
||||
setRequireSessionNickname,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -722,6 +680,7 @@ export default function KvmIdRoute() {
|
|||
// Handle session-related events
|
||||
if (resp.method === "sessionsUpdated" ||
|
||||
resp.method === "modeChanged" ||
|
||||
resp.method === "connectionModeChanged" ||
|
||||
resp.method === "otherSessionConnected" ||
|
||||
resp.method === "primaryControlRequested" ||
|
||||
resp.method === "primaryControlApproved" ||
|
||||
|
|
@ -735,7 +694,6 @@ export default function KvmIdRoute() {
|
|||
setAccessDenied(true);
|
||||
}
|
||||
|
||||
// Keep legacy behavior for otherSessionConnected
|
||||
if (resp.method === "otherSessionConnected") {
|
||||
navigateTo("/other-session");
|
||||
}
|
||||
|
|
@ -807,13 +765,11 @@ export default function KvmIdRoute() {
|
|||
|
||||
useEffect(() => {
|
||||
if (rpcDataChannel?.readyState !== "open") return;
|
||||
if (!hasPermission(Permission.VIDEO_VIEW)) return;
|
||||
send("getVideoState", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) return;
|
||||
const hdmiState = resp.result as Parameters<VideoState["setHdmiState"]>[0];
|
||||
setHdmiState(hdmiState);
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [rpcDataChannel?.readyState, send, setHdmiState]);
|
||||
|
||||
const [needLedState, setNeedLedState] = useState(true);
|
||||
|
|
@ -822,7 +778,6 @@ export default function KvmIdRoute() {
|
|||
useEffect(() => {
|
||||
if (rpcDataChannel?.readyState !== "open") return;
|
||||
if (!needLedState) return;
|
||||
if (!hasPermission(Permission.VIDEO_VIEW)) return;
|
||||
|
||||
send("getKeyboardLedState", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
|
|
@ -834,7 +789,6 @@ export default function KvmIdRoute() {
|
|||
}
|
||||
setNeedLedState(false);
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [rpcDataChannel?.readyState, send, setKeyboardLedState, keyboardLedState, needLedState]);
|
||||
|
||||
const [needKeyDownState, setNeedKeyDownState] = useState(true);
|
||||
|
|
@ -843,7 +797,6 @@ export default function KvmIdRoute() {
|
|||
useEffect(() => {
|
||||
if (rpcDataChannel?.readyState !== "open") return;
|
||||
if (!needKeyDownState) return;
|
||||
if (!hasPermission(Permission.VIDEO_VIEW)) return;
|
||||
|
||||
send("getKeyDownState", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
|
|
@ -861,7 +814,6 @@ export default function KvmIdRoute() {
|
|||
}
|
||||
setNeedKeyDownState(false);
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [keysDownState, needKeyDownState, rpcDataChannel?.readyState, send, setKeysDownState, setHidRpcDisabled]);
|
||||
|
||||
// When the update is successful, we need to refresh the client javascript and show a success modal
|
||||
|
|
@ -895,7 +847,6 @@ export default function KvmIdRoute() {
|
|||
|
||||
useEffect(() => {
|
||||
if (appVersion) return;
|
||||
if (!hasPermission(Permission.VIDEO_VIEW)) return;
|
||||
|
||||
getLocalVersion();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
|
@ -939,6 +890,7 @@ export default function KvmIdRoute() {
|
|||
]);
|
||||
|
||||
return (
|
||||
<PermissionsProvider>
|
||||
<FeatureFlagProvider appVersion={appVersion}>
|
||||
{!outlet && otaState.updating && (
|
||||
<AnimatePresence>
|
||||
|
|
@ -1114,6 +1066,7 @@ export default function KvmIdRoute() {
|
|||
show={currentMode === "pending"}
|
||||
/>
|
||||
</FeatureFlagProvider>
|
||||
</PermissionsProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
export enum Permission {
|
||||
VIDEO_VIEW = "video.view",
|
||||
KEYBOARD_INPUT = "keyboard.input",
|
||||
MOUSE_INPUT = "mouse.input",
|
||||
PASTE = "clipboard.paste",
|
||||
SESSION_TRANSFER = "session.transfer",
|
||||
SESSION_APPROVE = "session.approve",
|
||||
SESSION_KICK = "session.kick",
|
||||
SESSION_REQUEST_PRIMARY = "session.request_primary",
|
||||
SESSION_RELEASE_PRIMARY = "session.release_primary",
|
||||
SESSION_MANAGE = "session.manage",
|
||||
MOUNT_MEDIA = "mount.media",
|
||||
UNMOUNT_MEDIA = "mount.unmedia",
|
||||
MOUNT_LIST = "mount.list",
|
||||
EXTENSION_MANAGE = "extension.manage",
|
||||
EXTENSION_ATX = "extension.atx",
|
||||
EXTENSION_DC = "extension.dc",
|
||||
EXTENSION_SERIAL = "extension.serial",
|
||||
EXTENSION_WOL = "extension.wol",
|
||||
SETTINGS_READ = "settings.read",
|
||||
SETTINGS_WRITE = "settings.write",
|
||||
SETTINGS_ACCESS = "settings.access",
|
||||
SYSTEM_REBOOT = "system.reboot",
|
||||
SYSTEM_UPDATE = "system.update",
|
||||
SYSTEM_NETWORK = "system.network",
|
||||
POWER_CONTROL = "power.control",
|
||||
USB_CONTROL = "usb.control",
|
||||
TERMINAL_ACCESS = "terminal.access",
|
||||
SERIAL_ACCESS = "serial.access",
|
||||
}
|
||||
Loading…
Reference in New Issue