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>
)
}