handmade-revolt/src/context/modals/components/MFAFlow.tsx

174 lines
5.0 KiB
TypeScript

import { Archive } from "@styled-icons/boxicons-regular";
import { Key, Keyboard } from "@styled-icons/boxicons-solid";
import { API } from "revolt.js";
import { Text } from "preact-i18n";
import {
useCallback,
useEffect,
useLayoutEffect,
useState,
} from "preact/hooks";
import {
Category,
CategoryButton,
InputBox,
Modal,
Preloader,
} from "@revoltchat/ui";
import { noopTrue } from "../../../lib/js";
import { ModalProps } from "../types";
/**
* Mapping of MFA methods to icons
*/
const ICONS: Record<API.MFAMethod, React.FC<any>> = {
Password: Keyboard,
Totp: Key,
Recovery: Archive,
};
/**
* Component for handling challenge entry
*/
function ResponseEntry({
type,
value,
onChange,
}: {
type: API.MFAMethod;
value?: API.MFAResponse;
onChange: (v: API.MFAResponse) => void;
}) {
return (
<>
<Category compact>
<Text id={`login.${type.toLowerCase()}`} />
</Category>
{type === "Password" && (
<InputBox
type="password"
value={(value as { password: string })?.password}
onChange={(e) =>
onChange({ password: e.currentTarget.value })
}
/>
)}
</>
);
}
/**
* MFA ticket creation flow
*/
export default function MFAFlow({ onClose, ...props }: ModalProps<"mfa_flow">) {
const [methods, setMethods] = useState<API.MFAMethod[] | undefined>(
props.state === "unknown" ? props.available_methods : undefined,
);
// Current state of the modal
const [selectedMethod, setSelected] = useState<API.MFAMethod>();
const [response, setResponse] = useState<API.MFAResponse>();
// Fetch available methods if they have not been provided.
useEffect(() => {
if (!methods && props.state === "known") {
props.client.api.get("/auth/mfa/methods").then(setMethods);
}
}, []);
// Always select first available method if only one available.
useLayoutEffect(() => {
if (methods && methods.length === 1) {
setSelected(methods[0]);
}
}, [methods]);
// Callback to generate a new ticket or send response back up the chain.
const generateTicket = useCallback(async () => {
if (response) {
if (props.state === "known") {
const ticket = await props.client.api.put(
"/auth/mfa/ticket",
response,
);
props.callback(ticket);
} else {
props.callback(response);
}
return true;
}
return false;
}, [response]);
return (
<Modal
title="Confirm action."
description={
selectedMethod
? "Please confirm using selected method."
: "Please select a method to authenticate your request."
}
actions={
selectedMethod
? [
{
palette: "primary",
children: "Confirm",
onClick: generateTicket,
confirmation: true,
},
{
palette: "plain",
children:
methods!.length === 1 ? "Cancel" : "Back",
onClick: () =>
methods!.length === 1
? true
: void setSelected(undefined),
},
]
: [
{
palette: "plain",
children: "Cancel",
onClick: noopTrue,
},
]
}
onClose={onClose}>
{methods ? (
selectedMethod ? (
<ResponseEntry
type={selectedMethod}
value={response}
onChange={setResponse}
/>
) : (
methods.map((method) => {
const Icon = ICONS[method];
return (
<CategoryButton
key={method}
action="chevron"
icon={<Icon size={24} />}
onClick={() => setSelected(method)}>
{method}
</CategoryButton>
);
})
)
) : (
<Preloader type="ring" />
)}
</Modal>
);
}