diff --git a/bun.lockb b/bun.lockb index f99ad27..ca15788 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 4563846..44370a9 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "dependencies": { "crypto": "^1.0.1", "express-fingerprint": "^1.2.2", + "hono": "^4.3.0", "node-fetch": "^3.3.2", "react-otp-input": "^3.1.1", "twilio-cli": "^5.19.4" diff --git a/packages/client/src/components/AuthFlow.tsx b/packages/client/src/components/AuthFlow.tsx deleted file mode 100644 index 7c56b4c..0000000 --- a/packages/client/src/components/AuthFlow.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import { ColumnLayout, Container, Header, ProgressBar, Spinner } from "@cloudscape-design/components"; -import { useContext, useEffect, useState } from "react"; -import QRCode from "react-qr-code"; -import { Action } from "../types/Action"; -import { ResponseHandler } from "../handlers/RepsonseHandler"; -import { useNavigate } from "react-router-dom"; -import { AlertContext } from "../contexts/AlertContext"; - -export async function loader({ params }: {params: {action: string}}) { - return { action: params.action }; -} - -const WAIT_MS: number = 3000; -const COUNTDOWN_TIME_SECONDS = 100; - -export interface IAuthPageProps { - subdomain: string; - action: Action; - options?: RequestInit; - responseHandler: ResponseHandler; - handlerMetadata?: any; -}; - -export default function AuthFlow(props: IAuthPageProps) { - const { subdomain, action, options, responseHandler, handlerMetadata } = props; - const [ loading, setLoading ] = useState(true); - const [ qrContent, setQrContent ] = useState(""); - const [ challenge, setChallenge ] = useState(""); - const [ countdown, setCountdown ] = useState(COUNTDOWN_TIME_SECONDS); - - const { setContent } = useContext(AlertContext); - - const navigate = useNavigate(); - - useEffect(() => { - fetch(`/api/lnurl/login?subdomain=${subdomain}`) - .then(res => { - if (res.status === 429) { - throw new Error("Too many requests, try again later"); - } - return res; - }) - .then(res => res.json()) - .then(res => { - setQrContent(res.lnurl); - setLoading(false); - setChallenge(res.challenge); - setCountdown(COUNTDOWN_TIME_SECONDS); - }) - .catch((err) => { - console.log(err); - setContent({ - type: "error", - message: err?.message || "Unknown error occurred", - }); - navigate("/"); - }); - }, [action]); - - useEffect(() => { - if (challenge === "") { - return; - } - - const timer = setInterval(() => { - fetch(`/api/actions/${action}/${challenge}`, options) - .then(res => { - if (res.status === 401) { - throw new Error("Challenge not met"); - } - - return res; - }) - .then((res) => responseHandler(res, (err) => { - if (err) { - setContent({ - message: err.message, - type: "error", - }) - } else { - setContent({ - type: "success", - message: `${action} success!` - }) - } - setChallenge(""); - navigate("/"); - }, handlerMetadata)) - .catch((err) => console.error(err)); - }, WAIT_MS); - - return () => { - clearInterval(timer); - } - }, [challenge]); - - useEffect(() => { - if (countdown === 0) { - setContent({ - type: "error", - message: "Authentication timed out after 100s, try again", - }); - navigate("/"); - return; - } - const countdownTimer = setTimeout(() => { - setCountdown(countdown - 1); - }, 1000); - - return () => { - clearTimeout(countdownTimer); - } - }, [countdown]); - - return ( - -
- - -
Authentication for {action} on {subdomain}
- {loading && } - {!loading && ( - <> - -
- -
- - - )} -
-
-
- - ) -} \ No newline at end of file diff --git a/packages/client/src/contexts/AlertContext.tsx b/packages/client/src/contexts/AlertContext.tsx deleted file mode 100644 index 39fb14a..0000000 --- a/packages/client/src/contexts/AlertContext.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { AlertProps } from "@cloudscape-design/components"; -import React from "react"; - -export interface AlertContent { - type?: AlertProps.Type; - message?: string; -} - -export const AlertContext = React.createContext<{ content: AlertContent, setContent: (content: AlertContent) => void }>({ - content: {}, - setContent: () => null, -}); \ No newline at end of file diff --git a/packages/client/src/handlers/DownloadFileHandler.ts b/packages/client/src/handlers/DownloadFileHandler.ts deleted file mode 100644 index 8744a00..0000000 --- a/packages/client/src/handlers/DownloadFileHandler.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { ResponseHandler } from "./RepsonseHandler"; - -export interface DownloadMetadata { - name?: string; - type?: string; -} - -const DownloadFileHandler: ResponseHandler = (res, next, extra) => { - if (res.status === 404) { - console.log("Nothing to download"); - return res.json().then(next); - } - return res.blob() - .then(data => { - let a = document.createElement("a"); - a.href = window.URL.createObjectURL(data); - a.download = `${extra?.name || "file"}.${extra?.type || "txt"}`; - a.click(); - next() - }) - .catch(next); -} - -export default DownloadFileHandler; \ No newline at end of file diff --git a/packages/client/src/handlers/HandlerRegistry.ts b/packages/client/src/handlers/HandlerRegistry.ts deleted file mode 100644 index bd07001..0000000 --- a/packages/client/src/handlers/HandlerRegistry.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Action } from "../types/Action"; -import DownloadFileHandler from "./DownloadFileHandler"; -import NoopHandler from "./NoopHandler"; -import { ResponseHandler } from "./RepsonseHandler"; - -export function getHandler(action: Action): ResponseHandler { - switch(action) { - case Action.DOWNLOAD: return DownloadFileHandler; - case Action.UPLOAD: return NoopHandler; - case Action.DELETE: return NoopHandler; - } -} \ No newline at end of file diff --git a/packages/client/src/handlers/NoopHandler.ts b/packages/client/src/handlers/NoopHandler.ts deleted file mode 100644 index 36344ed..0000000 --- a/packages/client/src/handlers/NoopHandler.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ResponseHandler } from "./RepsonseHandler"; - -const NoopHandler: ResponseHandler = (res, next) => { - return new Promise(async () => { - if (res.status >= 500) { - res = { message: res.statusText } as any; - } - else if (res.status !== 200) { - res = await res.json(); - } - next(res.status !== 200 ? res: undefined); - }); -} - -export default NoopHandler; diff --git a/packages/client/src/handlers/RepsonseHandler.ts b/packages/client/src/handlers/RepsonseHandler.ts deleted file mode 100644 index 1f046a3..0000000 --- a/packages/client/src/handlers/RepsonseHandler.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface Callback { - (err?: any): void; -} - -export interface ResponseHandler { - (res: Response, next: Callback, options?: ExtraMetadata): Promise; -} \ No newline at end of file diff --git a/packages/client/src/pages/AuthPage.tsx b/packages/client/src/pages/AuthPage.tsx deleted file mode 100644 index 54393e8..0000000 --- a/packages/client/src/pages/AuthPage.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import React, { useState } from "react"; -import { useLoaderData } from "react-router-dom"; -import { Action } from "../types/Action"; -import { isInEnum } from "../utils/EnumUtils"; -import AuthFlow from "../components/AuthFlow"; -import { ColumnLayout, Container, FileUpload, FormField, Select } from "@cloudscape-design/components"; -import { getHandler } from "../handlers/HandlerRegistry"; -import { DownloadMetadata } from "../handlers/DownloadFileHandler"; - -export interface IAuthPageLoader { - action: Action; -} - -export async function loader({ params }: any) { - if (!isInEnum(Action, params.action)) { - throw new Error("Not a valid action"); - } - return { action: params.action }; -} - -interface SelectOption { - label?: string; - value?: string; -} - -const selectOptions: SelectOption[] = [ - { label: "backup", value: "backup" }, - { label: "key", value: "key" }, -]; - -export function AuthPage() { - const { action } = useLoaderData() as IAuthPageLoader; - const [selectedOption, setSelectedOption] = useState({value: ""}); - - const [files, setFiles] = useState([]); - - const fileReady = action !== Action.UPLOAD || files.length > 0; - - let options: RequestInit = {}; - let handlerMetadata: DownloadMetadata = { - name: selectedOption.value || "file", - type: "dat", - } - - if (action === Action.DELETE) { - options = { - method: 'delete' - } - } - - if (action === Action.UPLOAD) { - if (files.length > 0) { - const formData = new FormData(); - formData.append("file", files[0]); - - options = { - method: 'post', - body: formData, - } - console.log(options); - } - } - - return ( - - - -