Settings Access page

This commit is contained in:
Marc Brooks 2025-10-13 16:06:48 -05:00
parent 567a6d5cbc
commit c6cb2e9cb6
No known key found for this signature in database
GPG Key ID: 583A6AF2D6AE1DC6
3 changed files with 110 additions and 62 deletions

View File

@ -457,5 +457,53 @@
"wake_on_lan_magic_sent_success": "Magic Packet sent successfully", "wake_on_lan_magic_sent_success": "Magic Packet sent successfully",
"wake_on_lan": "Wake On LAN", "wake_on_lan": "Wake On LAN",
"welcome_to_jetkvm_description": "Control any computer remotely", "welcome_to_jetkvm_description": "Control any computer remotely",
"welcome_to_jetkvm": "Welcome to JetKVM" "welcome_to_jetkvm": "Welcome to JetKVM",
"access_adopt_kvm": "Adopt KVM to Cloud",
"access_adopted_message": "Your device is adopted to the Cloud",
"access_auth_mode_no_password": "Current mode: No password",
"access_auth_mode_password": "Current mode: Password protected",
"access_authentication_mode_title": "Authentication Mode",
"access_certificate_label": "Certificate",
"access_change_password_button": "Change Password",
"access_change_password_description": "Update your device access password",
"access_change_password_title": "Change Password",
"access_cloud_api_url_label": "Cloud API URL",
"access_cloud_app_url_label": "Cloud Application URL",
"access_cloud_provider_description": "Select the cloud provider for your device",
"access_cloud_provider_title": "Cloud Provider",
"access_cloud_security_title": "Cloud Security",
"access_confirm_deregister": "Are you sure you want to de-register this device?",
"access_deregister": "De-register from Cloud",
"access_description": "Manage the Access Control of the device",
"access_disable_protection": "Disable Protection",
"access_enable_password": "Enable Password",
"access_failed_deregister": "Failed to de-register device: {error}",
"access_failed_update_cloud_url": "Failed to update cloud URL: {error}",
"access_failed_update_tls": "Failed to update TLS settings: {error}",
"access_github_link": "GitHub",
"access_https_description": "Configure secure HTTPS access to your device",
"access_https_mode_title": "HTTPS Mode",
"access_learn_security": "Learn about our cloud security",
"access_local_description": "Manage the mode of local access to the device",
"access_local_title": "Local",
"access_no_device_id": "No device ID available",
"access_private_key_description": "For security reasons, it will not be displayed after saving.",
"access_private_key_label": "Private Key",
"access_provider_custom": "Custom",
"access_provider_jetkvm": "JetKVM Cloud",
"access_remote_description": "Manage the mode of Remote access to the device",
"access_security_encryption": "End-to-end encryption using WebRTC (DTLS and SRTP)",
"access_security_oidc": "OIDC (OpenID Connect) authentication",
"access_security_open_source": "All cloud components are open-source and available on GitHub.",
"access_security_streams": "All streams encrypted in transit",
"access_security_zero_trust": "Zero Trust security model",
"access_title": "Access",
"access_tls_certificate_description": "Paste your TLS certificate below. For certificate chains, include the entire chain (leaf, intermediate, and root certificates).",
"access_tls_certificate_title": "TLS Certificate",
"access_tls_custom": "Custom",
"access_tls_disabled": "Disabled",
"access_tls_self_signed": "Self-signed",
"access_tls_updated": "TLS settings updated successfully",
"access_update_tls_settings": "Update TLS Settings"
} }

View File

