Skip to content

Commit e4bb4f2

Browse files
feat(cloud): Add support for custom cloud app URL configuration (#207)
* feat(cloud): Add support for custom cloud app URL configuration - Extend CloudState and Config to include CloudAppURL - Update RPC methods to handle both API and app URLs - Modify cloud adoption and settings routes to support custom app URLs - Remove hardcoded cloud app URL environment file - Simplify cloud URL configuration in UI * fix(cloud): Improve cloud URL configuration and adoption flow - Update error handling in cloud URL configuration RPC method - Modify cloud adoption route to support dynamic cloud URLs - Remove hardcoded default cloud URLs in device access settings - Refactor cloud adoption click handler to be more flexible * refactor(cloud): Simplify cloud URL configuration RPC method - Update rpcSetCloudUrl to return only an error - Remove unnecessary boolean return value - Improve error handling consistency * refactor(ui): Simplify cloud provider configuration and URL handling
1 parent 482c64a commit e4bb4f2

File tree

10 files changed

+113
-115
lines changed

10 files changed

+113
-115
lines changed

cloud.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,12 +264,14 @@ func RunWebsocketClient() {
264264
type CloudState struct {
265265
Connected bool `json:"connected"`
266266
URL string `json:"url,omitempty"`
267+
AppURL string `json:"appUrl,omitempty"`
267268
}
268269

269270
func rpcGetCloudState() CloudState {
270271
return CloudState{
271272
Connected: config.CloudToken != "" && config.CloudURL != "",
272273
URL: config.CloudURL,
274+
AppURL: config.CloudAppURL,
273275
}
274276
}
275277

config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type UsbConfig struct {
2222

2323
type Config struct {
2424
CloudURL string `json:"cloud_url"`
25+
CloudAppURL string `json:"cloud_app_url"`
2526
CloudToken string `json:"cloud_token"`
2627
GoogleIdentity string `json:"google_identity"`
2728
JigglerEnabled bool `json:"jiggler_enabled"`
@@ -43,6 +44,7 @@ const configPath = "/userdata/kvm_config.json"
4344

4445
var defaultConfig = &Config{
4546
CloudURL: "https://api.jetkvm.com",
47+
CloudAppURL: "https://app.jetkvm.com",
4648
AutoUpdateEnabled: true, // Set a default value
4749
ActiveExtension: "",
4850
DisplayMaxBrightness: 64,

jsonrpc.go

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -753,37 +753,17 @@ func rpcSetSerialSettings(settings SerialSettings) error {
753753
return nil
754754
}
755755

756-
func rpcSetCloudUrl(url string) error {
757-
if url == "" {
758-
// Reset to default by removing from config
759-
config.CloudURL = defaultConfig.CloudURL
760-
} else {
761-
config.CloudURL = url
762-
}
756+
func rpcSetCloudUrl(apiUrl string, appUrl string) error {
757+
config.CloudURL = apiUrl
758+
config.CloudAppURL = appUrl
763759

764760
if err := SaveConfig(); err != nil {
765761
return fmt.Errorf("failed to save config: %w", err)
766762
}
767-
return nil
768-
}
769-
770-
func rpcGetCloudUrl() (string, error) {
771-
return config.CloudURL, nil
772-
}
773763

774-
func rpcResetCloudUrl() error {
775-
// Reset to default by removing from config
776-
config.CloudURL = defaultConfig.CloudURL
777-
if err := SaveConfig(); err != nil {
778-
return fmt.Errorf("failed to reset cloud URL: %w", err)
779-
}
780764
return nil
781765
}
782766

783-
func rpcGetDefaultCloudUrl() (string, error) {
784-
return defaultConfig.CloudURL, nil
785-
}
786-
787767
var rpcHandlers = map[string]RPCHandler{
788768
"ping": {Func: rpcPing},
789769
"getDeviceID": {Func: rpcGetDeviceID},
@@ -842,8 +822,5 @@ var rpcHandlers = map[string]RPCHandler{
842822
"setATXPowerAction": {Func: rpcSetATXPowerAction, Params: []string{"action"}},
843823
"getSerialSettings": {Func: rpcGetSerialSettings},
844824
"setSerialSettings": {Func: rpcSetSerialSettings, Params: []string{"settings"}},
845-
"setCloudUrl": {Func: rpcSetCloudUrl, Params: []string{"url"}},
846-
"getCloudUrl": {Func: rpcGetCloudUrl},
847-
"resetCloudUrl": {Func: rpcResetCloudUrl},
848-
"getDefaultCloudUrl": {Func: rpcGetDefaultCloudUrl},
825+
"setCloudUrl": {Func: rpcSetCloudUrl, Params: []string{"apiUrl", "appUrl"}},
849826
}

ui/.env.device

Lines changed: 0 additions & 2 deletions
This file was deleted.

ui/src/components/USBConfigDialog.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { InputFieldWithLabel } from "./InputField";
33
import { UsbConfigState } from "@/hooks/stores";
44
import { useEffect, useCallback, useState } from "react";
55
import { useJsonRpc } from "../hooks/useJsonRpc";
6-
import { USBConfig } from "../routes/devices.$id.settings.hardware";
6+
import { USBConfig } from "./UsbConfigSetting";
77

88
export default function UpdateUsbConfigModal({
99
onSetUsbConfig,

ui/src/main.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,6 @@ if (isOnDevice) {
177177
},
178178
],
179179
},
180-
181180
{
182181
path: "/adopt",
183182
element: <AdoptRoute />,

ui/src/routes/adopt.tsx

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { LoaderFunctionArgs, redirect } from "react-router-dom";
22
import api from "../api";
3-
import { CLOUD_APP, DEVICE_API } from "@/ui.config";
3+
import { DEVICE_API } from "@/ui.config";
4+
5+
export interface CloudState {
6+
connected: boolean;
7+
url: string;
8+
appUrl: string;
9+
}
410

511
const loader = async ({ request }: LoaderFunctionArgs) => {
612
const url = new URL(request.url);
@@ -11,14 +17,21 @@ const loader = async ({ request }: LoaderFunctionArgs) => {
1117
const oidcGoogle = searchParams.get("oidcGoogle");
1218
const clientId = searchParams.get("clientId");
1319

14-
const res = await api.POST(`${DEVICE_API}/cloud/register`, {
15-
token: tempToken,
16-
oidcGoogle,
17-
clientId,
18-
});
20+
const [cloudStateResponse, registerResponse] = await Promise.all([
21+
api.GET(`${DEVICE_API}/cloud/state`),
22+
api.POST(`${DEVICE_API}/cloud/register`, {
23+
token: tempToken,
24+
oidcGoogle,
25+
clientId,
26+
}),
27+
]);
28+
29+
if (!cloudStateResponse.ok) throw new Error("Failed to get cloud state");
30+
const cloudState = (await cloudStateResponse.json()) as CloudState;
31+
32+
if (!registerResponse.ok) throw new Error("Failed to register device");
1933

20-
if (!res.ok) throw new Error("Failed to register device");
21-
return redirect(CLOUD_APP + `/devices/${deviceId}/setup`);
34+
return redirect(cloudState.appUrl + `/devices/${deviceId}/setup`);
2235
};
2336

2437
export default function AdoptRoute() {

ui/src/routes/devices.$id.settings.access._index.tsx

Lines changed: 72 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { SettingsPageHeader } from "@components/SettingsPageheader";
22
import { SettingsItem } from "./devices.$id.settings";
33
import { useLoaderData, useNavigate } from "react-router-dom";
44
import { Button, LinkButton } from "../components/Button";
5-
import { CLOUD_APP, DEVICE_API } from "../ui.config";
5+
import { DEVICE_API } from "../ui.config";
66
import api from "../api";
77
import { LocalDevice } from "./devices.$id";
88
import { useDeviceUiNavigation } from "../hooks/useAppNavigation";
@@ -15,6 +15,7 @@ import { InputFieldWithLabel } from "../components/InputField";
1515
import { SelectMenuBasic } from "../components/SelectMenuBasic";
1616
import { SettingsSectionHeader } from "../components/SettingsSectionHeader";
1717
import { isOnDevice } from "../main";
18+
import { CloudState } from "./adopt";
1819

1920
export const loader = async () => {
2021
if (isOnDevice) {
@@ -36,38 +37,30 @@ export default function SettingsAccessIndexRoute() {
3637

3738
const [isAdopted, setAdopted] = useState(false);
3839
const [deviceId, setDeviceId] = useState<string | null>(null);
39-
const [cloudUrl, setCloudUrl] = useState("");
40-
const [cloudProviders, setCloudProviders] = useState<
41-
{ value: string; label: string }[] | null
42-
>([{ value: "https://api.jetkvm.com", label: "JetKVM Cloud" }]);
40+
const [cloudApiUrl, setCloudApiUrl] = useState("");
41+
const [cloudAppUrl, setCloudAppUrl] = useState("");
4342

44-
// The default value is just there so it doesn't flicker while we fetch the default Cloud URL and available providers
45-
const [selectedUrlOption, setSelectedUrlOption] = useState<string>(
46-
"https://api.jetkvm.com",
47-
);
48-
49-
const [defaultCloudUrl, setDefaultCloudUrl] = useState<string>("");
50-
51-
const syncCloudUrl = useCallback(() => {
52-
send("getCloudUrl", {}, resp => {
53-
if ("error" in resp) return;
54-
const url = resp.result as string;
55-
setCloudUrl(url);
56-
// Check if the URL matches any predefined option
57-
if (cloudProviders?.some(provider => provider.value === url)) {
58-
setSelectedUrlOption(url);
59-
} else {
60-
setSelectedUrlOption("custom");
61-
// setCustomCloudUrl(url);
62-
}
63-
});
64-
}, [cloudProviders, send]);
43+
// Use a simple string identifier for the selected provider
44+
const [selectedProvider, setSelectedProvider] = useState<string>("jetkvm");
6545

6646
const getCloudState = useCallback(() => {
6747
send("getCloudState", {}, resp => {
6848
if ("error" in resp) return console.error(resp.error);
69-
const cloudState = resp.result as { connected: boolean };
49+
const cloudState = resp.result as CloudState;
7050
setAdopted(cloudState.connected);
51+
setCloudApiUrl(cloudState.url);
52+
53+
if (cloudState.appUrl) setCloudAppUrl(cloudState.appUrl);
54+
55+
// Find if the API URL matches any of our predefined providers
56+
const isAPIJetKVMProd = cloudState.url === "https://api.jetkvm.com";
57+
const isAppJetKVMProd = cloudState.appUrl === "https://app.jetkvm.com";
58+
59+
if (isAPIJetKVMProd && isAppJetKVMProd) {
60+
setSelectedProvider("jetkvm");
61+
} else {
62+
setSelectedProvider("custom");
63+
}
7164
});
7265
}, [send]);
7366

@@ -88,42 +81,50 @@ export default function SettingsAccessIndexRoute() {
8881
};
8982

9083
const onCloudAdoptClick = useCallback(
91-
(url: string) => {
84+
(cloudApiUrl: string, cloudAppUrl: string) => {
9285
if (!deviceId) {
9386
notifications.error("No device ID available");
9487
return;
9588
}
9689

97-
send("setCloudUrl", { url }, resp => {
90+
send("setCloudUrl", { apiUrl: cloudApiUrl, appUrl: cloudAppUrl }, resp => {
9891
if ("error" in resp) {
9992
notifications.error(
10093
`Failed to update cloud URL: ${resp.error.data || "Unknown error"}`,
10194
);
10295
return;
10396
}
104-
syncCloudUrl();
105-
notifications.success("Cloud URL updated successfully");
10697

10798
const returnTo = new URL(window.location.href);
10899
returnTo.pathname = "/adopt";
109100
returnTo.search = "";
110101
returnTo.hash = "";
111102
window.location.href =
112-
CLOUD_APP + "/signup?deviceId=" + deviceId + `&returnTo=${returnTo.toString()}`;
103+
cloudAppUrl +
104+
"/signup?deviceId=" +
105+
deviceId +
106+
`&returnTo=${returnTo.toString()}`;
113107
});
114108
},
115-
[deviceId, syncCloudUrl, send],
109+
[deviceId, send],
116110
);
117111

118-
useEffect(() => {
119-
if (!defaultCloudUrl) return;
120-
setSelectedUrlOption(defaultCloudUrl);
121-
setCloudProviders([
122-
{ value: defaultCloudUrl, label: "JetKVM Cloud" },
123-
{ value: "custom", label: "Custom" },
124-
]);
125-
}, [defaultCloudUrl]);
112+
// Handle provider selection change
113+
const handleProviderChange = (value: string) => {
114+
setSelectedProvider(value);
126115

116+
// If selecting a predefined provider, update both URLs
117+
if (value === "jetkvm") {
118+
setCloudApiUrl("https://api.jetkvm.com");
119+
setCloudAppUrl("https://app.jetkvm.com");
120+
} else {
121+
if (cloudApiUrl || cloudAppUrl) return;
122+
setCloudApiUrl("");
123+
setCloudAppUrl("");
124+
}
125+
};
126+
127+
// Fetch device ID and cloud state on component mount
127128
useEffect(() => {
128129
getCloudState();
129130

@@ -133,18 +134,6 @@ export default function SettingsAccessIndexRoute() {
133134
});
134135
}, [send, getCloudState]);
135136

136-
useEffect(() => {
137-
send("getDefaultCloudUrl", {}, resp => {
138-
if ("error" in resp) return console.error(resp.error);
139-
setDefaultCloudUrl(resp.result as string);
140-
});
141-
}, [cloudProviders, syncCloudUrl, send]);
142-
143-
useEffect(() => {
144-
if (!cloudProviders?.length) return;
145-
syncCloudUrl();
146-
}, [cloudProviders, syncCloudUrl]);
147-
148137
return (
149138
<div className="space-y-4">
150139
<SettingsPageHeader
@@ -219,34 +208,42 @@ export default function SettingsAccessIndexRoute() {
219208
>
220209
<SelectMenuBasic
221210
size="SM"
222-
value={selectedUrlOption}
223-
onChange={e => {
224-
const value = e.target.value;
225-
setSelectedUrlOption(value);
226-
}}
227-
options={cloudProviders ?? []}
211+
value={selectedProvider}
212+
onChange={e => handleProviderChange(e.target.value)}
213+
options={[
214+
{ value: "jetkvm", label: "JetKVM Cloud" },
215+
{ value: "custom", label: "Custom" },
216+
]}
228217
/>
229218
</SettingsItem>
230219

231-
{selectedUrlOption === "custom" && (
232-
<div className="mt-4 flex items-end gap-x-2 space-y-4">
233-
<InputFieldWithLabel
234-
size="SM"
235-
label="Custom Cloud URL"
236-
value={cloudUrl}
237-
onChange={e => setCloudUrl(e.target.value)}
238-
placeholder="https://api.example.com"
239-
/>
220+
{selectedProvider === "custom" && (
221+
<div className="mt-4 space-y-4">
222+
<div className="flex items-end gap-x-2">
223+
<InputFieldWithLabel
224+
size="SM"
225+
label="Cloud API URL"
226+
value={cloudApiUrl}
227+
onChange={e => setCloudApiUrl(e.target.value)}
228+
placeholder="https://api.example.com"
229+
/>
230+
</div>
231+
<div className="flex items-end gap-x-2">
232+
<InputFieldWithLabel
233+
size="SM"
234+
label="Cloud App URL"
235+
value={cloudAppUrl}
236+
onChange={e => setCloudAppUrl(e.target.value)}
237+
placeholder="https://app.example.com"
238+
/>
239+
</div>
240240
</div>
241241
)}
242242
</>
243243
)}
244244

245-
{/*
246-
We do the harcoding here to avoid flickering when the default Cloud URL being fetched.
247-
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.
248-
*/}
249-
{selectedUrlOption === (defaultCloudUrl || "https://api.jetkvm.com") && (
245+
{/* Show security info for JetKVM Cloud */}
246+
{selectedProvider === "jetkvm" && (
250247
<GridCard>
251248
<div className="flex items-start gap-x-4 p-4">
252249
<ShieldCheckIcon className="mt-1 h-8 w-8 shrink-0 text-blue-600 dark:text-blue-500" />
@@ -295,7 +292,7 @@ export default function SettingsAccessIndexRoute() {
295292
{!isAdopted ? (
296293
<div className="flex items-end gap-x-2">
297294
<Button
298-
onClick={() => onCloudAdoptClick(cloudUrl)}
295+
onClick={() => onCloudAdoptClick(cloudApiUrl, cloudAppUrl)}
299296
size="SM"
300297
theme="primary"
301298
text="Adopt KVM to Cloud"
@@ -305,7 +302,7 @@ export default function SettingsAccessIndexRoute() {
305302
<div>
306303
<div className="space-y-2">
307304
<p className="text-sm text-slate-600 dark:text-slate-300">
308-
Your device is adopted to JetKVM Cloud
305+
Your device is adopted to the Cloud
309306
</p>
310307
<div>
311308
<Button

ui/src/ui.config.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
export const CLOUD_API = import.meta.env.VITE_CLOUD_API;
2-
export const CLOUD_APP = import.meta.env.VITE_CLOUD_APP;
32

43
// In device mode, an empty string uses the current hostname (the JetKVM device's IP) as the API endpoint
54
export const DEVICE_API = "";

0 commit comments

Comments
 (0)