mirror of https://github.com/jetkvm/kvm.git
Compare commits
1 Commits
ec0d5d7cb7
...
50e1cf06b8
| Author | SHA1 | Date |
|---|---|---|
|
|
50e1cf06b8 |
|
|
@ -96,11 +96,11 @@ func handleRequestSessionApprovalRPC(session *Session) (any, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateNickname(nickname string) error {
|
func validateNickname(nickname string) error {
|
||||||
if len(nickname) < minNicknameLength {
|
if len(nickname) < 2 {
|
||||||
return fmt.Errorf("nickname must be at least %d characters", minNicknameLength)
|
return errors.New("nickname must be at least 2 characters")
|
||||||
}
|
}
|
||||||
if len(nickname) > maxNicknameLength {
|
if len(nickname) > 30 {
|
||||||
return fmt.Errorf("nickname must be %d characters or less", maxNicknameLength)
|
return errors.New("nickname must be 30 characters or less")
|
||||||
}
|
}
|
||||||
if !isValidNickname(nickname) {
|
if !isValidNickname(nickname) {
|
||||||
return errors.New("nickname can only contain letters, numbers, spaces, and - _ . @")
|
return errors.New("nickname can only contain letters, numbers, spaces, and - _ . @")
|
||||||
|
|
|
||||||
|
|
@ -25,9 +25,8 @@ const (
|
||||||
// Timing constants for session management
|
// Timing constants for session management
|
||||||
const (
|
const (
|
||||||
// Broadcast throttling (DoS protection)
|
// Broadcast throttling (DoS protection)
|
||||||
globalBroadcastDelay = 100 * time.Millisecond // Minimum time between global session broadcasts
|
globalBroadcastDelay = 100 * time.Millisecond // Minimum time between global session broadcasts
|
||||||
sessionBroadcastDelay = 50 * time.Millisecond // Minimum time between broadcasts to a single session
|
sessionBroadcastDelay = 50 * time.Millisecond // Minimum time between broadcasts to a single session
|
||||||
broadcastQueueCapacity = 100 // Maximum pending broadcasts before drops occur
|
|
||||||
|
|
||||||
// Session timeout defaults
|
// Session timeout defaults
|
||||||
defaultPendingSessionTimeout = 1 * time.Minute // Timeout for pending sessions (DoS protection)
|
defaultPendingSessionTimeout = 1 * time.Minute // Timeout for pending sessions (DoS protection)
|
||||||
|
|
@ -155,7 +154,7 @@ func NewSessionManager(logger *zerolog.Logger) *SessionManager {
|
||||||
logger: logger,
|
logger: logger,
|
||||||
maxSessions: maxSessions,
|
maxSessions: maxSessions,
|
||||||
primaryTimeout: primaryTimeout,
|
primaryTimeout: primaryTimeout,
|
||||||
broadcastQueue: make(chan struct{}, broadcastQueueCapacity),
|
broadcastQueue: make(chan struct{}, 100),
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
@ -167,11 +166,13 @@ func NewSessionManager(logger *zerolog.Logger) *SessionManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sm *SessionManager) AddSession(session *Session, clientSettings *SessionSettings) error {
|
func (sm *SessionManager) AddSession(session *Session, clientSettings *SessionSettings) error {
|
||||||
|
// Basic input validation
|
||||||
if session == nil {
|
if session == nil {
|
||||||
sm.logger.Error().Msg("AddSession: session is nil")
|
sm.logger.Error().Msg("AddSession: session is nil")
|
||||||
return errors.New("session cannot be nil")
|
return errors.New("session cannot be nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate nickname if provided (matching frontend validation)
|
||||||
if session.Nickname != "" {
|
if session.Nickname != "" {
|
||||||
if len(session.Nickname) < minNicknameLength {
|
if len(session.Nickname) < minNicknameLength {
|
||||||
return fmt.Errorf("nickname must be at least %d characters", minNicknameLength)
|
return fmt.Errorf("nickname must be at least %d characters", minNicknameLength)
|
||||||
|
|
@ -179,6 +180,7 @@ func (sm *SessionManager) AddSession(session *Session, clientSettings *SessionSe
|
||||||
if len(session.Nickname) > maxNicknameLength {
|
if len(session.Nickname) > maxNicknameLength {
|
||||||
return fmt.Errorf("nickname must be %d characters or less", maxNicknameLength)
|
return fmt.Errorf("nickname must be %d characters or less", maxNicknameLength)
|
||||||
}
|
}
|
||||||
|
// Note: Pattern validation is done in RPC layer, not here for performance
|
||||||
}
|
}
|
||||||
if len(session.Identity) > maxIdentityLength {
|
if len(session.Identity) > maxIdentityLength {
|
||||||
return fmt.Errorf("identity too long (max %d characters)", maxIdentityLength)
|
return fmt.Errorf("identity too long (max %d characters)", maxIdentityLength)
|
||||||
|
|
@ -222,25 +224,30 @@ func (sm *SessionManager) AddSession(session *Session, clientSettings *SessionSe
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if a session with this ID already exists (reconnection)
|
||||||
if existing, exists := sm.sessions[session.ID]; exists {
|
if existing, exists := sm.sessions[session.ID]; exists {
|
||||||
if existing.Identity != session.Identity || existing.Source != session.Source {
|
if existing.Identity != session.Identity || existing.Source != session.Source {
|
||||||
return fmt.Errorf("session ID already in use by different user (identity mismatch)")
|
return fmt.Errorf("session ID already in use by different user (identity mismatch)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close old connection to prevent multiple active connections for same session ID
|
||||||
if existing.peerConnection != nil {
|
if existing.peerConnection != nil {
|
||||||
existing.peerConnection.Close()
|
existing.peerConnection.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update the existing session with new connection details
|
||||||
existing.peerConnection = session.peerConnection
|
existing.peerConnection = session.peerConnection
|
||||||
existing.VideoTrack = session.VideoTrack
|
existing.VideoTrack = session.VideoTrack
|
||||||
existing.ControlChannel = session.ControlChannel
|
existing.ControlChannel = session.ControlChannel
|
||||||
existing.RPCChannel = session.RPCChannel
|
existing.RPCChannel = session.RPCChannel
|
||||||
existing.HidChannel = session.HidChannel
|
existing.HidChannel = session.HidChannel
|
||||||
existing.flushCandidates = session.flushCandidates
|
existing.flushCandidates = session.flushCandidates
|
||||||
|
// Preserve mode and nickname
|
||||||
session.Mode = existing.Mode
|
session.Mode = existing.Mode
|
||||||
session.Nickname = existing.Nickname
|
session.Nickname = existing.Nickname
|
||||||
session.CreatedAt = existing.CreatedAt
|
session.CreatedAt = existing.CreatedAt
|
||||||
|
|
||||||
|
// Ensure session has auto-generated nickname if needed
|
||||||
sm.ensureNickname(session)
|
sm.ensureNickname(session)
|
||||||
|
|
||||||
if !nicknameReserved && session.Nickname != "" {
|
if !nicknameReserved && session.Nickname != "" {
|
||||||
|
|
@ -249,15 +256,17 @@ func (sm *SessionManager) AddSession(session *Session, clientSettings *SessionSe
|
||||||
|
|
||||||
sm.sessions[session.ID] = session
|
sm.sessions[session.ID] = session
|
||||||
|
|
||||||
|
// If this was the primary, try to restore primary status
|
||||||
if existing.Mode == SessionModePrimary {
|
if existing.Mode == SessionModePrimary {
|
||||||
isBlacklisted := sm.isSessionBlacklisted(session.ID)
|
isBlacklisted := sm.isSessionBlacklisted(session.ID)
|
||||||
// SECURITY: Prevent dual-primary - only restore if no other primary exists
|
// SECURITY: Prevent dual-primary window - only restore if no other primary exists
|
||||||
primaryExists := sm.primarySessionID != "" && sm.sessions[sm.primarySessionID] != nil
|
primaryExists := sm.primarySessionID != "" && sm.sessions[sm.primarySessionID] != nil
|
||||||
if sm.lastPrimaryID == session.ID && !isBlacklisted && !primaryExists {
|
if sm.lastPrimaryID == session.ID && !isBlacklisted && !primaryExists {
|
||||||
sm.primarySessionID = session.ID
|
sm.primarySessionID = session.ID
|
||||||
sm.lastPrimaryID = ""
|
sm.lastPrimaryID = ""
|
||||||
delete(sm.reconnectGrace, session.ID)
|
delete(sm.reconnectGrace, session.ID)
|
||||||
} else {
|
} else {
|
||||||
|
// Grace period expired, another session took over, or primary already exists
|
||||||
session.Mode = SessionModeObserver
|
session.Mode = SessionModeObserver
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -270,18 +279,22 @@ func (sm *SessionManager) AddSession(session *Session, clientSettings *SessionSe
|
||||||
return ErrMaxSessionsReached
|
return ErrMaxSessionsReached
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate ID if not set
|
||||||
if session.ID == "" {
|
if session.ID == "" {
|
||||||
session.ID = uuid.New().String()
|
session.ID = uuid.New().String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set nickname from client settings if provided
|
||||||
if clientSettings != nil && clientSettings.Nickname != "" {
|
if clientSettings != nil && clientSettings.Nickname != "" {
|
||||||
session.Nickname = clientSettings.Nickname
|
session.Nickname = clientSettings.Nickname
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use global settings for requirements (not client-provided)
|
||||||
globalSettings := currentSessionSettings
|
globalSettings := currentSessionSettings
|
||||||
|
|
||||||
primaryExists := sm.primarySessionID != "" && sm.sessions[sm.primarySessionID] != nil
|
primaryExists := sm.primarySessionID != "" && sm.sessions[sm.primarySessionID] != nil
|
||||||
|
|
||||||
|
// Check if there's an active grace period for a primary session (different from this session)
|
||||||
hasActivePrimaryGracePeriod := false
|
hasActivePrimaryGracePeriod := false
|
||||||
if sm.lastPrimaryID != "" && sm.lastPrimaryID != session.ID {
|
if sm.lastPrimaryID != "" && sm.lastPrimaryID != session.ID {
|
||||||
if graceTime, exists := sm.reconnectGrace[sm.lastPrimaryID]; exists {
|
if graceTime, exists := sm.reconnectGrace[sm.lastPrimaryID]; exists {
|
||||||
|
|
@ -298,6 +311,7 @@ func (sm *SessionManager) AddSession(session *Session, clientSettings *SessionSe
|
||||||
isBlacklisted := sm.isSessionBlacklisted(session.ID)
|
isBlacklisted := sm.isSessionBlacklisted(session.ID)
|
||||||
isOnlySession := len(sm.sessions) == 0
|
isOnlySession := len(sm.sessions) == 0
|
||||||
|
|
||||||
|
// Determine if this session should become primary
|
||||||
canBecomePrimary := !primaryExists && !hasActivePrimaryGracePeriod
|
canBecomePrimary := !primaryExists && !hasActivePrimaryGracePeriod
|
||||||
isReconnectingPrimary := wasWithinGracePeriod && wasPreviouslyPrimary
|
isReconnectingPrimary := wasWithinGracePeriod && wasPreviouslyPrimary
|
||||||
isNewEligibleSession := !wasWithinGracePeriod && (!isBlacklisted || isOnlySession)
|
isNewEligibleSession := !wasWithinGracePeriod && (!isBlacklisted || isOnlySession)
|
||||||
|
|
@ -310,7 +324,7 @@ func (sm *SessionManager) AddSession(session *Session, clientSettings *SessionSe
|
||||||
sm.primarySessionID = session.ID
|
sm.primarySessionID = session.ID
|
||||||
sm.lastPrimaryID = ""
|
sm.lastPrimaryID = ""
|
||||||
|
|
||||||
// Clear grace periods when new primary is established
|
// Clear all existing grace periods when a new primary is established
|
||||||
for oldSessionID := range sm.reconnectGrace {
|
for oldSessionID := range sm.reconnectGrace {
|
||||||
delete(sm.reconnectGrace, oldSessionID)
|
delete(sm.reconnectGrace, oldSessionID)
|
||||||
}
|
}
|
||||||
|
|
@ -332,6 +346,7 @@ func (sm *SessionManager) AddSession(session *Session, clientSettings *SessionSe
|
||||||
requiresNickname := globalSettings.RequireNickname
|
requiresNickname := globalSettings.RequireNickname
|
||||||
hasNickname := session.Nickname != "" && len(session.Nickname) > 0
|
hasNickname := session.Nickname != "" && len(session.Nickname) > 0
|
||||||
|
|
||||||
|
// Only send approval request if nickname is not required OR already provided
|
||||||
if !requiresNickname || hasNickname {
|
if !requiresNickname || hasNickname {
|
||||||
go func() {
|
go func() {
|
||||||
writeJSONRPCEvent("newSessionPending", map[string]interface{}{
|
writeJSONRPCEvent("newSessionPending", map[string]interface{}{
|
||||||
|
|
@ -342,8 +357,12 @@ func (sm *SessionManager) AddSession(session *Session, clientSettings *SessionSe
|
||||||
}, primary)
|
}, primary)
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
// If nickname is required and missing, the approval request will be sent
|
||||||
|
// later when updateSessionNickname is called (see jsonrpc.go:232-242)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// No primary exists and approval is required, OR approval is not required
|
||||||
|
// In either case, this session becomes an observer
|
||||||
session.Mode = SessionModeObserver
|
session.Mode = SessionModeObserver
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -605,10 +624,12 @@ func (sm *SessionManager) SetPrimarySession(sessionID string) error {
|
||||||
// Sessions in pending state cannot receive video
|
// Sessions in pending state cannot receive video
|
||||||
// Sessions that require nickname but don't have one also cannot receive video (if enforced)
|
// Sessions that require nickname but don't have one also cannot receive video (if enforced)
|
||||||
func (sm *SessionManager) CanReceiveVideo(session *Session, settings *SessionSettings) bool {
|
func (sm *SessionManager) CanReceiveVideo(session *Session, settings *SessionSettings) bool {
|
||||||
|
// Check if session has video view permission
|
||||||
if !session.HasPermission(PermissionVideoView) {
|
if !session.HasPermission(PermissionVideoView) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If nickname is required and session doesn't have one, block video
|
||||||
if settings != nil && settings.RequireNickname && session.Nickname == "" {
|
if settings != nil && settings.RequireNickname && session.Nickname == "" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,18 +42,11 @@ import NicknameModal from "@components/NicknameModal";
|
||||||
import AccessDeniedOverlay from "@components/AccessDeniedOverlay";
|
import AccessDeniedOverlay from "@components/AccessDeniedOverlay";
|
||||||
import PendingApprovalOverlay from "@components/PendingApprovalOverlay";
|
import PendingApprovalOverlay from "@components/PendingApprovalOverlay";
|
||||||
import DashboardNavbar from "@components/Header";
|
import DashboardNavbar from "@components/Header";
|
||||||
const ConnectionStatsSidebar = lazy(() => import("@/components/sidebar/connectionStats"));
|
const ConnectionStatsSidebar = lazy(() => import('@/components/sidebar/connectionStats'));
|
||||||
const Terminal = lazy(() => import("@components/Terminal"));
|
const Terminal = lazy(() => import('@components/Terminal'));
|
||||||
const UpdateInProgressStatusCard = lazy(
|
const UpdateInProgressStatusCard = lazy(() => import("@/components/UpdateInProgressStatusCard"));
|
||||||
() => import("@/components/UpdateInProgressStatusCard"),
|
|
||||||
);
|
|
||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
import {
|
import { JsonRpcRequest, JsonRpcResponse, RpcMethodNotFound, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
JsonRpcRequest,
|
|
||||||
JsonRpcResponse,
|
|
||||||
RpcMethodNotFound,
|
|
||||||
useJsonRpc,
|
|
||||||
} from "@/hooks/useJsonRpc";
|
|
||||||
import {
|
import {
|
||||||
ConnectionFailedOverlay,
|
ConnectionFailedOverlay,
|
||||||
LoadingConnectionOverlay,
|
LoadingConnectionOverlay,
|
||||||
|
|
@ -142,25 +135,15 @@ export default function KvmIdRoute() {
|
||||||
const authMode = "authMode" in loaderResp ? loaderResp.authMode : null;
|
const authMode = "authMode" in loaderResp ? loaderResp.authMode : null;
|
||||||
|
|
||||||
const params = useParams() as { id: string };
|
const params = useParams() as { id: string };
|
||||||
const {
|
const { sidebarView, setSidebarView, disableVideoFocusTrap, setDisableVideoFocusTrap, rebootState, setRebootState } = useUiStore();
|
||||||
sidebarView,
|
|
||||||
setSidebarView,
|
|
||||||
disableVideoFocusTrap,
|
|
||||||
setDisableVideoFocusTrap,
|
|
||||||
rebootState,
|
|
||||||
setRebootState,
|
|
||||||
} = useUiStore();
|
|
||||||
const [queryParams, setQueryParams] = useSearchParams();
|
const [queryParams, setQueryParams] = useSearchParams();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
peerConnection,
|
peerConnection, setPeerConnection,
|
||||||
setPeerConnection,
|
peerConnectionState, setPeerConnectionState,
|
||||||
peerConnectionState,
|
|
||||||
setPeerConnectionState,
|
|
||||||
setMediaStream,
|
setMediaStream,
|
||||||
setRpcDataChannel,
|
setRpcDataChannel,
|
||||||
isTurnServerInUse,
|
isTurnServerInUse, setTurnServerInUse,
|
||||||
setTurnServerInUse,
|
|
||||||
rpcDataChannel,
|
rpcDataChannel,
|
||||||
setTransceiver,
|
setTransceiver,
|
||||||
setRpcHidChannel,
|
setRpcHidChannel,
|
||||||
|
|
@ -179,14 +162,12 @@ export default function KvmIdRoute() {
|
||||||
const { currentSessionId, currentMode, setCurrentSession } = useSessionStore();
|
const { currentSessionId, currentMode, setCurrentSession } = useSessionStore();
|
||||||
const { nickname, setNickname } = useSharedSessionStore();
|
const { nickname, setNickname } = useSharedSessionStore();
|
||||||
const { setRequireSessionApproval, setRequireSessionNickname } = useSettingsStore();
|
const { setRequireSessionApproval, setRequireSessionNickname } = useSettingsStore();
|
||||||
const [globalSessionSettings, setGlobalSessionSettings] = useState<{
|
const [globalSessionSettings, setGlobalSessionSettings] = useState<{requireApproval: boolean, requireNickname: boolean} | null>(null);
|
||||||
requireApproval: boolean;
|
|
||||||
requireNickname: boolean;
|
|
||||||
} | null>(null);
|
|
||||||
|
|
||||||
const [loadingMessage, setLoadingMessage] = useState("Connecting to device...");
|
const [loadingMessage, setLoadingMessage] = useState("Connecting to device...");
|
||||||
const cleanupAndStopReconnecting = useCallback(
|
const cleanupAndStopReconnecting = useCallback(
|
||||||
function cleanupAndStopReconnecting() {
|
function cleanupAndStopReconnecting() {
|
||||||
|
|
||||||
setConnectionFailed(true);
|
setConnectionFailed(true);
|
||||||
if (peerConnection) {
|
if (peerConnection) {
|
||||||
setPeerConnectionState(peerConnection.connectionState);
|
setPeerConnectionState(peerConnection.connectionState);
|
||||||
|
|
@ -283,7 +264,7 @@ export default function KvmIdRoute() {
|
||||||
},
|
},
|
||||||
|
|
||||||
onClose(_event) {
|
onClose(_event) {
|
||||||
// Handled by onReconnectStop instead
|
// We don't want to close everything down, we wait for the reconnect to stop instead
|
||||||
},
|
},
|
||||||
|
|
||||||
onError(event) {
|
onError(event) {
|
||||||
|
|
@ -328,7 +309,7 @@ export default function KvmIdRoute() {
|
||||||
if (sessionSettings) {
|
if (sessionSettings) {
|
||||||
setGlobalSessionSettings({
|
setGlobalSessionSettings({
|
||||||
requireNickname: sessionSettings.requireNickname || false,
|
requireNickname: sessionSettings.requireNickname || false,
|
||||||
requireApproval: sessionSettings.requireApproval || false,
|
requireApproval: sessionSettings.requireApproval || false
|
||||||
});
|
});
|
||||||
// Also update the settings store for approval handling
|
// Also update the settings store for approval handling
|
||||||
setRequireSessionApproval(sessionSettings.requireApproval || false);
|
setRequireSessionApproval(sessionSettings.requireApproval || false);
|
||||||
|
|
@ -337,6 +318,7 @@ export default function KvmIdRoute() {
|
||||||
|
|
||||||
// If the device version is not set, we can assume the device is using the legacy signaling
|
// If the device version is not set, we can assume the device is using the legacy signaling
|
||||||
if (!deviceVersion) {
|
if (!deviceVersion) {
|
||||||
|
|
||||||
// Now we don't need the websocket connection anymore, as we've established that we need to use the legacy signaling
|
// Now we don't need the websocket connection anymore, as we've established that we need to use the legacy signaling
|
||||||
// which does everything over HTTP(at least from the perspective of the client)
|
// which does everything over HTTP(at least from the perspective of the client)
|
||||||
isLegacySignalingEnabled.current = true;
|
isLegacySignalingEnabled.current = true;
|
||||||
|
|
@ -360,10 +342,7 @@ export default function KvmIdRoute() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!peerConnection) {
|
if (!peerConnection) {
|
||||||
console.warn(
|
console.warn("[Websocket] Ignoring message because peerConnection is not ready:", parsedMessage.type);
|
||||||
"[Websocket] Ignoring message because peerConnection is not ready:",
|
|
||||||
parsedMessage.type,
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (parsedMessage.type === "answer") {
|
if (parsedMessage.type === "answer") {
|
||||||
|
|
@ -387,18 +366,15 @@ export default function KvmIdRoute() {
|
||||||
if (parsedMessage.sessionId && parsedMessage.mode) {
|
if (parsedMessage.sessionId && parsedMessage.mode) {
|
||||||
handleSessionResponse({
|
handleSessionResponse({
|
||||||
sessionId: parsedMessage.sessionId,
|
sessionId: parsedMessage.sessionId,
|
||||||
mode: parsedMessage.mode,
|
mode: parsedMessage.mode
|
||||||
});
|
});
|
||||||
|
|
||||||
// Store sessionId via zustand (persists to sessionStorage for per-tab isolation)
|
// Store sessionId via zustand (persists to sessionStorage for per-tab isolation)
|
||||||
setCurrentSession(parsedMessage.sessionId, parsedMessage.mode);
|
setCurrentSession(parsedMessage.sessionId, parsedMessage.mode);
|
||||||
if (
|
if (parsedMessage.requireNickname !== undefined && parsedMessage.requireApproval !== undefined) {
|
||||||
parsedMessage.requireNickname !== undefined &&
|
|
||||||
parsedMessage.requireApproval !== undefined
|
|
||||||
) {
|
|
||||||
setGlobalSessionSettings({
|
setGlobalSessionSettings({
|
||||||
requireNickname: parsedMessage.requireNickname,
|
requireNickname: parsedMessage.requireNickname,
|
||||||
requireApproval: parsedMessage.requireApproval,
|
requireApproval: parsedMessage.requireApproval
|
||||||
});
|
});
|
||||||
// Also update the settings store for approval handling
|
// Also update the settings store for approval handling
|
||||||
setRequireSessionApproval(parsedMessage.requireApproval);
|
setRequireSessionApproval(parsedMessage.requireApproval);
|
||||||
|
|
@ -409,10 +385,8 @@ export default function KvmIdRoute() {
|
||||||
// 1. Nickname is required by backend settings
|
// 1. Nickname is required by backend settings
|
||||||
// 2. We don't already have a nickname
|
// 2. We don't already have a nickname
|
||||||
// This happens even for pending sessions so the nickname is included in approval
|
// This happens even for pending sessions so the nickname is included in approval
|
||||||
const hasNickname =
|
const hasNickname = parsedMessage.nickname && parsedMessage.nickname.length > 0;
|
||||||
parsedMessage.nickname && parsedMessage.nickname.length > 0;
|
const requiresNickname = parsedMessage.requireNickname || globalSessionSettings?.requireNickname;
|
||||||
const requiresNickname =
|
|
||||||
parsedMessage.requireNickname || globalSessionSettings?.requireNickname;
|
|
||||||
|
|
||||||
if (requiresNickname && !hasNickname) {
|
if (requiresNickname && !hasNickname) {
|
||||||
setShowNicknameModal(true);
|
setShowNicknameModal(true);
|
||||||
|
|
@ -453,22 +427,18 @@ export default function KvmIdRoute() {
|
||||||
peerConnection?.iceConnectionState === "connected";
|
peerConnection?.iceConnectionState === "connected";
|
||||||
|
|
||||||
if (!isConnectionHealthy) {
|
if (!isConnectionHealthy) {
|
||||||
console.log(
|
console.log(`[Websocket] Mode changed to ${newMode}, connection unhealthy, reconnecting...`);
|
||||||
`[Websocket] Mode changed to ${newMode}, connection unhealthy, reconnecting...`,
|
|
||||||
);
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
peerConnection?.close();
|
peerConnection?.close();
|
||||||
setupPeerConnection();
|
setupPeerConnection();
|
||||||
}, 500);
|
}, 500);
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
console.log(`[Websocket] Mode changed to ${newMode}, connection healthy, skipping reconnect`);
|
||||||
`[Websocket] Mode changed to ${newMode}, connection healthy, skipping reconnect`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const sendWebRTCSignal = useCallback(
|
const sendWebRTCSignal = useCallback(
|
||||||
|
|
@ -570,8 +540,8 @@ export default function KvmIdRoute() {
|
||||||
sessionId: storeSessionId || undefined,
|
sessionId: storeSessionId || undefined,
|
||||||
userAgent: navigator.userAgent,
|
userAgent: navigator.userAgent,
|
||||||
sessionSettings: {
|
sessionSettings: {
|
||||||
nickname: storeNickname || undefined,
|
nickname: storeNickname || undefined
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -635,13 +605,10 @@ export default function KvmIdRoute() {
|
||||||
setRpcHidUnreliableChannel(rpcHidUnreliableChannel);
|
setRpcHidUnreliableChannel(rpcHidUnreliableChannel);
|
||||||
};
|
};
|
||||||
|
|
||||||
const rpcHidUnreliableNonOrderedChannel = pc.createDataChannel(
|
const rpcHidUnreliableNonOrderedChannel = pc.createDataChannel("hidrpc-unreliable-nonordered", {
|
||||||
"hidrpc-unreliable-nonordered",
|
ordered: false,
|
||||||
{
|
maxRetransmits: 0,
|
||||||
ordered: false,
|
});
|
||||||
maxRetransmits: 0,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
rpcHidUnreliableNonOrderedChannel.binaryType = "arraybuffer";
|
rpcHidUnreliableNonOrderedChannel.binaryType = "arraybuffer";
|
||||||
rpcHidUnreliableNonOrderedChannel.onopen = () => {
|
rpcHidUnreliableNonOrderedChannel.onopen = () => {
|
||||||
setRpcHidUnreliableNonOrderedChannel(rpcHidUnreliableNonOrderedChannel);
|
setRpcHidUnreliableNonOrderedChannel(rpcHidUnreliableNonOrderedChannel);
|
||||||
|
|
@ -732,24 +699,19 @@ export default function KvmIdRoute() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fire and forget
|
// Fire and forget
|
||||||
api
|
api.POST(`${CLOUD_API}/webrtc/turn_activity`, {
|
||||||
.POST(`${CLOUD_API}/webrtc/turn_activity`, {
|
bytesReceived: bytesReceivedDelta,
|
||||||
bytesReceived: bytesReceivedDelta,
|
bytesSent: bytesSentDelta,
|
||||||
bytesSent: bytesSentDelta,
|
}).catch(() => {
|
||||||
})
|
// we don't care about errors here, but we don't want unhandled promise rejections
|
||||||
.catch(() => {
|
});
|
||||||
// we don't care about errors here, but we don't want unhandled promise rejections
|
|
||||||
});
|
|
||||||
}, 10000);
|
}, 10000);
|
||||||
|
|
||||||
const { setNetworkState } = useNetworkStateStore();
|
const { setNetworkState } = useNetworkStateStore();
|
||||||
const { setHdmiState } = useVideoStore();
|
const { setHdmiState } = useVideoStore();
|
||||||
const {
|
const {
|
||||||
keyboardLedState,
|
keyboardLedState, setKeyboardLedState,
|
||||||
setKeyboardLedState,
|
keysDownState, setKeysDownState, setUsbState,
|
||||||
keysDownState,
|
|
||||||
setKeysDownState,
|
|
||||||
setUsbState,
|
|
||||||
} = useHidStore();
|
} = useHidStore();
|
||||||
const setHidRpcDisabled = useRTCStore(state => state.setHidRpcDisabled);
|
const setHidRpcDisabled = useRTCStore(state => state.setHidRpcDisabled);
|
||||||
|
|
||||||
|
|
@ -758,17 +720,15 @@ export default function KvmIdRoute() {
|
||||||
|
|
||||||
function onJsonRpcRequest(resp: JsonRpcRequest) {
|
function onJsonRpcRequest(resp: JsonRpcRequest) {
|
||||||
// Handle session-related events
|
// Handle session-related events
|
||||||
if (
|
if (resp.method === "sessionsUpdated" ||
|
||||||
resp.method === "sessionsUpdated" ||
|
resp.method === "modeChanged" ||
|
||||||
resp.method === "modeChanged" ||
|
resp.method === "connectionModeChanged" ||
|
||||||
resp.method === "connectionModeChanged" ||
|
resp.method === "otherSessionConnected" ||
|
||||||
resp.method === "otherSessionConnected" ||
|
resp.method === "primaryControlRequested" ||
|
||||||
resp.method === "primaryControlRequested" ||
|
resp.method === "primaryControlApproved" ||
|
||||||
resp.method === "primaryControlApproved" ||
|
resp.method === "primaryControlDenied" ||
|
||||||
resp.method === "primaryControlDenied" ||
|
resp.method === "newSessionPending" ||
|
||||||
resp.method === "newSessionPending" ||
|
resp.method === "sessionAccessDenied") {
|
||||||
resp.method === "sessionAccessDenied"
|
|
||||||
) {
|
|
||||||
handleRpcEvent(resp.method, resp.params);
|
handleRpcEvent(resp.method, resp.params);
|
||||||
|
|
||||||
// Show access denied overlay if our session was denied
|
// Show access denied overlay if our session was denied
|
||||||
|
|
@ -849,7 +809,7 @@ export default function KvmIdRoute() {
|
||||||
newSessionRequest,
|
newSessionRequest,
|
||||||
handleApproveNewSession,
|
handleApproveNewSession,
|
||||||
handleDenyNewSession,
|
handleDenyNewSession,
|
||||||
closeNewSessionRequest,
|
closeNewSessionRequest
|
||||||
} = useSessionManagement(send);
|
} = useSessionManagement(send);
|
||||||
|
|
||||||
const { hasPermission, isLoading: isLoadingPermissions } = usePermissions();
|
const { hasPermission, isLoading: isLoadingPermissions } = usePermissions();
|
||||||
|
|
@ -863,13 +823,7 @@ export default function KvmIdRoute() {
|
||||||
const hdmiState = resp.result as Parameters<VideoState["setHdmiState"]>[0];
|
const hdmiState = resp.result as Parameters<VideoState["setHdmiState"]>[0];
|
||||||
setHdmiState(hdmiState);
|
setHdmiState(hdmiState);
|
||||||
});
|
});
|
||||||
}, [
|
}, [rpcDataChannel?.readyState, hasPermission, isLoadingPermissions, send, setHdmiState]);
|
||||||
rpcDataChannel?.readyState,
|
|
||||||
hasPermission,
|
|
||||||
isLoadingPermissions,
|
|
||||||
send,
|
|
||||||
setHdmiState,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const [needLedState, setNeedLedState] = useState(true);
|
const [needLedState, setNeedLedState] = useState(true);
|
||||||
|
|
||||||
|
|
@ -888,15 +842,7 @@ export default function KvmIdRoute() {
|
||||||
}
|
}
|
||||||
setNeedLedState(false);
|
setNeedLedState(false);
|
||||||
});
|
});
|
||||||
}, [
|
}, [rpcDataChannel?.readyState, send, setKeyboardLedState, keyboardLedState, needLedState, hasPermission, isLoadingPermissions]);
|
||||||
rpcDataChannel?.readyState,
|
|
||||||
send,
|
|
||||||
setKeyboardLedState,
|
|
||||||
keyboardLedState,
|
|
||||||
needLedState,
|
|
||||||
hasPermission,
|
|
||||||
isLoadingPermissions,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const [needKeyDownState, setNeedKeyDownState] = useState(true);
|
const [needKeyDownState, setNeedKeyDownState] = useState(true);
|
||||||
|
|
||||||
|
|
@ -908,10 +854,7 @@ export default function KvmIdRoute() {
|
||||||
send("getKeyDownState", {}, (resp: JsonRpcResponse) => {
|
send("getKeyDownState", {}, (resp: JsonRpcResponse) => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
if (resp.error.code === RpcMethodNotFound) {
|
if (resp.error.code === RpcMethodNotFound) {
|
||||||
console.warn(
|
console.warn("Failed to get key down state, switching to old-school", resp.error);
|
||||||
"Failed to get key down state, switching to old-school",
|
|
||||||
resp.error,
|
|
||||||
);
|
|
||||||
setHidRpcDisabled(true);
|
setHidRpcDisabled(true);
|
||||||
} else {
|
} else {
|
||||||
console.error("Failed to get key down state", resp.error);
|
console.error("Failed to get key down state", resp.error);
|
||||||
|
|
@ -922,16 +865,7 @@ export default function KvmIdRoute() {
|
||||||
}
|
}
|
||||||
setNeedKeyDownState(false);
|
setNeedKeyDownState(false);
|
||||||
});
|
});
|
||||||
}, [
|
}, [keysDownState, needKeyDownState, rpcDataChannel?.readyState, send, setKeysDownState, setHidRpcDisabled, hasPermission, isLoadingPermissions]);
|
||||||
keysDownState,
|
|
||||||
needKeyDownState,
|
|
||||||
rpcDataChannel?.readyState,
|
|
||||||
send,
|
|
||||||
setKeysDownState,
|
|
||||||
setHidRpcDisabled,
|
|
||||||
hasPermission,
|
|
||||||
isLoadingPermissions,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 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
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -976,9 +910,7 @@ export default function KvmIdRoute() {
|
||||||
|
|
||||||
// Rebooting takes priority over connection status
|
// Rebooting takes priority over connection status
|
||||||
if (rebootState?.isRebooting) {
|
if (rebootState?.isRebooting) {
|
||||||
return (
|
return <RebootingOverlay show={true} postRebootAction={rebootState.postRebootAction} />;
|
||||||
<RebootingOverlay show={true} postRebootAction={rebootState.postRebootAction} />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasConnectionFailed =
|
const hasConnectionFailed =
|
||||||
|
|
@ -1005,186 +937,184 @@ export default function KvmIdRoute() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}, [
|
}, [location.pathname, rebootState?.isRebooting, rebootState?.postRebootAction, connectionFailed, peerConnectionState, peerConnection, setupPeerConnection, loadingMessage]);
|
||||||
location.pathname,
|
|
||||||
rebootState?.isRebooting,
|
|
||||||
rebootState?.postRebootAction,
|
|
||||||
connectionFailed,
|
|
||||||
peerConnectionState,
|
|
||||||
peerConnection,
|
|
||||||
setupPeerConnection,
|
|
||||||
loadingMessage,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PermissionsProvider>
|
<PermissionsProvider>
|
||||||
<FeatureFlagProvider appVersion={appVersion}>
|
<FeatureFlagProvider appVersion={appVersion}>
|
||||||
{!outlet && otaState.updating && (
|
{!outlet && otaState.updating && (
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="pointer-events-none fixed inset-0 top-16 z-10 mx-auto flex h-full w-full max-w-xl translate-y-8 items-start justify-center"
|
className="pointer-events-none fixed inset-0 top-16 z-10 mx-auto flex h-full w-full max-w-xl translate-y-8 items-start justify-center"
|
||||||
initial={{ opacity: 0, y: -20 }}
|
initial={{ opacity: 0, y: -20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
exit={{ opacity: 0, y: -20 }}
|
exit={{ opacity: 0, y: -20 }}
|
||||||
transition={{ duration: 0.3, ease: "easeInOut" }}
|
transition={{ duration: 0.3, ease: "easeInOut" }}
|
||||||
>
|
|
||||||
<UpdateInProgressStatusCard />
|
|
||||||
</motion.div>
|
|
||||||
</AnimatePresence>
|
|
||||||
)}
|
|
||||||
<div className="relative h-full">
|
|
||||||
<FocusTrap
|
|
||||||
paused={disableVideoFocusTrap}
|
|
||||||
focusTrapOptions={{
|
|
||||||
allowOutsideClick: true,
|
|
||||||
escapeDeactivates: false,
|
|
||||||
fallbackFocus: "#videoFocusTrap",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div className="absolute top-0">
|
<UpdateInProgressStatusCard />
|
||||||
<button className="absolute top-0" tabIndex={-1} id="videoFocusTrap" />
|
</motion.div>
|
||||||
</div>
|
</AnimatePresence>
|
||||||
</FocusTrap>
|
)}
|
||||||
|
<div className="relative h-full">
|
||||||
<div className="grid h-full grid-rows-(--grid-headerBody) select-none">
|
<FocusTrap
|
||||||
<DashboardNavbar
|
paused={disableVideoFocusTrap}
|
||||||
primaryLinks={
|
focusTrapOptions={{
|
||||||
isOnDevice ? [] : [{ title: "Cloud Devices", to: "/devices" }]
|
allowOutsideClick: true,
|
||||||
}
|
escapeDeactivates: false,
|
||||||
showConnectionStatus={true}
|
fallbackFocus: "#videoFocusTrap",
|
||||||
isLoggedIn={authMode === "password" || !!user}
|
|
||||||
userEmail={user?.email}
|
|
||||||
picture={user?.picture}
|
|
||||||
kvmName={deviceName ?? "JetKVM Device"}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="relative flex h-full w-full overflow-hidden">
|
|
||||||
{/* Only show video feed if nickname is set (when required) and not pending approval */}
|
|
||||||
{!showNicknameModal && currentMode !== "pending" ? (
|
|
||||||
<>
|
|
||||||
<WebRTCVideo hasConnectionIssues={!!ConnectionStatusElement} />
|
|
||||||
<div
|
|
||||||
style={{ animationDuration: "500ms" }}
|
|
||||||
className="pointer-events-none absolute inset-0 flex animate-slideUpFade items-center justify-center p-4"
|
|
||||||
>
|
|
||||||
<div className="relative h-full max-h-[720px] w-full max-w-[1280px] rounded-md">
|
|
||||||
{!!ConnectionStatusElement && ConnectionStatusElement}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="flex flex-1 items-center justify-center bg-slate-900">
|
|
||||||
<div className="text-center text-slate-400">
|
|
||||||
{showNicknameModal && <p>Please set your nickname to continue</p>}
|
|
||||||
{currentMode === "pending" && <p>Waiting for session approval...</p>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<SidebarContainer sidebarView={sidebarView} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="z-50"
|
|
||||||
onClick={e => e.stopPropagation()}
|
|
||||||
onMouseUp={e => e.stopPropagation()}
|
|
||||||
onMouseDown={e => e.stopPropagation()}
|
|
||||||
onKeyUp={e => e.stopPropagation()}
|
|
||||||
onKeyDown={e => {
|
|
||||||
e.stopPropagation();
|
|
||||||
if (e.key === "Escape") navigateTo("/");
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Modal open={outlet !== null} onClose={onModalClose}>
|
<div className="absolute top-0">
|
||||||
{/* The 'used by other session' modal needs to have access to the connectWebRTC function */}
|
<button className="absolute top-0" tabIndex={-1} id="videoFocusTrap" />
|
||||||
<Outlet context={{ setupPeerConnection }} />
|
</div>
|
||||||
</Modal>
|
</FocusTrap>
|
||||||
|
|
||||||
<NicknameModal
|
<div className="grid h-full grid-rows-(--grid-headerBody) select-none">
|
||||||
isOpen={showNicknameModal}
|
<DashboardNavbar
|
||||||
onSubmit={async nickname => {
|
primaryLinks={isOnDevice ? [] : [{ title: "Cloud Devices", to: "/devices" }]}
|
||||||
setNickname(nickname);
|
showConnectionStatus={true}
|
||||||
setShowNicknameModal(false);
|
isLoggedIn={authMode === "password" || !!user}
|
||||||
setDisableVideoFocusTrap(false);
|
userEmail={user?.email}
|
||||||
|
picture={user?.picture}
|
||||||
if (currentSessionId && send) {
|
kvmName={deviceName ?? "JetKVM Device"}
|
||||||
try {
|
|
||||||
await sessionApi.updateNickname(send, currentSessionId, nickname);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to update nickname:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onSkip={() => {
|
|
||||||
setShowNicknameModal(false);
|
|
||||||
setDisableVideoFocusTrap(false);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
<div className="relative flex h-full w-full overflow-hidden">
|
||||||
|
{/* Only show video feed if nickname is set (when required) and not pending approval */}
|
||||||
|
{(!showNicknameModal && currentMode !== "pending") ? (
|
||||||
|
<>
|
||||||
|
<WebRTCVideo hasConnectionIssues={!!ConnectionStatusElement} />
|
||||||
|
<div
|
||||||
|
style={{ animationDuration: "500ms" }}
|
||||||
|
className="animate-slideUpFade pointer-events-none absolute inset-0 flex items-center justify-center p-4"
|
||||||
|
>
|
||||||
|
<div className="relative h-full max-h-[720px] w-full max-w-[1280px] rounded-md">
|
||||||
|
{!!ConnectionStatusElement && ConnectionStatusElement}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="flex-1 bg-slate-900 flex items-center justify-center">
|
||||||
|
<div className="text-slate-400 text-center">
|
||||||
|
{showNicknameModal && <p>Please set your nickname to continue</p>}
|
||||||
|
{currentMode === "pending" && <p>Waiting for session approval...</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<SidebarContainer sidebarView={sidebarView} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{kvmTerminal && (
|
<div
|
||||||
<Terminal type="kvm" dataChannel={kvmTerminal} title="KVM Terminal" />
|
className="z-50"
|
||||||
)}
|
onClick={e => e.stopPropagation()}
|
||||||
|
onMouseUp={e => e.stopPropagation()}
|
||||||
|
onMouseDown={e => e.stopPropagation()}
|
||||||
|
onKeyUp={e => e.stopPropagation()}
|
||||||
|
onKeyDown={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (e.key === "Escape") navigateTo("/");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Modal open={outlet !== null} onClose={onModalClose}>
|
||||||
|
{/* The 'used by other session' modal needs to have access to the connectWebRTC function */}
|
||||||
|
<Outlet context={{ setupPeerConnection }} />
|
||||||
|
</Modal>
|
||||||
|
|
||||||
{serialConsole && (
|
<NicknameModal
|
||||||
<Terminal type="serial" dataChannel={serialConsole} title="Serial Console" />
|
isOpen={showNicknameModal}
|
||||||
)}
|
onSubmit={async (nickname) => {
|
||||||
|
setNickname(nickname);
|
||||||
|
setShowNicknameModal(false);
|
||||||
|
setDisableVideoFocusTrap(false);
|
||||||
|
|
||||||
{/* Unified Session Request Dialog */}
|
if (currentSessionId && send) {
|
||||||
{(primaryControlRequest || newSessionRequest) && (
|
try {
|
||||||
<UnifiedSessionRequestDialog
|
await sessionApi.updateNickname(send, currentSessionId, nickname);
|
||||||
request={
|
} catch (error) {
|
||||||
primaryControlRequest
|
console.error("Failed to update nickname:", error);
|
||||||
? {
|
}
|
||||||
id: primaryControlRequest.requestId,
|
|
||||||
type: "primary_control",
|
|
||||||
source: primaryControlRequest.source,
|
|
||||||
identity: primaryControlRequest.identity,
|
|
||||||
nickname: primaryControlRequest.nickname,
|
|
||||||
}
|
|
||||||
: newSessionRequest
|
|
||||||
? {
|
|
||||||
id: newSessionRequest.sessionId,
|
|
||||||
type: "session_approval",
|
|
||||||
source: newSessionRequest.source,
|
|
||||||
identity: newSessionRequest.identity,
|
|
||||||
nickname: newSessionRequest.nickname,
|
|
||||||
}
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
onApprove={
|
|
||||||
primaryControlRequest
|
|
||||||
? handleApprovePrimaryRequest
|
|
||||||
: handleApproveNewSession
|
|
||||||
}
|
|
||||||
onDeny={
|
|
||||||
primaryControlRequest ? handleDenyPrimaryRequest : handleDenyNewSession
|
|
||||||
}
|
|
||||||
onDismiss={
|
|
||||||
primaryControlRequest ? closePrimaryControlRequest : closeNewSessionRequest
|
|
||||||
}
|
|
||||||
onClose={
|
|
||||||
primaryControlRequest ? closePrimaryControlRequest : closeNewSessionRequest
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<AccessDeniedOverlay
|
|
||||||
show={accessDenied}
|
|
||||||
message="Your session access was denied by the primary session"
|
|
||||||
onRequestApproval={async () => {
|
|
||||||
if (!send) return;
|
|
||||||
try {
|
|
||||||
await sessionApi.requestSessionApproval(send);
|
|
||||||
setAccessDenied(false);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to re-request approval:", error);
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onSkip={() => {
|
||||||
|
setShowNicknameModal(false);
|
||||||
|
setDisableVideoFocusTrap(false);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<PendingApprovalOverlay show={currentMode === "pending"} />
|
{kvmTerminal && (
|
||||||
|
<Terminal type="kvm" dataChannel={kvmTerminal} title="KVM Terminal" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{serialConsole && (
|
||||||
|
<Terminal type="serial" dataChannel={serialConsole} title="Serial Console" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Unified Session Request Dialog */}
|
||||||
|
{(primaryControlRequest || newSessionRequest) && (
|
||||||
|
<UnifiedSessionRequestDialog
|
||||||
|
request={
|
||||||
|
primaryControlRequest
|
||||||
|
? {
|
||||||
|
id: primaryControlRequest.requestId,
|
||||||
|
type: "primary_control",
|
||||||
|
source: primaryControlRequest.source,
|
||||||
|
identity: primaryControlRequest.identity,
|
||||||
|
nickname: primaryControlRequest.nickname,
|
||||||
|
}
|
||||||
|
: newSessionRequest
|
||||||
|
? {
|
||||||
|
id: newSessionRequest.sessionId,
|
||||||
|
type: "session_approval",
|
||||||
|
source: newSessionRequest.source,
|
||||||
|
identity: newSessionRequest.identity,
|
||||||
|
nickname: newSessionRequest.nickname,
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
onApprove={
|
||||||
|
primaryControlRequest
|
||||||
|
? handleApprovePrimaryRequest
|
||||||
|
: handleApproveNewSession
|
||||||
|
}
|
||||||
|
onDeny={
|
||||||
|
primaryControlRequest
|
||||||
|
? handleDenyPrimaryRequest
|
||||||
|
: handleDenyNewSession
|
||||||
|
}
|
||||||
|
onDismiss={
|
||||||
|
primaryControlRequest
|
||||||
|
? closePrimaryControlRequest
|
||||||
|
: closeNewSessionRequest
|
||||||
|
}
|
||||||
|
onClose={
|
||||||
|
primaryControlRequest
|
||||||
|
? closePrimaryControlRequest
|
||||||
|
: closeNewSessionRequest
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<AccessDeniedOverlay
|
||||||
|
show={accessDenied}
|
||||||
|
message="Your session access was denied by the primary session"
|
||||||
|
onRequestApproval={async () => {
|
||||||
|
if (!send) return;
|
||||||
|
try {
|
||||||
|
await sessionApi.requestSessionApproval(send);
|
||||||
|
setAccessDenied(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to re-request approval:", error);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PendingApprovalOverlay
|
||||||
|
show={currentMode === "pending"}
|
||||||
|
/>
|
||||||
</FeatureFlagProvider>
|
</FeatureFlagProvider>
|
||||||
</PermissionsProvider>
|
</PermissionsProvider>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue