This commit is contained in:
Siyuan Miao
2025-03-18 17:25:03 +01:00
parent 4c37f7e079
commit 82c018a2f6
11 changed files with 858 additions and 129 deletions

View File

@@ -14,11 +14,18 @@ import notifications from "@/notifications";
import { DEVICE_API } from "@/ui.config";
import { useJsonRpc } from "@/hooks/useJsonRpc";
import { isOnDevice } from "@/main";
import { TextAreaWithLabel } from "@components/TextArea";
import { LocalDevice } from "./devices.$id";
import { SettingsItem } from "./devices.$id.settings";
import { CloudState } from "./adopt";
export interface TLSState {
mode: "self-signed" | "custom" | "disabled";
certificate?: string;
privateKey?: string;
}
export const loader = async () => {
if (isOnDevice) {
const status = await api
@@ -44,6 +51,9 @@ export default function SettingsAccessIndexRoute() {
// Use a simple string identifier for the selected provider
const [selectedProvider, setSelectedProvider] = useState<string>("jetkvm");
const [tlsMode, setTlsMode] = useState<string>("unknown");
const [tlsCert, setTlsCert] = useState<string>("");
const [tlsKey, setTlsKey] = useState<string>("");
const getCloudState = useCallback(() => {
send("getCloudState", {}, resp => {
@@ -66,6 +76,17 @@ export default function SettingsAccessIndexRoute() {
});
}, [send]);
const getTLSState = useCallback(() => {
send("getTLSState", {}, resp => {
if ("error" in resp) return console.error(resp.error);
const tlsState = resp.result as TLSState;
setTlsMode(tlsState.mode);
if (tlsState.certificate) setTlsCert(tlsState.certificate);
if (tlsState.privateKey) setTlsKey(tlsState.privateKey);
});
}, [send]);
const deregisterDevice = async () => {
send("deregisterDevice", {}, resp => {
if ("error" in resp) {
@@ -126,15 +147,62 @@ export default function SettingsAccessIndexRoute() {
}
};
// Function to update TLS state - accepts a mode parameter
const updateTlsState = useCallback(
(mode: string, cert?: string, key?: string) => {
const state = { mode } as TLSState;
if (cert && key) {
state.certificate = cert;
state.privateKey = key;
}
send("setTLSState", { state }, resp => {
if ("error" in resp) {
notifications.error(
`Failed to update TLS settings: ${resp.error.data || "Unknown error"}`,
);
return;
}
notifications.success("TLS settings updated successfully");
});
},
[send],
);
// Handle TLS mode change
const handleTlsModeChange = (value: string) => {
setTlsMode(value);
// For "disabled" and "self-signed" modes, immediately apply the settings
if (value !== "custom") {
updateTlsState(value);
}
};
const handleTlsCertChange = (value: string) => {
setTlsCert(value);
};
const handleTlsKeyChange = (value: string) => {
setTlsKey(value);
};
// Update the custom TLS settings button click handler
const handleCustomTlsUpdate = () => {
updateTlsState(tlsMode, tlsCert, tlsKey);
};
// Fetch device ID and cloud state on component mount
useEffect(() => {
getCloudState();
getTLSState();
send("getDeviceID", {}, async resp => {
if ("error" in resp) return console.error(resp.error);
setDeviceId(resp.result as string);
});
}, [send, getCloudState]);
}, [send, getCloudState, getTLSState]);
return (
<div className="space-y-4">
@@ -150,30 +218,95 @@ export default function SettingsAccessIndexRoute() {
title="Local"
description="Manage the mode of local access to the device"
/>
<SettingsItem
title="Authentication Mode"
description={`Current mode: ${loaderData.authMode === "password" ? "Password protected" : "No password"}`}
>
{loaderData.authMode === "password" ? (
<Button
<>
<SettingsItem
title="HTTPS Mode"
badge="Experimental"
description="Configure secure HTTPS access to your device"
>
<SelectMenuBasic
size="SM"
theme="light"
text="Disable Protection"
onClick={() => {
navigateTo("./local-auth", { state: { init: "deletePassword" } });
}}
/>
) : (
<Button
size="SM"
theme="light"
text="Enable Password"
onClick={() => {
navigateTo("./local-auth", { state: { init: "createPassword" } });
}}
value={tlsMode}
onChange={e => handleTlsModeChange(e.target.value)}
disabled={tlsMode === "unknown"}
options={[
{ value: "disabled", label: "Disabled" },
{ value: "self-signed", label: "Self-signed" },
{ value: "custom", label: "Custom" },
]}
/>
</SettingsItem>
{tlsMode === "custom" && (
<div className="mt-4 space-y-4">
<div className="space-y-4">
<SettingsItem
title="TLS Certificate"
description="Paste your TLS certificate below. For certificate chains, include the entire chain (leaf, intermediate, and root certificates)."
/>
<div className="space-y-4">
<TextAreaWithLabel
label="Certificate"
rows={3}
placeholder={
"-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
}
value={tlsCert}
onChange={e => handleTlsCertChange(e.target.value)}
/>
</div>
<div className="space-y-4">
<div className="space-y-4">
<TextAreaWithLabel
label="Private Key"
description="For security reasons, it will not be displayed after saving."
rows={3}
placeholder={
"-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"
}
value={tlsKey}
onChange={e => handleTlsKeyChange(e.target.value)}
/>
</div>
</div>
</div>
<div className="flex items-center gap-x-2">
<Button
size="SM"
theme="primary"
text="Update TLS Settings"
onClick={handleCustomTlsUpdate}
/>
</div>
</div>
)}
</SettingsItem>
<SettingsItem
title="Authentication Mode"
description={`Current mode: ${loaderData.authMode === "password" ? "Password protected" : "No password"}`}
>
{loaderData.authMode === "password" ? (
<Button
size="SM"
theme="light"
text="Disable Protection"
onClick={() => {
navigateTo("./local-auth", { state: { init: "deletePassword" } });
}}
/>
) : (
<Button
size="SM"
theme="light"
text="Enable Password"
onClick={() => {
navigateTo("./local-auth", { state: { init: "createPassword" } });
}}
/>
)}
</SettingsItem>
</>
{loaderData.authMode === "password" && (
<SettingsItem

View File

@@ -246,6 +246,7 @@ export function SettingsItem({
children,
className,
loading,
badge,
}: {
title: string;
description: string | React.ReactNode;
@@ -253,6 +254,7 @@ export function SettingsItem({
className?: string;
name?: string;
loading?: boolean;
badge?: string;
}) {
return (
<label
@@ -263,10 +265,17 @@ export function SettingsItem({
>
<div className="space-y-0.5">
<div className="flex items-center gap-x-2">
<h3 className="text-base font-semibold text-black dark:text-white">{title}</h3>
<div className="flex items-center text-base font-semibold text-black dark:text-white">
{title}
{badge && (
<span className="ml-2 rounded-full bg-red-500 px-2 py-1 text-[10px] font-medium leading-none text-white dark:border dark:border-red-700 dark:bg-red-800 dark:text-red-50">
{badge}
</span>
)}
</div>
{loading && <LoadingSpinner className="h-4 w-4 text-blue-500" />}
</div>
<p className="text-sm text-slate-700 dark:text-slate-300">{description}</p>
<div className="text-sm text-slate-700 dark:text-slate-300">{description}</div>
</div>
{children ? <div>{children}</div> : null}
</label>