feat: edit ssh key
This commit is contained in:
parent
1510b16ceb
commit
f4227fa121
@ -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",
|
||||
|
@ -16,5 +16,10 @@
|
||||
<path d="M24 0V24H0V0H24Z" fill="white" fill-opacity="0.01"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.0589 19.2443C12.8824 20.0009 15.764 19.0421 17.5934 16.9994C18.146 16.3822 19.0943 16.33 19.7115 16.8826C20.3286 17.4353 20.3808 18.3836 19.8282 19.0008C17.2737 21.8532 13.2404 23.2026 9.28249 22.1421C3.6811 20.6412 0.35698 14.8837 1.85787 9.2823C3.35876 3.68091 9.1163 0.356795 14.7177 1.85768C18.9224 2.98433 21.8407 6.50832 22.4032 10.5596C22.4653 11.0066 22.4987 11.4603 22.502 11.9179C22.5117 13.2319 21.0529 13.9572 20.01 13.2545L17.3364 11.4531C15.8701 10.4651 16.8533 8.17943 18.579 8.56459L18.6789 8.58688C17.7458 6.76269 16.0738 5.32688 13.9412 4.75546C9.94024 3.6834 5.82771 6.05777 4.75565 10.0588C3.68358 14.0598 6.05795 18.1723 10.0589 19.2443Z" fill="#F7F9FC"/>
|
||||
</svg>
|
||||
<svg id="pencil" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M24 0V24H0V0H24Z" fill="white" fill-opacity="0.01"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.1315 3.16087C18.9599 1.9893 17.0604 1.9893 15.8888 3.16087L15.1817 3.86798L20.1315 8.81773L20.8386 8.11062C22.0101 6.93905 22.0101 5.03955 20.8386 3.86798L20.1315 3.16087ZM18.7172 10.2319L13.7675 5.28219L4.6765 14.3732C4.47771 14.572 4.33879 14.8226 4.27557 15.0966L3.24752 19.5515C3.08116 20.2723 3.72726 20.9182 4.44797 20.7519L8.90288 19.7239C9.17681 19.6606 9.42746 19.5217 9.62625 19.3229L18.7172 10.2319Z" fill="#F7F9FC"/>
|
||||
</svg>
|
||||
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 3.5 KiB |
28
src/api.ts
28
src/api.ts
@ -103,6 +103,10 @@ export interface VmPayment {
|
||||
is_paid: boolean;
|
||||
}
|
||||
|
||||
export interface PathVm {
|
||||
ssh_key_id?: number;
|
||||
}
|
||||
|
||||
export class LNVpsApi {
|
||||
constructor(
|
||||
readonly url: string,
|
||||
@ -123,6 +127,13 @@ export class LNVpsApi {
|
||||
return data;
|
||||
}
|
||||
|
||||
async patchVm(id: number, req: PathVm) {
|
||||
const { data } = await this.#handleResponse<ApiResponse<void>>(
|
||||
await this.#req(`/api/v1/vm/${id}`, "PATCH", req),
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
async startVm(id: number) {
|
||||
const { data } = await this.#handleResponse<ApiResponse<VmInstance>>(
|
||||
await this.#req(`/api/v1/vm/${id}/start`, "PATCH"),
|
||||
@ -196,19 +207,20 @@ 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(
|
||||
const ws = new WebSocket(
|
||||
`${u}?auth=${base64.encode(
|
||||
new TextEncoder().encode(JSON.stringify(auth)),
|
||||
)}`);
|
||||
)}`,
|
||||
);
|
||||
return await new Promise<WebSocket>((resolve, reject) => {
|
||||
ws.onopen = () => {
|
||||
resolve(ws);
|
||||
}
|
||||
};
|
||||
ws.onerror = (e) => {
|
||||
reject(e)
|
||||
reject(e);
|
||||
};
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
async #handleResponse<T extends ApiResponseBase>(rsp: Response) {
|
||||
if (rsp.ok) {
|
||||
@ -230,7 +242,7 @@ export class LNVpsApi {
|
||||
.kind(EventKind.HttpAuthentication)
|
||||
.tag(["u", url])
|
||||
.tag(["method", method]);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
async #auth(url: string, method: string) {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import classNames from "classnames";
|
||||
import { MouseEventHandler } from "react";
|
||||
|
||||
type Props = {
|
||||
@ -15,7 +16,7 @@ export function Icon(props: Props) {
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
className={props.className}
|
||||
className={classNames(props.className, "cursor-pointer")}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
<use href={href} />
|
||||
|
@ -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 ? (
|
||||
<AsyncButton
|
||||
onClick={async () => {
|
||||
if (window.nostr) {
|
||||
await loginNip7(system);
|
||||
} else {
|
||||
navigate("/new-account");
|
||||
}
|
||||
navigate("/login");
|
||||
}}
|
||||
>
|
||||
Sign In
|
||||
</AsyncButton>
|
||||
) : (
|
||||
<Link to="/account">
|
||||
<Profile link={NostrLink.publicKey(login.pubkey)} />
|
||||
<Profile link={NostrLink.publicKey(login.publicKey)} />
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
@ -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),
|
||||
|
104
src/components/ssh-keys.tsx
Normal file
104
src/components/ssh-keys.tsx
Normal file
@ -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<Array<UserSshKey>>([]);
|
||||
|
||||
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 (
|
||||
<div className="flex flex-col gap-2">
|
||||
{sshKeys.length > 0 && (
|
||||
<>
|
||||
<b>Select SSH Key:</b>
|
||||
<select
|
||||
className="bg-neutral-900 p-2 rounded-xl"
|
||||
value={selectedKey}
|
||||
onChange={(e) => setSelectedKey(Number(e.target.value))}
|
||||
>
|
||||
{sshKeys.map((a) => (
|
||||
<option value={a.id}>{a.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</>
|
||||
)}
|
||||
{!showAddKey && sshKeys.length > 0 && (
|
||||
<AsyncButton onClick={() => setShowAddKey(true)}>
|
||||
Add new SSH key
|
||||
</AsyncButton>
|
||||
)}
|
||||
{(showAddKey || sshKeys.length === 0) && (
|
||||
<>
|
||||
<b>Add SSH Key:</b>
|
||||
<textarea
|
||||
rows={5}
|
||||
placeholder="ssh-[rsa|ed25519] AA== id"
|
||||
value={newKey}
|
||||
onChange={(e) => setNewKey(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Key name"
|
||||
value={newKeyName}
|
||||
onChange={(e) => setNewKeyName(e.target.value)}
|
||||
/>
|
||||
<AsyncButton
|
||||
disabled={newKey.length < 10 || newKeyName.length < 2}
|
||||
onClick={addNewKey}
|
||||
>
|
||||
Add Key
|
||||
</AsyncButton>
|
||||
{newKeyError && <b className="text-red-500">{newKeyError}</b>}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,9 +1,19 @@
|
||||
import { useSyncExternalStore } from "react";
|
||||
import { Login } from "../login";
|
||||
import { useContext, useSyncExternalStore } from "react";
|
||||
import { LoginState } from "../login";
|
||||
import { SnortContext } from "@snort/system-react";
|
||||
|
||||
export default function useLogin() {
|
||||
return useSyncExternalStore(
|
||||
(c) => Login.hook(c),
|
||||
() => Login.snapshot(),
|
||||
const session = useSyncExternalStore(
|
||||
(c) => LoginState.hook(c),
|
||||
() => LoginState.snapshot(),
|
||||
);
|
||||
const system = useContext(SnortContext);
|
||||
return session
|
||||
? {
|
||||
type: session.type,
|
||||
publicKey: session.publicKey,
|
||||
builder: LoginState.getSigner(),
|
||||
system,
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
|
129
src/login.ts
129
src/login.ts
@ -1,53 +1,112 @@
|
||||
import { ExternalStore } from "@snort/shared";
|
||||
import {
|
||||
EventSigner,
|
||||
EventPublisher,
|
||||
Nip46Signer,
|
||||
Nip7Signer,
|
||||
PrivateKeySigner,
|
||||
SystemInterface,
|
||||
UserState,
|
||||
} from "@snort/system";
|
||||
|
||||
class LoginShell extends ExternalStore<UserState<void> | undefined> {
|
||||
#state?: UserState<void>;
|
||||
|
||||
async login(signer: EventSigner, system: SystemInterface) {
|
||||
if (this.#state !== undefined) {
|
||||
throw new Error("Already logged in");
|
||||
export interface LoginSession {
|
||||
type: "nip7" | "nsec" | "nip46";
|
||||
publicKey: string;
|
||||
privateKey?: string;
|
||||
bunker?: string;
|
||||
}
|
||||
class LoginStore extends ExternalStore<LoginSession | undefined> {
|
||||
#session?: LoginSession;
|
||||
#signer?: EventPublisher;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const s = window.localStorage.getItem("session");
|
||||
if (s) {
|
||||
this.#session = JSON.parse(s);
|
||||
// patch session
|
||||
if (this.#session) {
|
||||
this.#session.type ??= "nip7";
|
||||
}
|
||||
}
|
||||
const pubkey = await signer.getPubKey();
|
||||
this.#state = new UserState<void>(pubkey);
|
||||
await this.#state.init(signer, system);
|
||||
this.#state.on("change", () => this.notifyChange());
|
||||
this.notifyChange();
|
||||
}
|
||||
|
||||
takeSnapshot() {
|
||||
return this.#state;
|
||||
return this.#session ? { ...this.#session } : undefined;
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.#session = undefined;
|
||||
this.#signer = undefined;
|
||||
this.#save();
|
||||
}
|
||||
|
||||
login(pubkey: string, type: LoginSession["type"] = "nip7") {
|
||||
this.#session = {
|
||||
type: type ?? "nip7",
|
||||
publicKey: pubkey,
|
||||
};
|
||||
this.#save();
|
||||
}
|
||||
|
||||
loginPrivateKey(key: string) {
|
||||
const s = new PrivateKeySigner(key);
|
||||
this.#session = {
|
||||
type: "nsec",
|
||||
publicKey: s.getPubKey(),
|
||||
privateKey: key,
|
||||
};
|
||||
this.#save();
|
||||
}
|
||||
|
||||
loginBunker(url: string, localKey: string, remotePubkey: string) {
|
||||
this.#session = {
|
||||
type: "nip46",
|
||||
publicKey: remotePubkey,
|
||||
privateKey: localKey,
|
||||
bunker: url,
|
||||
};
|
||||
this.#save();
|
||||
}
|
||||
|
||||
getSigner() {
|
||||
if (!this.#signer && this.#session) {
|
||||
switch (this.#session.type) {
|
||||
case "nsec":
|
||||
this.#signer = new EventPublisher(
|
||||
new PrivateKeySigner(this.#session.privateKey!),
|
||||
this.#session.publicKey,
|
||||
);
|
||||
break;
|
||||
case "nip46":
|
||||
this.#signer = new EventPublisher(
|
||||
new Nip46Signer(
|
||||
this.#session.bunker!,
|
||||
new PrivateKeySigner(this.#session.privateKey!),
|
||||
),
|
||||
this.#session.publicKey,
|
||||
);
|
||||
break;
|
||||
case "nip7":
|
||||
this.#signer = new EventPublisher(
|
||||
new Nip7Signer(),
|
||||
this.#session.publicKey,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
export const Login = new LoginShell();
|
||||
if (this.#signer) {
|
||||
return this.#signer;
|
||||
}
|
||||
throw "Signer not setup!";
|
||||
}
|
||||
|
||||
export async function loginNip7(system: SystemInterface) {
|
||||
const signer = new Nip7Signer();
|
||||
const pubkey = await signer.getPubKey();
|
||||
if (pubkey) {
|
||||
await Login.login(signer, system);
|
||||
#save() {
|
||||
if (this.#session) {
|
||||
window.localStorage.setItem("session", JSON.stringify(this.#session));
|
||||
} else {
|
||||
throw new Error("No nostr extension found");
|
||||
window.localStorage.removeItem("session");
|
||||
}
|
||||
this.notifyChange();
|
||||
}
|
||||
}
|
||||
|
||||
export async function loginPrivateKey(
|
||||
system: SystemInterface,
|
||||
key: string | Uint8Array | PrivateKeySigner,
|
||||
) {
|
||||
const signer =
|
||||
key instanceof PrivateKeySigner ? key : new PrivateKeySigner(key);
|
||||
const pubkey = signer.getPubKey();
|
||||
if (pubkey) {
|
||||
await Login.login(signer, system);
|
||||
} else {
|
||||
throw new Error("No nostr extension found");
|
||||
}
|
||||
}
|
||||
export const LoginState = new LoginStore();
|
||||
|
@ -31,7 +31,7 @@ const router = createBrowserRouter([
|
||||
element: <HomePage />,
|
||||
},
|
||||
{
|
||||
path: "/new-account",
|
||||
path: "/login",
|
||||
element: <SignUpPage />,
|
||||
},
|
||||
{
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { LNVpsApi, VmInstance } from "../api";
|
||||
import useLogin from "../hooks/login";
|
||||
import { EventPublisher } from "@snort/system";
|
||||
import { ApiUrl } from "../const";
|
||||
import VpsInstanceRow from "../components/vps-instance";
|
||||
|
||||
@ -10,11 +9,8 @@ export default function AccountPage() {
|
||||
const [vms, setVms] = useState<Array<VmInstance>>([]);
|
||||
|
||||
async function loadVms() {
|
||||
if (!login?.signer) return;
|
||||
const api = new LNVpsApi(
|
||||
ApiUrl,
|
||||
new EventPublisher(login.signer, login.pubkey),
|
||||
);
|
||||
if (!login?.builder) return;
|
||||
const api = new LNVpsApi(ApiUrl, login.builder);
|
||||
const vms = await api.listVms();
|
||||
setVms(vms);
|
||||
}
|
||||
|
@ -1,75 +1,34 @@
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { LNVpsApi, UserSshKey, VmOsImage, VmTemplate } from "../api";
|
||||
import { LNVpsApi, VmOsImage, VmTemplate } from "../api";
|
||||
import { useEffect, useState } from "react";
|
||||
import CostLabel from "../components/cost";
|
||||
import { ApiUrl } from "../const";
|
||||
import { EventPublisher } from "@snort/system";
|
||||
import useLogin from "../hooks/login";
|
||||
import { AsyncButton } from "../components/button";
|
||||
import classNames from "classnames";
|
||||
import VpsResources from "../components/vps-resources";
|
||||
import OsImageName from "../components/os-image-name";
|
||||
import SSHKeySelector from "../components/ssh-keys";
|
||||
|
||||
export default function OrderPage() {
|
||||
const { state } = useLocation();
|
||||
const login = useLogin();
|
||||
const navigate = useNavigate();
|
||||
const template = state as VmTemplate | undefined;
|
||||
const [newKey, setNewKey] = useState("");
|
||||
const [newKeyError, setNewKeyError] = useState("");
|
||||
const [newKeyName, setNewKeyName] = useState("");
|
||||
const [useImage, setUseImage] = useState(-1);
|
||||
const [useSshKey, setUseSshKey] = useState(-1);
|
||||
const [showAddKey, setShowAddKey] = useState(false);
|
||||
const [images, setImages] = useState<Array<VmOsImage>>([]);
|
||||
const [sshKeys, setSshKeys] = useState<Array<UserSshKey>>([]);
|
||||
const [orderError, setOrderError] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (!login?.signer) return;
|
||||
const api = new LNVpsApi(
|
||||
ApiUrl,
|
||||
new EventPublisher(login.signer, login.pubkey),
|
||||
);
|
||||
if (!login?.builder) return;
|
||||
const api = new LNVpsApi(ApiUrl, login.builder);
|
||||
api.listOsImages().then((a) => setImages(a));
|
||||
api.listSshKeys().then((a) => {
|
||||
setSshKeys(a);
|
||||
if (a.length > 0) {
|
||||
setUseSshKey(a[0].id);
|
||||
} else {
|
||||
setShowAddKey(true);
|
||||
}
|
||||
});
|
||||
}, [login]);
|
||||
|
||||
async function addNewKey() {
|
||||
if (!login?.signer) return;
|
||||
setNewKeyError("");
|
||||
const api = new LNVpsApi(
|
||||
ApiUrl,
|
||||
new EventPublisher(login.signer, login.pubkey),
|
||||
);
|
||||
|
||||
try {
|
||||
const nk = await api.addSshKey(newKeyName, newKey);
|
||||
setNewKey("");
|
||||
setNewKeyName("");
|
||||
setUseSshKey(nk.id);
|
||||
setShowAddKey(false);
|
||||
api.listSshKeys().then((a) => setSshKeys(a));
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
setNewKeyError(e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function createOrder() {
|
||||
if (!login?.signer || !template) return;
|
||||
const api = new LNVpsApi(
|
||||
ApiUrl,
|
||||
new EventPublisher(login.signer, login.pubkey),
|
||||
);
|
||||
if (!login?.builder || !template) return;
|
||||
const api = new LNVpsApi(ApiUrl, login.builder);
|
||||
|
||||
setOrderError("");
|
||||
try {
|
||||
@ -122,51 +81,7 @@ export default function OrderPage() {
|
||||
))}
|
||||
</div>
|
||||
<hr />
|
||||
<div className="flex flex-col gap-2">
|
||||
{sshKeys.length > 0 && (
|
||||
<>
|
||||
<b>Select SSH Key:</b>
|
||||
<select
|
||||
className="bg-neutral-900 p-2 rounded-xl"
|
||||
value={useSshKey}
|
||||
onChange={(e) => setUseSshKey(Number(e.target.value))}
|
||||
>
|
||||
{sshKeys.map((a) => (
|
||||
<option value={a.id}>{a.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</>
|
||||
)}
|
||||
{!showAddKey && sshKeys.length > 0 && (
|
||||
<AsyncButton onClick={() => setShowAddKey(true)}>
|
||||
Add new SSH key
|
||||
</AsyncButton>
|
||||
)}
|
||||
{(showAddKey || sshKeys.length === 0) && (
|
||||
<>
|
||||
<b>Add SSH Key:</b>
|
||||
<textarea
|
||||
rows={5}
|
||||
placeholder="ssh-[rsa|ed25519] AA== id"
|
||||
value={newKey}
|
||||
onChange={(e) => setNewKey(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Key name"
|
||||
value={newKeyName}
|
||||
onChange={(e) => setNewKeyName(e.target.value)}
|
||||
/>
|
||||
<AsyncButton
|
||||
disabled={newKey.length < 10 || newKeyName.length < 2}
|
||||
onClick={addNewKey}
|
||||
>
|
||||
Add Key
|
||||
</AsyncButton>
|
||||
{newKeyError && <b className="text-red-500">{newKeyError}</b>}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<SSHKeySelector selectedKey={useSshKey} setSelectedKey={setUseSshKey} />
|
||||
<AsyncButton
|
||||
disabled={useSshKey === -1 || useImage === -1}
|
||||
onClick={createOrder}
|
||||
|
@ -1,12 +1,17 @@
|
||||
import { EventPublisher, PrivateKeySigner } from "@snort/system";
|
||||
import {
|
||||
EventPublisher,
|
||||
Nip46Signer,
|
||||
Nip7Signer,
|
||||
PrivateKeySigner,
|
||||
} from "@snort/system";
|
||||
import { AsyncButton } from "../components/button";
|
||||
import { useContext, useState } from "react";
|
||||
import { bech32ToHex, hexToBech32 } from "@snort/shared";
|
||||
import { openFile } from "../utils";
|
||||
import { SnortContext } from "@snort/system-react";
|
||||
import { Blossom } from "../blossom";
|
||||
import { loginPrivateKey } from "../login";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { LoginState } from "../login";
|
||||
|
||||
export default function SignUpPage() {
|
||||
const [name, setName] = useState("");
|
||||
@ -44,16 +49,23 @@ export default function SignUpPage() {
|
||||
picture: pic,
|
||||
});
|
||||
system.BroadcastEvent(ev);
|
||||
await loginPrivateKey(system, key);
|
||||
LoginState.loginPrivateKey(key.privateKey);
|
||||
navigate("/");
|
||||
}
|
||||
|
||||
async function loginKey() {
|
||||
setError("");
|
||||
try {
|
||||
const key = bech32ToHex(keyIn);
|
||||
await loginPrivateKey(system, new PrivateKeySigner(key));
|
||||
if (keyIn.startsWith("nsec1")) {
|
||||
LoginState.loginPrivateKey(bech32ToHex(keyIn));
|
||||
navigate("/");
|
||||
} else if (keyIn.startsWith("bunker://")) {
|
||||
const signer = new Nip46Signer(keyIn);
|
||||
await signer.init();
|
||||
const pubkey = await signer.getPubKey();
|
||||
LoginState.loginBunker(keyIn, signer.privateKey!, pubkey);
|
||||
navigate("/");
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
setError(e.message);
|
||||
@ -71,9 +83,26 @@ export default function SignUpPage() {
|
||||
value={keyIn}
|
||||
onChange={(e) => setKeyIn(e.target.value)}
|
||||
/>
|
||||
<AsyncButton onClick={loginKey} disabled={!keyIn.startsWith("nsec")}>
|
||||
<AsyncButton
|
||||
onClick={loginKey}
|
||||
disabled={!keyIn.startsWith("nsec") && !keyIn.startsWith("bunker://")}
|
||||
>
|
||||
Login
|
||||
</AsyncButton>
|
||||
{window.nostr && (
|
||||
<div className="flex flex-col gap-4">
|
||||
Browser Extension:
|
||||
<AsyncButton
|
||||
onClick={async () => {
|
||||
const pk = await new Nip7Signer().getPubKey();
|
||||
LoginState.login(pk);
|
||||
navigate("/");
|
||||
}}
|
||||
>
|
||||
Nostr Extension
|
||||
</AsyncButton>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-4 items-center my-6">
|
||||
<div className="text-xl">OR</div>
|
||||
<div className="h-[1px] bg-neutral-800 w-full"></div>
|
||||
|
@ -1,10 +1,11 @@
|
||||
import "@xterm/xterm/css/xterm.css";
|
||||
|
||||
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
||||
import { LNVpsApi, VmInstance, VmPayment } from "../api";
|
||||
import VpsInstanceRow from "../components/vps-instance";
|
||||
import useLogin from "../hooks/login";
|
||||
import { ApiUrl } from "../const";
|
||||
import { EventPublisher } from "@snort/system";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import VpsPayment from "../components/vps-payment";
|
||||
import CostLabel from "../components/cost";
|
||||
import { AsyncButton } from "../components/button";
|
||||
@ -12,8 +13,10 @@ import { AttachAddon } from "@xterm/addon-attach";
|
||||
import { Terminal } from "@xterm/xterm";
|
||||
import { FitAddon } from "@xterm/addon-fit";
|
||||
import { WebglAddon } from "@xterm/addon-webgl";
|
||||
import "@xterm/xterm/css/xterm.css";
|
||||
import { toEui64 } from "../utils";
|
||||
import { Icon } from "../components/icon";
|
||||
import Modal from "../components/modal";
|
||||
import SSHKeySelector from "../components/ssh-keys";
|
||||
|
||||
const fit = new FitAddon();
|
||||
|
||||
@ -23,28 +26,28 @@ export default function VmPage() {
|
||||
const login = useLogin();
|
||||
const navigate = useNavigate();
|
||||
const [payment, setPayment] = useState<VmPayment>();
|
||||
const [term, setTerm] = useState<Terminal>()
|
||||
const [term, setTerm] = useState<Terminal>();
|
||||
const termRef = useRef<HTMLDivElement | null>(null);
|
||||
const [editKey, setEditKey] = useState(false);
|
||||
const [key, setKey] = useState(state?.ssh_key_id ?? -1);
|
||||
|
||||
const api = useMemo(() => {
|
||||
if (!login?.builder) return;
|
||||
return new LNVpsApi(ApiUrl, login.builder);
|
||||
}, [login]);
|
||||
|
||||
const renew = useCallback(
|
||||
async function () {
|
||||
if (!login?.signer || !state) return;
|
||||
const api = new LNVpsApi(
|
||||
ApiUrl,
|
||||
new EventPublisher(login.signer, login.pubkey),
|
||||
);
|
||||
if (!api || !state) return;
|
||||
const p = await api.renewVm(state.id);
|
||||
setPayment(p);
|
||||
},
|
||||
[login, state],
|
||||
[api, state],
|
||||
);
|
||||
|
||||
async function openTerminal() {
|
||||
if (!login?.signer || !state) return;
|
||||
const api = new LNVpsApi(
|
||||
ApiUrl,
|
||||
new EventPublisher(login.signer, login.pubkey),
|
||||
);
|
||||
if (!login?.builder || !state) return;
|
||||
const api = new LNVpsApi(ApiUrl, login.builder);
|
||||
const ws = await api.connect_terminal(state.id);
|
||||
const te = new Terminal();
|
||||
const webgl = new WebglAddon();
|
||||
@ -55,7 +58,7 @@ export default function VmPage() {
|
||||
te.loadAddon(fit);
|
||||
te.onResize(({ cols, rows }) => {
|
||||
ws.send(`${cols}:${rows}`);
|
||||
})
|
||||
});
|
||||
const attach = new AttachAddon(ws);
|
||||
te.loadAddon(attach);
|
||||
setTerm(te);
|
||||
@ -81,21 +84,9 @@ export default function VmPage() {
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<VpsInstanceRow vm={state} actions={false} />
|
||||
<VpsInstanceRow vm={state} actions={true} />
|
||||
{action === undefined && (
|
||||
<>
|
||||
<div className="text-xl">Renewal</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<div>{new Date(state.expires).toDateString()}</div>
|
||||
{state.template?.cost_plan && (
|
||||
<div>
|
||||
<CostLabel cost={state.template?.cost_plan} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<AsyncButton onClick={() => navigate("/vm/renew", { state })}>
|
||||
Extend Now
|
||||
</AsyncButton>
|
||||
<div className="flex gap-4 items-center">
|
||||
<div className="text-xl">Network:</div>
|
||||
{(state.ip_assignments?.length ?? 0) === 0 && (
|
||||
@ -118,7 +109,21 @@ export default function VmPage() {
|
||||
<div className="text-sm bg-neutral-900 px-3 py-1 rounded-lg">
|
||||
{state.ssh_key?.name}
|
||||
</div>
|
||||
<Icon name="pencil" onClick={() => setEditKey(true)} />
|
||||
</div>
|
||||
<hr />
|
||||
<div className="text-xl">Renewal</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<div>{new Date(state.expires).toDateString()}</div>
|
||||
{state.template?.cost_plan && (
|
||||
<div>
|
||||
<CostLabel cost={state.template?.cost_plan} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<AsyncButton onClick={() => navigate("/vm/renew", { state })}>
|
||||
Extend Now
|
||||
</AsyncButton>
|
||||
{/*
|
||||
{!term && <AsyncButton onClick={openTerminal}>Connect Terminal</AsyncButton>}
|
||||
{term && <div className="border p-2" ref={termRef}></div>}*/}
|
||||
@ -131,11 +136,8 @@ export default function VmPage() {
|
||||
<VpsPayment
|
||||
payment={payment}
|
||||
onPaid={async () => {
|
||||
if (!login?.signer || !state) return;
|
||||
const api = new LNVpsApi(
|
||||
ApiUrl,
|
||||
new EventPublisher(login.signer, login.pubkey),
|
||||
);
|
||||
if (!login?.builder || !state) return;
|
||||
const api = new LNVpsApi(ApiUrl, login.builder);
|
||||
const newState = await api.getVm(state.id);
|
||||
navigate("/vm", {
|
||||
state: newState,
|
||||
@ -146,6 +148,30 @@ export default function VmPage() {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{editKey && (
|
||||
<Modal id="edit-ssh-key" onClose={() => setEditKey(false)}>
|
||||
<SSHKeySelector selectedKey={key} setSelectedKey={setKey} />
|
||||
<div className="flex flex-col gap-4 mt-8">
|
||||
<small>After selecting a new key, please restart the VM.</small>
|
||||
<AsyncButton
|
||||
onClick={async () => {
|
||||
if (!state) return;
|
||||
await api?.patchVm(state.id, {
|
||||
ssh_key_id: key,
|
||||
});
|
||||
const ns = await api?.getVm(state?.id);
|
||||
navigate(".", {
|
||||
state: ns,
|
||||
replace: true,
|
||||
});
|
||||
setEditKey(false);
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</AsyncButton>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
22
src/utils.ts
22
src/utils.ts
@ -31,13 +31,23 @@ export async function openFile(): Promise<File | undefined> {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export function toEui64(prefix: string, mac: string) {
|
||||
const macData = base16.decode(mac.replace(/:/g, "").toUpperCase());
|
||||
const macExtended = new Uint8Array([...macData.subarray(0, 3), 0xff, 0xfe, ...macData.subarray(3, 6)])
|
||||
const macExtended = new Uint8Array([
|
||||
...macData.subarray(0, 3),
|
||||
0xff,
|
||||
0xfe,
|
||||
...macData.subarray(3, 6),
|
||||
]);
|
||||
macExtended[0] |= 0x02;
|
||||
return (prefix + base16.encode(macExtended.subarray(0, 2)) + ":"
|
||||
+ base16.encode(macExtended.subarray(2, 4)) + ":"
|
||||
+ base16.encode(macExtended.subarray(4, 6)) + ":"
|
||||
+ base16.encode(macExtended.subarray(6, 8))).toLowerCase();
|
||||
return (
|
||||
prefix +
|
||||
base16.encode(macExtended.subarray(0, 2)) +
|
||||
":" +
|
||||
base16.encode(macExtended.subarray(2, 4)) +
|
||||
":" +
|
||||
base16.encode(macExtended.subarray(4, 6)) +
|
||||
":" +
|
||||
base16.encode(macExtended.subarray(6, 8))
|
||||
).toLowerCase();
|
||||
}
|
22
yarn.lock
22
yarn.lock
@ -758,20 +758,20 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@snort/system-react@npm:^1.5.7":
|
||||
version: 1.5.7
|
||||
resolution: "@snort/system-react@npm:1.5.7"
|
||||
"@snort/system-react@npm:^1.6.1":
|
||||
version: 1.6.1
|
||||
resolution: "@snort/system-react@npm:1.6.1"
|
||||
dependencies:
|
||||
"@snort/shared": "npm:^1.0.17"
|
||||
"@snort/system": "npm:^1.5.7"
|
||||
"@snort/system": "npm:^1.6.1"
|
||||
react: "npm:^18.2.0"
|
||||
checksum: 10c0/b8261d72bef88fc6baa91f3f3765a7e65e7775e0f87142079ec425fdc3639871da068b2bfc9c4a2c0f15a20c7c76ffe84d9bc8cbc1e6ef5a76989cd78d3f7049
|
||||
checksum: 10c0/fe3180c1f4341df4fd585c4031ef632a7d59c7637c6aed16ea254a1c22a66e288516d96939c0080efd0fb73940531855a59a24fae5f82c326f6544362eb0394c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@snort/system@npm:^1.5.7":
|
||||
version: 1.5.7
|
||||
resolution: "@snort/system@npm:1.5.7"
|
||||
"@snort/system@npm:^1.6.1":
|
||||
version: 1.6.1
|
||||
resolution: "@snort/system@npm:1.6.1"
|
||||
dependencies:
|
||||
"@noble/ciphers": "npm:^0.6.0"
|
||||
"@noble/curves": "npm:^1.4.0"
|
||||
@ -786,7 +786,7 @@ __metadata:
|
||||
nostr-social-graph: "npm:^1.0.3"
|
||||
uuid: "npm:^9.0.0"
|
||||
ws: "npm:^8.14.0"
|
||||
checksum: 10c0/9b1d6e36dfc3c0845754d4f2c10eb39665a2c4c4c61a07635e0b792a352f8566dbd79561561568c182272cd92b0d2c421ef137775b16872b7e28fa39366e2094
|
||||
checksum: 10c0/5da01450970f4a51df98369c4cdd2b8886add3480e7962d11ed17498cf7db421bcd5e585abe532ff754efc0bf26b5734b9b24005f958802357ec390ccb74d281
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -2379,8 +2379,8 @@ __metadata:
|
||||
"@eslint/js": "npm:^9.8.0"
|
||||
"@scure/base": "npm:^1.2.1"
|
||||
"@snort/shared": "npm:^1.0.17"
|
||||
"@snort/system": "npm:^1.5.7"
|
||||
"@snort/system-react": "npm:^1.5.7"
|
||||
"@snort/system": "npm:^1.6.1"
|
||||
"@snort/system-react": "npm:^1.6.1"
|
||||
"@types/react": "npm:^18.3.3"
|
||||
"@types/react-dom": "npm:^18.3.0"
|
||||
"@vitejs/plugin-react": "npm:^4.3.1"
|
||||
|
Loading…
x
Reference in New Issue
Block a user