141 lines
4.0 KiB
TypeScript
141 lines
4.0 KiB
TypeScript
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>
|
|
|
|
)
|
|
} |