for-legacy-web/src/components/settings/account/MultiFactorAuthentication.tsx

223 lines
6.6 KiB
TypeScript

import { ListOl } from "@styled-icons/boxicons-regular";
import { Lock } from "@styled-icons/boxicons-solid";
import { API } from "revolt.js";
import { Text } from "preact-i18n";
import { useCallback, useEffect, useState } from "preact/hooks";
import { Category, CategoryButton, Error, Tip } from "@revoltchat/ui";
import { useSession } from "../../../controllers/client/ClientController";
import { takeError } from "../../../controllers/client/jsx/error";
import { modalController } from "../../../controllers/modals/ModalController";
/**
* Temporary helper function for Axios config
* @param token Token
* @returns Headers
*/
export function toConfig(token: string) {
return {
headers: {
"X-MFA-Ticket": token,
},
};
}
/**
* Component for configuring MFA on an account.
*/
export default function MultiFactorAuthentication() {
// Pull in prerequisites
const session = useSession()!;
const client = session.client!;
// Keep track of MFA state
const [mfa, setMFA] = useState<API.MultiFactorStatus>();
const [error, setError] = useState<string>();
// Fetch the current MFA status on account
useEffect(() => {
if (!mfa && session.state === "Online") {
client!.api
.get("/auth/mfa/")
.then(setMFA)
.catch((err) => setError(takeError(err)));
}
}, [mfa, client, session.state]);
// Action called when recovery code button is pressed
const recoveryAction = useCallback(async () => {
// Perform MFA flow first
const ticket = await modalController.mfaFlow(client);
// Check whether action was cancelled
if (typeof ticket === "undefined") {
return;
}
// Decide whether to generate or fetch.
let codes;
if (mfa!.recovery_active) {
// Fetch existing recovery codes
codes = await client.api.post(
"/auth/mfa/recovery",
undefined,
toConfig(ticket.token),
);
} else {
// Generate new recovery codes
codes = await client.api.patch(
"/auth/mfa/recovery",
undefined,
toConfig(ticket.token),
);
setMFA({
...mfa!,
recovery_active: true,
});
}
// Display the codes to the user
modalController.push({
type: "mfa_recovery",
client,
codes,
});
}, [mfa]);
// Action called when TOTP button is pressed
const totpAction = useCallback(async () => {
// Perform MFA flow first
const ticket = await modalController.mfaFlow(client);
// Check whether action was cancelled
if (typeof ticket === "undefined") {
return;
}
// Decide whether to disable or enable.
if (mfa!.totp_mfa) {
// Disable TOTP authentication
await client.api.delete(
"/auth/mfa/totp",
{},
toConfig(ticket.token),
);
setMFA({
...mfa!,
totp_mfa: false,
});
} else {
// Generate a TOTP secret
const { secret } = await client.api.post(
"/auth/mfa/totp",
undefined,
toConfig(ticket.token),
);
// Open secret modal
let success;
while (!success) {
try {
// Make the user generator a token
const totp_code = await modalController.mfaEnableTOTP(
secret,
client.user!.username,
);
if (totp_code) {
// Check whether it is valid
await client.api.put(
"/auth/mfa/totp",
{
totp_code,
},
toConfig(ticket.token),
);
// Mark as successful and activated
success = true;
setMFA({
...mfa!,
totp_mfa: true,
});
} else {
break;
}
} catch (err) {}
}
}
}, [mfa]);
const mfaActive = !!mfa?.totp_mfa;
return (
<>
<h3>
<Text id="app.settings.pages.account.2fa.title" />
</h3>
<h5>
<Text id="app.settings.pages.account.2fa.description" />
</h5>
{error && (
<Category compact>
<Error error={error} />
</Category>
)}
<CategoryButton
icon={<ListOl size={24} />}
description={
<Text
id={`app.settings.pages.account.2fa.${
mfa?.recovery_active
? "view_recovery"
: "generate_recovery"
}_long`}
/>
}
disabled={!mfa}
onClick={recoveryAction}>
<Text
id={`app.settings.pages.account.2fa.${
mfa?.recovery_active
? "view_recovery"
: "generate_recovery"
}`}
/>
</CategoryButton>
<CategoryButton
icon={
<Lock
size={24}
color={!mfa?.totp_mfa ? "var(--error)" : undefined}
/>
}
description={"Set up time-based one-time password."}
disabled={!mfa || (!mfa.recovery_active && !mfa.totp_mfa)}
onClick={totpAction}>
<Text
id={`app.settings.pages.account.2fa.${
mfa?.totp_mfa ? "remove" : "add"
}_auth`}
/>
</CategoryButton>
{mfa && (
<Tip palette={mfaActive ? "primary" : "error"}>
<Text
id={`app.settings.pages.account.2fa.two_factor_${
mfaActive ? "on" : "off"
}`}
/>
</Tip>
)}
</>
);
}