feat(cloud): Add custom cloud API URL configuration support (#181)

* feat(cloud): Add custom cloud API URL configuration support

- Implement RPC methods to set, get, and reset cloud URL
- Update cloud registration to remove hardcoded cloud API URL
- Modify UI to allow configuring custom cloud API URL in developer settings
- Remove environment-specific cloud configuration files
- Simplify cloud URL configuration in UI config

* fix(ui): Update cloud app URL to production environment in device mode

* refactor(ui): Remove SIGNAL_API env & Rename to DEVICE_API to make clear distinction between  CLOUD_API and DEVICE_API.

* feat(ui): Only show Cloud API URL Change on device mode

* fix(cloud): Don't override the CloudURL on deregistration from the cloud.
This commit is contained in:
Adam Shiervani
2025-02-25 16:10:46 +01:00
committed by GitHub
parent de5403eada
commit 7304e6b672
22 changed files with 201 additions and 113 deletions

View File

@@ -14,7 +14,7 @@ import PeerConnectionStatusCard from "@components/PeerConnectionStatusCard";
import api from "../api";
import { isOnDevice } from "../main";
import { Button, LinkButton } from "./Button";
import { CLOUD_API, SIGNAL_API } from "@/ui.config";
import { CLOUD_API, DEVICE_API } from "@/ui.config";
interface NavbarProps {
isLoggedIn: boolean;
@@ -38,7 +38,7 @@ export default function DashboardNavbar({
const navigate = useNavigate();
const onLogout = useCallback(async () => {
const logoutUrl = isOnDevice
? `${SIGNAL_API}/auth/logout`
? `${DEVICE_API}/auth/logout`
: `${CLOUD_API}/logout`;
const res = await api.POST(logoutUrl);
if (!res.ok) return;

View File

@@ -35,7 +35,7 @@ import { ExclamationTriangleIcon } from "@heroicons/react/20/solid";
import notifications from "../notifications";
import Fieldset from "./Fieldset";
import { isOnDevice } from "../main";
import { SIGNAL_API } from "@/ui.config";
import { DEVICE_API } from "@/ui.config";
export default function MountMediaModal({
open,
@@ -1120,7 +1120,7 @@ function UploadFileView({
alreadyUploadedBytes: number,
dataChannel: string,
) {
const uploadUrl = `${SIGNAL_API}/storage/upload?uploadId=${dataChannel}`;
const uploadUrl = `${DEVICE_API}/storage/upload?uploadId=${dataChannel}`;
const xhr = new XMLHttpRequest();
xhr.open("POST", uploadUrl, true);

View File

@@ -26,7 +26,8 @@ import LocalAuthPasswordDialog from "@/components/LocalAuthPasswordDialog";
import { LocalDevice } from "@routes/devices.$id";
import { useRevalidator } from "react-router-dom";
import { ShieldCheckIcon } from "@heroicons/react/20/solid";
import { CLOUD_APP, SIGNAL_API } from "@/ui.config";
import { CLOUD_APP, DEVICE_API } from "@/ui.config";
import { InputFieldWithLabel } from "../InputField";
export function SettingsItem({
title,
@@ -277,6 +278,51 @@ export default function SettingsSidebar() {
}
};
const [cloudUrl, setCloudUrl] = useState("");
useEffect(() => {
send("getCloudUrl", {}, resp => {
if ("error" in resp) return;
setCloudUrl(resp.result as string);
});
}, [send]);
const getCloudUrl = useCallback(() => {
send("getCloudUrl", {}, resp => {
if ("error" in resp) return;
setCloudUrl(resp.result as string);
});
}, [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]);
useEffect(() => {
getCloudState();
@@ -363,12 +409,19 @@ export default function SettingsSidebar() {
if ("error" in resp) return;
setUsbEmulationEnabled(resp.result as boolean);
});
}, [getCloudState, send, setBacklightSettings, setDeveloperMode, setHideCursor, setJiggler]);
}, [
getCloudState,
send,
setBacklightSettings,
setDeveloperMode,
setHideCursor,
setJiggler,
]);
const getDevice = useCallback(async () => {
try {
const status = await api
.GET(`${SIGNAL_API}/device`)
.GET(`${DEVICE_API}/device`)
.then(res => res.json() as Promise<LocalDevice>);
setLocalDevice(status);
} catch (error) {
@@ -920,25 +973,58 @@ export default function SettingsSidebar() {
</SettingsItem>
{settings.developerMode && (
<div className="space-y-4">
<TextAreaWithLabel
label="SSH Public Key"
value={sshKey || ""}
rows={3}
onChange={e => handleSSHKeyChange(e.target.value)}
placeholder="Enter your SSH public key"
/>
<p className="text-xs text-slate-600 dark:text-slate-400">
The default SSH user is <strong>root</strong>.
</p>
<div className="flex items-center gap-x-2">
<Button
size="SM"
theme="primary"
text="Update SSH Key"
onClick={handleUpdateSSHKey}
<div>
<div className="space-y-4">
<TextAreaWithLabel
label="SSH Public Key"
value={sshKey || ""}
rows={3}
onChange={e => handleSSHKeyChange(e.target.value)}
placeholder="Enter your SSH public key"
/>
<p className="text-xs text-slate-600 dark:text-slate-400">
The default SSH user is <strong>root</strong>.
</p>
<div className="flex items-center gap-x-2">
<Button
size="SM"
theme="primary"
text="Update SSH Key"
onClick={handleUpdateSSHKey}
/>
</div>
</div>
{isOnDevice && (
<div className="mt-4 space-y-4">
<SettingsItem
title="Cloud API URL"
description="Connect to a custom JetKVM Cloud API"
/>
<InputFieldWithLabel
size="SM"
label="Cloud URL"
value={cloudUrl}
onChange={e => setCloudUrl(e.target.value)}
placeholder="https://api.jetkvm.com"
/>
<div className="flex items-center gap-x-2">
<Button
size="SM"
theme="primary"
text="Save Cloud URL"
onClick={() => handleCloudUrlChange(cloudUrl)}
/>
<Button
size="SM"
theme="light"
text="Restore to default"
onClick={handleResetCloudUrl}
/>
</div>
</div>
)}
</div>
)}
<SettingsItem