- Add hardware fingerprinting with cross-platform support - Implement secure device registration flow with X.509 certificates - Add WebSocket real-time communication for device status - Create comprehensive device management dashboard - Establish zero-trust security architecture with multi-layer protection - Add database migrations for device registration entities - Implement Rust edge client with hardware identification - Add certificate management and automated provisioning system 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
382 lines
12 KiB
TypeScript
382 lines
12 KiB
TypeScript
"use client"
|
|
|
|
import React, { createContext, useContext, useReducer, useEffect, useCallback } from 'react'
|
|
import {
|
|
DeviceRegistrationSession,
|
|
RegistrationStatus,
|
|
DeviceDto,
|
|
InitiateRegistrationRequest,
|
|
WebSocketDeviceEvent
|
|
} from '../types/device'
|
|
import { devicesApi } from '../services/devices'
|
|
import { deviceWebSocketService, WebSocketEventHandlers } from '../services/websocket'
|
|
import { useAuth } from './auth-context'
|
|
|
|
// State interface
|
|
interface DeviceRegistrationState {
|
|
currentSession: DeviceRegistrationSession | null
|
|
registeredDevices: DeviceDto[]
|
|
isLoading: boolean
|
|
error: string | null
|
|
wsConnected: boolean
|
|
qrCodeUrl: string | null
|
|
}
|
|
|
|
// Action types
|
|
type DeviceRegistrationAction =
|
|
| { type: 'SET_LOADING'; payload: boolean }
|
|
| { type: 'SET_ERROR'; payload: string | null }
|
|
| { type: 'SET_CURRENT_SESSION'; payload: DeviceRegistrationSession | null }
|
|
| { type: 'SET_REGISTERED_DEVICES'; payload: DeviceDto[] }
|
|
| { type: 'ADD_DEVICE'; payload: DeviceDto }
|
|
| { type: 'UPDATE_DEVICE'; payload: DeviceDto }
|
|
| { type: 'REMOVE_DEVICE'; payload: string }
|
|
| { type: 'SET_WS_CONNECTED'; payload: boolean }
|
|
| { type: 'SET_QR_CODE_URL'; payload: string | null }
|
|
| { type: 'UPDATE_SESSION_STATUS'; payload: RegistrationStatus }
|
|
| { type: 'RESET_STATE' }
|
|
|
|
// Initial state
|
|
const initialState: DeviceRegistrationState = {
|
|
currentSession: null,
|
|
registeredDevices: [],
|
|
isLoading: false,
|
|
error: null,
|
|
wsConnected: false,
|
|
qrCodeUrl: null,
|
|
}
|
|
|
|
// Reducer
|
|
function deviceRegistrationReducer(
|
|
state: DeviceRegistrationState,
|
|
action: DeviceRegistrationAction
|
|
): DeviceRegistrationState {
|
|
switch (action.type) {
|
|
case 'SET_LOADING':
|
|
return { ...state, isLoading: action.payload }
|
|
|
|
case 'SET_ERROR':
|
|
return { ...state, error: action.payload, isLoading: false }
|
|
|
|
case 'SET_CURRENT_SESSION':
|
|
return { ...state, currentSession: action.payload }
|
|
|
|
case 'SET_REGISTERED_DEVICES':
|
|
return { ...state, registeredDevices: action.payload }
|
|
|
|
case 'ADD_DEVICE':
|
|
return {
|
|
...state,
|
|
registeredDevices: [...state.registeredDevices, action.payload]
|
|
}
|
|
|
|
case 'UPDATE_DEVICE':
|
|
return {
|
|
...state,
|
|
registeredDevices: state.registeredDevices.map(device =>
|
|
device.id === action.payload.id ? action.payload : device
|
|
)
|
|
}
|
|
|
|
case 'REMOVE_DEVICE':
|
|
return {
|
|
...state,
|
|
registeredDevices: state.registeredDevices.filter(
|
|
device => device.id !== action.payload
|
|
)
|
|
}
|
|
|
|
case 'SET_WS_CONNECTED':
|
|
return { ...state, wsConnected: action.payload }
|
|
|
|
case 'SET_QR_CODE_URL':
|
|
return { ...state, qrCodeUrl: action.payload }
|
|
|
|
case 'UPDATE_SESSION_STATUS':
|
|
return {
|
|
...state,
|
|
currentSession: state.currentSession
|
|
? { ...state.currentSession, status: action.payload }
|
|
: null
|
|
}
|
|
|
|
case 'RESET_STATE':
|
|
return initialState
|
|
|
|
default:
|
|
return state
|
|
}
|
|
}
|
|
|
|
// Context interface
|
|
interface DeviceRegistrationContextType {
|
|
state: DeviceRegistrationState
|
|
|
|
// Registration actions
|
|
initiateRegistration: (request: InitiateRegistrationRequest) => Promise<void>
|
|
cancelRegistration: () => Promise<void>
|
|
refreshSession: () => Promise<void>
|
|
|
|
// Device management actions
|
|
refreshDevices: () => Promise<void>
|
|
updateDevice: (deviceId: string, updates: Partial<DeviceDto>) => Promise<void>
|
|
deleteDevice: (deviceId: string) => Promise<void>
|
|
|
|
// WebSocket actions
|
|
connectWebSocket: () => Promise<void>
|
|
disconnectWebSocket: () => void
|
|
|
|
// Utility actions
|
|
clearError: () => void
|
|
resetState: () => void
|
|
}
|
|
|
|
const DeviceRegistrationContext = createContext<DeviceRegistrationContextType | undefined>(undefined)
|
|
|
|
// Provider component
|
|
interface DeviceRegistrationProviderProps {
|
|
children: React.ReactNode
|
|
}
|
|
|
|
export function DeviceRegistrationProvider({ children }: DeviceRegistrationProviderProps) {
|
|
const [state, dispatch] = useReducer(deviceRegistrationReducer, initialState)
|
|
const { user, isAuthenticated } = useAuth()
|
|
|
|
// WebSocket event handlers
|
|
const wsEventHandlers: WebSocketEventHandlers = {
|
|
onConnect: () => {
|
|
dispatch({ type: 'SET_WS_CONNECTED', payload: true })
|
|
},
|
|
|
|
onDisconnect: () => {
|
|
dispatch({ type: 'SET_WS_CONNECTED', payload: false })
|
|
},
|
|
|
|
onError: (error) => {
|
|
console.error('WebSocket error:', error)
|
|
dispatch({ type: 'SET_ERROR', payload: error.message })
|
|
},
|
|
|
|
onDeviceConnected: (data) => {
|
|
if (state.currentSession && data.sessionId === state.currentSession.id) {
|
|
dispatch({ type: 'UPDATE_SESSION_STATUS', payload: RegistrationStatus.DEVICE_CONNECTED })
|
|
}
|
|
},
|
|
|
|
onRegistrationCompleted: (data) => {
|
|
if (state.currentSession && data.sessionId === state.currentSession.id) {
|
|
dispatch({ type: 'UPDATE_SESSION_STATUS', payload: RegistrationStatus.COMPLETED })
|
|
|
|
// Add the new device to the list
|
|
if (data.device) {
|
|
dispatch({ type: 'ADD_DEVICE', payload: data.device })
|
|
}
|
|
|
|
// Clear current session after a delay
|
|
setTimeout(() => {
|
|
dispatch({ type: 'SET_CURRENT_SESSION', payload: null })
|
|
dispatch({ type: 'SET_QR_CODE_URL', payload: null })
|
|
}, 2000)
|
|
}
|
|
},
|
|
|
|
onRegistrationFailed: (data) => {
|
|
if (state.currentSession && data.sessionId === state.currentSession.id) {
|
|
dispatch({ type: 'UPDATE_SESSION_STATUS', payload: RegistrationStatus.FAILED })
|
|
dispatch({ type: 'SET_ERROR', payload: data.error || 'Registration failed' })
|
|
}
|
|
},
|
|
|
|
onDeviceStatusChanged: (data) => {
|
|
// Update device status in the list
|
|
dispatch({ type: 'UPDATE_DEVICE', payload: data.device })
|
|
},
|
|
}
|
|
|
|
// Initialize WebSocket event handlers
|
|
useEffect(() => {
|
|
deviceWebSocketService.setEventHandlers(wsEventHandlers)
|
|
}, [state.currentSession?.id])
|
|
|
|
// Connect WebSocket when authenticated
|
|
useEffect(() => {
|
|
if (isAuthenticated && !state.wsConnected) {
|
|
connectWebSocket()
|
|
} else if (!isAuthenticated && state.wsConnected) {
|
|
disconnectWebSocket()
|
|
}
|
|
}, [isAuthenticated])
|
|
|
|
// Load devices on mount when authenticated
|
|
useEffect(() => {
|
|
if (isAuthenticated) {
|
|
refreshDevices()
|
|
}
|
|
}, [isAuthenticated])
|
|
|
|
// Cleanup on unmount
|
|
useEffect(() => {
|
|
return () => {
|
|
disconnectWebSocket()
|
|
}
|
|
}, [])
|
|
|
|
// Actions implementation
|
|
const initiateRegistration = useCallback(async (request: InitiateRegistrationRequest) => {
|
|
dispatch({ type: 'SET_LOADING', payload: true })
|
|
dispatch({ type: 'SET_ERROR', payload: null })
|
|
|
|
try {
|
|
const response = await devicesApi.initiateRegistration(request)
|
|
|
|
// Transform the response into a session object
|
|
const session: DeviceRegistrationSession = {
|
|
id: response.claim_id,
|
|
userProfileId: user?.id || '',
|
|
registrationCode: response.claim_token,
|
|
pinCode: response.fallback_pin,
|
|
status: RegistrationStatus.WAITING_FOR_DEVICE,
|
|
expiresAt: response.expires_at,
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString()
|
|
}
|
|
|
|
dispatch({ type: 'SET_CURRENT_SESSION', payload: session })
|
|
dispatch({ type: 'SET_QR_CODE_URL', payload: response.qr_code_url })
|
|
|
|
// Join the registration session room
|
|
if (state.wsConnected) {
|
|
deviceWebSocketService.joinRegistrationSession(response.claim_id)
|
|
}
|
|
|
|
} catch (error) {
|
|
dispatch({ type: 'SET_ERROR', payload: error instanceof Error ? error.message : 'Failed to initiate registration' })
|
|
} finally {
|
|
dispatch({ type: 'SET_LOADING', payload: false })
|
|
}
|
|
}, [state.wsConnected, user?.id])
|
|
|
|
const cancelRegistration = useCallback(async () => {
|
|
if (!state.currentSession) return
|
|
|
|
dispatch({ type: 'SET_LOADING', payload: true })
|
|
|
|
try {
|
|
await devicesApi.cancelRegistration(state.currentSession.id)
|
|
|
|
// Leave the registration session room
|
|
if (state.wsConnected) {
|
|
deviceWebSocketService.leaveRegistrationSession(state.currentSession.id)
|
|
}
|
|
|
|
dispatch({ type: 'SET_CURRENT_SESSION', payload: null })
|
|
dispatch({ type: 'SET_QR_CODE_URL', payload: null })
|
|
|
|
} catch (error) {
|
|
dispatch({ type: 'SET_ERROR', payload: error instanceof Error ? error.message : 'Failed to cancel registration' })
|
|
} finally {
|
|
dispatch({ type: 'SET_LOADING', payload: false })
|
|
}
|
|
}, [state.currentSession?.id, state.wsConnected])
|
|
|
|
const refreshSession = useCallback(async () => {
|
|
if (!state.currentSession) return
|
|
|
|
try {
|
|
const session = await devicesApi.getRegistrationSession(state.currentSession.id)
|
|
dispatch({ type: 'SET_CURRENT_SESSION', payload: session })
|
|
} catch (error) {
|
|
dispatch({ type: 'SET_ERROR', payload: error instanceof Error ? error.message : 'Failed to refresh session' })
|
|
}
|
|
}, [state.currentSession?.id])
|
|
|
|
const refreshDevices = useCallback(async () => {
|
|
if (!isAuthenticated) return
|
|
|
|
try {
|
|
const response = await devicesApi.getDevices()
|
|
dispatch({ type: 'SET_REGISTERED_DEVICES', payload: response.devices })
|
|
} catch (error) {
|
|
dispatch({ type: 'SET_ERROR', payload: error instanceof Error ? error.message : 'Failed to load devices' })
|
|
}
|
|
}, [isAuthenticated])
|
|
|
|
const updateDevice = useCallback(async (deviceId: string, updates: Partial<DeviceDto>) => {
|
|
dispatch({ type: 'SET_LOADING', payload: true })
|
|
|
|
try {
|
|
const updatedDevice = await devicesApi.updateDevice(deviceId, updates)
|
|
dispatch({ type: 'UPDATE_DEVICE', payload: updatedDevice })
|
|
} catch (error) {
|
|
dispatch({ type: 'SET_ERROR', payload: error instanceof Error ? error.message : 'Failed to update device' })
|
|
} finally {
|
|
dispatch({ type: 'SET_LOADING', payload: false })
|
|
}
|
|
}, [])
|
|
|
|
const deleteDevice = useCallback(async (deviceId: string) => {
|
|
dispatch({ type: 'SET_LOADING', payload: true })
|
|
|
|
try {
|
|
await devicesApi.deleteDevice(deviceId)
|
|
dispatch({ type: 'REMOVE_DEVICE', payload: deviceId })
|
|
} catch (error) {
|
|
dispatch({ type: 'SET_ERROR', payload: error instanceof Error ? error.message : 'Failed to delete device' })
|
|
} finally {
|
|
dispatch({ type: 'SET_LOADING', payload: false })
|
|
}
|
|
}, [])
|
|
|
|
const connectWebSocket = useCallback(async () => {
|
|
try {
|
|
const token = localStorage.getItem('accessToken')
|
|
if (!token) throw new Error('No authentication token')
|
|
|
|
await deviceWebSocketService.connect(token)
|
|
} catch (error) {
|
|
dispatch({ type: 'SET_ERROR', payload: error instanceof Error ? error.message : 'Failed to connect to real-time updates' })
|
|
}
|
|
}, [])
|
|
|
|
const disconnectWebSocket = useCallback(() => {
|
|
deviceWebSocketService.disconnect()
|
|
dispatch({ type: 'SET_WS_CONNECTED', payload: false })
|
|
}, [])
|
|
|
|
const clearError = useCallback(() => {
|
|
dispatch({ type: 'SET_ERROR', payload: null })
|
|
}, [])
|
|
|
|
const resetState = useCallback(() => {
|
|
dispatch({ type: 'RESET_STATE' })
|
|
}, [])
|
|
|
|
const contextValue: DeviceRegistrationContextType = {
|
|
state,
|
|
initiateRegistration,
|
|
cancelRegistration,
|
|
refreshSession,
|
|
refreshDevices,
|
|
updateDevice,
|
|
deleteDevice,
|
|
connectWebSocket,
|
|
disconnectWebSocket,
|
|
clearError,
|
|
resetState,
|
|
}
|
|
|
|
return (
|
|
<DeviceRegistrationContext.Provider value={contextValue}>
|
|
{children}
|
|
</DeviceRegistrationContext.Provider>
|
|
)
|
|
}
|
|
|
|
// Hook for using the context
|
|
export function useDeviceRegistration() {
|
|
const context = useContext(DeviceRegistrationContext)
|
|
if (context === undefined) {
|
|
throw new Error('useDeviceRegistration must be used within a DeviceRegistrationProvider')
|
|
}
|
|
return context
|
|
} |