@ -1,7 +1,7 @@
import { redirect } from "react-router"; import { redirect } from "react-router";
import type { LoaderFunction, LoaderFunctionArgs } from "react-router"; import type { LoaderFunction, LoaderFunctionArgs } from "react-router";
import { getDeviceUiPath } from "../hooks/useAppNavigation"; import { getDeviceUiPath } from "@hooks/useAppNavigation";
const loader: LoaderFunction = ({ params }: LoaderFunctionArgs) => { const loader: LoaderFunction = ({ params }: LoaderFunctionArgs) => {
return redirect(getDeviceUiPath("/settings/general", params.id)); return redirect(getDeviceUiPath("/settings/general", params.id));

View File

@ -1,22 +1,22 @@
import { useLoaderData, useNavigate } from "react-router";
import type { LoaderFunction } from "react-router";
import { ShieldCheckIcon } from "@heroicons/react/24/outline";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { useLoaderData, useNavigate, type LoaderFunction } from "react-router";
import { ShieldCheckIcon } from "@heroicons/react/24/outline";
import api from "@/api"; import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
import { SettingsPageHeader } from "@components/SettingsPageheader"; import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
import { GridCard } from "@/components/Card"; import { GridCard } from "@components/Card";
import { Button, LinkButton } from "@/components/Button"; import { Button, LinkButton } from "@components/Button";
import { InputFieldWithLabel } from "@/components/InputField"; import { InputFieldWithLabel } from "@components/InputField";
import { SelectMenuBasic } from "@/components/SelectMenuBasic"; import { SelectMenuBasic } from "@components/SelectMenuBasic";
import { SettingsItem } from "@components/SettingsItem"; import { SettingsItem } from "@components/SettingsItem";
import { SettingsSectionHeader } from "@/components/SettingsSectionHeader"; import { SettingsPageHeader } from "@components/SettingsPageheader";
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation"; import { SettingsSectionHeader } from "@components/SettingsSectionHeader";
import { TextAreaWithLabel } from "@components/TextArea";
import api from "@/api";
import notifications from "@/notifications"; import notifications from "@/notifications";
import { DEVICE_API } from "@/ui.config"; import { DEVICE_API } from "@/ui.config";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import { isOnDevice } from "@/main"; import { isOnDevice } from "@/main";
import { TextAreaWithLabel } from "@components/TextArea"; import { m } from "@localizations/messages.js";
import { LocalDevice } from "./devices.$id"; import { LocalDevice } from "./devices.$id";
import { CloudState } from "./adopt"; import { CloudState } from "./adopt";
@ -92,7 +92,7 @@ export default function SettingsAccessIndexRoute() {
send("deregisterDevice", {}, (resp: JsonRpcResponse) => { send("deregisterDevice", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(
`Failed to de-register device: ${resp.error.data || "Unknown error"}`, m.access_failed_deregister({ error: resp.error.data || "Unknown error" }),
); );
return; return;
} }
@ -107,14 +107,14 @@ export default function SettingsAccessIndexRoute() {
const onCloudAdoptClick = useCallback( const onCloudAdoptClick = useCallback(
(cloudApiUrl: string, cloudAppUrl: string) => { (cloudApiUrl: string, cloudAppUrl: string) => {
if (!deviceId) { if (!deviceId) {
notifications.error("No device ID available"); notifications.error(m.access_no_device_id());
return; return;
} }
send("setCloudUrl", { apiUrl: cloudApiUrl, appUrl: cloudAppUrl }, (resp: JsonRpcResponse) => { send("setCloudUrl", { apiUrl: cloudApiUrl, appUrl: cloudAppUrl }, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(
`Failed to update cloud URL: ${resp.error.data || "Unknown error"}`, m.access_failed_update_cloud_url({ error: resp.error.data || "Unknown error" }),
); );
return; return;
} }
@ -160,12 +160,12 @@ export default function SettingsAccessIndexRoute() {
send("setTLSState", { state }, (resp: JsonRpcResponse) => { send("setTLSState", { state }, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(
`Failed to update TLS settings: ${resp.error.data || "Unknown error"}`, m.access_failed_update_tls({ error: resp.error.data || "Unknown error" }),
); );
return; return;
} }
notifications.success("TLS settings updated successfully"); notifications.success(m.access_tls_updated());
}); });
}, [send]); }, [send]);
@ -206,22 +206,22 @@ export default function SettingsAccessIndexRoute() {
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<SettingsPageHeader <SettingsPageHeader
title="Access" title={m.access_title()}
description="Manage the Access Control of the device" description={m.access_description()}
/> />
{loaderData?.authMode && ( {loaderData?.authMode && (
<> <>
<div className="space-y-4"> <div className="space-y-4">
<SettingsSectionHeader <SettingsSectionHeader
title="Local" title={m.access_local_title()}
description="Manage the mode of local access to the device" description={m.access_local_description()}
/> />
<> <>
<SettingsItem <SettingsItem
title="HTTPS Mode" title={m.access_https_mode_title()}
badge="Experimental" badge="Experimental"
description="Configure secure HTTPS access to your device" description={m.access_https_description()}
> >
<SelectMenuBasic <SelectMenuBasic
size="SM" size="SM"
@ -229,9 +229,9 @@ export default function SettingsAccessIndexRoute() {
onChange={e => handleTlsModeChange(e.target.value)} onChange={e => handleTlsModeChange(e.target.value)}
disabled={tlsMode === "unknown"} disabled={tlsMode === "unknown"}
options={[ options={[
{ value: "disabled", label: "Disabled" }, { value: "disabled", label: m.access_tls_disabled() },
{ value: "self-signed", label: "Self-signed" }, { value: "self-signed", label: m.access_tls_self_signed() },
{ value: "custom", label: "Custom" }, { value: "custom", label: m.access_tls_custom() },
]} ]}
/> />
</SettingsItem> </SettingsItem>
@ -240,12 +240,12 @@ export default function SettingsAccessIndexRoute() {
<div className="mt-4 space-y-4"> <div className="mt-4 space-y-4">
<div className="space-y-4"> <div className="space-y-4">
<SettingsItem <SettingsItem
title="TLS Certificate" title={m.access_tls_certificate_title()}
description="Paste your TLS certificate below. For certificate chains, include the entire chain (leaf, intermediate, and root certificates)." description={m.access_tls_certificate_description()}
/> />
<div className="space-y-4"> <div className="space-y-4">
<TextAreaWithLabel <TextAreaWithLabel
label="Certificate" label={m.access_certificate_label()}
rows={3} rows={3}
placeholder={ placeholder={
"-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----" "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
@ -258,8 +258,8 @@ export default function SettingsAccessIndexRoute() {
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-4"> <div className="space-y-4">
<TextAreaWithLabel <TextAreaWithLabel
label="Private Key" label={m.access_private_key_label()}
description="For security reasons, it will not be displayed after saving." description={m.access_private_key_description()}
rows={3} rows={3}
placeholder={ placeholder={
"-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----" "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"
@ -274,7 +274,7 @@ export default function SettingsAccessIndexRoute() {
<Button <Button
size="SM" size="SM"
theme="primary" theme="primary"
text="Update TLS Settings" text={m.access_update_tls_settings()}
onClick={handleCustomTlsUpdate} onClick={handleCustomTlsUpdate}
/> />
</div> </div>
@ -282,14 +282,14 @@ export default function SettingsAccessIndexRoute() {
)} )}
<SettingsItem <SettingsItem
title="Authentication Mode" title={m.access_authentication_mode_title()}
description={`Current mode: ${loaderData.authMode === "password" ? "Password protected" : "No password"}`} description={loaderData.authMode === "password" ? m.access_auth_mode_password() : m.access_auth_mode_no_password()}
> >
{loaderData.authMode === "password" ? ( {loaderData.authMode === "password" ? (
<Button <Button
size="SM" size="SM"
theme="light" theme="light"
text="Disable Protection" text={m.access_disable_protection()}
onClick={() => { onClick={() => {
navigateTo("./local-auth", { state: { init: "deletePassword" } }); navigateTo("./local-auth", { state: { init: "deletePassword" } });
}} }}
@ -298,7 +298,7 @@ export default function SettingsAccessIndexRoute() {
<Button <Button
size="SM" size="SM"
theme="light" theme="light"
text="Enable Password" text={m.access_enable_password()}
onClick={() => { onClick={() => {
navigateTo("./local-auth", { state: { init: "createPassword" } }); navigateTo("./local-auth", { state: { init: "createPassword" } });
}} }}
@ -309,13 +309,13 @@ export default function SettingsAccessIndexRoute() {
{loaderData.authMode === "password" && ( {loaderData.authMode === "password" && (
<SettingsItem <SettingsItem
title="Change Password" title={m.access_change_password_title()}
description="Update your device access password" description={m.access_change_password_description()}
> >
<Button <Button
size="SM" size="SM"
theme="light" theme="light"
text="Change Password" text={m.access_change_password_button()}
onClick={() => { onClick={() => {
navigateTo("./local-auth", { state: { init: "updatePassword" } }); navigateTo("./local-auth", { state: { init: "updatePassword" } });
}} }}
@ -330,23 +330,23 @@ export default function SettingsAccessIndexRoute() {
<div className="space-y-4"> <div className="space-y-4">
<SettingsSectionHeader <SettingsSectionHeader
title="Remote" title="Remote"
description="Manage the mode of Remote access to the device" description={m.access_remote_description()}
/> />
<div className="space-y-4"> <div className="space-y-4">
{!isAdopted && ( {!isAdopted && (
<> <>
<SettingsItem <SettingsItem
title="Cloud Provider" title={m.access_cloud_provider_title()}
description="Select the cloud provider for your device" description={m.access_cloud_provider_description()}
> >
<SelectMenuBasic <SelectMenuBasic
size="SM" size="SM"
value={selectedProvider} value={selectedProvider}
onChange={e => handleProviderChange(e.target.value)} onChange={e => handleProviderChange(e.target.value)}
options={[ options={[
{ value: "jetkvm", label: "JetKVM Cloud" }, { value: "jetkvm", label: m.access_provider_jetkvm() },
{ value: "custom", label: "Custom" }, { value: "custom", label: m.access_provider_custom() },
]} ]}
/> />
</SettingsItem> </SettingsItem>
@ -356,7 +356,7 @@ export default function SettingsAccessIndexRoute() {
<div className="flex items-end gap-x-2"> <div className="flex items-end gap-x-2">
<InputFieldWithLabel <InputFieldWithLabel
size="SM" size="SM"
label="Cloud API URL" label={m.access_cloud_api_url_label()}
value={cloudApiUrl} value={cloudApiUrl}
onChange={e => setCloudApiUrl(e.target.value)} onChange={e => setCloudApiUrl(e.target.value)}
placeholder="https://api.example.com" placeholder="https://api.example.com"
@ -365,7 +365,7 @@ export default function SettingsAccessIndexRoute() {
<div className="flex items-end gap-x-2"> <div className="flex items-end gap-x-2">
<InputFieldWithLabel <InputFieldWithLabel
size="SM" size="SM"
label="Cloud App URL" label={m.access_cloud_app_url_label()}
value={cloudAppUrl} value={cloudAppUrl}
onChange={e => setCloudAppUrl(e.target.value)} onChange={e => setCloudAppUrl(e.target.value)}
placeholder="https://app.example.com" placeholder="https://app.example.com"
@ -384,26 +384,26 @@ export default function SettingsAccessIndexRoute() {
<div className="space-y-3"> <div className="space-y-3">
<div className="space-y-2"> <div className="space-y-2">
<h3 className="text-base font-bold text-slate-900 dark:text-white"> <h3 className="text-base font-bold text-slate-900 dark:text-white">
Cloud Security {m.access_cloud_security_title()}
</h3> </h3>
<div> <div>
<ul className="list-disc space-y-1 pl-5 text-xs text-slate-700 dark:text-slate-300"> <ul className="list-disc space-y-1 pl-5 text-xs text-slate-700 dark:text-slate-300">
<li>End-to-end encryption using WebRTC (DTLS and SRTP)</li> <li>{m.access_security_encryption()}</li>
<li>Zero Trust security model</li> <li>{m.access_security_zero_trust()}</li>
<li>OIDC (OpenID Connect) authentication</li> <li>{m.access_security_oidc()}</li>
<li>All streams encrypted in transit</li> <li>{m.access_security_streams()}</li>
</ul> </ul>
</div> </div>
<div className="text-xs text-slate-700 dark:text-slate-300"> <div className="text-xs text-slate-700 dark:text-slate-300">
All cloud components are open-source and available on{" "} {m.access_security_open_source()}{" "}
<a <a
href="https://github.com/jetkvm" href="https://github.com/jetkvm"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="font-medium text-blue-600 hover:text-blue-800 dark:text-blue-500 dark:hover:text-blue-400" className="font-medium text-blue-600 hover:text-blue-800 dark:text-blue-500 dark:hover:text-blue-400"
> >
GitHub {m.access_github_link()}
</a> </a>
. .
</div> </div>
@ -415,7 +415,7 @@ export default function SettingsAccessIndexRoute() {
to="https://jetkvm.com/docs/networking/remote-access" to="https://jetkvm.com/docs/networking/remote-access"
size="SM" size="SM"
theme="light" theme="light"
text="Learn about our cloud security" text={m.access_learn_security()}
/> />
</div> </div>
</div> </div>
@ -429,32 +429,32 @@ export default function SettingsAccessIndexRoute() {
onClick={() => onCloudAdoptClick(cloudApiUrl, cloudAppUrl)} onClick={() => onCloudAdoptClick(cloudApiUrl, cloudAppUrl)}
size="SM" size="SM"
theme="primary" theme="primary"
text="Adopt KVM to Cloud" text={m.access_adopt_kvm()}
/> />
</div> </div>
) : ( ) : (
<div> <div>
<div className="space-y-2"> <div className="space-y-2">
<p className="text-sm text-slate-600 dark:text-slate-300"> <p className="text-sm text-slate-600 dark:text-slate-300">
Your device is adopted to the Cloud {m.access_adopted_message()}
</p> </p>
<div> <div>
<Button <Button
size="SM" size="SM"
theme="light" theme="light"
text="De-register from Cloud" text={m.access_deregister()}
className="text-red-600" className="text-red-600"
onClick={() => { onClick={() => {
if (deviceId) { if (deviceId) {
if ( if (
window.confirm( window.confirm(
"Are you sure you want to de-register this device?", m.access_confirm_deregister(),
) )
) { ) {
deregisterDevice(); deregisterDevice();
} }
} else { } else {
notifications.error("No device ID available"); notifications.error(m.access_no_device_id());
} }
}} }}
/> />