mirror of https://github.com/jetkvm/kvm.git
Compare commits
3 Commits
50e1cf06b8
...
ec0d5d7cb7
| Author | SHA1 | Date |
|---|---|---|
|
|
ec0d5d7cb7 | |
|
|
2e4a49feb6 | |
|
|
1671a7706b |
|
|
@ -96,11 +96,11 @@ func handleRequestSessionApprovalRPC(session *Session) (any, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateNickname(nickname string) error {
|
func validateNickname(nickname string) error {
|
||||||
if len(nickname) < 2 {
|
if len(nickname) < minNicknameLength {
|
||||||
return errors.New("nickname must be at least 2 characters")
|
return fmt.Errorf("nickname must be at least %d characters", minNicknameLength)
|
||||||
}
|
}
|
||||||
if len(nickname) > 30 {
|
if len(nickname) > maxNicknameLength {
|
||||||
return errors.New("nickname must be 30 characters or less")
|
return fmt.Errorf("nickname must be %d characters or less", maxNicknameLength)
|
||||||
}
|
}
|
||||||
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,8 +25,9 @@ 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)
|
||||||
|
|
@ -154,7 +155,7 @@ func NewSessionManager(logger *zerolog.Logger) *SessionManager {
|
||||||
logger: logger,
|
logger: logger,
|
||||||
maxSessions: maxSessions,
|
maxSessions: maxSessions,
|
||||||
primaryTimeout: primaryTimeout,
|
primaryTimeout: primaryTimeout,
|
||||||
broadcastQueue: make(chan struct{}, 100),
|
broadcastQueue: make(chan struct{}, broadcastQueueCapacity),
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
@ -166,13 +167,11 @@ 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)
|
||||||
|
|
@ -180,7 +179,6 @@ 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)
|
||||||
|
|
@ -224,30 +222,25 @@ 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 != "" {
|
||||||
|
|
@ -256,17 +249,15 @@ 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 window - only restore if no other primary exists
|
// SECURITY: Prevent dual-primary - 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -279,22 +270,18 @@ 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 {
|
||||||
|
|
@ -311,7 +298,6 @@ 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)
|
||||||
|
|
@ -324,7 +310,7 @@ func (sm *SessionManager) AddSession(session *Session, clientSettings *SessionSe
|
||||||
sm.primarySessionID = session.ID
|
sm.primarySessionID = session.ID
|
||||||
sm.lastPrimaryID = ""
|
sm.lastPrimaryID = ""
|
||||||
|
|
||||||
// Clear all existing grace periods when a new primary is established
|
// Clear grace periods when new primary is established
|
||||||
for oldSessionID := range sm.reconnectGrace {
|
for oldSessionID := range sm.reconnectGrace {
|
||||||
delete(sm.reconnectGrace, oldSessionID)
|
delete(sm.reconnectGrace, oldSessionID)
|
||||||
}
|
}
|
||||||
|
|
@ -346,7 +332,6 @@ 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{}{
|
||||||
|
|
@ -357,12 +342,8 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -624,12 +605,10 @@ 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,11 +42,18 @@ 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(() => import("@/components/UpdateInProgressStatusCard"));
|
const UpdateInProgressStatusCard = lazy(
|
||||||
|
() => import("@/components/UpdateInProgressStatusCard"),
|
||||||
|
);
|
||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
import { JsonRpcRequest, JsonRpcResponse, RpcMethodNotFound, useJsonRpc } from "@/hooks/useJsonRpc";
|
import {
|
||||||
|
JsonRpcRequest,
|
||||||
|
JsonRpcResponse,
|
||||||
|
RpcMethodNotFound,
|
||||||
|
useJsonRpc,
|
||||||
|
} from "@/hooks/useJsonRpc";
|
||||||
import {
|
import {
|
||||||
ConnectionFailedOverlay,
|
ConnectionFailedOverlay,
|
||||||
LoadingConnectionOverlay,
|
LoadingConnectionOverlay,
|
||||||
|
|
@ -135,15 +142,25 @@ 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 { sidebarView, setSidebarView, disableVideoFocusTrap, setDisableVideoFocusTrap, rebootState, setRebootState } = useUiStore();
|
const {
|
||||||
|
sidebarView,
|
||||||
|
setSidebarView,
|
||||||
|
disableVideoFocusTrap,
|
||||||
|
setDisableVideoFocusTrap,
|
||||||
|
rebootState,
|
||||||
|
setRebootState,
|
||||||
|
} = useUiStore();
|
||||||
const [queryParams, setQueryParams] = useSearchParams();
|
const [queryParams, setQueryParams] = useSearchParams();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
peerConnection, setPeerConnection,
|
peerConnection,
|
||||||
peerConnectionState, setPeerConnectionState,
|
setPeerConnection,
|
||||||
|
peerConnectionState,
|
||||||
|
setPeerConnectionState,
|
||||||
setMediaStream,
|
setMediaStream,
|
||||||
setRpcDataChannel,
|
setRpcDataChannel,
|
||||||
isTurnServerInUse, setTurnServerInUse,
|
isTurnServerInUse,
|
||||||
|
setTurnServerInUse,
|
||||||
rpcDataChannel,
|
rpcDataChannel,
|
||||||
setTransceiver,
|
setTransceiver,
|
||||||
setRpcHidChannel,
|
setRpcHidChannel,
|
||||||
|
|
@ -162,12 +179,14 @@ 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<{requireApproval: boolean, requireNickname: boolean} | null>(null);
|
const [globalSessionSettings, setGlobalSessionSettings] = useState<{
|
||||||
|
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);
|
||||||
|
|
@ -264,7 +283,7 @@ export default function KvmIdRoute() {
|
||||||
},
|
},
|
||||||
|
|
||||||
onClose(_event) {
|
onClose(_event) {
|
||||||
// We don't want to close everything down, we wait for the reconnect to stop instead
|
// Handled by onReconnectStop instead
|
||||||
},
|
},
|
||||||
|
|
||||||
onError(event) {
|
onError(event) {
|
||||||
|
|
@ -309,7 +328,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);
|
||||||
|
|
@ -318,7 +337,6 @@ 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;
|
||||||
|
|
@ -342,7 +360,10 @@ export default function KvmIdRoute() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!peerConnection) {
|
if (!peerConnection) {
|
||||||
console.warn("[Websocket] Ignoring message because peerConnection is not ready:", parsedMessage.type);
|
console.warn(
|
||||||
|
"[Websocket] Ignoring message because peerConnection is not ready:",
|
||||||
|
parsedMessage.type,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (parsedMessage.type === "answer") {
|
if (parsedMessage.type === "answer") {
|
||||||
|
|
@ -366,15 +387,18 @@ 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 (parsedMessage.requireNickname !== undefined && parsedMessage.requireApproval !== undefined) {
|
if (
|
||||||
|
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);
|
||||||
|
|
@ -385,8 +409,10 @@ 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 = parsedMessage.nickname && parsedMessage.nickname.length > 0;
|
const hasNickname =
|
||||||
const requiresNickname = parsedMessage.requireNickname || globalSessionSettings?.requireNickname;
|
parsedMessage.nickname && parsedMessage.nickname.length > 0;
|
||||||
|
const requiresNickname =
|
||||||
|
parsedMessage.requireNickname || globalSessionSettings?.requireNickname;
|
||||||
|
|
||||||
if (requiresNickname && !hasNickname) {
|
if (requiresNickname && !hasNickname) {
|
||||||
setShowNicknameModal(true);
|
setShowNicknameModal(true);
|
||||||
|
|
@ -427,18 +453,22 @@ export default function KvmIdRoute() {
|
||||||
peerConnection?.iceConnectionState === "connected";
|
peerConnection?.iceConnectionState === "connected";
|
||||||
|
|
||||||
if (!isConnectionHealthy) {
|
if (!isConnectionHealthy) {
|
||||||
console.log(`[Websocket] Mode changed to ${newMode}, connection unhealthy, reconnecting...`);
|
console.log(
|
||||||
|
`[Websocket] Mode changed to ${newMode}, connection unhealthy, reconnecting...`,
|
||||||
|
);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
peerConnection?.close();
|
peerConnection?.close();
|
||||||
setupPeerConnection();
|
setupPeerConnection();
|
||||||
}, 500);
|
}, 500);
|
||||||
} else {
|
} else {
|
||||||
console.log(`[Websocket] Mode changed to ${newMode}, connection healthy, skipping reconnect`);
|
console.log(
|
||||||
|
`[Websocket] Mode changed to ${newMode}, connection healthy, skipping reconnect`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const sendWebRTCSignal = useCallback(
|
const sendWebRTCSignal = useCallback(
|
||||||
|
|
@ -540,8 +570,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) {
|
||||||
|
|
@ -605,10 +635,13 @@ export default function KvmIdRoute() {
|
||||||
setRpcHidUnreliableChannel(rpcHidUnreliableChannel);
|
setRpcHidUnreliableChannel(rpcHidUnreliableChannel);
|
||||||
};
|
};
|
||||||
|
|
||||||
const rpcHidUnreliableNonOrderedChannel = pc.createDataChannel("hidrpc-unreliable-nonordered", {
|
const rpcHidUnreliableNonOrderedChannel = pc.createDataChannel(
|
||||||
ordered: false,
|
"hidrpc-unreliable-nonordered",
|
||||||
maxRetransmits: 0,
|
{
|
||||||
});
|
ordered: false,
|
||||||
|
maxRetransmits: 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
rpcHidUnreliableNonOrderedChannel.binaryType = "arraybuffer";
|
rpcHidUnreliableNonOrderedChannel.binaryType = "arraybuffer";
|
||||||
rpcHidUnreliableNonOrderedChannel.onopen = () => {
|
rpcHidUnreliableNonOrderedChannel.onopen = () => {
|
||||||
setRpcHidUnreliableNonOrderedChannel(rpcHidUnreliableNonOrderedChannel);
|
setRpcHidUnreliableNonOrderedChannel(rpcHidUnreliableNonOrderedChannel);
|
||||||
|
|
@ -699,19 +732,24 @@ export default function KvmIdRoute() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fire and forget
|
// Fire and forget
|
||||||
api.POST(`${CLOUD_API}/webrtc/turn_activity`, {
|
api
|
||||||
bytesReceived: bytesReceivedDelta,
|
.POST(`${CLOUD_API}/webrtc/turn_activity`, {
|
||||||
bytesSent: bytesSentDelta,
|
bytesReceived: bytesReceivedDelta,
|
||||||
}).catch(() => {
|
bytesSent: bytesSentDelta,
|
||||||
// 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, setKeyboardLedState,
|
keyboardLedState,
|
||||||
keysDownState, setKeysDownState, setUsbState,
|
setKeyboardLedState,
|
||||||
|
keysDownState,
|
||||||
|
setKeysDownState,
|
||||||
|
setUsbState,
|
||||||
} = useHidStore();
|
} = useHidStore();
|
||||||
const setHidRpcDisabled = useRTCStore(state => state.setHidRpcDisabled);
|
const setHidRpcDisabled = useRTCStore(state => state.setHidRpcDisabled);
|
||||||
|
|
||||||
|
|
@ -720,15 +758,17 @@ export default function KvmIdRoute() {
|
||||||
|
|
||||||
function onJsonRpcRequest(resp: JsonRpcRequest) {
|
function onJsonRpcRequest(resp: JsonRpcRequest) {
|
||||||
// Handle session-related events
|
// Handle session-related events
|
||||||
if (resp.method === "sessionsUpdated" ||
|
if (
|
||||||
resp.method === "modeChanged" ||
|
resp.method === "sessionsUpdated" ||
|
||||||
resp.method === "connectionModeChanged" ||
|
resp.method === "modeChanged" ||
|
||||||
resp.method === "otherSessionConnected" ||
|
resp.method === "connectionModeChanged" ||
|
||||||
resp.method === "primaryControlRequested" ||
|
resp.method === "otherSessionConnected" ||
|
||||||
resp.method === "primaryControlApproved" ||
|
resp.method === "primaryControlRequested" ||
|
||||||
resp.method === "primaryControlDenied" ||
|
resp.method === "primaryControlApproved" ||
|
||||||
resp.method === "newSessionPending" ||
|
resp.method === "primaryControlDenied" ||
|
||||||
resp.method === "sessionAccessDenied") {
|
resp.method === "newSessionPending" ||
|
||||||
|
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
|
||||||
|
|
@ -809,7 +849,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();
|
||||||
|
|
@ -823,7 +863,13 @@ 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);
|
||||||
|
|
||||||
|
|
@ -842,7 +888,15 @@ 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);
|
||||||
|
|
||||||
|
|
@ -854,7 +908,10 @@ 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("Failed to get key down state, switching to old-school", resp.error);
|
console.warn(
|
||||||
|
"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);
|
||||||
|
|
@ -865,7 +922,16 @@ 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(() => {
|
||||||
|
|
@ -910,7 +976,9 @@ export default function KvmIdRoute() {
|
||||||
|
|
||||||
// Rebooting takes priority over connection status
|
// Rebooting takes priority over connection status
|
||||||
if (rebootState?.isRebooting) {
|
if (rebootState?.isRebooting) {
|
||||||
return <RebootingOverlay show={true} postRebootAction={rebootState.postRebootAction} />;
|
return (
|
||||||
|
<RebootingOverlay show={true} postRebootAction={rebootState.postRebootAction} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasConnectionFailed =
|
const hasConnectionFailed =
|
||||||
|
|
@ -937,184 +1005,186 @@ 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",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<UpdateInProgressStatusCard />
|
<div className="absolute top-0">
|
||||||
</motion.div>
|
<button className="absolute top-0" tabIndex={-1} id="videoFocusTrap" />
|
||||||
</AnimatePresence>
|
</div>
|
||||||
)}
|
</FocusTrap>
|
||||||
<div className="relative h-full">
|
|
||||||
<FocusTrap
|
|
||||||
paused={disableVideoFocusTrap}
|
|
||||||
focusTrapOptions={{
|
|
||||||
allowOutsideClick: true,
|
|
||||||
escapeDeactivates: false,
|
|
||||||
fallbackFocus: "#videoFocusTrap",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="absolute top-0">
|
|
||||||
<button className="absolute top-0" tabIndex={-1} id="videoFocusTrap" />
|
|
||||||
</div>
|
|
||||||
</FocusTrap>
|
|
||||||
|
|
||||||
<div className="grid h-full grid-rows-(--grid-headerBody) select-none">
|
<div className="grid h-full grid-rows-(--grid-headerBody) select-none">
|
||||||
<DashboardNavbar
|
<DashboardNavbar
|
||||||
primaryLinks={isOnDevice ? [] : [{ title: "Cloud Devices", to: "/devices" }]}
|
primaryLinks={
|
||||||
showConnectionStatus={true}
|
isOnDevice ? [] : [{ title: "Cloud Devices", to: "/devices" }]
|
||||||
isLoggedIn={authMode === "password" || !!user}
|
}
|
||||||
userEmail={user?.email}
|
showConnectionStatus={true}
|
||||||
picture={user?.picture}
|
isLoggedIn={authMode === "password" || !!user}
|
||||||
kvmName={deviceName ?? "JetKVM Device"}
|
userEmail={user?.email}
|
||||||
/>
|
picture={user?.picture}
|
||||||
|
kvmName={deviceName ?? "JetKVM Device"}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="relative flex h-full w-full overflow-hidden">
|
||||||
<div className="relative flex h-full w-full overflow-hidden">
|
{/* Only show video feed if nickname is set (when required) and not pending approval */}
|
||||||
{/* Only show video feed if nickname is set (when required) and not pending approval */}
|
{!showNicknameModal && currentMode !== "pending" ? (
|
||||||
{(!showNicknameModal && currentMode !== "pending") ? (
|
<>
|
||||||
<>
|
<WebRTCVideo hasConnectionIssues={!!ConnectionStatusElement} />
|
||||||
<WebRTCVideo hasConnectionIssues={!!ConnectionStatusElement} />
|
<div
|
||||||
<div
|
style={{ animationDuration: "500ms" }}
|
||||||
style={{ animationDuration: "500ms" }}
|
className="pointer-events-none absolute inset-0 flex animate-slideUpFade items-center justify-center p-4"
|
||||||
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">
|
||||||
<div className="relative h-full max-h-[720px] w-full max-w-[1280px] rounded-md">
|
{!!ConnectionStatusElement && ConnectionStatusElement}
|
||||||
{!!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>
|
||||||
</div>
|
</div>
|
||||||
</>
|
)}
|
||||||
) : (
|
<SidebarContainer sidebarView={sidebarView} />
|
||||||
<div className="flex-1 bg-slate-900 flex items-center justify-center">
|
</div>
|
||||||
<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>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="z-50"
|
className="z-50"
|
||||||
onClick={e => e.stopPropagation()}
|
onClick={e => e.stopPropagation()}
|
||||||
onMouseUp={e => e.stopPropagation()}
|
onMouseUp={e => e.stopPropagation()}
|
||||||
onMouseDown={e => e.stopPropagation()}
|
onMouseDown={e => e.stopPropagation()}
|
||||||
onKeyUp={e => e.stopPropagation()}
|
onKeyUp={e => e.stopPropagation()}
|
||||||
onKeyDown={e => {
|
onKeyDown={e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (e.key === "Escape") navigateTo("/");
|
if (e.key === "Escape") navigateTo("/");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Modal open={outlet !== null} onClose={onModalClose}>
|
<Modal open={outlet !== null} onClose={onModalClose}>
|
||||||
{/* The 'used by other session' modal needs to have access to the connectWebRTC function */}
|
{/* The 'used by other session' modal needs to have access to the connectWebRTC function */}
|
||||||
<Outlet context={{ setupPeerConnection }} />
|
<Outlet context={{ setupPeerConnection }} />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<NicknameModal
|
<NicknameModal
|
||||||
isOpen={showNicknameModal}
|
isOpen={showNicknameModal}
|
||||||
onSubmit={async (nickname) => {
|
onSubmit={async nickname => {
|
||||||
setNickname(nickname);
|
setNickname(nickname);
|
||||||
setShowNicknameModal(false);
|
setShowNicknameModal(false);
|
||||||
setDisableVideoFocusTrap(false);
|
setDisableVideoFocusTrap(false);
|
||||||
|
|
||||||
if (currentSessionId && send) {
|
if (currentSessionId && send) {
|
||||||
try {
|
try {
|
||||||
await sessionApi.updateNickname(send, currentSessionId, nickname);
|
await sessionApi.updateNickname(send, currentSessionId, nickname);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to update nickname:", error);
|
console.error("Failed to update nickname:", error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}}
|
||||||
|
onSkip={() => {
|
||||||
|
setShowNicknameModal(false);
|
||||||
|
setDisableVideoFocusTrap(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{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);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onSkip={() => {
|
|
||||||
setShowNicknameModal(false);
|
|
||||||
setDisableVideoFocusTrap(false);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
{kvmTerminal && (
|
<PendingApprovalOverlay show={currentMode === "pending"} />
|
||||||
<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