remove unused UI
This commit is contained in:
parent
309c70f42e
commit
9de9dcb59b
@ -17,6 +17,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"crypto": "^1.0.1",
|
"crypto": "^1.0.1",
|
||||||
"express-fingerprint": "^1.2.2",
|
"express-fingerprint": "^1.2.2",
|
||||||
|
"hono": "^4.3.0",
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
"react-otp-input": "^3.1.1",
|
"react-otp-input": "^3.1.1",
|
||||||
"twilio-cli": "^5.19.4"
|
"twilio-cli": "^5.19.4"
|
||||||
|
|||||||
@ -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>
|
|
||||||
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -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,
|
|
||||||
});
|
|
||||||
@ -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;
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
export interface Callback {
|
|
||||||
(err?: any): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ResponseHandler<ExtraMetadata = any> {
|
|
||||||
(res: Response, next: Callback, options?: ExtraMetadata): Promise<void>;
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
export function isInEnum(en: any, item: string): boolean {
|
|
||||||
return Object.values(en).includes(item);
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
Loading…
x
Reference in New Issue
Block a user