feat: backend api

This commit is contained in:
kieran 2024-11-26 15:29:18 +00:00
parent daa640068b
commit 3c7736dae7
No known key found for this signature in database
GPG Key ID: DE71CEB3925BE941
34 changed files with 1147 additions and 697 deletions

View File

@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/jpeg" href="/logo.jpg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Bitcoin Lightning VPS provider" />
@ -14,7 +14,7 @@
<meta property="og:type" content="website" />
<meta property="og:title" content="LNVPS" />
<meta property="og:description" content="Bitcoin Lightning VPS provider" />
<meta property="og:image" content="/logo.png" />
<meta property="og:image" content="/logo.jpg" />
<title>LNVPS</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
@ -27,6 +27,5 @@
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<script src="https://btcpay.v0l.io/modal/btcpay.js"></script>
</body>
</html>

View File

@ -10,11 +10,15 @@
"preview": "vite preview"
},
"dependencies": {
"@scure/base": "^1.2.1",
"@snort/shared": "^1.0.17",
"@snort/system": "^1.5.1",
"@snort/system-react": "^1.5.1",
"@snort/system": "^1.5.7",
"@snort/system-react": "^1.5.7",
"classnames": "^2.5.1",
"qr-code-styling": "^1.8.4",
"react": "^18.3.1",
"react-dom": "^18.3.1"
"react-dom": "^18.3.1",
"react-router-dom": "^7.0.1"
},
"devDependencies": {
"@eslint/js": "^9.8.0",

BIN
public/logo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,111 +0,0 @@
import { SnortContext } from "@snort/system-react";
import { CostInterval, DiskType, MachineSpec } from "./api";
import VpsCard from "./components/vps-card";
import { GiB, NostrProfile } from "./const";
import { NostrSystem } from "@snort/system";
import Profile from "./components/profile";
import LoginButton from "./components/login-button";
import pgp from "../public/lnvps.asc?url";
const Offers: Array<MachineSpec> = [
{
id: "2x2x80",
location: "IE",
active: true,
cpu: 2,
ram: 2 * GiB,
disk: {
type: DiskType.SSD,
size: 80 * GiB,
},
cost: {
interval: CostInterval.Month,
count: 3,
currency: "EUR",
},
},
{
id: "4x4x160",
location: "IE",
active: true,
cpu: 4,
ram: 4 * GiB,
disk: {
type: DiskType.SSD,
size: 160 * GiB,
},
cost: {
interval: CostInterval.Month,
count: 5,
currency: "EUR",
},
},
{
id: "8x8x400",
location: "IE",
active: true,
cpu: 8,
ram: 8 * GiB,
disk: {
type: DiskType.SSD,
size: 400 * GiB,
},
cost: {
interval: CostInterval.Month,
count: 12,
currency: "EUR",
},
},
];
const system = new NostrSystem({
automaticOutboxModel: false,
buildFollowGraph: false,
});
[
"wss://relay.snort.social/",
"wss://relay.damus.io/",
"wss://relay.nostr.band/",
"wss://nos.lol/",
].forEach((a) => system.ConnectToRelay(a, { read: true, write: true }));
export default function App() {
return (
<SnortContext.Provider value={system}>
<div className="w-[700px] mx-auto m-2 p-2">
<div className="flex items-center justify-between">
LNVPS
<LoginButton />
</div>
<h1>VPS Offers</h1>
<div className="flex flex-col gap-2">
<div className="grid grid-cols-3 gap-2">
{Offers.map((a) => (
<VpsCard spec={a} key={a.id} />
))}
</div>
<small>
All VPS come with 1x IPv4 and 1x IPv6 address and unmetered
traffic
</small>
<div className="flex flex-col gap-4">
<b>
Please email <a href="mailto:sales@lnvps.net">sales</a> after
paying the invoice with your order id, desired OS and ssh key.
</b>
<b>You can also find us on nostr: </b>
<a target="_blank" href={`nostr:${NostrProfile.encode()}`}>
<Profile link={NostrProfile} />
</a>
<div>
<a target="_blank" href="http://speedtest.v0l.io">Speedtest</a> | <a href={pgp}>PGP</a>
</div>
</div>
</div>
</div>
</SnortContext.Provider>
);
}

View File

@ -1,30 +1,215 @@
export interface MachineSpec {
id: string;
active: boolean;
import { EventKind, EventPublisher } from "@snort/system";
import { base64 } from "@scure/base";
export interface ApiResponseBase {
error?: string;
}
export type ApiResponse<T> = ApiResponseBase & {
data: T;
};
export interface VmCostPlan {
id: number;
name: string;
created: Date;
amount: number;
currency: "EUR" | "BTC";
interval_amount: number;
interval_type: string;
}
export interface VmHostRegion {
id: number;
name: string;
enabled: boolean;
}
export interface VmTemplate {
id: number;
name: string;
enabled: boolean;
created: Date;
expires?: Date;
cpu: number;
ram: number;
disk: {
type: DiskType;
size: number;
memory: number;
disk_size: number;
disk_type: string;
disk_interface: string;
cost_plan_id: number;
region_id: number;
cost_plan?: VmCostPlan;
region?: VmHostRegion;
}
export interface VmStatus {
state: "running" | "stopped";
cpu_usage: number;
mem_usage: number;
uptime: number;
net_in: number;
net_out: number;
disk_write: number;
disk_read: number;
}
export interface VmInstance {
id: number;
host_id: number;
user_id: number;
image_id: number;
template_id: number;
ssh_key_id: number;
created: Date;
expires: Date;
cpu: number;
memory: number;
disk_size: number;
disk_id: number;
status?: VmStatus;
template?: VmTemplate;
image?: VmOsImage;
ssh_key?: UserSshKey;
}
export interface VmOsImage {
id: number;
distribution: string;
flavour: string;
version: string;
release_date: string;
}
export interface UserSshKey {
id: number;
name: string;
}
export interface VmPayment {
id: string;
invoice: string;
created: string;
expires: string;
amount: number;
is_paid: boolean;
}
export class LNVpsApi {
constructor(
readonly url: string,
readonly publisher: EventPublisher | undefined,
) {}
async listVms() {
const { data } = await this.#handleResponse<ApiResponse<Array<VmInstance>>>(
await this.#req("/api/v1/vm", "GET"),
);
return data;
}
async getVm(id: number) {
const { data } = await this.#handleResponse<ApiResponse<VmInstance>>(
await this.#req(`/api/v1/vm/${id}`, "GET"),
);
return data;
}
async listOffers() {
const { data } = await this.#handleResponse<ApiResponse<Array<VmTemplate>>>(
await this.#req("/api/v1/vm/templates", "GET"),
);
return data;
}
async listOsImages() {
const { data } = await this.#handleResponse<ApiResponse<Array<VmOsImage>>>(
await this.#req("/api/v1/image", "GET"),
);
return data;
}
async listSshKeys() {
const { data } = await this.#handleResponse<ApiResponse<Array<UserSshKey>>>(
await this.#req("/api/v1/ssh-key", "GET"),
);
return data;
}
async addSshKey(name: string, key: string) {
const { data } = await this.#handleResponse<ApiResponse<UserSshKey>>(
await this.#req("/api/v1/ssh-key", "POST", {
name,
key_data: key,
}),
);
return data;
}
async orderVm(template_id: number, image_id: number, ssh_key_id: number) {
const { data } = await this.#handleResponse<ApiResponse<VmInstance>>(
await this.#req("/api/v1/vm", "POST", {
template_id,
image_id,
ssh_key_id,
}),
);
return data;
}
async renewVm(vm_id: number) {
const { data } = await this.#handleResponse<ApiResponse<VmPayment>>(
await this.#req(`/api/v1/vm/${vm_id}/renew`, "GET"),
);
return data;
}
async paymentStatus(id: string) {
const { data } = await this.#handleResponse<ApiResponse<VmPayment>>(
await this.#req(`/api/v1/payment/${id}`, "GET"),
);
return data;
}
async #handleResponse<T extends ApiResponseBase>(rsp: Response) {
if (rsp.ok) {
return (await rsp.json()) as T;
} else {
const text = await rsp.text();
try {
const obj = JSON.parse(text) as ApiResponseBase;
throw new Error(obj.error);
} catch {
throw new Error(text);
}
}
}
async #req(path: string, method: "GET" | "POST" | "DELETE", body?: object) {
const auth = async (url: string, method: string) => {
const auth = await this.publisher?.generic((eb) => {
return eb
.kind(EventKind.HttpAuthentication)
.tag(["u", url])
.tag(["method", method]);
});
if (auth) {
return `Nostr ${base64.encode(
new TextEncoder().encode(JSON.stringify(auth)),
)}`;
}
};
cost: {
interval: CostInterval;
count: number;
currency: CostCurrency;
};
location: string;
}
export enum DiskType {
HDD,
SSD,
const u = `${this.url}${path}`;
return await fetch(u, {
method,
body: body ? JSON.stringify(body) : undefined,
headers: {
accept: "application/json",
"content-type": "application/json",
authorization: (await auth(u, method)) ?? "",
},
});
}
export enum CostInterval {
Hour,
Day,
Month,
Year,
}
export type CostCurrency = "EUR" | "USD" | "BTC";

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -1,15 +1,25 @@
import classNames from "classnames";
import { forwardRef, HTMLProps } from "react";
export type AsyncButtonProps = {
onClick?: (e: React.MouseEvent) => Promise<void>;
onClick?: (e: React.MouseEvent) => Promise<void> | void;
} & Omit<HTMLProps<HTMLButtonElement>, "type" | "ref" | "onClick">;
const AsyncButton = forwardRef<HTMLButtonElement, AsyncButtonProps>(
function AsyncButton(props, ref) {
function AsyncButton({ className, ...props }, ref) {
const hasBg = className?.includes("bg-");
return (
<button
ref={ref}
className="bg-slate-700 py-1 px-2 rounded-xl"
className={classNames(
"py-1 px-2 rounded-xl font-medium",
{
"bg-neutral-800 cursor-not-allowed text-neutral-500":
!hasBg && props.disabled === true,
"bg-neutral-900": !hasBg && !props.disabled,
},
className,
)}
{...props}
>
{props.children}

View File

@ -1,22 +1,20 @@
import { CostInterval, MachineSpec } from "../api";
import { VmCostPlan } from "../api";
export default function CostLabel({ cost }: { cost: MachineSpec["cost"] }) {
function intervalName(n: number) {
export default function CostLabel({ cost }: { cost: VmCostPlan }) {
function intervalName(n: string) {
switch (n) {
case CostInterval.Hour:
return "Hour";
case CostInterval.Day:
case "day":
return "Day";
case CostInterval.Month:
case "month":
return "Month";
case CostInterval.Year:
case "year":
return "Year";
}
}
return (
<>
{cost.count} {cost.currency}/{intervalName(cost.interval)}
{cost.amount} {cost.currency}/{intervalName(cost.interval_type)}
</>
);
}

26
src/components/icon.tsx Normal file
View File

@ -0,0 +1,26 @@
import { MouseEventHandler } from "react";
import Icons from "../icons.svg";
type Props = {
name: string;
size?: number;
className?: string;
onClick?: MouseEventHandler<SVGSVGElement>;
};
export function Icon(props: Props) {
const size = props.size || 20;
const href = `${Icons}#${props.name}`;
return (
<svg
width={size}
height={size}
className={props.className}
onClick={props.onClick}
>
<use href={href} />
</svg>
);
}

View File

@ -5,6 +5,7 @@ import { loginNip7 } from "../login";
import useLogin from "../hooks/login";
import Profile from "./profile";
import { NostrLink } from "@snort/system";
import { Link } from "react-router-dom";
export default function LoginButton() {
const system = useContext(SnortContext);
@ -19,6 +20,8 @@ export default function LoginButton() {
Sign In
</AsyncButton>
) : (
<Link to="/account">
<Profile link={NostrLink.publicKey(login.pubkey)} />
</Link>
);
}

85
src/components/modal.tsx Normal file
View File

@ -0,0 +1,85 @@
import classNames from "classnames";
import React, { ReactNode, useEffect } from "react";
import { createPortal } from "react-dom";
import { AsyncButton } from "./button";
export interface ModalProps {
id: string;
className?: string;
bodyClassName?: string;
onClose?: (e: React.MouseEvent | KeyboardEvent) => void;
onClick?: (e: React.MouseEvent) => void;
children?: ReactNode;
showClose?: boolean;
ready?: boolean;
largeModal?: boolean;
}
export default function Modal(props: ModalProps) {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape" && props.onClose) {
props.onClose(e);
}
};
useEffect(() => {
document.body.classList.add("scroll-lock");
document.addEventListener("keydown", handleKeyDown);
return () => {
document.body.classList.remove("scroll-lock");
document.removeEventListener("keydown", handleKeyDown);
};
}, []);
const handleBackdropClick = (e: React.MouseEvent) => {
e.stopPropagation();
props.onClose?.(e);
};
return createPortal(
<div
className={classNames(
"z-[42] w-screen h-screen top-0 left-0 fixed bg-black/80 flex justify-center overflow-y-auto",
)}
onMouseDown={handleBackdropClick}
onClick={(e) => {
e.stopPropagation();
}}
>
<div
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",
{
"max-xl:-translate-y-[calc(100vh-100dvh)]": props.ready ?? true,
"max-xl:translate-y-[50vh]": !(props.ready ?? true),
"lg:w-[50vw]": !(props.largeModal ?? false),
"lg:w-[80vw]": props.largeModal ?? false,
},
)
}
onMouseDown={(e) => e.stopPropagation()}
onClick={(e) => {
e.stopPropagation();
props.onClick?.(e);
}}
>
{(props.showClose ?? true) && (
<div className="absolute right-4 top-4">
<AsyncButton
onClick={async (e) => {
e.stopPropagation();
props.onClose?.(e);
}}
className="rounded-full aspect-square bg-layer-2 p-3"
/>
</div>
)}
{props.children}
</div>
</div>,
document.body,
) as JSX.Element;
}

View File

@ -0,0 +1,9 @@
import { VmOsImage } from "../api";
export default function OsImageName({ image }: { image: VmOsImage }) {
return (
<span>
{image.distribution} {image.version}
</span>
);
}

View File

@ -1,75 +0,0 @@
.btcpay-form {
display: inline-flex;
align-items: center;
justify-content: center;
}
.btcpay-form--inline {
flex-direction: row;
}
.btcpay-form--block {
flex-direction: column;
}
.btcpay-form--inline .submit {
margin-left: 15px;
}
.btcpay-form--block select {
margin-bottom: 10px;
}
.btcpay-form .btcpay-custom-container {
text-align: center;
}
.btcpay-custom {
display: flex;
align-items: center;
justify-content: center;
}
.btcpay-form .plus-minus {
cursor: pointer;
font-size: 25px;
line-height: 25px;
background: #dfe0e1;
height: 30px;
width: 45px;
border: none;
border-radius: 60px;
margin: auto 5px;
display: inline-flex;
justify-content: center;
}
.btcpay-form select {
-moz-appearance: none;
-webkit-appearance: none;
appearance: none;
color: currentColor;
background: transparent;
border: 1px solid transparent;
display: block;
padding: 1px;
margin-left: auto;
margin-right: auto;
font-size: 11px;
cursor: pointer;
}
.btcpay-form select:hover {
border-color: #ccc;
}
.btcpay-form option {
color: #000;
background: rgba(0, 0, 0, 0.1);
}
.btcpay-input-price {
-moz-appearance: textfield;
border: none;
box-shadow: none;
text-align: center;
font-size: 25px;
margin: auto;
border-radius: 5px;
line-height: 35px;
background: #fff;
}
.btcpay-input-price::-webkit-outer-spin-button,
.btcpay-input-price::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}

View File

@ -1,6 +1,8 @@
import { MachineSpec } from "../api";
import "./pay-button.css";
import { ReactNode } from "react";
import { VmTemplate } from "../api";
import useLogin from "../hooks/login";
import { AsyncButton } from "./button";
import { useNavigate } from "react-router-dom";
declare global {
interface Window {
@ -10,55 +12,33 @@ declare global {
}
}
export default function VpsPayButton({ spec }: { spec: MachineSpec }) {
const serverUrl = "https://btcpay.v0l.io/api/v1/invoices";
export default function VpsPayButton({ spec }: { spec: VmTemplate }) {
const login = useLogin();
const classNames =
"w-full text-center text-lg uppercase rounded-xl py-3 font-bold cursor-pointer select-none";
const navigte = useNavigate();
function handleFormSubmit(event: React.FormEvent) {
event.preventDefault();
const form = event.target as HTMLFormElement;
const xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function () {
if (this.readyState == 4 && this.status == 200 && this.responseText) {
window.btcpay?.appendInvoiceFrame(
JSON.parse(this.responseText).invoiceId,
);
}
};
xhttp.open("POST", serverUrl, true);
xhttp.send(new FormData(form));
function placeholder(inner: ReactNode) {
return <div className={`${classNames} bg-red-900`}>{inner}</div>;
}
if (!spec.active) {
if (!spec.enabled) {
return placeholder("Unavailable");
}
if (!login) {
return placeholder("Please Login");
}
return (
<div className="text-center text-xl uppercase bg-red-800 rounded-xl py-3 font-bold">
Unavailable
</div>
);
<AsyncButton
className={`${classNames} bg-green-800`}
onClick={() =>
navigte("/order", {
state: spec,
})
}
return (
<form
method="POST"
action={serverUrl}
className="btcpay-form btcpay-form--block w-full"
onSubmit={handleFormSubmit}
>
<input
type="hidden"
name="storeId"
value="CdaHy1puLx4kLC9BG3A9mu88XNyLJukMJRuuhAfbDrxg"
/>
<input type="hidden" name="jsonResponse" value="true" />
<input type="hidden" name="orderId" value={spec.id} />
<input type="hidden" name="price" value={spec.cost.count} />
<input type="hidden" name="currency" value={spec.cost.currency} />
<input
type="image"
className="w-full"
name="submit"
src="https://btcpay.v0l.io/img/paybutton/pay.svg"
alt="Pay with BTCPay Server, a Self-Hosted Bitcoin Payment Processor"
/>
</form>
Buy Now
</AsyncButton>
);
}

50
src/components/qr.tsx Normal file
View File

@ -0,0 +1,50 @@
import QRCodeStyling from "qr-code-styling";
import { useEffect, useRef } from "react";
export interface QrCodeProps {
data?: string;
link?: string;
avatar?: string;
height?: number;
width?: number;
className?: string;
}
export default function QrCode(props: QrCodeProps) {
const qrRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if ((props.data?.length ?? 0) > 0 && qrRef.current) {
const qr = new QRCodeStyling({
width: props.width || 256,
height: props.height || 256,
data: props.data,
margin: 5,
type: "canvas",
image: props.avatar,
dotsOptions: {
type: "rounded",
},
cornersSquareOptions: {
type: "extra-rounded",
},
imageOptions: {
crossOrigin: "anonymous",
},
});
qrRef.current.innerHTML = "";
qr.append(qrRef.current);
if (props.link) {
qrRef.current.onclick = function () {
const elm = document.createElement("a");
elm.href = props.link ?? "";
elm.click();
};
}
} else if (qrRef.current) {
qrRef.current.innerHTML = "";
}
}, [props.data, props.link, props.width, props.height, props.avatar]);
return <div className={props.className} ref={qrRef}></div>;
}

View File

@ -0,0 +1,28 @@
import { VmInstance } from "../api";
import { Icon } from "./icon";
export default function VmActions({ vm }: { vm: VmInstance }) {
const state = vm.status?.state;
if (!state) return;
return (
<div className="flex flex-col gap-1">
<div className="flex gap-2">
<Icon
name={state === "running" ? "stop" : "start"}
className="bg-neutral-700 p-2 rounded-lg hover:bg-neutral-600"
size={40}
/>
<Icon
name="delete"
className="bg-neutral-700 p-2 rounded-lg hover:bg-neutral-600"
size={40}
/>
<Icon
name="refresh-1"
className="bg-neutral-700 p-2 rounded-lg hover:bg-neutral-600"
size={40}
/>
</div>
</div>
);
}

View File

@ -1,28 +1,23 @@
import { DiskType, MachineSpec } from "../api";
import { VmTemplate } from "../api";
import BytesSize from "./bytes";
import CostLabel from "./cost";
import VpsPayButton from "./pay-button";
export default function VpsCard({ spec }: { spec: MachineSpec }) {
export default function VpsCard({ spec }: { spec: VmTemplate }) {
return (
<div className="rounded-xl border border-neutral-600 px-3 py-2">
<h2>{spec.id}</h2>
<h2>{spec.name}</h2>
<ul>
<li>CPU: {spec.cpu}vCPU</li>
<li>
RAM: <BytesSize value={spec.ram} />
RAM: <BytesSize value={spec.memory} />
</li>
<li>
{spec.disk.type === DiskType.SSD ? "SSD" : "HDD"}:{" "}
<BytesSize value={spec.disk.size} />
</li>
<li>
Location: {spec.location}
{spec.disk_type.toUpperCase()}: <BytesSize value={spec.disk_size} />
</li>
<li>Location: {spec.region?.name}</li>
</ul>
<h2>
<CostLabel cost={spec.cost} />
</h2>
<h2>{spec.cost_plan && <CostLabel cost={spec.cost_plan} />}</h2>
<VpsPayButton spec={spec} />
</div>
);

View File

@ -0,0 +1,37 @@
import { Link } from "react-router-dom";
import { VmInstance } from "../api";
import OsImageName from "./os-image-name";
import VpsResources from "./vps-resources";
import VmActions from "./vps-actions";
export default function VpsInstanceRow({ vm }: { vm: VmInstance }) {
const expires = new Date(vm.expires);
const isExpired = expires <= new Date();
return (
<div className="flex justify-between items-center rounded-xl bg-neutral-900 px-3 py-2 cursor-pointer hover:bg-neutral-800">
<div className="flex flex-col gap-2">
<div>
<span className="text-sm text-neutral-400">#{vm.id}</span>
&nbsp;
{vm.template?.name}
&nbsp;
<span className="text-sm text-neutral-400">
<OsImageName image={vm.image!} />
</span>
</div>
<VpsResources vm={vm} />
</div>
<div className="flex gap-2 items-center">
{isExpired && (
<>
<Link to="/vm/renew" className="text-red-500 text-sm" state={vm}>
Expired
</Link>
</>
)}
{!isExpired && <VmActions vm={vm} />}
</div>
</div>
);
}

View File

@ -0,0 +1,58 @@
import { useEffect } from "react";
import { LNVpsApi, VmPayment } from "../api";
import QrCode from "./qr";
import useLogin from "../hooks/login";
import { ApiUrl } from "../const";
import { EventPublisher } from "@snort/system";
export default function VpsPayment({
payment,
onPaid,
}: {
payment: VmPayment;
onPaid?: () => void;
}) {
const login = useLogin();
const ln = `lightning:${payment.invoice}`;
async function checkPayment(api: LNVpsApi) {
try {
const st = await api.paymentStatus(payment.id);
if (st.is_paid) {
onPaid?.();
return true;
}
} catch (e) {
console.error(e);
}
return false;
}
useEffect(() => {
if (!login?.signer) return;
const api = new LNVpsApi(
ApiUrl,
new EventPublisher(login.signer, login.pubkey),
);
const tx = setInterval(async () => {
if (await checkPayment(api)) {
clearInterval(tx);
}
}, 2_000);
return () => clearInterval(tx);
}, [login]);
return (
<div className="flex flex-col gap-4 rounded-xl p-3 bg-neutral-900 items-center">
<QrCode
data={ln}
link={ln}
width={512}
height={512}
avatar="/logo.jpg"
className="cursor-pointer rounded-xl overflow-hidden"
/>
{(payment.amount / 1000).toLocaleString()} sats
</div>
);
}

View File

@ -0,0 +1,31 @@
import { VmInstance, VmTemplate } from "../api";
import BytesSize from "./bytes";
export default function VpsResources({ vm }: { vm: VmInstance | VmTemplate }) {
const diskType = "template_id" in vm ? vm.template?.disk_type : vm.disk_type;
const region =
"region_id" in vm ? vm.region?.name : vm.template?.region?.name;
const status = "status" in vm ? vm.status : undefined;
return (
<>
<div className="text-xs text-neutral-400">
{vm.cpu} vCPU, <BytesSize value={vm.memory} /> RAM,{" "}
<BytesSize value={vm.disk_size} /> {diskType?.toUpperCase()},{" "}
{region && <>Location: {region}</>}
</div>
{status && status.state === "running" && (
<div className="text-sm text-neutral-200">
<div className="w-2 h-2 rounded-full bg-green-800 inline-block"></div>{" "}
{(100 * status.cpu_usage).toFixed(1)}% CPU,{" "}
{(100 * status.mem_usage).toFixed(0)}% RAM
</div>
)}
{status && status.state === "stopped" && (
<div className="text-sm text-neutral-200">
<div className="w-2 h-2 rounded-full bg-red-800 inline-block"></div>{" "}
Stopped
</div>
)}
</>
);
}

View File

@ -12,6 +12,9 @@ export const GB = KB * 1000;
export const TB = GB * 1000;
export const PB = TB * 1000;
//export const ApiUrl = "http://localhost:8000";
export const ApiUrl = "https://api.lnvps.net";
export const NostrProfile = new NostrLink(
NostrPrefix.Profile,
"fcd818454002a6c47a980393f0549ac6e629d28d5688114bb60d831b5c1832a7",

View File

@ -3,10 +3,7 @@ import { Login } from "../login";
export default function useLogin() {
return useSyncExternalStore(
(c) => {
Login?.on("change", c);
return () => Login?.off("change", c);
},
() => Login,
(c) => Login.hook(c),
() => Login.snapshot(),
);
}

20
src/icons.svg Normal file
View File

@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg">
<defs>
<svg id="start" 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="M13.5 3C13.5 2.17157 12.8284 1.5 12 1.5C11.1716 1.5 10.5 2.17157 10.5 3V13C10.5 13.8284 11.1716 14.5 12 14.5C12.8284 14.5 13.5 13.8284 13.5 13V3ZM7.85385 5.7491C8.54371 5.29043 8.73113 4.35936 8.27245 3.6695C7.81378 2.97963 6.8827 2.79222 6.19284 3.2509C3.36739 5.12948 1.5 8.34635 1.5 12C1.5 17.799 6.20101 22.5 12 22.5C17.799 22.5 22.5 17.799 22.5 12C22.5 8.34635 20.6326 5.12948 17.8072 3.2509C17.1173 2.79222 16.1862 2.97963 15.7275 3.6695C15.2689 4.35936 15.4563 5.29043 16.1461 5.7491C18.1708 7.09528 19.5 9.39275 19.5 12C19.5 16.1422 16.1421 19.5 12 19.5C7.85786 19.5 4.5 16.1422 4.5 12C4.5 9.39275 5.82917 7.09528 7.85385 5.7491Z" fill="#F7F9FC"/>
</svg>
<svg id="stop" viewBox="0 0 24 24" fill="none">
<path d="M24 0V24H0V0H24Z" fill="white" fill-opacity="0.01"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 6C4 4.89543 4.89543 4 6 4H18C19.1046 4 20 4.89543 20 6V18C20 19.1046 19.1046 20 18 20H6C4.89543 20 4 19.1046 4 18V6Z" fill="white"/>
</svg>
<svg id="delete" 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="M9 3C9 2.44772 9.44772 2 10 2H14C14.5523 2 15 2.44772 15 3C15 3.55228 14.5523 4 14 4H10C9.44772 4 9 3.55228 9 3ZM5.92032 5H4C3.44772 5 3 5.44772 3 6C3 6.55228 3.44772 7 4 7H4.99745L5.9362 20.1425C6.01096 21.1891 6.88184 22 7.93112 22H16.0689C17.1182 22 17.989 21.1891 18.0638 20.1425L19.0025 7H20C20.5523 7 21 6.55228 21 6C21 5.44772 20.5523 5 20 5H18.0797C18.0735 4.99994 18.0673 4.99994 18.0611 5H5.93889C5.93271 4.99994 5.92652 4.99994 5.92032 5Z" fill="#F7F9FC"/>
</svg>
<svg id="refresh-1" 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="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>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -15,13 +15,23 @@ body {
h1 {
@apply text-2xl font-medium my-2;
}
h2 {
@apply text-xl font-medium my-2;
}
h3 {
@apply text-lg font-medium my-2;
}
a {
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
hr {
@apply border-neutral-800;
}

View File

@ -1,13 +1,37 @@
import { Nip7Signer, SystemInterface, UserState } from "@snort/system";
import { ExternalStore } from "@snort/shared";
import {
EventSigner,
Nip7Signer,
SystemInterface,
UserState,
} from "@snort/system";
export let Login: UserState<void> | undefined;
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");
}
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;
}
}
export const Login = new LoginShell();
export async function loginNip7(system: SystemInterface) {
const signer = new Nip7Signer();
const pubkey = await signer.getPubKey();
if (pubkey) {
Login = new UserState<void>(pubkey);
await Login.init(signer, system);
await Login.login(signer, system);
} else {
throw new Error("No nostr extension found");
}

View File

@ -1,10 +1,54 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
import { RouterProvider, createBrowserRouter } from "react-router-dom";
import { NostrSystem } from "@snort/system";
import { SnortContext } from "@snort/system-react";
import Layout from "./pages/layout.tsx";
import HomePage from "./pages/home.tsx";
import OrderPage from "./pages/order.tsx";
import VmPage from "./pages/vm.tsx";
import AccountPage from "./pages/account.tsx";
const system = new NostrSystem({
automaticOutboxModel: false,
buildFollowGraph: false,
});
[
"wss://relay.snort.social/",
"wss://relay.damus.io/",
"wss://relay.nostr.band/",
"wss://nos.lol/",
].forEach((a) => system.ConnectToRelay(a, { read: true, write: true }));
const router = createBrowserRouter([
{
element: <Layout />,
children: [
{
path: "/",
element: <HomePage />,
},
{
path: "/account",
element: <AccountPage />,
},
{
path: "/order",
element: <OrderPage />,
},
{
path: "/vm/:action?",
element: <VmPage />,
},
],
},
]);
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
<SnortContext.Provider value={system}>
<RouterProvider router={router} />
</SnortContext.Provider>
</StrictMode>,
);

31
src/pages/account.tsx Normal file
View File

@ -0,0 +1,31 @@
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";
export default function AccountPage() {
const login = useLogin();
const [vms, setVms] = useState<Array<VmInstance>>([]);
useEffect(() => {
if (!login?.signer) return;
const api = new LNVpsApi(
ApiUrl,
new EventPublisher(login.signer, login.pubkey),
);
api.listVms().then(setVms);
}, [login]);
return (
<>
<h3>My Resources</h3>
<div className="flex flex-col gap-2">
{vms.map((a) => (
<VpsInstanceRow key={a.id} vm={a} />
))}
</div>
</>
);
}

43
src/pages/home.tsx Normal file
View File

@ -0,0 +1,43 @@
import { useState, useEffect } from "react";
import { VmTemplate, LNVpsApi } from "../api";
import Profile from "../components/profile";
import VpsCard from "../components/vps-card";
import { ApiUrl, NostrProfile } from "../const";
export default function HomePage() {
const [offers, setOffers] = useState<Array<VmTemplate>>([]);
useEffect(() => {
const api = new LNVpsApi(ApiUrl, undefined);
api.listOffers().then((o) => setOffers(o));
}, []);
return (
<>
<h1>VPS Offers</h1>
<div className="flex flex-col gap-2">
<div className="grid grid-cols-3 gap-2">
{offers.map((a) => (
<VpsCard spec={a} key={a.id} />
))}
</div>
<small>
All VPS come with 1x IPv4 and 1x IPv6 address and unmetered traffic
</small>
<div className="flex flex-col gap-4">
<b>You can also find us on nostr: </b>
<a target="_blank" href={`nostr:${NostrProfile.encode()}`}>
<Profile link={NostrProfile} />
</a>
<div>
<a target="_blank" href="http://speedtest.v0l.io">
Speedtest
</a>{" "}
| <a href="/public/lnvps.asc">PGP</a>
</div>
</div>
</div>
</>
);
}

15
src/pages/layout.tsx Normal file
View File

@ -0,0 +1,15 @@
import { Link, Outlet } from "react-router-dom";
import LoginButton from "../components/login-button";
export default function Layout() {
return (
<div className="w-[700px] mx-auto m-2 p-2">
<div className="flex items-center justify-between mb-4">
<Link to="/">LNVPS</Link>
<LoginButton />
</div>
<Outlet />
</div>
);
}

181
src/pages/order.tsx Normal file
View File

@ -0,0 +1,181 @@
import { useLocation, useNavigate } from "react-router-dom";
import { LNVpsApi, UserSshKey, 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";
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),
);
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),
);
setOrderError("");
try {
const newVm = await api.orderVm(template.id, useImage, useSshKey);
navigate("/vm/renew", {
state: newVm,
});
} catch (e) {
if (e instanceof Error) {
setOrderError(e.message);
}
}
}
const sortedImages = images.sort(
(a, b) =>
new Date(b.release_date).getTime() - new Date(a.release_date).getTime(),
);
if (!template || !login) {
return <h3>No order found</h3>;
}
return (
<div className="flex flex-col gap-4">
<div className="text-xl">New Order</div>
<div className="flex justify-between items-center rounded-xl bg-neutral-900 px-4 py-3">
<div className="flex flex-col gap-1">
<div>{template.name}</div>
<VpsResources vm={template} />
</div>
{template.cost_plan && <CostLabel cost={template.cost_plan} />}
</div>
<hr />
<div className="flex flex-col gap-2">
<b>Select OS:</b>
{sortedImages.map((a) => (
<div
className={classNames(
"flex justify-between items-center rounded-xl px-4 py-3 cursor-pointer",
{
"bg-neutral-900": useImage !== a.id,
"bg-neutral-700": useImage === a.id,
},
)}
onClick={() => setUseImage(a.id)}
>
<OsImageName image={a} />
</div>
))}
</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
className="border-none rounded-xl bg-neutral-900 p-2"
rows={5}
placeholder="ssh-[rsa|ed25519] AA== id"
value={newKey}
onChange={(e) => setNewKey(e.target.value)}
/>
<input
type="text"
className="border-none rounded-xl bg-neutral-900 p-2"
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>
<AsyncButton
disabled={useSshKey === -1 || useImage === -1}
onClick={createOrder}
>
Create Order
</AsyncButton>
{orderError && <b className="text-red-500">{orderError}</b>}
</div>
);
}

66
src/pages/vm.tsx Normal file
View File

@ -0,0 +1,66 @@
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, useState } from "react";
import VpsPayment from "../components/vps-payment";
export default function VmPage() {
const { state } = useLocation() as { state?: VmInstance };
const { action } = useParams();
const login = useLogin();
const navigate = useNavigate();
const [payment, setPayment] = useState<VmPayment>();
const renew = useCallback(
async function () {
if (!login?.signer || !state) return;
const api = new LNVpsApi(
ApiUrl,
new EventPublisher(login.signer, login.pubkey),
);
const p = await api.renewVm(state.id);
setPayment(p);
},
[login, state],
);
useEffect(() => {
switch (action) {
case "renew":
renew();
}
}, [renew, action]);
if (!state) {
return <h2>No VM selected</h2>;
}
return (
<div className="flex flex-col gap-4">
<VpsInstanceRow vm={state} />
{action === "renew" && (
<>
<h3>Renew VPS</h3>
{payment && (
<VpsPayment
payment={payment}
onPaid={async () => {
if (!login?.signer || !state) return;
const api = new LNVpsApi(
ApiUrl,
new EventPublisher(login.signer, login.pubkey),
);
const newState = await api.getVm(state.id);
navigate("/vm", {
state: newState,
});
}}
/>
)}
</>
)}
</div>
);
}

View File

@ -9,7 +9,7 @@
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"isolatedModules": false,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",

500
yarn.lock
View File

@ -532,13 +532,6 @@ __metadata:
languageName: node
linkType: hard
"@noble/ciphers@npm:^0.5.1":
version: 0.5.3
resolution: "@noble/ciphers@npm:0.5.3"
checksum: 10c0/2303217304baf51ec6caa2d984f4e640a66d3d586162ed8fecf37a00268fbf362e22cd5bceae4b0ccda4fa06ad0cb294d6a6b158260bbd2eac6a3dc0448f5254
languageName: node
linkType: hard
"@noble/ciphers@npm:^0.6.0":
version: 0.6.0
resolution: "@noble/ciphers@npm:0.6.0"
@ -546,15 +539,6 @@ __metadata:
languageName: node
linkType: hard
"@noble/curves@npm:1.2.0":
version: 1.2.0
resolution: "@noble/curves@npm:1.2.0"
dependencies:
"@noble/hashes": "npm:1.3.2"
checksum: 10c0/0bac7d1bbfb3c2286910b02598addd33243cb97c3f36f987ecc927a4be8d7d88e0fcb12b0f0ef8a044e7307d1844dd5c49bb724bfa0a79c8ec50ba60768c97f6
languageName: node
linkType: hard
"@noble/curves@npm:^1.4.0":
version: 1.6.0
resolution: "@noble/curves@npm:1.6.0"
@ -564,50 +548,13 @@ __metadata:
languageName: node
linkType: hard
"@noble/curves@npm:~1.1.0":
version: 1.1.0
resolution: "@noble/curves@npm:1.1.0"
dependencies:
"@noble/hashes": "npm:1.3.1"
checksum: 10c0/81115c3ebfa7e7da2d7e18d44d686f98dc6d35dbde3964412c05707c92d0994a01545bc265d5c0bc05c8c49333f75b99c9acef6750f5a79b3abcc8e0546acf88
languageName: node
linkType: hard
"@noble/hashes@npm:1.3.1":
version: 1.3.1
resolution: "@noble/hashes@npm:1.3.1"
checksum: 10c0/86512713aaf338bced594bc2046ab249fea4e1ba1e7f2ecd02151ef1b8536315e788c11608fafe1b56f04fad1aa3c602da7e5f8e5fcd5f8b0aa94435fe65278e
languageName: node
linkType: hard
"@noble/hashes@npm:1.3.2":
version: 1.3.2
resolution: "@noble/hashes@npm:1.3.2"
checksum: 10c0/2482cce3bce6a596626f94ca296e21378e7a5d4c09597cbc46e65ffacc3d64c8df73111f2265444e36a3168208628258bbbaccba2ef24f65f58b2417638a20e7
languageName: node
linkType: hard
"@noble/hashes@npm:1.5.0, @noble/hashes@npm:^1.3.1, @noble/hashes@npm:^1.4.0":
"@noble/hashes@npm:1.5.0, @noble/hashes@npm:^1.4.0":
version: 1.5.0
resolution: "@noble/hashes@npm:1.5.0"
checksum: 10c0/1b46539695fbfe4477c0822d90c881a04d4fa2921c08c552375b444a48cac9930cb1ee68de0a3c7859e676554d0f3771999716606dc4d8f826e414c11692cdd9
languageName: node
linkType: hard
"@noble/hashes@npm:~1.3.0, @noble/hashes@npm:~1.3.1":
version: 1.3.3
resolution: "@noble/hashes@npm:1.3.3"
checksum: 10c0/23c020b33da4172c988e44100e33cd9f8f6250b68b43c467d3551f82070ebd9716e0d9d2347427aa3774c85934a35fa9ee6f026fca2117e3fa12db7bedae7668
languageName: node
linkType: hard
"@noble/secp256k1@npm:^2.0.0":
version: 2.1.0
resolution: "@noble/secp256k1@npm:2.1.0"
checksum: 10c0/b4c7edd2a5ec5acf294546cd06d08dc2a2a2b2ebe34a6da1f2f5104f56983f81dd31c261ad365c6b9757d1c54017fc3363331ee33bba8715ff94c2bc954313cc
languageName: node
linkType: hard
"@nodelib/fs.scandir@npm:2.1.5":
version: 2.1.5
resolution: "@nodelib/fs.scandir@npm:2.1.5"
@ -635,26 +582,6 @@ __metadata:
languageName: node
linkType: hard
"@nostr-dev-kit/ndk@npm:^2.8.2":
version: 2.10.0
resolution: "@nostr-dev-kit/ndk@npm:2.10.0"
dependencies:
"@noble/curves": "npm:^1.4.0"
"@noble/hashes": "npm:^1.3.1"
"@noble/secp256k1": "npm:^2.0.0"
"@scure/base": "npm:^1.1.1"
debug: "npm:^4.3.4"
light-bolt11-decoder: "npm:^3.0.0"
node-fetch: "npm:^3.3.1"
nostr-tools: "npm:^2.7.1"
tseep: "npm:^1.1.1"
typescript-lru-cache: "npm:^2.0.0"
utf8-buffer: "npm:^1.0.0"
websocket-polyfill: "npm:^0.0.3"
checksum: 10c0/be71574ab583c8b337fd5089ac4b025e214d4977a3ca2b030c51ecbc9f5f68f2831881dbb4a7c45f8dac70830488a93a2df6cb691ae520fc257acd4640460f33
languageName: node
linkType: hard
"@npmcli/agent@npm:^2.0.0":
version: 2.2.2
resolution: "@npmcli/agent@npm:2.2.2"
@ -803,31 +730,17 @@ __metadata:
languageName: node
linkType: hard
"@scure/base@npm:^1.1.1, @scure/base@npm:^1.1.6, @scure/base@npm:~1.1.0":
"@scure/base@npm:^1.1.6":
version: 1.1.9
resolution: "@scure/base@npm:1.1.9"
checksum: 10c0/77a06b9a2db8144d22d9bf198338893d77367c51b58c72b99df990c0a11f7cadd066d4102abb15e3ca6798d1529e3765f55c4355742465e49aed7a0c01fe76e8
languageName: node
linkType: hard
"@scure/bip32@npm:1.3.1":
version: 1.3.1
resolution: "@scure/bip32@npm:1.3.1"
dependencies:
"@noble/curves": "npm:~1.1.0"
"@noble/hashes": "npm:~1.3.1"
"@scure/base": "npm:~1.1.0"
checksum: 10c0/9ff0ad56f512794aed1ed62e582bf855db829e688235420a116b210169dc31e3e2a8cc4a908126aaa07b6dcbcc4cd085eb12f9d0a8b507a88946d6171a437195
languageName: node
linkType: hard
"@scure/bip39@npm:1.2.1":
"@scure/base@npm:^1.2.1":
version: 1.2.1
resolution: "@scure/bip39@npm:1.2.1"
dependencies:
"@noble/hashes": "npm:~1.3.0"
"@scure/base": "npm:~1.1.0"
checksum: 10c0/fe951f69dd5a7cdcefbe865bce1b160d6b59ba19bd01d09f0718e54fce37a7d8be158b32f5455f0e9c426a7fbbede3e019bf0baa99bacc88ef26a76a07e115d4
resolution: "@scure/base@npm:1.2.1"
checksum: 10c0/e61068854370855b89c50c28fa4092ea6780f1e0db64ea94075ab574ebcc964f719a3120dc708db324991f4b3e652d92ebda03fce2bf6a4900ceeacf9c0ff933
languageName: node
linkType: hard
@ -845,36 +758,35 @@ __metadata:
languageName: node
linkType: hard
"@snort/system-react@npm:^1.5.1":
version: 1.5.1
resolution: "@snort/system-react@npm:1.5.1"
"@snort/system-react@npm:^1.5.7":
version: 1.5.7
resolution: "@snort/system-react@npm:1.5.7"
dependencies:
"@snort/shared": "npm:^1.0.17"
"@snort/system": "npm:^1.5.1"
"@snort/system": "npm:^1.5.7"
react: "npm:^18.2.0"
checksum: 10c0/62d55d928f5ef22081e93a96fcff4f30b599a84b67396c93687f51ffeae8cb7ca578b00d033c0e7f731182669f8ae47c86b4497500ace9cdc197d19d4caecb62
checksum: 10c0/b8261d72bef88fc6baa91f3f3765a7e65e7775e0f87142079ec425fdc3639871da068b2bfc9c4a2c0f15a20c7c76ffe84d9bc8cbc1e6ef5a76989cd78d3f7049
languageName: node
linkType: hard
"@snort/system@npm:^1.5.1":
version: 1.5.1
resolution: "@snort/system@npm:1.5.1"
"@snort/system@npm:^1.5.7":
version: 1.5.7
resolution: "@snort/system@npm:1.5.7"
dependencies:
"@noble/ciphers": "npm:^0.6.0"
"@noble/curves": "npm:^1.4.0"
"@noble/hashes": "npm:^1.4.0"
"@nostr-dev-kit/ndk": "npm:^2.8.2"
"@scure/base": "npm:^1.1.6"
"@snort/shared": "npm:^1.0.17"
"@stablelib/xchacha20": "npm:^1.0.1"
debug: "npm:^4.3.4"
eventemitter3: "npm:^5.0.1"
isomorphic-ws: "npm:^5.0.0"
lokijs: "npm:^1.5.12"
lru-cache: "npm:^10.2.0"
nostr-social-graph: "npm:^1.0.3"
uuid: "npm:^9.0.0"
ws: "npm:^8.14.0"
checksum: 10c0/e682b0b739b2d2d5177e37071d7ef2c71dcff42677dba149ac1271596504de2ddbda6c7e8fc3eee3cf2fc175c00cd97fb0cbdf40a96311b59a9e8029e15f03b9
checksum: 10c0/9b1d6e36dfc3c0845754d4f2c10eb39665a2c4c4c61a07635e0b792a352f8566dbd79561561568c182272cd92b0d2c421ef137775b16872b7e28fa39366e2094
languageName: node
linkType: hard
@ -963,6 +875,13 @@ __metadata:
languageName: node
linkType: hard
"@types/cookie@npm:^0.6.0":
version: 0.6.0
resolution: "@types/cookie@npm:0.6.0"
checksum: 10c0/5b326bd0188120fb32c0be086b141b1481fec9941b76ad537f9110e10d61ee2636beac145463319c71e4be67a17e85b81ca9e13ceb6e3bb63b93d16824d6c149
languageName: node
linkType: hard
"@types/estree@npm:1.0.5":
version: 1.0.5
resolution: "@types/estree@npm:1.0.5"
@ -1334,16 +1253,6 @@ __metadata:
languageName: node
linkType: hard
"bufferutil@npm:^4.0.1":
version: 4.0.8
resolution: "bufferutil@npm:4.0.8"
dependencies:
node-gyp: "npm:latest"
node-gyp-build: "npm:^4.3.0"
checksum: 10c0/36cdc5b53a38d9f61f89fdbe62029a2ebcd020599862253fefebe31566155726df9ff961f41b8c97b02b4c12b391ef97faf94e2383392654cf8f0ed68f76e47c
languageName: node
linkType: hard
"cacache@npm:^18.0.0":
version: 18.0.4
resolution: "cacache@npm:18.0.4"
@ -1432,6 +1341,13 @@ __metadata:
languageName: node
linkType: hard
"classnames@npm:^2.5.1":
version: 2.5.1
resolution: "classnames@npm:2.5.1"
checksum: 10c0/afff4f77e62cea2d79c39962980bf316bacb0d7c49e13a21adaadb9221e1c6b9d3cdb829d8bb1b23c406f4e740507f37e1dcf506f7e3b7113d17c5bab787aa69
languageName: node
linkType: hard
"clean-stack@npm:^2.0.0":
version: 2.2.0
resolution: "clean-stack@npm:2.2.0"
@ -1492,6 +1408,13 @@ __metadata:
languageName: node
linkType: hard
"cookie@npm:^1.0.1":
version: 1.0.2
resolution: "cookie@npm:1.0.2"
checksum: 10c0/fd25fe79e8fbcfcaf6aa61cd081c55d144eeeba755206c058682257cb38c4bd6795c6620de3f064c740695bb65b7949ebb1db7a95e4636efb8357a335ad3f54b
languageName: node
linkType: hard
"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.2":
version: 7.0.3
resolution: "cross-spawn@npm:7.0.3"
@ -1519,23 +1442,6 @@ __metadata:
languageName: node
linkType: hard
"d@npm:1, d@npm:^1.0.1, d@npm:^1.0.2":
version: 1.0.2
resolution: "d@npm:1.0.2"
dependencies:
es5-ext: "npm:^0.10.64"
type: "npm:^2.7.2"
checksum: 10c0/3e6ede10cd3b77586c47da48423b62bed161bf1a48bdbcc94d87263522e22f5dfb0e678a6dba5323fdc14c5d8612b7f7eb9e7d9e37b2e2d67a7bf9f116dabe5a
languageName: node
linkType: hard
"data-uri-to-buffer@npm:^4.0.0":
version: 4.0.1
resolution: "data-uri-to-buffer@npm:4.0.1"
checksum: 10c0/20a6b93107597530d71d4cb285acee17f66bcdfc03fd81040921a81252f19db27588d87fc8fc69e1950c55cfb0bf8ae40d0e5e21d907230813eb5d5a7f9eb45b
languageName: node
linkType: hard
"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4":
version: 4.3.6
resolution: "debug@npm:4.3.6"
@ -1548,15 +1454,6 @@ __metadata:
languageName: node
linkType: hard
"debug@npm:^2.2.0":
version: 2.6.9
resolution: "debug@npm:2.6.9"
dependencies:
ms: "npm:2.0.0"
checksum: 10c0/121908fb839f7801180b69a7e218a40b5a0b718813b886b7d6bdb82001b931c938e2941d1e4450f33a1b1df1da653f5f7a0440c197f29fbf8a6e9d45ff6ef589
languageName: node
linkType: hard
"deep-is@npm:^0.1.3":
version: 0.1.4
resolution: "deep-is@npm:0.1.4"
@ -1638,39 +1535,6 @@ __metadata:
languageName: node
linkType: hard
"es5-ext@npm:^0.10.35, es5-ext@npm:^0.10.62, es5-ext@npm:^0.10.63, es5-ext@npm:^0.10.64, es5-ext@npm:~0.10.14":
version: 0.10.64
resolution: "es5-ext@npm:0.10.64"
dependencies:
es6-iterator: "npm:^2.0.3"
es6-symbol: "npm:^3.1.3"
esniff: "npm:^2.0.1"
next-tick: "npm:^1.1.0"
checksum: 10c0/4459b6ae216f3c615db086e02437bdfde851515a101577fd61b19f9b3c1ad924bab4d197981eb7f0ccb915f643f2fc10ff76b97a680e96cbb572d15a27acd9a3
languageName: node
linkType: hard
"es6-iterator@npm:^2.0.3":
version: 2.0.3
resolution: "es6-iterator@npm:2.0.3"
dependencies:
d: "npm:1"
es5-ext: "npm:^0.10.35"
es6-symbol: "npm:^3.1.1"
checksum: 10c0/91f20b799dba28fb05bf623c31857fc1524a0f1c444903beccaf8929ad196c8c9ded233e5ac7214fc63a92b3f25b64b7f2737fcca8b1f92d2d96cf3ac902f5d8
languageName: node
linkType: hard
"es6-symbol@npm:^3.1.1, es6-symbol@npm:^3.1.3":
version: 3.1.4
resolution: "es6-symbol@npm:3.1.4"
dependencies:
d: "npm:^1.0.2"
ext: "npm:^1.7.0"
checksum: 10c0/777bf3388db5d7919e09a0fd175aa5b8a62385b17cb2227b7a137680cba62b4d9f6193319a102642aa23d5840d38a62e4784f19cfa5be4a2210a3f0e9b23d15d
languageName: node
linkType: hard
"esbuild@npm:^0.21.3":
version: 0.21.5
resolution: "esbuild@npm:0.21.5"
@ -1858,18 +1722,6 @@ __metadata:
languageName: node
linkType: hard
"esniff@npm:^2.0.1":
version: 2.0.1
resolution: "esniff@npm:2.0.1"
dependencies:
d: "npm:^1.0.1"
es5-ext: "npm:^0.10.62"
event-emitter: "npm:^0.3.5"
type: "npm:^2.7.2"
checksum: 10c0/7efd8d44ac20e5db8cb0ca77eb65eca60628b2d0f3a1030bcb05e71cc40e6e2935c47b87dba3c733db12925aa5b897f8e0e7a567a2c274206f184da676ea2e65
languageName: node
linkType: hard
"espree@npm:^10.0.1, espree@npm:^10.1.0":
version: 10.1.0
resolution: "espree@npm:10.1.0"
@ -1913,16 +1765,6 @@ __metadata:
languageName: node
linkType: hard
"event-emitter@npm:^0.3.5":
version: 0.3.5
resolution: "event-emitter@npm:0.3.5"
dependencies:
d: "npm:1"
es5-ext: "npm:~0.10.14"
checksum: 10c0/75082fa8ffb3929766d0f0a063bfd6046bd2a80bea2666ebaa0cfd6f4a9116be6647c15667bea77222afc12f5b4071b68d393cf39fdaa0e8e81eda006160aff0
languageName: node
linkType: hard
"eventemitter3@npm:^5.0.1":
version: 5.0.1
resolution: "eventemitter3@npm:5.0.1"
@ -1937,15 +1779,6 @@ __metadata:
languageName: node
linkType: hard
"ext@npm:^1.7.0":
version: 1.7.0
resolution: "ext@npm:1.7.0"
dependencies:
type: "npm:^2.7.2"
checksum: 10c0/a8e5f34e12214e9eee3a4af3b5c9d05ba048f28996450975b369fc86e5d0ef13b6df0615f892f5396a9c65d616213c25ec5b0ad17ef42eac4a500512a19da6c7
languageName: node
linkType: hard
"fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3":
version: 3.1.3
resolution: "fast-deep-equal@npm:3.1.3"
@ -1989,16 +1822,6 @@ __metadata:
languageName: node
linkType: hard
"fetch-blob@npm:^3.1.2, fetch-blob@npm:^3.1.4":
version: 3.2.0
resolution: "fetch-blob@npm:3.2.0"
dependencies:
node-domexception: "npm:^1.0.0"
web-streams-polyfill: "npm:^3.0.3"
checksum: 10c0/60054bf47bfa10fb0ba6cb7742acec2f37c1f56344f79a70bb8b1c48d77675927c720ff3191fa546410a0442c998d27ab05e9144c32d530d8a52fbe68f843b69
languageName: node
linkType: hard
"file-entry-cache@npm:^8.0.0":
version: 8.0.0
resolution: "file-entry-cache@npm:8.0.0"
@ -2054,15 +1877,6 @@ __metadata:
languageName: node
linkType: hard
"formdata-polyfill@npm:^4.0.10":
version: 4.0.10
resolution: "formdata-polyfill@npm:4.0.10"
dependencies:
fetch-blob: "npm:^3.1.2"
checksum: 10c0/5392ec484f9ce0d5e0d52fb5a78e7486637d516179b0eb84d81389d7eccf9ca2f663079da56f761355c0a65792810e3b345dc24db9a8bbbcf24ef3c8c88570c6
languageName: node
linkType: hard
"fraction.js@npm:^4.3.7":
version: 4.3.7
resolution: "fraction.js@npm:4.3.7"
@ -2366,13 +2180,6 @@ __metadata:
languageName: node
linkType: hard
"is-typedarray@npm:^1.0.0":
version: 1.0.0
resolution: "is-typedarray@npm:1.0.0"
checksum: 10c0/4c096275ba041a17a13cca33ac21c16bc4fd2d7d7eb94525e7cd2c2f2c1a3ab956e37622290642501ff4310601e413b675cf399ad6db49855527d2163b3eeeec
languageName: node
linkType: hard
"isexe@npm:^2.0.0":
version: 2.0.0
resolution: "isexe@npm:2.0.0"
@ -2536,21 +2343,25 @@ __metadata:
resolution: "lnvps_web@workspace:."
dependencies:
"@eslint/js": "npm:^9.8.0"
"@scure/base": "npm:^1.2.1"
"@snort/shared": "npm:^1.0.17"
"@snort/system": "npm:^1.5.1"
"@snort/system-react": "npm:^1.5.1"
"@snort/system": "npm:^1.5.7"
"@snort/system-react": "npm:^1.5.7"
"@types/react": "npm:^18.3.3"
"@types/react-dom": "npm:^18.3.0"
"@vitejs/plugin-react": "npm:^4.3.1"
autoprefixer: "npm:^10.4.20"
classnames: "npm:^2.5.1"
eslint: "npm:^9.8.0"
eslint-plugin-react-hooks: "npm:^5.1.0-rc.0"
eslint-plugin-react-refresh: "npm:^0.4.9"
globals: "npm:^15.9.0"
postcss: "npm:^8.4.41"
prettier: "npm:^3.3.3"
qr-code-styling: "npm:^1.8.4"
react: "npm:^18.3.1"
react-dom: "npm:^18.3.1"
react-router-dom: "npm:^7.0.1"
tailwindcss: "npm:^3.4.8"
typescript: "npm:^5.5.3"
typescript-eslint: "npm:^8.0.0"
@ -2574,13 +2385,6 @@ __metadata:
languageName: node
linkType: hard
"lokijs@npm:^1.5.12":
version: 1.5.12
resolution: "lokijs@npm:1.5.12"
checksum: 10c0/275ca25174d5174f2126559aad7eedccd8a9759906f650c1bda2f11edd7ed5139fdda8f09f312443261335fdf266883972edb910a948190961689cac7dbbff2a
languageName: node
linkType: hard
"loose-envify@npm:^1.1.0":
version: 1.4.0
resolution: "loose-envify@npm:1.4.0"
@ -2756,13 +2560,6 @@ __metadata:
languageName: node
linkType: hard
"ms@npm:2.0.0":
version: 2.0.0
resolution: "ms@npm:2.0.0"
checksum: 10c0/f8fda810b39fd7255bbdc451c46286e549794fcc700dc9cd1d25658bbc4dc2563a5de6fe7c60f798a16a60c6ceb53f033cb353f493f0cf63e5199b702943159d
languageName: node
linkType: hard
"ms@npm:2.1.2":
version: 2.1.2
resolution: "ms@npm:2.1.2"
@ -2804,42 +2601,6 @@ __metadata:
languageName: node
linkType: hard
"next-tick@npm:^1.1.0":
version: 1.1.0
resolution: "next-tick@npm:1.1.0"
checksum: 10c0/3ba80dd805fcb336b4f52e010992f3e6175869c8d88bf4ff0a81d5d66e6049f89993463b28211613e58a6b7fe93ff5ccbba0da18d4fa574b96289e8f0b577f28
languageName: node
linkType: hard
"node-domexception@npm:^1.0.0":
version: 1.0.0
resolution: "node-domexception@npm:1.0.0"
checksum: 10c0/5e5d63cda29856402df9472335af4bb13875e1927ad3be861dc5ebde38917aecbf9ae337923777af52a48c426b70148815e890a5d72760f1b4d758cc671b1a2b
languageName: node
linkType: hard
"node-fetch@npm:^3.3.1":
version: 3.3.2
resolution: "node-fetch@npm:3.3.2"
dependencies:
data-uri-to-buffer: "npm:^4.0.0"
fetch-blob: "npm:^3.1.4"
formdata-polyfill: "npm:^4.0.10"
checksum: 10c0/f3d5e56190562221398c9f5750198b34cf6113aa304e34ee97c94fd300ec578b25b2c2906edba922050fce983338fde0d5d34fcb0fc3336ade5bd0e429ad7538
languageName: node
linkType: hard
"node-gyp-build@npm:^4.3.0":
version: 4.8.2
resolution: "node-gyp-build@npm:4.8.2"
bin:
node-gyp-build: bin.js
node-gyp-build-optional: optional.js
node-gyp-build-test: build-test.js
checksum: 10c0/d816b43974d31d6257b6e87d843f2626c72389a285208394bc57a7766b210454d2642860a5e5b5c333d8ecabaeabad3b31b94f58cf8ca1aabdef0c320d02baaa
languageName: node
linkType: hard
"node-gyp@npm:latest":
version: 10.2.0
resolution: "node-gyp@npm:10.2.0"
@ -2892,33 +2653,10 @@ __metadata:
languageName: node
linkType: hard
"nostr-tools@npm:^2.7.1":
version: 2.7.2
resolution: "nostr-tools@npm:2.7.2"
dependencies:
"@noble/ciphers": "npm:^0.5.1"
"@noble/curves": "npm:1.2.0"
"@noble/hashes": "npm:1.3.1"
"@scure/base": "npm:1.1.1"
"@scure/bip32": "npm:1.3.1"
"@scure/bip39": "npm:1.2.1"
nostr-wasm: "npm:v0.1.0"
peerDependencies:
typescript: ">=5.0.0"
dependenciesMeta:
nostr-wasm:
optional: true
peerDependenciesMeta:
typescript:
optional: true
checksum: 10c0/66872704b3748c85e195a7f58425b204088b4432d5ff2d6c196988332e5cea6af543dd9b4f26523c42676960c9cc227ee207d056767f8cd70a479774094778af
languageName: node
linkType: hard
"nostr-wasm@npm:v0.1.0":
version: 0.1.0
resolution: "nostr-wasm@npm:0.1.0"
checksum: 10c0/a8a674c0e038d5f790840e442a80587f6eca0810e01f3101828c34517f5c3238f510ef49f53b3f596e8effb32eb64993c57248aa25b9ccfa9386e4421c837edb
"nostr-social-graph@npm:^1.0.3":
version: 1.0.5
resolution: "nostr-social-graph@npm:1.0.5"
checksum: 10c0/529865166127f1eba686453563458f948f5365200a1976f3b8eaa9203f6aa091c54d5fbe678267bf9e2a4a57cf364d7fc37861c373f6093cc1e38e3733e690f5
languageName: node
linkType: hard
@ -3180,6 +2918,22 @@ __metadata:
languageName: node
linkType: hard
"qr-code-styling@npm:^1.8.4":
version: 1.8.4
resolution: "qr-code-styling@npm:1.8.4"
dependencies:
qrcode-generator: "npm:^1.4.4"
checksum: 10c0/1656833f0ebc7000b4376e1bbe9e1133699b391589f7917eb3b28eeeb32ed45c143f25c4a8a9d2feeb8b0724f1717b41019e012972ac1df022d559f55c6a2947
languageName: node
linkType: hard
"qrcode-generator@npm:^1.4.4":
version: 1.4.4
resolution: "qrcode-generator@npm:1.4.4"
checksum: 10c0/3249fcff98cb9fa17c21329d3dfd895e294a2d6ea48161f7b377010779d41f0cd88668b7fb3478a659725061bb0a770b40a227c2f4853e8c4a6b947a9e8bf17a
languageName: node
linkType: hard
"queue-microtask@npm:^1.2.2":
version: 1.2.3
resolution: "queue-microtask@npm:1.2.3"
@ -3206,6 +2960,36 @@ __metadata:
languageName: node
linkType: hard
"react-router-dom@npm:^7.0.1":
version: 7.0.1
resolution: "react-router-dom@npm:7.0.1"
dependencies:
react-router: "npm:7.0.1"
peerDependencies:
react: ">=18"
react-dom: ">=18"
checksum: 10c0/aebc0af4ea4ac4e9482b0501c06f3af42adc68b4064c556b00954dcb7a6ec233d41f3eac86e388b9a8068e8ba5d44b854b7e29c806af9ed6a8798682d6984b46
languageName: node
linkType: hard
"react-router@npm:7.0.1":
version: 7.0.1
resolution: "react-router@npm:7.0.1"
dependencies:
"@types/cookie": "npm:^0.6.0"
cookie: "npm:^1.0.1"
set-cookie-parser: "npm:^2.6.0"
turbo-stream: "npm:2.4.0"
peerDependencies:
react: ">=18"
react-dom: ">=18"
peerDependenciesMeta:
react-dom:
optional: true
checksum: 10c0/aac4c9989ae6b9cf989b5ddcda88f505ba0704a4e4b37ae04c819c2bd02f080361f9eb1793695e3ecf41080d91b79aee454c3163b586d1b19ceca13f6eacec0e
languageName: node
linkType: hard
"react@npm:^18.2.0, react@npm:^18.3.1":
version: 18.3.1
resolution: "react@npm:18.3.1"
@ -3386,6 +3170,13 @@ __metadata:
languageName: node
linkType: hard
"set-cookie-parser@npm:^2.6.0":
version: 2.7.1
resolution: "set-cookie-parser@npm:2.7.1"
checksum: 10c0/060c198c4c92547ac15988256f445eae523f57f2ceefeccf52d30d75dedf6bff22b9c26f756bd44e8e560d44ff4ab2130b178bd2e52ef5571bf7be3bd7632d9a
languageName: node
linkType: hard
"shebang-command@npm:^2.0.0":
version: 2.0.0
resolution: "shebang-command@npm:2.0.0"
@ -3661,17 +3452,10 @@ __metadata:
languageName: node
linkType: hard
"tseep@npm:^1.1.1":
version: 1.2.2
resolution: "tseep@npm:1.2.2"
checksum: 10c0/f85c45ed7fbf6bbd6551819b7446db1fe45641896cfea2b7efc776c6388db8ede953ccd21eaa814b3edcce53ecc56a279c7c53e84ba9308dd4541e8c824dcef1
languageName: node
linkType: hard
"tstl@npm:^2.0.7":
version: 2.5.16
resolution: "tstl@npm:2.5.16"
checksum: 10c0/3815f34a2bc062e35defb4d23768ea99a099847dd36d392c3ccef9d90f865331d9ea03677fc50a3e110da3a93af8abd5bcc43e4b409be012bad5a4773b6e11c1
"turbo-stream@npm:2.4.0":
version: 2.4.0
resolution: "turbo-stream@npm:2.4.0"
checksum: 10c0/e68b2569f1f16e6e9633d090c6024b2ae9f0e97bfeacb572451ca3732e120ebbb546f3bc4afc717c46cb57b5aea6104e04ef497f9912eef6a7641e809518e98a
languageName: node
linkType: hard
@ -3684,22 +3468,6 @@ __metadata:
languageName: node
linkType: hard
"type@npm:^2.7.2":
version: 2.7.3
resolution: "type@npm:2.7.3"
checksum: 10c0/dec6902c2c42fcb86e3adf8cdabdf80e5ef9de280872b5fd547351e9cca2fe58dd2aa6d2547626ddff174145db272f62d95c7aa7038e27c11315657d781a688d
languageName: node
linkType: hard
"typedarray-to-buffer@npm:^3.1.5":
version: 3.1.5
resolution: "typedarray-to-buffer@npm:3.1.5"
dependencies:
is-typedarray: "npm:^1.0.0"
checksum: 10c0/4ac5b7a93d604edabf3ac58d3a2f7e07487e9f6e98195a080e81dbffdc4127817f470f219d794a843b87052cedef102b53ac9b539855380b8c2172054b7d5027
languageName: node
linkType: hard
"typescript-eslint@npm:^8.0.0":
version: 8.0.1
resolution: "typescript-eslint@npm:8.0.1"
@ -3714,13 +3482,6 @@ __metadata:
languageName: node
linkType: hard
"typescript-lru-cache@npm:^2.0.0":
version: 2.0.0
resolution: "typescript-lru-cache@npm:2.0.0"
checksum: 10c0/69864dd8a3538f18002c50a644ef7a7f2d5e320a12fa6266b8c715d6530fec38e475349cd35f75b5196a39d1a28f8b12ebf16afce699b743bdf385dce7df1e0e
languageName: node
linkType: hard
"typescript@npm:^5.5.3":
version: 5.5.4
resolution: "typescript@npm:5.5.4"
@ -3782,23 +3543,6 @@ __metadata:
languageName: node
linkType: hard
"utf-8-validate@npm:^5.0.2":
version: 5.0.10
resolution: "utf-8-validate@npm:5.0.10"
dependencies:
node-gyp: "npm:latest"
node-gyp-build: "npm:^4.3.0"
checksum: 10c0/23cd6adc29e6901aa37ff97ce4b81be9238d0023c5e217515b34792f3c3edb01470c3bd6b264096dd73d0b01a1690b57468de3a24167dd83004ff71c51cc025f
languageName: node
linkType: hard
"utf8-buffer@npm:^1.0.0":
version: 1.0.0
resolution: "utf8-buffer@npm:1.0.0"
checksum: 10c0/3cf47ba9ba9ca756734bbe3b139adf04c0e1419e36ecdeebfc930ae702c0b15042225cd483b9d933600353822201e0db3640e1206829823062a5accee0821027
languageName: node
linkType: hard
"util-deprecate@npm:^1.0.2":
version: 1.0.2
resolution: "util-deprecate@npm:1.0.2"
@ -3858,37 +3602,6 @@ __metadata:
languageName: node
linkType: hard
"web-streams-polyfill@npm:^3.0.3":
version: 3.3.3
resolution: "web-streams-polyfill@npm:3.3.3"
checksum: 10c0/64e855c47f6c8330b5436147db1c75cb7e7474d924166800e8e2aab5eb6c76aac4981a84261dd2982b3e754490900b99791c80ae1407a9fa0dcff74f82ea3a7f
languageName: node
linkType: hard
"websocket-polyfill@npm:^0.0.3":
version: 0.0.3
resolution: "websocket-polyfill@npm:0.0.3"
dependencies:
tstl: "npm:^2.0.7"
websocket: "npm:^1.0.28"
checksum: 10c0/b8a16af58489f0dd8e896ad986547979544d90c1b21a1777abf2432b899df9fca713b43a0ac50afc995ef7ba28bde01ec1a5391d746a9d14209e54c31054f428
languageName: node
linkType: hard
"websocket@npm:^1.0.28":
version: 1.0.35
resolution: "websocket@npm:1.0.35"
dependencies:
bufferutil: "npm:^4.0.1"
debug: "npm:^2.2.0"
es5-ext: "npm:^0.10.63"
typedarray-to-buffer: "npm:^3.1.5"
utf-8-validate: "npm:^5.0.2"
yaeti: "npm:^0.0.6"
checksum: 10c0/8be9a68dc0228f18058c9010d1308479f05050af8f6d68b9dbc6baebd9ab484c15a24b2521a5d742a9d78e62ee19194c532992f1047a9b9adf8c3eedb0b1fcdc
languageName: node
linkType: hard
"which@npm:^2.0.1":
version: 2.0.2
resolution: "which@npm:2.0.2"
@ -3955,13 +3668,6 @@ __metadata:
languageName: node
linkType: hard
"yaeti@npm:^0.0.6":
version: 0.0.6
resolution: "yaeti@npm:0.0.6"
checksum: 10c0/4e88702d8b34d7b61c1c4ec674422b835d453b8f8a6232be41e59fc98bc4d9ab6d5abd2da55bab75dfc07ae897fdc0c541f856ce3ab3b17de1630db6161aa3f6
languageName: node
linkType: hard
"yallist@npm:^3.0.2":
version: 3.1.1
resolution: "yallist@npm:3.1.1"