From 6beb41f30c4f73b5b4c4f67a74b6b8eb7d824085 Mon Sep 17 00:00:00 2001 From: Adam Shiervani Date: Thu, 27 Feb 2025 15:11:06 +0100 Subject: [PATCH] refactor(ui): Reorganize device access and security settings This commit introduces several changes to the device access and security settings: - Renamed "Security" section to "Access" in settings navigation - Moved local authentication routes from security to access - Removed deprecated security settings route - Added new route for device access settings with cloud and local authentication management - Updated cloud URL and adoption logic to be part of the access settings - Simplified routing and component structure for better user experience --- jsonrpc.go | 5 + ui/.env.device | 2 +- ui/src/components/Modal.tsx | 4 +- ui/src/components/SelectMenuBasic.tsx | 4 +- ui/src/main.tsx | 25 +- .../devices.$id.settings.access._index.tsx | 330 ++++++++++++++++++ ...evices.$id.settings.access.local-auth.tsx} | 2 +- .../routes/devices.$id.settings.advanced.tsx | 72 ---- .../devices.$id.settings.general._index.tsx | 139 +------- .../devices.$id.settings.security._index.tsx | 72 ---- ui/src/routes/devices.$id.settings.tsx | 34 +- 11 files changed, 376 insertions(+), 313 deletions(-) create mode 100644 ui/src/routes/devices.$id.settings.access._index.tsx rename ui/src/routes/{devices.$id.settings.security.local-auth.tsx => devices.$id.settings.access.local-auth.tsx} (99%) delete mode 100644 ui/src/routes/devices.$id.settings.security._index.tsx diff --git a/jsonrpc.go b/jsonrpc.go index a07d461..9d10cb4 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -780,6 +780,10 @@ func rpcResetCloudUrl() error { return nil } +func rpcGetDefaultCloudUrl() (string, error) { + return defaultConfig.CloudURL, nil +} + var rpcHandlers = map[string]RPCHandler{ "ping": {Func: rpcPing}, "getDeviceID": {Func: rpcGetDeviceID}, @@ -841,4 +845,5 @@ var rpcHandlers = map[string]RPCHandler{ "setCloudUrl": {Func: rpcSetCloudUrl, Params: []string{"url"}}, "getCloudUrl": {Func: rpcGetCloudUrl}, "resetCloudUrl": {Func: rpcResetCloudUrl}, + "getDefaultCloudUrl": {Func: rpcGetDefaultCloudUrl}, } diff --git a/ui/.env.device b/ui/.env.device index 252fac4..c8d7c4f 100644 --- a/ui/.env.device +++ b/ui/.env.device @@ -1,2 +1,2 @@ # Used in settings page to know where to link to when user wants to adopt a device to the cloud -VITE_CLOUD_APP=http://localhost:5173 +VITE_CLOUD_APP=http://app.jetkvm.com diff --git a/ui/src/components/Modal.tsx b/ui/src/components/Modal.tsx index b6dac20..49fb0c3 100644 --- a/ui/src/components/Modal.tsx +++ b/ui/src/components/Modal.tsx @@ -21,11 +21,11 @@ const Modal = React.memo(function Modal({ />
{/* TODO: This doesn't work well with other-sessions */} -
+
; +} & Partial>; const sizes = { XS: "h-[24.5px] pl-3 pr-8 text-xs", @@ -69,7 +69,7 @@ export const SelectMenuBasic = React.forwardRef, }, - - { - path: "local-auth", - element: , - }, { path: "mount", element: , @@ -157,16 +152,16 @@ if (isOnDevice) { element: , }, { - path: "security", + path: "access", children: [ { index: true, - element: , - loader: SettingsSecurityIndexRoute.loader, + element: , + loader: SettingsAccessIndexRoute.loader, }, { path: "local-auth", - element: , + element: , }, ], }, @@ -266,16 +261,16 @@ if (isOnDevice) { element: , }, { - path: "security", + path: "access", children: [ { index: true, - element: , - loader: SettingsSecurityIndexRoute.loader, + element: , + loader: SettingsAccessIndexRoute.loader, }, { path: "local-auth", - element: , + element: , }, ], }, diff --git a/ui/src/routes/devices.$id.settings.access._index.tsx b/ui/src/routes/devices.$id.settings.access._index.tsx new file mode 100644 index 0000000..7d16005 --- /dev/null +++ b/ui/src/routes/devices.$id.settings.access._index.tsx @@ -0,0 +1,330 @@ +import { SectionHeader } from "@/components/SectionHeader"; +import { SettingsItem } from "./devices.$id.settings"; +import { useLoaderData } from "react-router-dom"; +import { Button, LinkButton } from "../components/Button"; +import { CLOUD_APP, DEVICE_API } from "../ui.config"; +import api from "../api"; +import { LocalDevice } from "./devices.$id"; +import { useDeviceUiNavigation } from "../hooks/useAppNavigation"; +import { isOnDevice } from "../main"; +import { GridCard } from "../components/Card"; +import { ShieldCheckIcon } from "@heroicons/react/24/outline"; +import notifications from "../notifications"; +import { useCallback, useEffect, useState } from "react"; +import { useJsonRpc } from "../hooks/useJsonRpc"; +import { InputFieldWithLabel } from "../components/InputField"; +import { SelectMenuBasic } from "../components/SelectMenuBasic"; + +export const loader = async () => { + const status = await api + .GET(`${DEVICE_API}/device`) + .then(res => res.json() as Promise); + return status; +}; + +export default function SettingsAccessIndexRoute() { + const { authMode } = useLoaderData() as LocalDevice; + const { navigateTo } = useDeviceUiNavigation(); + + const [send] = useJsonRpc(); + + const [isAdopted, setAdopted] = useState(false); + const [deviceId, setDeviceId] = useState(null); + const [cloudUrl, setCloudUrl] = useState(""); + const [cloudProviders, setCloudProviders] = useState< + { value: string; label: string }[] | null + >([{ value: "https://api.jetkvm.com", label: "JetKVM Cloud" }]); + + // The default value is just there so it doesn't flicker while we fetch the default Cloud URL and available providers + const [selectedUrlOption, setSelectedUrlOption] = useState( + "https://api.jetkvm.com", + ); + + const [defaultCloudUrl, setDefaultCloudUrl] = useState(""); + + const syncCloudUrl = useCallback(() => { + send("getCloudUrl", {}, resp => { + if ("error" in resp) return; + const url = resp.result as string; + setCloudUrl(url); + // Check if the URL matches any predefined option + if (cloudProviders?.some(provider => provider.value === url)) { + setSelectedUrlOption(url); + } else { + setSelectedUrlOption("custom"); + // setCustomCloudUrl(url); + } + }); + }, [cloudProviders, send]); + + const getCloudState = useCallback(() => { + send("getCloudState", {}, resp => { + if ("error" in resp) return console.error(resp.error); + const cloudState = resp.result as { connected: boolean }; + setAdopted(cloudState.connected); + }); + }, [send]); + + const deregisterDevice = async () => { + send("deregisterDevice", {}, resp => { + if ("error" in resp) { + notifications.error( + `Failed to de-register device: ${resp.error.data || "Unknown error"}`, + ); + return; + } + getCloudState(); + return; + }); + }; + + const onCloudAdoptClick = useCallback( + (url: string) => { + if (!deviceId) { + notifications.error("No device ID available"); + return; + } + + send("setCloudUrl", { url }, resp => { + if ("error" in resp) { + notifications.error( + `Failed to update cloud URL: ${resp.error.data || "Unknown error"}`, + ); + return; + } + syncCloudUrl(); + notifications.success("Cloud URL updated successfully"); + + const returnTo = new URL(window.location.href); + returnTo.pathname = "/adopt"; + returnTo.search = ""; + returnTo.hash = ""; + window.location.href = + CLOUD_APP + "/signup?deviceId=" + deviceId + `&returnTo=${returnTo.toString()}`; + }); + }, + [deviceId, syncCloudUrl, send], + ); + + useEffect(() => { + if (!defaultCloudUrl) return; + setSelectedUrlOption(defaultCloudUrl); + setCloudProviders([ + { value: defaultCloudUrl, label: "JetKVM Cloud" }, + { value: "custom", label: "Custom" }, + ]); + }, [defaultCloudUrl]); + + useEffect(() => { + getCloudState(); + + send("getDeviceID", {}, async resp => { + if ("error" in resp) return console.error(resp.error); + setDeviceId(resp.result as string); + }); + }, [send, getCloudState]); + + useEffect(() => { + send("getDefaultCloudUrl", {}, resp => { + if ("error" in resp) return console.error(resp.error); + setDefaultCloudUrl(resp.result as string); + }); + }, [cloudProviders, syncCloudUrl, send]); + + useEffect(() => { + if (!cloudProviders?.length) return; + syncCloudUrl(); + }, [cloudProviders, syncCloudUrl]); + + console.log("is adopted:", isAdopted); + + return ( +
+ + +
+ + + {authMode === "password" ? ( +
+
+
+ + + {isOnDevice && ( + <> +
+ {!isAdopted && ( + <> + + { + const value = e.target.value; + setSelectedUrlOption(value); + }} + options={cloudProviders ?? []} + /> + + + {selectedUrlOption === "custom" && ( +
+ setCloudUrl(e.target.value)} + placeholder="https://api.example.com" + /> +
+ )} + + )} + + {/* + We do the harcoding here to avoid flickering when the default Cloud URL being fetched. + I've tried to avoid harcoding api.jetkvm.com, but it's the only reasonable way I could think of to avoid flickering for now. + */} + {selectedUrlOption === (defaultCloudUrl || "https://api.jetkvm.com") && ( + +
+ +
+
+

+ Cloud Security +

+
+
    +
  • End-to-end encryption using WebRTC (DTLS and SRTP)
  • +
  • Zero Trust security model
  • +
  • OIDC (OpenID Connect) authentication
  • +
  • All streams encrypted in transit
  • +
+
+ +
+ All cloud components are open-source and available on{" "} + + GitHub + + . +
+
+
+ +
+ +
+
+
+
+ )} + + {!isAdopted ? ( +
+
+ ) : ( +
+
+

+ Your device is adopted to JetKVM Cloud +

+
+
+
+
+ )} +
+ + )} +
+
+ ); +} diff --git a/ui/src/routes/devices.$id.settings.security.local-auth.tsx b/ui/src/routes/devices.$id.settings.access.local-auth.tsx similarity index 99% rename from ui/src/routes/devices.$id.settings.security.local-auth.tsx rename to ui/src/routes/devices.$id.settings.access.local-auth.tsx index 702bfba..4f07bd8 100644 --- a/ui/src/routes/devices.$id.settings.security.local-auth.tsx +++ b/ui/src/routes/devices.$id.settings.access.local-auth.tsx @@ -6,7 +6,7 @@ import { useLocalAuthModalStore } from "@/hooks/stores"; import { useLocation, useRevalidator } from "react-router-dom"; import { useDeviceUiNavigation } from "@/hooks/useAppNavigation"; -export default function LocalAuthRoute() { +export default function SecurityAccessLocalAuthRoute() { const { setModalView } = useLocalAuthModalStore(); const { navigateTo } = useDeviceUiNavigation(); const location = useLocation(); diff --git a/ui/src/routes/devices.$id.settings.advanced.tsx b/ui/src/routes/devices.$id.settings.advanced.tsx index ae8996e..a46c8c4 100644 --- a/ui/src/routes/devices.$id.settings.advanced.tsx +++ b/ui/src/routes/devices.$id.settings.advanced.tsx @@ -8,7 +8,6 @@ import { useCallback, useState, useEffect } from "react"; import notifications from "../notifications"; import { TextAreaWithLabel } from "../components/TextArea"; import { isOnDevice } from "../main"; -import { InputFieldWithLabel } from "../components/InputField"; import { Button } from "../components/Button"; import { useSettingsStore } from "../hooks/stores"; import { GridCard } from "@components/Card"; @@ -16,16 +15,11 @@ import { GridCard } from "@components/Card"; export default function SettingsAdvancedRoute() { const [send] = useJsonRpc(); - const [cloudUrl, setCloudUrl] = useState(""); const [sshKey, setSSHKey] = useState(""); const setDeveloperMode = useSettingsStore(state => state.setDeveloperMode); const settings = useSettingsStore(); useEffect(() => { - send("getCloudUrl", {}, resp => { - if ("error" in resp) return; - setCloudUrl(resp.result as string); - }); send("getDevModeState", {}, resp => { if ("error" in resp) return; @@ -51,12 +45,6 @@ export default function SettingsAdvancedRoute() { }); }, [send]); - const getCloudUrl = useCallback(() => { - send("getCloudUrl", {}, resp => { - if ("error" in resp) return; - setCloudUrl(resp.result as string); - }); - }, [send]); const handleUsbEmulationToggle = useCallback( (enabled: boolean) => { @@ -74,35 +62,6 @@ export default function SettingsAdvancedRoute() { [getUsbEmulationState, send], ); - const handleCloudUrlChange = useCallback( - (url: string) => { - send("setCloudUrl", { url }, resp => { - if ("error" in resp) { - notifications.error( - `Failed to update cloud URL: ${resp.error.data || "Unknown error"}`, - ); - return; - } - getCloudUrl(); - notifications.success("Cloud URL updated successfully"); - }); - }, - [send, getCloudUrl], - ); - - const handleResetCloudUrl = useCallback(() => { - send("resetCloudUrl", {}, resp => { - if ("error" in resp) { - notifications.error( - `Failed to reset cloud URL: ${resp.error.data || "Unknown error"}`, - ); - return; - } - getCloudUrl(); - notifications.success("Cloud URL reset to default successfully"); - }); - }, [send, getCloudUrl]); - const handleResetConfig = useCallback(() => { send("resetConfig", {}, resp => { if ("error" in resp) { @@ -244,37 +203,6 @@ export default function SettingsAdvancedRoute() { )} - {isOnDevice && settings.developerMode && ( -
- - - setCloudUrl(e.target.value)} - placeholder="https://api.jetkvm.com" - /> - -
-
-
- )} {isOnDevice && settings.developerMode && (
(null); - const [isAdopted, setAdopted] = useState(false); const [currentVersions, setCurrentVersions] = useState<{ appVersion: string; systemVersion: string; } | null>(null); - const getCloudState = useCallback(() => { - send("getCloudState", {}, resp => { - if ("error" in resp) return console.error(resp.error); - const cloudState = resp.result as { connected: boolean }; - setAdopted(cloudState.connected); - }); - }, [send]); - - const deregisterDevice = async () => { - send("deregisterDevice", {}, resp => { - if ("error" in resp) { - notifications.error( - `Failed to de-register device: ${resp.error.data || "Unknown error"}`, - ); - return; - } - getCloudState(); - return; - }); - }; - const getCurrentVersions = useCallback(() => { send("getUpdateStatus", {}, resp => { if ("error" in resp) return; @@ -61,12 +34,6 @@ export default function SettingsGeneralRoute() { useEffect(() => { getCurrentVersions(); - getCloudState(); - send("getDeviceID", {}, async resp => { - if ("error" in resp) return console.error(resp.error); - setDeviceId(resp.result as string); - }); - send("getAutoUpdateState", {}, resp => { if ("error" in resp) return; setAutoUpdate(resp.result as boolean); @@ -76,7 +43,7 @@ export default function SettingsGeneralRoute() { if ("error" in resp) return; setDevChannel(resp.result as boolean); }); - }, [getCurrentVersions, getCloudState, send]); + }, [getCurrentVersions, send]); const handleAutoUpdateChange = (enabled: boolean) => { send("setAutoUpdateState", { enabled }, resp => { @@ -164,108 +131,6 @@ export default function SettingsGeneralRoute() {
- - {isOnDevice && ( - <> -
- -
- - - -
- -
-
-

- Cloud Security -

-
-
    -
  • End-to-end encryption using WebRTC (DTLS and SRTP)
  • -
  • Zero Trust security model
  • -
  • OIDC (OpenID Connect) authentication
  • -
  • All streams encrypted in transit
  • -
-
- -
- All cloud components are open-source and available on{" "} - - GitHub - - . -
-
-
- -
- -
-
-
-
- - {!isAdopted ? ( -
- -
- ) : ( -
-
-

- Your device is adopted to JetKVM Cloud -

-
-
-
-
- )} -
- - )}
); diff --git a/ui/src/routes/devices.$id.settings.security._index.tsx b/ui/src/routes/devices.$id.settings.security._index.tsx deleted file mode 100644 index d2856e7..0000000 --- a/ui/src/routes/devices.$id.settings.security._index.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { SectionHeader } from "@/components/SectionHeader"; -import { SettingsItem } from "./devices.$id.settings"; -import { useLoaderData } from "react-router-dom"; -import { Button } from "../components/Button"; -import { DEVICE_API } from "../ui.config"; -import api from "../api"; -import { LocalDevice } from "./devices.$id"; -import { useDeviceUiNavigation } from "../hooks/useAppNavigation"; - -export const loader = async () => { - const status = await api - .GET(`${DEVICE_API}/device`) - .then(res => res.json() as Promise); - return status; -}; - -export default function SettingsSecurityIndexRoute() { - const { authMode } = useLoaderData() as LocalDevice; - const { navigateTo } = useDeviceUiNavigation(); - - return ( -
- - -
- - {authMode === "password" ? ( -
-
- ); -} diff --git a/ui/src/routes/devices.$id.settings.tsx b/ui/src/routes/devices.$id.settings.tsx index d26c488..6084afb 100644 --- a/ui/src/routes/devices.$id.settings.tsx +++ b/ui/src/routes/devices.$id.settings.tsx @@ -79,15 +79,27 @@ export default function SettingsRoute() {
- +
+ +
+
+ +
{/* Gradient overlay for left side - only visible on mobile when scrolled */} @@ -160,12 +172,12 @@ export default function SettingsRoute() {
(isActive ? "active" : "")} >
-

Security

+

Access