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 SessionPopover from "@/components/popovers/SessionPopover";
|
||||||
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
||||||
import { useSessionStore } from "@/stores/sessionStore";
|
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({
|
export default function Actionbar({
|
||||||
requestFullscreen,
|
requestFullscreen,
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,8 @@ import Container from "@components/Container";
|
||||||
import { useMacrosStore } from "@/hooks/stores";
|
import { useMacrosStore } from "@/hooks/stores";
|
||||||
import useKeyboard from "@/hooks/useKeyboard";
|
import useKeyboard from "@/hooks/useKeyboard";
|
||||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
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() {
|
export default function MacroBar() {
|
||||||
const { macros, initialized, loadMacros, setSendFn } = useMacrosStore();
|
const { macros, initialized, loadMacros, setSendFn } = useMacrosStore();
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@ import clsx from "clsx";
|
||||||
import { useSessionStore } from "@/stores/sessionStore";
|
import { useSessionStore } from "@/stores/sessionStore";
|
||||||
import { sessionApi } from "@/api/sessionApi";
|
import { sessionApi } from "@/api/sessionApi";
|
||||||
import { Button } from "@/components/Button";
|
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;
|
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 clsx from "clsx";
|
||||||
|
|
||||||
import { formatters } from "@/utils";
|
import { formatters } from "@/utils";
|
||||||
import { usePermissions, Permission } from "@/hooks/usePermissions";
|
import { usePermissions } from "@/hooks/usePermissions";
|
||||||
|
import { Permission } from "@/types/permissions";
|
||||||
|
|
||||||
interface Session {
|
interface Session {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,8 @@ import {
|
||||||
useSettingsStore,
|
useSettingsStore,
|
||||||
useVideoStore,
|
useVideoStore,
|
||||||
} from "@/hooks/stores";
|
} 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 useMouse from "@/hooks/useMouse";
|
||||||
|
|
||||||
import {
|
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 { PermissionsContext } from "@/contexts/PermissionsContext";
|
||||||
import { useSessionStore } from "@/stores/sessionStore";
|
import { Permission } from "@/types/permissions";
|
||||||
import { useRTCStore } from "@/hooks/stores";
|
|
||||||
|
|
||||||
type RpcSendFunction = (method: string, params: Record<string, unknown>, callback: (response: { result?: unknown; error?: { message: string } }) => void) => void;
|
export interface PermissionsContextValue {
|
||||||
|
|
||||||
// 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;
|
|
||||||
permissions: Record<string, boolean>;
|
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() {
|
export function usePermissions(): PermissionsContextValue {
|
||||||
const { currentMode } = useSessionStore();
|
const context = useContext(PermissionsContext);
|
||||||
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";
|
|
||||||
|
|
||||||
|
if (context === undefined) {
|
||||||
return {
|
return {
|
||||||
permissions,
|
permissions: {},
|
||||||
isLoading,
|
isLoading: true,
|
||||||
hasPermission,
|
hasPermission: () => false,
|
||||||
hasAnyPermission,
|
hasAnyPermission: () => false,
|
||||||
hasAllPermissions,
|
hasAllPermissions: () => false,
|
||||||
isPrimary,
|
isPrimary: () => false,
|
||||||
isObserver,
|
isObserver: () => false,
|
||||||
isPending,
|
isPending: () => false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,10 @@ interface ModeChangedData {
|
||||||
mode: string;
|
mode: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ConnectionModeChangedData {
|
||||||
|
newMode: string;
|
||||||
|
}
|
||||||
|
|
||||||
export function useSessionEvents(sendFn: RpcSendFunction | null) {
|
export function useSessionEvents(sendFn: RpcSendFunction | null) {
|
||||||
const {
|
const {
|
||||||
currentMode,
|
currentMode,
|
||||||
|
|
@ -27,7 +31,6 @@ export function useSessionEvents(sendFn: RpcSendFunction | null) {
|
||||||
const sendFnRef = useRef(sendFn);
|
const sendFnRef = useRef(sendFn);
|
||||||
sendFnRef.current = sendFn;
|
sendFnRef.current = sendFn;
|
||||||
|
|
||||||
// Handle session-related RPC events
|
|
||||||
const handleSessionEvent = (method: string, params: unknown) => {
|
const handleSessionEvent = (method: string, params: unknown) => {
|
||||||
switch (method) {
|
switch (method) {
|
||||||
case "sessionsUpdated":
|
case "sessionsUpdated":
|
||||||
|
|
@ -36,6 +39,9 @@ export function useSessionEvents(sendFn: RpcSendFunction | null) {
|
||||||
case "modeChanged":
|
case "modeChanged":
|
||||||
handleModeChanged(params as ModeChangedData);
|
handleModeChanged(params as ModeChangedData);
|
||||||
break;
|
break;
|
||||||
|
case "connectionModeChanged":
|
||||||
|
handleConnectionModeChanged(params as ConnectionModeChangedData);
|
||||||
|
break;
|
||||||
case "hidReadyForPrimary":
|
case "hidReadyForPrimary":
|
||||||
handleHidReadyForPrimary();
|
handleHidReadyForPrimary();
|
||||||
break;
|
break;
|
||||||
|
|
@ -103,23 +109,25 @@ export function useSessionEvents(sendFn: RpcSendFunction | null) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleConnectionModeChanged = (data: ConnectionModeChangedData) => {
|
||||||
|
if (data.newMode) {
|
||||||
|
handleModeChanged({ mode: data.newMode });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleHidReadyForPrimary = () => {
|
const handleHidReadyForPrimary = () => {
|
||||||
// Backend signals that HID system is ready for primary session re-initialization
|
|
||||||
const { rpcHidChannel } = useRTCStore.getState();
|
const { rpcHidChannel } = useRTCStore.getState();
|
||||||
if (rpcHidChannel?.readyState === "open") {
|
if (rpcHidChannel?.readyState === "open") {
|
||||||
// Trigger HID re-handshake
|
|
||||||
rpcHidChannel.dispatchEvent(new Event("open"));
|
rpcHidChannel.dispatchEvent(new Event("open"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOtherSessionConnected = () => {
|
const handleOtherSessionConnected = () => {
|
||||||
// Another session is trying to connect
|
|
||||||
notify.warning("Another session is connecting", {
|
notify.warning("Another session is connecting", {
|
||||||
duration: 5000
|
duration: 5000
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fetch initial sessions when component mounts
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!sendFnRef.current) return;
|
if (!sendFnRef.current) return;
|
||||||
|
|
||||||
|
|
@ -136,7 +144,6 @@ export function useSessionEvents(sendFn: RpcSendFunction | null) {
|
||||||
fetchSessions();
|
fetchSessions();
|
||||||
}, [setSessions, setSessionError]);
|
}, [setSessions, setSessionError]);
|
||||||
|
|
||||||
// Set up periodic session refresh
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!sendFnRef.current) return;
|
if (!sendFnRef.current) return;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@ import { useEffect, useCallback, useState } from "react";
|
||||||
import { useSessionStore } from "@/stores/sessionStore";
|
import { useSessionStore } from "@/stores/sessionStore";
|
||||||
import { useSessionEvents } from "@/hooks/useSessionEvents";
|
import { useSessionEvents } from "@/hooks/useSessionEvents";
|
||||||
import { useSettingsStore } from "@/hooks/stores";
|
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;
|
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
|
clearSession
|
||||||
} = useSessionStore();
|
} = useSessionStore();
|
||||||
|
|
||||||
const { hasPermission } = usePermissions();
|
const { hasPermission, isLoading: isLoadingPermissions } = usePermissions();
|
||||||
|
|
||||||
const { requireSessionApproval } = useSettingsStore();
|
const { requireSessionApproval } = useSettingsStore();
|
||||||
const { handleSessionEvent } = useSessionEvents(sendFn);
|
const { handleSessionEvent } = useSessionEvents(sendFn);
|
||||||
const [primaryControlRequest, setPrimaryControlRequest] = useState<PrimaryControlRequest | null>(null);
|
const [primaryControlRequest, setPrimaryControlRequest] = useState<PrimaryControlRequest | null>(null);
|
||||||
const [newSessionRequest, setNewSessionRequest] = useState<NewSessionRequest | null>(null);
|
const [newSessionRequest, setNewSessionRequest] = useState<NewSessionRequest | null>(null);
|
||||||
|
|
||||||
// Handle session info from WebRTC answer
|
|
||||||
const handleSessionResponse = useCallback((response: SessionResponse) => {
|
const handleSessionResponse = useCallback((response: SessionResponse) => {
|
||||||
if (response.sessionId && response.mode) {
|
if (response.sessionId && response.mode) {
|
||||||
setCurrentSession(response.sessionId, response.mode as "primary" | "observer" | "queued" | "pending");
|
setCurrentSession(response.sessionId, response.mode as "primary" | "observer" | "queued" | "pending");
|
||||||
}
|
}
|
||||||
}, [setCurrentSession]);
|
}, [setCurrentSession]);
|
||||||
|
|
||||||
// Handle approval of primary control request
|
|
||||||
const handleApprovePrimaryRequest = useCallback(async (requestId: string) => {
|
const handleApprovePrimaryRequest = useCallback(async (requestId: string) => {
|
||||||
if (!sendFn) return;
|
if (!sendFn) return;
|
||||||
|
|
||||||
|
|
@ -63,7 +62,6 @@ export function useSessionManagement(sendFn: RpcSendFunction | null) {
|
||||||
});
|
});
|
||||||
}, [sendFn]);
|
}, [sendFn]);
|
||||||
|
|
||||||
// Handle denial of primary control request
|
|
||||||
const handleDenyPrimaryRequest = useCallback(async (requestId: string) => {
|
const handleDenyPrimaryRequest = useCallback(async (requestId: string) => {
|
||||||
if (!sendFn) return;
|
if (!sendFn) return;
|
||||||
|
|
||||||
|
|
@ -80,7 +78,6 @@ export function useSessionManagement(sendFn: RpcSendFunction | null) {
|
||||||
});
|
});
|
||||||
}, [sendFn]);
|
}, [sendFn]);
|
||||||
|
|
||||||
// Handle approval of new session
|
|
||||||
const handleApproveNewSession = useCallback(async (sessionId: string) => {
|
const handleApproveNewSession = useCallback(async (sessionId: string) => {
|
||||||
if (!sendFn) return;
|
if (!sendFn) return;
|
||||||
|
|
||||||
|
|
@ -97,7 +94,6 @@ export function useSessionManagement(sendFn: RpcSendFunction | null) {
|
||||||
});
|
});
|
||||||
}, [sendFn]);
|
}, [sendFn]);
|
||||||
|
|
||||||
// Handle denial of new session
|
|
||||||
const handleDenyNewSession = useCallback(async (sessionId: string) => {
|
const handleDenyNewSession = useCallback(async (sessionId: string) => {
|
||||||
if (!sendFn) return;
|
if (!sendFn) return;
|
||||||
|
|
||||||
|
|
@ -114,34 +110,30 @@ export function useSessionManagement(sendFn: RpcSendFunction | null) {
|
||||||
});
|
});
|
||||||
}, [sendFn]);
|
}, [sendFn]);
|
||||||
|
|
||||||
// Handle RPC events
|
|
||||||
const handleRpcEvent = useCallback((method: string, params: unknown) => {
|
const handleRpcEvent = useCallback((method: string, params: unknown) => {
|
||||||
// Pass session events to the session event handler
|
|
||||||
if (method === "sessionsUpdated" ||
|
if (method === "sessionsUpdated" ||
|
||||||
method === "modeChanged" ||
|
method === "modeChanged" ||
|
||||||
|
method === "connectionModeChanged" ||
|
||||||
method === "otherSessionConnected") {
|
method === "otherSessionConnected") {
|
||||||
handleSessionEvent(method, params);
|
handleSessionEvent(method, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle new session approval request (only if approval is required and user has permission)
|
if (method === "newSessionPending" && requireSessionApproval) {
|
||||||
if (method === "newSessionPending" && requireSessionApproval && hasPermission(Permission.SESSION_APPROVE)) {
|
if (isLoadingPermissions || hasPermission(Permission.SESSION_APPROVE)) {
|
||||||
setNewSessionRequest(params as NewSessionRequest);
|
setNewSessionRequest(params as NewSessionRequest);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle primary control request
|
|
||||||
if (method === "primaryControlRequested") {
|
if (method === "primaryControlRequested") {
|
||||||
setPrimaryControlRequest(params as PrimaryControlRequest);
|
setPrimaryControlRequest(params as PrimaryControlRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle approval/denial responses
|
|
||||||
if (method === "primaryControlApproved") {
|
if (method === "primaryControlApproved") {
|
||||||
// Clear requesting state in store
|
|
||||||
const { setRequestingPrimary } = useSessionStore.getState();
|
const { setRequestingPrimary } = useSessionStore.getState();
|
||||||
setRequestingPrimary(false);
|
setRequestingPrimary(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (method === "primaryControlDenied") {
|
if (method === "primaryControlDenied") {
|
||||||
// Clear requesting state and show error
|
|
||||||
const { setRequestingPrimary, setSessionError } = useSessionStore.getState();
|
const { setRequestingPrimary, setSessionError } = useSessionStore.getState();
|
||||||
setRequestingPrimary(false);
|
setRequestingPrimary(false);
|
||||||
setSessionError("Your primary control request was denied");
|
setSessionError("Your primary control request was denied");
|
||||||
|
|
@ -152,9 +144,14 @@ export function useSessionManagement(sendFn: RpcSendFunction | null) {
|
||||||
const errorParams = params as { message?: string };
|
const errorParams = params as { message?: string };
|
||||||
setSessionError(errorParams.message || "Session access was denied by the primary session");
|
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(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
clearSession();
|
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 { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
||||||
import { UsbDeviceSetting } from "@components/UsbDeviceSetting";
|
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 notifications from "../notifications";
|
||||||
import { UsbInfoSetting } from "../components/UsbInfoSetting";
|
import { UsbInfoSetting } from "../components/UsbInfoSetting";
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@ import {
|
||||||
} from "@heroicons/react/16/solid";
|
} from "@heroicons/react/16/solid";
|
||||||
|
|
||||||
import { useJsonRpc, JsonRpcResponse } from "@/hooks/useJsonRpc";
|
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 { useSettingsStore } from "@/hooks/stores";
|
||||||
import { notify } from "@/notifications";
|
import { notify } from "@/notifications";
|
||||||
import Card from "@/components/Card";
|
import Card from "@/components/Card";
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,8 @@ import { LinkButton } from "@components/Button";
|
||||||
import { FeatureFlag } from "@components/FeatureFlag";
|
import { FeatureFlag } from "@components/FeatureFlag";
|
||||||
import { useUiStore } from "@/hooks/stores";
|
import { useUiStore } from "@/hooks/stores";
|
||||||
import { useSessionStore } from "@/stores/sessionStore";
|
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. */
|
/* 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() {
|
export default function SettingsRoute() {
|
||||||
|
|
@ -34,7 +35,7 @@ export default function SettingsRoute() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLoading && !permissions[Permission.SETTINGS_ACCESS] && currentMode !== null) {
|
if (!isLoading && !permissions[Permission.SETTINGS_ACCESS] && currentMode !== null) {
|
||||||
navigate("/devices/local", { replace: true });
|
navigate("/", { replace: true });
|
||||||
}
|
}
|
||||||
}, [permissions, isLoading, currentMode, navigate]);
|
}, [permissions, isLoading, currentMode, navigate]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ import useWebSocket from "react-use-websocket";
|
||||||
import { CLOUD_API, DEVICE_API } from "@/ui.config";
|
import { CLOUD_API, DEVICE_API } from "@/ui.config";
|
||||||
import api from "@/api";
|
import api from "@/api";
|
||||||
import { checkAuth, isInCloud, isOnDevice } from "@/main";
|
import { checkAuth, isInCloud, isOnDevice } from "@/main";
|
||||||
import { usePermissions, Permission } from "@/hooks/usePermissions";
|
|
||||||
import { cx } from "@/cva.config";
|
import { cx } from "@/cva.config";
|
||||||
import {
|
import {
|
||||||
KeyboardLedState,
|
KeyboardLedState,
|
||||||
|
|
@ -54,6 +53,7 @@ import {
|
||||||
} from "@/components/VideoOverlay";
|
} from "@/components/VideoOverlay";
|
||||||
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
||||||
import { FeatureFlagProvider } from "@/providers/FeatureFlagProvider";
|
import { FeatureFlagProvider } from "@/providers/FeatureFlagProvider";
|
||||||
|
import { PermissionsProvider } from "@/providers/PermissionsProvider";
|
||||||
import { DeviceStatus } from "@routes/welcome-local";
|
import { DeviceStatus } from "@routes/welcome-local";
|
||||||
import { useVersion } from "@/hooks/useVersion";
|
import { useVersion } from "@/hooks/useVersion";
|
||||||
import { useSessionManagement } from "@/hooks/useSessionManagement";
|
import { useSessionManagement } from "@/hooks/useSessionManagement";
|
||||||
|
|
@ -159,7 +159,6 @@ export default function KvmIdRoute() {
|
||||||
const { nickname, setNickname } = useSharedSessionStore();
|
const { nickname, setNickname } = useSharedSessionStore();
|
||||||
const { setRequireSessionApproval, setRequireSessionNickname } = useSettingsStore();
|
const { setRequireSessionApproval, setRequireSessionNickname } = useSettingsStore();
|
||||||
const [globalSessionSettings, setGlobalSessionSettings] = useState<{requireApproval: boolean, requireNickname: boolean} | null>(null);
|
const [globalSessionSettings, setGlobalSessionSettings] = useState<{requireApproval: boolean, requireNickname: boolean} | null>(null);
|
||||||
const { hasPermission } = usePermissions();
|
|
||||||
|
|
||||||
const [loadingMessage, setLoadingMessage] = useState("Connecting to device...");
|
const [loadingMessage, setLoadingMessage] = useState("Connecting to device...");
|
||||||
const cleanupAndStopReconnecting = useCallback(
|
const cleanupAndStopReconnecting = useCallback(
|
||||||
|
|
@ -549,44 +548,6 @@ export default function KvmIdRoute() {
|
||||||
const rpcDataChannel = pc.createDataChannel("rpc");
|
const rpcDataChannel = pc.createDataChannel("rpc");
|
||||||
rpcDataChannel.onopen = () => {
|
rpcDataChannel.onopen = () => {
|
||||||
setRpcDataChannel(rpcDataChannel);
|
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");
|
const rpcHidChannel = pc.createDataChannel("hidrpc");
|
||||||
|
|
@ -627,9 +588,6 @@ export default function KvmIdRoute() {
|
||||||
setRpcHidUnreliableNonOrderedChannel,
|
setRpcHidUnreliableNonOrderedChannel,
|
||||||
setRpcHidUnreliableChannel,
|
setRpcHidUnreliableChannel,
|
||||||
setTransceiver,
|
setTransceiver,
|
||||||
hasPermission,
|
|
||||||
setRequireSessionApproval,
|
|
||||||
setRequireSessionNickname,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -722,6 +680,7 @@ export default function KvmIdRoute() {
|
||||||
// Handle session-related events
|
// Handle session-related events
|
||||||
if (resp.method === "sessionsUpdated" ||
|
if (resp.method === "sessionsUpdated" ||
|
||||||
resp.method === "modeChanged" ||
|
resp.method === "modeChanged" ||
|
||||||
|
resp.method === "connectionModeChanged" ||
|
||||||
resp.method === "otherSessionConnected" ||
|
resp.method === "otherSessionConnected" ||
|
||||||
resp.method === "primaryControlRequested" ||
|
resp.method === "primaryControlRequested" ||
|
||||||
resp.method === "primaryControlApproved" ||
|
resp.method === "primaryControlApproved" ||
|
||||||
|
|
@ -735,7 +694,6 @@ export default function KvmIdRoute() {
|
||||||
setAccessDenied(true);
|
setAccessDenied(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep legacy behavior for otherSessionConnected
|
|
||||||
if (resp.method === "otherSessionConnected") {
|
if (resp.method === "otherSessionConnected") {
|
||||||
navigateTo("/other-session");
|
navigateTo("/other-session");
|
||||||
}
|
}
|
||||||
|
|
@ -807,13 +765,11 @@ export default function KvmIdRoute() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (rpcDataChannel?.readyState !== "open") return;
|
if (rpcDataChannel?.readyState !== "open") return;
|
||||||
if (!hasPermission(Permission.VIDEO_VIEW)) return;
|
|
||||||
send("getVideoState", {}, (resp: JsonRpcResponse) => {
|
send("getVideoState", {}, (resp: JsonRpcResponse) => {
|
||||||
if ("error" in resp) return;
|
if ("error" in resp) return;
|
||||||
const hdmiState = resp.result as Parameters<VideoState["setHdmiState"]>[0];
|
const hdmiState = resp.result as Parameters<VideoState["setHdmiState"]>[0];
|
||||||
setHdmiState(hdmiState);
|
setHdmiState(hdmiState);
|
||||||
});
|
});
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [rpcDataChannel?.readyState, send, setHdmiState]);
|
}, [rpcDataChannel?.readyState, send, setHdmiState]);
|
||||||
|
|
||||||
const [needLedState, setNeedLedState] = useState(true);
|
const [needLedState, setNeedLedState] = useState(true);
|
||||||
|
|
@ -822,7 +778,6 @@ export default function KvmIdRoute() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (rpcDataChannel?.readyState !== "open") return;
|
if (rpcDataChannel?.readyState !== "open") return;
|
||||||
if (!needLedState) return;
|
if (!needLedState) return;
|
||||||
if (!hasPermission(Permission.VIDEO_VIEW)) return;
|
|
||||||
|
|
||||||
send("getKeyboardLedState", {}, (resp: JsonRpcResponse) => {
|
send("getKeyboardLedState", {}, (resp: JsonRpcResponse) => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
|
|
@ -834,7 +789,6 @@ export default function KvmIdRoute() {
|
||||||
}
|
}
|
||||||
setNeedLedState(false);
|
setNeedLedState(false);
|
||||||
});
|
});
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [rpcDataChannel?.readyState, send, setKeyboardLedState, keyboardLedState, needLedState]);
|
}, [rpcDataChannel?.readyState, send, setKeyboardLedState, keyboardLedState, needLedState]);
|
||||||
|
|
||||||
const [needKeyDownState, setNeedKeyDownState] = useState(true);
|
const [needKeyDownState, setNeedKeyDownState] = useState(true);
|
||||||
|
|
@ -843,7 +797,6 @@ export default function KvmIdRoute() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (rpcDataChannel?.readyState !== "open") return;
|
if (rpcDataChannel?.readyState !== "open") return;
|
||||||
if (!needKeyDownState) return;
|
if (!needKeyDownState) return;
|
||||||
if (!hasPermission(Permission.VIDEO_VIEW)) return;
|
|
||||||
|
|
||||||
send("getKeyDownState", {}, (resp: JsonRpcResponse) => {
|
send("getKeyDownState", {}, (resp: JsonRpcResponse) => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
|
|
@ -861,7 +814,6 @@ export default function KvmIdRoute() {
|
||||||
}
|
}
|
||||||
setNeedKeyDownState(false);
|
setNeedKeyDownState(false);
|
||||||
});
|
});
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [keysDownState, needKeyDownState, rpcDataChannel?.readyState, send, setKeysDownState, setHidRpcDisabled]);
|
}, [keysDownState, needKeyDownState, rpcDataChannel?.readyState, send, setKeysDownState, setHidRpcDisabled]);
|
||||||
|
|
||||||
// When the update is successful, we need to refresh the client javascript and show a success modal
|
// 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(() => {
|
useEffect(() => {
|
||||||
if (appVersion) return;
|
if (appVersion) return;
|
||||||
if (!hasPermission(Permission.VIDEO_VIEW)) return;
|
|
||||||
|
|
||||||
getLocalVersion();
|
getLocalVersion();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
|
@ -939,6 +890,7 @@ export default function KvmIdRoute() {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<PermissionsProvider>
|
||||||
<FeatureFlagProvider appVersion={appVersion}>
|
<FeatureFlagProvider appVersion={appVersion}>
|
||||||
{!outlet && otaState.updating && (
|
{!outlet && otaState.updating && (
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
|
|
@ -1114,6 +1066,7 @@ export default function KvmIdRoute() {
|
||||||
show={currentMode === "pending"}
|
show={currentMode === "pending"}
|
||||||
/>
|
/>
|
||||||
</FeatureFlagProvider>
|
</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