mirror of https://github.com/jetkvm/kvm.git
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
This commit is contained in:
parent
de5403eada
commit
66b42a4858
1
cloud.go
1
cloud.go
|
@ -96,7 +96,6 @@ func handleCloudRegister(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
config.CloudToken = tokenResp.SecretToken
|
config.CloudToken = tokenResp.SecretToken
|
||||||
config.CloudURL = req.CloudAPI
|
|
||||||
|
|
||||||
provider, err := oidc.NewProvider(c, "https://accounts.google.com")
|
provider, err := oidc.NewProvider(c, "https://accounts.google.com")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
30
jsonrpc.go
30
jsonrpc.go
|
@ -730,6 +730,33 @@ func rpcSetSerialSettings(settings SerialSettings) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func rpcSetCloudUrl(url string) error {
|
||||||
|
if url == "" {
|
||||||
|
// Reset to default by removing from config
|
||||||
|
config.CloudURL = defaultConfig.CloudURL
|
||||||
|
} else {
|
||||||
|
config.CloudURL = url
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := SaveConfig(); err != nil {
|
||||||
|
return fmt.Errorf("failed to save config: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpcGetCloudUrl() (string, error) {
|
||||||
|
return config.CloudURL, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpcResetCloudUrl() error {
|
||||||
|
// Reset to default by removing from config
|
||||||
|
config.CloudURL = defaultConfig.CloudURL
|
||||||
|
if err := SaveConfig(); err != nil {
|
||||||
|
return fmt.Errorf("failed to reset cloud URL: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var rpcHandlers = map[string]RPCHandler{
|
var rpcHandlers = map[string]RPCHandler{
|
||||||
"ping": {Func: rpcPing},
|
"ping": {Func: rpcPing},
|
||||||
"getDeviceID": {Func: rpcGetDeviceID},
|
"getDeviceID": {Func: rpcGetDeviceID},
|
||||||
|
@ -786,4 +813,7 @@ var rpcHandlers = map[string]RPCHandler{
|
||||||
"setATXPowerAction": {Func: rpcSetATXPowerAction, Params: []string{"action"}},
|
"setATXPowerAction": {Func: rpcSetATXPowerAction, Params: []string{"action"}},
|
||||||
"getSerialSettings": {Func: rpcGetSerialSettings},
|
"getSerialSettings": {Func: rpcGetSerialSettings},
|
||||||
"setSerialSettings": {Func: rpcSetSerialSettings, Params: []string{"settings"}},
|
"setSerialSettings": {Func: rpcSetSerialSettings, Params: []string{"settings"}},
|
||||||
|
"setCloudUrl": {Func: rpcSetCloudUrl, Params: []string{"url"}},
|
||||||
|
"getCloudUrl": {Func: rpcGetCloudUrl},
|
||||||
|
"resetCloudUrl": {Func: rpcResetCloudUrl},
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
# No need for VITE_CLOUD_APP or VITE_SIGNAL_API, it's only needed for the device build
|
||||||
|
|
||||||
|
# We use this for all the cloud API requests from the browser
|
||||||
|
VITE_CLOUD_API=http://localhost:3000
|
|
@ -0,0 +1,4 @@
|
||||||
|
# No need for VITE_CLOUD_APP or VITE_SIGNAL_API, it's only needed for the device build
|
||||||
|
|
||||||
|
# We use this for all the cloud API requests from the browser
|
||||||
|
VITE_CLOUD_API=https://api.jetkvm.com
|
|
@ -0,0 +1,4 @@
|
||||||
|
# No need for VITE_SIGNAL_API or VITE_CLOUD_APP it's only needed for the device build
|
||||||
|
|
||||||
|
# We use this for all the cloud API requests from the browser
|
||||||
|
VITE_CLOUD_API=https://staging-api.jetkvm.com
|
|
@ -1,6 +0,0 @@
|
||||||
VITE_SIGNAL_API=http://localhost:3000
|
|
||||||
|
|
||||||
VITE_CLOUD_APP=http://localhost:5173
|
|
||||||
VITE_CLOUD_API=http://localhost:3000
|
|
||||||
|
|
||||||
VITE_JETKVM_HEAD=
|
|
|
@ -1,6 +1,5 @@
|
||||||
VITE_SIGNAL_API= # Uses the KVM device's IP address as the signal API endpoint
|
# Uses the KVM device's IP address as the signal API endpoint
|
||||||
|
VITE_SIGNAL_API=
|
||||||
|
|
||||||
VITE_CLOUD_APP=https://app.jetkvm.com
|
# Used in settings page to know where to link to when user wants to adopt a device to the cloud
|
||||||
VITE_CLOUD_API=https://api.jetkvm.com
|
VITE_CLOUD_APP=http://localhost:5173
|
||||||
|
|
||||||
VITE_JETKVM_HEAD=<script src="/device/ui-config.js"></script>
|
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
VITE_SIGNAL_API=https://api.jetkvm.com
|
|
||||||
|
|
||||||
VITE_CLOUD_APP=https://app.jetkvm.com
|
|
||||||
VITE_CLOUD_API=https://api.jetkvm.com
|
|
||||||
|
|
||||||
VITE_JETKVM_HEAD=
|
|
|
@ -1,4 +0,0 @@
|
||||||
VITE_SIGNAL_API=https://staging-api.jetkvm.com
|
|
||||||
|
|
||||||
VITE_CLOUD_APP=https://staging-app.jetkvm.com
|
|
||||||
VITE_CLOUD_API=https://staging-api.jetkvm.com
|
|
|
@ -28,7 +28,6 @@
|
||||||
<title>JetKVM</title>
|
<title>JetKVM</title>
|
||||||
<link rel="stylesheet" href="/fonts/fonts.css" />
|
<link rel="stylesheet" href="/fonts/fonts.css" />
|
||||||
<link rel="icon" href="/favicon.png" />
|
<link rel="icon" href="/favicon.png" />
|
||||||
%VITE_JETKVM_HEAD%
|
|
||||||
<script>
|
<script>
|
||||||
// Initial theme setup
|
// Initial theme setup
|
||||||
document.documentElement.classList.toggle(
|
document.documentElement.classList.toggle(
|
||||||
|
|
|
@ -8,11 +8,11 @@
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "./dev_device.sh",
|
"dev": "./dev_device.sh",
|
||||||
"dev:cloud": "vite dev --mode=development",
|
"dev:cloud": "vite dev --mode=cloud-development",
|
||||||
"build": "npm run build:prod",
|
"build": "npm run build:prod",
|
||||||
"build:device": "tsc && vite build --mode=device --emptyOutDir",
|
"build:device": "tsc && vite build --mode=device --emptyOutDir",
|
||||||
"build:staging": "tsc && vite build --mode=staging",
|
"build:staging": "tsc && vite build --mode=cloud-staging",
|
||||||
"build:prod": "tsc && vite build --mode=production",
|
"build:prod": "tsc && vite build --mode=cloud-production",
|
||||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
|
|
|
@ -27,6 +27,7 @@ import { LocalDevice } from "@routes/devices.$id";
|
||||||
import { useRevalidator } from "react-router-dom";
|
import { useRevalidator } from "react-router-dom";
|
||||||
import { ShieldCheckIcon } from "@heroicons/react/20/solid";
|
import { ShieldCheckIcon } from "@heroicons/react/20/solid";
|
||||||
import { CLOUD_APP, SIGNAL_API } from "@/ui.config";
|
import { CLOUD_APP, SIGNAL_API } from "@/ui.config";
|
||||||
|
import { InputFieldWithLabel } from "../InputField";
|
||||||
|
|
||||||
export function SettingsItem({
|
export function SettingsItem({
|
||||||
title,
|
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(() => {
|
useEffect(() => {
|
||||||
getCloudState();
|
getCloudState();
|
||||||
|
|
||||||
|
@ -363,7 +409,14 @@ export default function SettingsSidebar() {
|
||||||
if ("error" in resp) return;
|
if ("error" in resp) return;
|
||||||
setUsbEmulationEnabled(resp.result as boolean);
|
setUsbEmulationEnabled(resp.result as boolean);
|
||||||
});
|
});
|
||||||
}, [getCloudState, send, setBacklightSettings, setDeveloperMode, setHideCursor, setJiggler]);
|
}, [
|
||||||
|
getCloudState,
|
||||||
|
send,
|
||||||
|
setBacklightSettings,
|
||||||
|
setDeveloperMode,
|
||||||
|
setHideCursor,
|
||||||
|
setJiggler,
|
||||||
|
]);
|
||||||
|
|
||||||
const getDevice = useCallback(async () => {
|
const getDevice = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
|
@ -920,6 +973,7 @@ export default function SettingsSidebar() {
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
|
|
||||||
{settings.developerMode && (
|
{settings.developerMode && (
|
||||||
|
<div>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<TextAreaWithLabel
|
<TextAreaWithLabel
|
||||||
label="SSH Public Key"
|
label="SSH Public Key"
|
||||||
|
@ -940,6 +994,36 @@ export default function SettingsSidebar() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<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
|
<SettingsItem
|
||||||
title="Troubleshooting Mode"
|
title="Troubleshooting Mode"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { LoaderFunctionArgs, redirect } from "react-router-dom";
|
import { LoaderFunctionArgs, redirect } from "react-router-dom";
|
||||||
import api from "../api";
|
import api from "../api";
|
||||||
import { CLOUD_API, CLOUD_APP, SIGNAL_API } from "@/ui.config";
|
import { CLOUD_APP, SIGNAL_API } from "@/ui.config";
|
||||||
|
|
||||||
const loader = async ({ request }: LoaderFunctionArgs) => {
|
const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
|
@ -11,15 +11,11 @@ const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||||
const oidcGoogle = searchParams.get("oidcGoogle");
|
const oidcGoogle = searchParams.get("oidcGoogle");
|
||||||
const clientId = searchParams.get("clientId");
|
const clientId = searchParams.get("clientId");
|
||||||
|
|
||||||
const res = await api.POST(
|
const res = await api.POST(`${SIGNAL_API}/cloud/register`, {
|
||||||
`${SIGNAL_API}/cloud/register`,
|
|
||||||
{
|
|
||||||
token: tempToken,
|
token: tempToken,
|
||||||
cloudApi: CLOUD_API,
|
|
||||||
oidcGoogle,
|
oidcGoogle,
|
||||||
clientId,
|
clientId,
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (!res.ok) throw new Error("Failed to register device");
|
if (!res.ok) throw new Error("Failed to register device");
|
||||||
return redirect(CLOUD_APP + `/devices/${deviceId}/setup`);
|
return redirect(CLOUD_APP + `/devices/${deviceId}/setup`);
|
||||||
|
|
|
@ -1,23 +1,3 @@
|
||||||
interface JetKVMConfig {
|
export const CLOUD_API = import.meta.env.VITE_CLOUD_API;
|
||||||
CLOUD_API?: string;
|
export const CLOUD_APP = import.meta.env.VITE_CLOUD_APP;
|
||||||
CLOUD_APP?: string;
|
|
||||||
DEVICE_VERSION?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Window { JETKVM_CONFIG?: JetKVMConfig; }
|
|
||||||
}
|
|
||||||
|
|
||||||
const getAppURL = (api_url?: string) => {
|
|
||||||
if (!api_url) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const url = new URL(api_url);
|
|
||||||
url.host = url.host.replace(/api\./, "app.");
|
|
||||||
// remove the ending slash
|
|
||||||
return url.toString().replace(/\/$/, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CLOUD_API = window.JETKVM_CONFIG?.CLOUD_API || import.meta.env.VITE_CLOUD_API;
|
|
||||||
export const CLOUD_APP = window.JETKVM_CONFIG?.CLOUD_APP || getAppURL(CLOUD_API) || import.meta.env.VITE_CLOUD_APP;
|
|
||||||
export const SIGNAL_API = import.meta.env.VITE_SIGNAL_API;
|
export const SIGNAL_API = import.meta.env.VITE_SIGNAL_API;
|
||||||
|
|
|
@ -9,7 +9,7 @@ declare const process: {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default defineConfig(({ mode, command }) => {
|
export default defineConfig(({ mode, command }) => {
|
||||||
const isCloud = mode === "production";
|
const isCloud = mode.indexOf("cloud") !== -1;
|
||||||
const onDevice = mode === "device";
|
const onDevice = mode === "device";
|
||||||
const { JETKVM_PROXY_URL } = process.env;
|
const { JETKVM_PROXY_URL } = process.env;
|
||||||
|
|
||||||
|
@ -18,15 +18,17 @@ export default defineConfig(({ mode, command }) => {
|
||||||
build: { outDir: isCloud ? "dist" : "../static" },
|
build: { outDir: isCloud ? "dist" : "../static" },
|
||||||
server: {
|
server: {
|
||||||
host: "0.0.0.0",
|
host: "0.0.0.0",
|
||||||
proxy: JETKVM_PROXY_URL ? {
|
proxy: JETKVM_PROXY_URL
|
||||||
'/me': JETKVM_PROXY_URL,
|
? {
|
||||||
'/device': JETKVM_PROXY_URL,
|
"/me": JETKVM_PROXY_URL,
|
||||||
'/webrtc': JETKVM_PROXY_URL,
|
"/device": JETKVM_PROXY_URL,
|
||||||
'/auth': JETKVM_PROXY_URL,
|
"/webrtc": JETKVM_PROXY_URL,
|
||||||
'/storage': JETKVM_PROXY_URL,
|
"/auth": JETKVM_PROXY_URL,
|
||||||
'/cloud': JETKVM_PROXY_URL,
|
"/storage": JETKVM_PROXY_URL,
|
||||||
} : undefined
|
"/cloud": JETKVM_PROXY_URL,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
},
|
},
|
||||||
base: onDevice && command === 'build' ? "/static" : "/",
|
base: onDevice && command === "build" ? "/static" : "/",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue