diff --git a/package.json b/package.json index aedc9fb..320e277 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,8 @@ "dependencies": { "@scure/base": "^1.2.1", "@snort/shared": "^1.0.17", - "@snort/system": "^1.5.7", - "@snort/system-react": "^1.5.7", + "@snort/system": "^1.6.1", + "@snort/system-react": "^1.6.1", "@xterm/addon-attach": "^0.11.0", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-webgl": "^0.18.0", diff --git a/public/icons.svg b/public/icons.svg index cf1fbf1..c915434 100644 --- a/public/icons.svg +++ b/public/icons.svg @@ -16,5 +16,10 @@ + + + + + \ No newline at end of file diff --git a/src/api.ts b/src/api.ts index 0828d67..6ecc90c 100644 --- a/src/api.ts +++ b/src/api.ts @@ -103,11 +103,15 @@ export interface VmPayment { is_paid: boolean; } +export interface PathVm { + ssh_key_id?: number; +} + export class LNVpsApi { constructor( readonly url: string, readonly publisher: EventPublisher | undefined, - ) { } + ) {} async listVms() { const { data } = await this.#handleResponse>>( @@ -123,6 +127,13 @@ export class LNVpsApi { return data; } + async patchVm(id: number, req: PathVm) { + const { data } = await this.#handleResponse>( + await this.#req(`/api/v1/vm/${id}`, "PATCH", req), + ); + return data; + } + async startVm(id: number) { const { data } = await this.#handleResponse>( await this.#req(`/api/v1/vm/${id}/start`, "PATCH"), @@ -196,20 +207,21 @@ export class LNVpsApi { async connect_terminal(id: number) { const u = `${this.url}/api/v1/console/${id}`; const auth = await this.#auth_event(u, "GET"); - const ws = new WebSocket(`${u}?auth=${base64.encode( - new TextEncoder().encode(JSON.stringify(auth)), - )}`); + const ws = new WebSocket( + `${u}?auth=${base64.encode( + new TextEncoder().encode(JSON.stringify(auth)), + )}`, + ); return await new Promise((resolve, reject) => { ws.onopen = () => { resolve(ws); - } + }; ws.onerror = (e) => { - reject(e) - } - }) + reject(e); + }; + }); } - async #handleResponse(rsp: Response) { if (rsp.ok) { return (await rsp.json()) as T; @@ -230,7 +242,7 @@ export class LNVpsApi { .kind(EventKind.HttpAuthentication) .tag(["u", url]) .tag(["method", method]); - }) + }); } async #auth(url: string, method: string) { diff --git a/src/components/icon.tsx b/src/components/icon.tsx index 58e92cc..6421d26 100644 --- a/src/components/icon.tsx +++ b/src/components/icon.tsx @@ -1,3 +1,4 @@ +import classNames from "classnames"; import { MouseEventHandler } from "react"; type Props = { @@ -15,7 +16,7 @@ export function Icon(props: Props) { diff --git a/src/components/login-button.tsx b/src/components/login-button.tsx index 7eed3fc..1ac6848 100644 --- a/src/components/login-button.tsx +++ b/src/components/login-button.tsx @@ -1,32 +1,24 @@ -import { SnortContext } from "@snort/system-react"; -import { useContext } from "react"; import { AsyncButton } from "./button"; -import { loginNip7 } from "../login"; import useLogin from "../hooks/login"; import Profile from "./profile"; import { NostrLink } from "@snort/system"; import { Link, useNavigate } from "react-router-dom"; export default function LoginButton() { - const system = useContext(SnortContext); const login = useLogin(); const navigate = useNavigate(); return !login ? ( { - if (window.nostr) { - await loginNip7(system); - } else { - navigate("/new-account"); - } + navigate("/login"); }} > Sign In ) : ( - + ); } diff --git a/src/components/modal.tsx b/src/components/modal.tsx index 0f53e41..3614e8c 100644 --- a/src/components/modal.tsx +++ b/src/components/modal.tsx @@ -51,7 +51,7 @@ export default function Modal(props: ModalProps) { className={ props.bodyClassName ?? classNames( - "relative bg-layer-1 p-8 transition max-xl:rounded-t-3xl xl:rounded-3xl max-xl:mt-auto xl:my-auto max-lg:w-full", + "relative bg-neutral-700 p-8 transition max-xl:rounded-t-3xl xl:rounded-3xl max-xl:mt-auto xl:my-auto max-lg:w-full", { "max-xl:-translate-y-[calc(100vh-100dvh)]": props.ready ?? true, "max-xl:translate-y-[50vh]": !(props.ready ?? true), diff --git a/src/components/ssh-keys.tsx b/src/components/ssh-keys.tsx new file mode 100644 index 0000000..9b2c92f --- /dev/null +++ b/src/components/ssh-keys.tsx @@ -0,0 +1,104 @@ +import { useEffect, useMemo, useState } from "react"; +import { LNVpsApi, UserSshKey } from "../api"; +import useLogin from "../hooks/login"; +import { ApiUrl } from "../const"; +import { AsyncButton } from "./button"; + +export default function SSHKeySelector({ + selectedKey, + setSelectedKey, +}: { + selectedKey: UserSshKey["id"]; + setSelectedKey: (k: UserSshKey["id"]) => void; +}) { + const login = useLogin(); + const [newKey, setNewKey] = useState(""); + const [newKeyError, setNewKeyError] = useState(""); + const [newKeyName, setNewKeyName] = useState(""); + const [showAddKey, setShowAddKey] = useState(false); + const [sshKeys, setSshKeys] = useState>([]); + + const api = useMemo(() => { + if (!login?.builder) return; + const api = new LNVpsApi(ApiUrl, login.builder); + return api; + }, [login]); + + async function addNewKey() { + if (!api) return; + setNewKeyError(""); + + try { + const nk = await api.addSshKey(newKeyName, newKey); + setNewKey(""); + setNewKeyName(""); + setSelectedKey(nk.id); + setShowAddKey(false); + api.listSshKeys().then((a) => setSshKeys(a)); + } catch (e) { + if (e instanceof Error) { + setNewKeyError(e.message); + } + } + } + + useEffect(() => { + if (!api) return; + api.listSshKeys().then((a) => { + setSshKeys(a); + if (a.length > 0) { + setSelectedKey(a[0].id); + } else { + setShowAddKey(true); + } + }); + }, []); + + return ( +
+ {sshKeys.length > 0 && ( + <> + Select SSH Key: + + + )} + {!showAddKey && sshKeys.length > 0 && ( + setShowAddKey(true)}> + Add new SSH key + + )} + {(showAddKey || sshKeys.length === 0) && ( + <> + Add SSH Key: +