Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@ func handleCloudRegister(c *gin.Context) {
}

config.CloudToken = tokenResp.SecretToken
config.CloudURL = req.CloudAPI

provider, err := oidc.NewProvider(c, "https://accounts.google.com")
if err != nil {
Expand Down Expand Up @@ -298,8 +297,8 @@ func rpcDeregisterDevice() error {
// (e.g., wrong cloud token, already deregistered). Regardless of the reason, we can safely remove it.
if resp.StatusCode == http.StatusNotFound || (resp.StatusCode >= 200 && resp.StatusCode < 300) {
config.CloudToken = ""
config.CloudURL = ""
config.GoogleIdentity = ""

if err := SaveConfig(); err != nil {
return fmt.Errorf("failed to save configuration after deregistering: %w", err)
}
Expand Down
30 changes: 30 additions & 0 deletions jsonrpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,33 @@ func rpcSetSerialSettings(settings SerialSettings) error {
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{
"ping": {Func: rpcPing},
"getDeviceID": {Func: rpcGetDeviceID},
Expand Down Expand Up @@ -786,4 +813,7 @@ var rpcHandlers = map[string]RPCHandler{
"setATXPowerAction": {Func: rpcSetATXPowerAction, Params: []string{"action"}},
"getSerialSettings": {Func: rpcGetSerialSettings},
"setSerialSettings": {Func: rpcSetSerialSettings, Params: []string{"settings"}},
"setCloudUrl": {Func: rpcSetCloudUrl, Params: []string{"url"}},
"getCloudUrl": {Func: rpcGetCloudUrl},
"resetCloudUrl": {Func: rpcResetCloudUrl},
}
4 changes: 4 additions & 0 deletions ui/.env.cloud-development
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# No need for 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=http://localhost:3000
4 changes: 4 additions & 0 deletions ui/.env.cloud-production
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# No need for 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://api.jetkvm.com
4 changes: 4 additions & 0 deletions ui/.env.cloud-staging
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# No need for 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
6 changes: 0 additions & 6 deletions ui/.env.development

This file was deleted.

8 changes: 2 additions & 6 deletions ui/.env.device
Original file line number Diff line number Diff line change
@@ -1,6 +1,2 @@
VITE_SIGNAL_API= # Uses the KVM device's IP address as the signal API endpoint

VITE_CLOUD_APP=https://app.jetkvm.com
VITE_CLOUD_API=https://api.jetkvm.com

VITE_JETKVM_HEAD=<script src="/device/ui-config.js"></script>
# Used in settings page to know where to link to when user wants to adopt a device to the cloud
VITE_CLOUD_APP=http://localhost:5173
6 changes: 0 additions & 6 deletions ui/.env.production

This file was deleted.

4 changes: 0 additions & 4 deletions ui/.env.staging

This file was deleted.

1 change: 0 additions & 1 deletion ui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
<title>JetKVM</title>
<link rel="stylesheet" href="/fonts/fonts.css" />
<link rel="icon" href="/favicon.png" />
%VITE_JETKVM_HEAD%
<script>
// Initial theme setup
document.documentElement.classList.toggle(
Expand Down
6 changes: 3 additions & 3 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
},
"scripts": {
"dev": "./dev_device.sh",
"dev:cloud": "vite dev --mode=development",
"dev:cloud": "vite dev --mode=cloud-development",
"build": "npm run build:prod",
"build:device": "tsc && vite build --mode=device --emptyOutDir",
"build:staging": "tsc && vite build --mode=staging",
"build:prod": "tsc && vite build --mode=production",
"build:staging": "tsc && vite build --mode=cloud-staging",
"build:prod": "tsc && vite build --mode=cloud-production",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
Expand Down
4 changes: 2 additions & 2 deletions ui/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions ui/src/components/MountMediaDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down
126 changes: 106 additions & 20 deletions ui/src/components/sidebar/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down
16 changes: 6 additions & 10 deletions ui/src/routes/adopt.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { LoaderFunctionArgs, redirect } from "react-router-dom";
import api from "../api";
import { CLOUD_API, CLOUD_APP, SIGNAL_API } from "@/ui.config";
import { CLOUD_APP, DEVICE_API } from "@/ui.config";

const loader = async ({ request }: LoaderFunctionArgs) => {
const url = new URL(request.url);
Expand All @@ -11,15 +11,11 @@ const loader = async ({ request }: LoaderFunctionArgs) => {
const oidcGoogle = searchParams.get("oidcGoogle");
const clientId = searchParams.get("clientId");

const res = await api.POST(
`${SIGNAL_API}/cloud/register`,
{
token: tempToken,
cloudApi: CLOUD_API,
oidcGoogle,
clientId,
},
);
const res = await api.POST(`${DEVICE_API}/cloud/register`, {
token: tempToken,
oidcGoogle,
clientId,
});

if (!res.ok) throw new Error("Failed to register device");
return redirect(CLOUD_APP + `/devices/${deviceId}/setup`);
Expand Down
Loading