277 lines
7.4 KiB
TypeScript
277 lines
7.4 KiB
TypeScript
import { createFormControl, createFormGroup } from "solid-forms";
|
|
import { BiRegularArchive, BiSolidKey, BiSolidKeyboard } from "solid-icons/bi";
|
|
import {
|
|
For,
|
|
Match,
|
|
Switch,
|
|
createEffect,
|
|
createSignal,
|
|
onMount,
|
|
} from "solid-js";
|
|
|
|
import { Trans, useLingui } from "@lingui-solid/solid/macro";
|
|
import type { API } from "stoat.js";
|
|
|
|
import {
|
|
CategoryButton,
|
|
CircularProgress,
|
|
Column,
|
|
Dialog,
|
|
DialogProps,
|
|
Form2,
|
|
Text,
|
|
} from "@revolt/ui";
|
|
|
|
import { useModals } from "..";
|
|
import { Modals } from "../types";
|
|
|
|
/**
|
|
* Modal to create an MFA ticket
|
|
*/
|
|
export function MFAFlowModal(
|
|
props: DialogProps & Modals & { type: "mfa_flow" },
|
|
) {
|
|
const { t } = useLingui();
|
|
const { showError } = useModals();
|
|
|
|
// Keep track of available methods
|
|
const [methods, setMethods] = createSignal<API.MFAMethod[] | undefined>(
|
|
// eslint-disable-next-line solid/reactivity
|
|
props.state === "unknown" ? props.available_methods : undefined,
|
|
);
|
|
|
|
// Current state of the modal
|
|
const [selectedMethod, setSelected] = createSignal<API.MFAMethod>();
|
|
|
|
const group = createFormGroup({
|
|
password: createFormControl(""),
|
|
totp_code: createFormControl(""),
|
|
recovery_code: createFormControl(""),
|
|
});
|
|
|
|
// Fetch available methods if they have not been provided.
|
|
onMount(() => {
|
|
if (!methods() && props.state === "known") {
|
|
setMethods(props.mfa.availableMethods);
|
|
}
|
|
});
|
|
|
|
// Always select first available method if only one available.
|
|
createEffect(() => {
|
|
const list = methods();
|
|
if (list) {
|
|
setSelected(list.find((entry) => entry !== "Recovery"));
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Callback to generate a new ticket or send response back up the chain
|
|
*/
|
|
async function onSubmit() {
|
|
try {
|
|
const method = selectedMethod();
|
|
if (!method) return;
|
|
|
|
let mfa_response: API.MFAResponse;
|
|
|
|
switch (method) {
|
|
case "Password":
|
|
mfa_response = { password: group.controls.password.value };
|
|
break;
|
|
case "Totp":
|
|
mfa_response = { totp_code: group.controls.totp_code.value };
|
|
break;
|
|
case "Recovery":
|
|
mfa_response = { recovery_code: group.controls.recovery_code.value };
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
if (props.state === "known") {
|
|
const ticket = await props.mfa.createTicket(mfa_response);
|
|
props.callback(ticket);
|
|
} else {
|
|
props.callback(mfa_response);
|
|
}
|
|
|
|
props.onClose();
|
|
} catch (error) {
|
|
showError(error);
|
|
}
|
|
}
|
|
|
|
function onCancel() {
|
|
props.callback();
|
|
props.onClose();
|
|
}
|
|
|
|
function onBack() {
|
|
if (methods()!.length === 1) {
|
|
onCancel();
|
|
} else {
|
|
setSelected(undefined);
|
|
|
|
// Clear form values
|
|
group.controls.password.setValue("");
|
|
group.controls.totp_code.setValue("");
|
|
group.controls.recovery_code.setValue("");
|
|
}
|
|
}
|
|
|
|
function canSubmit() {
|
|
return (
|
|
Form2.canSubmit(group) &&
|
|
(group.controls.password.value ||
|
|
group.controls.totp_code.value ||
|
|
group.controls.recovery_code.value)
|
|
);
|
|
}
|
|
|
|
const getActions = () => {
|
|
if (selectedMethod()) {
|
|
return [
|
|
{
|
|
text: (
|
|
<Switch fallback={<Trans>Back</Trans>}>
|
|
<Match when={methods()!.length === 1}>
|
|
<Trans>Cancel</Trans>
|
|
</Match>
|
|
</Switch>
|
|
),
|
|
onClick: () => {
|
|
onBack();
|
|
return false;
|
|
},
|
|
},
|
|
{
|
|
text: <Trans>Confirm</Trans>,
|
|
onClick: () => {
|
|
onSubmit();
|
|
return false;
|
|
},
|
|
isDisabled: !canSubmit(),
|
|
},
|
|
];
|
|
}
|
|
|
|
return [
|
|
{
|
|
text: <Trans>Cancel</Trans>,
|
|
onClick: () => {
|
|
onCancel();
|
|
return false;
|
|
},
|
|
},
|
|
];
|
|
};
|
|
|
|
const submit = Form2.useSubmitHandler(group, onSubmit);
|
|
|
|
return (
|
|
<Dialog
|
|
show={props.show}
|
|
onClose={() =>
|
|
props.state === "unknown" || selectedMethod()
|
|
? undefined
|
|
: props.onClose()
|
|
}
|
|
title={<Trans>Confirm action</Trans>}
|
|
actions={getActions()}
|
|
isDisabled={group.isPending}
|
|
>
|
|
<form onSubmit={submit}>
|
|
<Column>
|
|
<Text>
|
|
<Switch
|
|
fallback={
|
|
<Trans>
|
|
Please select a method to authenticate your request.
|
|
</Trans>
|
|
}
|
|
>
|
|
<Match when={selectedMethod()}>
|
|
<Trans>
|
|
Please confirm this action using the selected method.
|
|
</Trans>
|
|
</Match>
|
|
</Switch>
|
|
</Text>
|
|
|
|
<Switch fallback={<CircularProgress />}>
|
|
<Match when={selectedMethod()}>
|
|
<Switch>
|
|
<Match when={selectedMethod() === "Password"}>
|
|
<Form2.TextField
|
|
name="password"
|
|
control={group.controls.password}
|
|
type="password"
|
|
label={t`Password`}
|
|
/>
|
|
</Match>
|
|
<Match when={selectedMethod() === "Totp"}>
|
|
<Form2.TextField
|
|
name="totp_code"
|
|
control={group.controls.totp_code}
|
|
type="text"
|
|
label={t`Authenticator App`}
|
|
/>
|
|
</Match>
|
|
<Match when={selectedMethod() === "Recovery"}>
|
|
<Form2.TextField
|
|
name="recovery_code"
|
|
control={group.controls.recovery_code}
|
|
type="text"
|
|
label={t`Recovery Code`}
|
|
/>
|
|
</Match>
|
|
</Switch>
|
|
</Match>
|
|
<Match when={methods()}>
|
|
<For each={methods()}>
|
|
{(method) => (
|
|
<CategoryButton
|
|
action="chevron"
|
|
icon={
|
|
<Switch>
|
|
<Match when={method === "Password"}>
|
|
<BiSolidKeyboard size={24} />
|
|
</Match>
|
|
<Match when={method === "Totp"}>
|
|
<BiSolidKey size={24} />
|
|
</Match>
|
|
<Match when={method === "Recovery"}>
|
|
<BiRegularArchive size={24} />
|
|
</Match>
|
|
</Switch>
|
|
}
|
|
onClick={() => {
|
|
setSelected(method);
|
|
// Clear form values when switching methods
|
|
group.controls.password.setValue("");
|
|
group.controls.totp_code.setValue("");
|
|
group.controls.recovery_code.setValue("");
|
|
}}
|
|
>
|
|
<Switch>
|
|
<Match when={method === "Password"}>
|
|
<Trans>Password</Trans>
|
|
</Match>
|
|
<Match when={method === "Totp"}>
|
|
<Trans>Authenticator App</Trans>
|
|
</Match>
|
|
<Match when={method === "Recovery"}>
|
|
<Trans>Recovery Code</Trans>
|
|
</Match>
|
|
</Switch>
|
|
</CategoryButton>
|
|
)}
|
|
</For>
|
|
</Match>
|
|
</Switch>
|
|
</Column>
|
|
</form>
|
|
</Dialog>
|
|
);
|
|
}
|