remove unused UI

This commit is contained in:
Martin Dimitrov 2024-05-03 14:28:56 -07:00
parent 309c70f42e
commit 9de9dcb59b
11 changed files with 1 additions and 382 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -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"

View File

@ -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<boolean>(true);
const [ qrContent, setQrContent ] = useState<string>("");
const [ challenge, setChallenge ] = useState<string>("");
const [ countdown, setCountdown ] = useState<number>(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 (
<ColumnLayout columns={3}>
<div></div>
<Container>
<ColumnLayout columns={1}>
<Header>Authentication for <code>{action}</code> on <code>{subdomain}</code></Header>
{loading && <Spinner size="big" />}
{!loading && (
<>
<ColumnLayout columns={3}>
<div></div>
<QRCode value={qrContent} />
</ColumnLayout>
<ProgressBar
label={"Remaining Time"}
description={"Complete authentication before the timer expires"}
additionalInfo={"Authentication expires after 100s to keep your files secure. Please complete the login challenge within the time remaining"}
value={countdown}
/>
</>
)}
</ColumnLayout>
</Container>
</ColumnLayout>
)
}

View File

@ -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,
});

View File

@ -1,24 +0,0 @@
import { ResponseHandler } from "./RepsonseHandler";
export interface DownloadMetadata {
name?: string;
type?: string;
}
const DownloadFileHandler: ResponseHandler<DownloadMetadata> = (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;

View File

@ -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;
}
}

View File

@ -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;

View File

@ -1,7 +0,0 @@
export interface Callback {
(err?: any): void;
}
export interface ResponseHandler<ExtraMetadata = any> {
(res: Response, next: Callback, options?: ExtraMetadata): Promise<void>;
}

View File

@ -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<SelectOption>({value: ""});
const [files, setFiles] = useState<File[]>([]);
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 (
<Container>
<ColumnLayout columns={1}>
<FormField label={"Domain"} description={"Select which domain to access"} stretch>
<Select
selectedOption={selectedOption}
onChange={({ detail }) => setSelectedOption(detail.selectedOption)}
options={selectOptions}
/>
</FormField>
{action === Action.UPLOAD &&
<FileUpload
value={files}
onChange={({ detail }) => setFiles(detail.value)}
i18nStrings={{
uploadButtonText:e=>e?"Choose files":"Choose file",
dropzoneText:e=>e?"Drop files to upload":"Drop file to upload",
removeFileAriaLabel:e=>`Remove file ${e+1}`,
limitShowFewer:"Show fewer files",
limitShowMore:"Show more files",
errorIconAriaLabel:"Error"
}}
/>
}
{fileReady && selectedOption.value &&
<AuthFlow
action={action}
subdomain={selectedOption.value}
responseHandler={getHandler(action)}
options={options}
handlerMetadata={handlerMetadata}
/>
}
</ColumnLayout>
</Container>
);
}

View File

@ -1,3 +0,0 @@
export function isInEnum(en: any, item: string): boolean {
return Object.values(en).includes(item);
}

View File

@ -1,65 +0,0 @@
import express from "express";
import { getRedisClient } from "../clients/db/RedisDbProvider";
import fileUpload from "express-fileupload";
const router = express.Router();
const client = await getRedisClient();
router.get('/download/:challenge', async(req, res) => {
const { challenge } = req.params;
const allowedAccess = await client.accessFromChallenge(challenge);
if (allowedAccess == null) {
res.status(401).json({ message: "Not authorized" });
return;
}
const storedData = await client.get(allowedAccess.key);
if (storedData == null) {
res.status(404).json({ message: "Nothing available to download; upload something first" });
return;
}
res.status(200).send(storedData);
});
router.post('/upload/:challenge', async(req, res) => {
const { challenge } = req.params;
const allowedAccess = await client.accessFromChallenge(challenge);
if (!allowedAccess) {
res.status(401).json({ message: "Not authorized" });
return;
}
const storedData = await client.exists(allowedAccess.key);
if (storedData) {
res.status(409).json({ message: "Already exists, delete first before uploading again" });
return;
}
await client.put(allowedAccess.key, (req.files?.file as fileUpload.UploadedFile).data.toString());
res.status(200).send({ message: "added to db" });
});
router.delete('/delete/:challenge', async(req, res) => {
const { challenge } = req.params;
const allowedAccess = await client.accessFromChallenge(challenge);
if (allowedAccess == null) {
res.status(401).json({ message: "Not authorized" });
return;
}
const storedData = await client.remove(allowedAccess.key);
res.status(200).send({ message: "deleted from DB" });
});
export default router;