mirror of
https://github.com/stoatchat/for-legacy-web.git
synced 2026-03-07 01:15:28 +00:00
feat: add MFA recovery codes
This commit is contained in:
@@ -2,6 +2,4 @@ import { observer } from "mobx-react-lite";
|
||||
|
||||
import { modalController } from ".";
|
||||
|
||||
export default observer(() => {
|
||||
return modalController.render();
|
||||
});
|
||||
export default observer(() => modalController.rendered);
|
||||
|
||||
@@ -22,12 +22,18 @@ 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,
|
||||
|
||||
75
src/context/modals/components/MFARecovery.tsx
Normal file
75
src/context/modals/components/MFARecovery.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
import { useCallback, useState } from "preact/hooks";
|
||||
|
||||
import { Modal } from "@revoltchat/ui";
|
||||
|
||||
import { noopTrue } from "../../../lib/js";
|
||||
|
||||
import { modalController } from "..";
|
||||
import { toConfig } from "../../../components/settings/account/MultiFactorAuthentication";
|
||||
import { ModalProps } from "../types";
|
||||
|
||||
/**
|
||||
* List of recovery codes
|
||||
*/
|
||||
const List = styled.div`
|
||||
display: grid;
|
||||
text-align: center;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
font-family: var(--monospace-font), monospace;
|
||||
|
||||
span {
|
||||
user-select: text;
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* Recovery codes modal
|
||||
*/
|
||||
export default function MFARecovery({
|
||||
codes,
|
||||
client,
|
||||
onClose,
|
||||
}: ModalProps<"mfa_recovery">) {
|
||||
// Keep track of changes to recovery codes
|
||||
const [known, setCodes] = useState(codes);
|
||||
|
||||
// Subroutine to reset recovery codes
|
||||
const reset = useCallback(async () => {
|
||||
const { token } = await modalController.mfaFlow(client);
|
||||
const codes = await client.api.patch(
|
||||
"/auth/mfa/recovery",
|
||||
undefined,
|
||||
toConfig(token),
|
||||
);
|
||||
setCodes(codes);
|
||||
return false;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Your recovery codes"
|
||||
description={"Please save these to a safe location."}
|
||||
actions={[
|
||||
{
|
||||
palette: "primary",
|
||||
children: "Done",
|
||||
onClick: noopTrue,
|
||||
confirmation: true,
|
||||
},
|
||||
{
|
||||
palette: "plain",
|
||||
children: "Reset",
|
||||
onClick: reset,
|
||||
},
|
||||
]}
|
||||
onClose={onClose}>
|
||||
<List>
|
||||
{known.map((code) => (
|
||||
<span>{code}</span>
|
||||
))}
|
||||
</List>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,15 @@
|
||||
import { action, computed, makeAutoObservable } from "mobx";
|
||||
import {
|
||||
action,
|
||||
computed,
|
||||
makeObservable,
|
||||
observable,
|
||||
runInAction,
|
||||
} from "mobx";
|
||||
import type { Client, API } from "revolt.js";
|
||||
import { ulid } from "ulid";
|
||||
|
||||
import MFAFlow from "./components/MFAFlow";
|
||||
import MFARecovery from "./components/MFARecovery";
|
||||
import Test from "./components/Test";
|
||||
import { Modal } from "./types";
|
||||
|
||||
@@ -17,15 +25,19 @@ class ModalController<T extends Modal> {
|
||||
constructor(components: Components) {
|
||||
this.components = components;
|
||||
|
||||
makeAutoObservable(this);
|
||||
this.pop = this.pop.bind(this);
|
||||
makeObservable(this, {
|
||||
stack: observable,
|
||||
push: action,
|
||||
remove: action,
|
||||
rendered: computed,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a new modal on the stack
|
||||
* @param modal Modal data
|
||||
*/
|
||||
@action push(modal: T) {
|
||||
push(modal: T) {
|
||||
this.stack = [
|
||||
...this.stack,
|
||||
{
|
||||
@@ -36,28 +48,57 @@ class ModalController<T extends Modal> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the top modal from the stack
|
||||
* Remove the keyed modal from the stack
|
||||
*/
|
||||
@action pop() {
|
||||
this.stack = this.stack.slice(0, this.stack.length - 1);
|
||||
remove(key: string) {
|
||||
this.stack = this.stack.filter((x) => x.key !== key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render modals
|
||||
*/
|
||||
@computed render() {
|
||||
get rendered() {
|
||||
return (
|
||||
<>
|
||||
{this.stack.map((modal) => {
|
||||
const Component = this.components[modal.type];
|
||||
return <Component {...modal} onClose={this.pop} />;
|
||||
return (
|
||||
<Component
|
||||
{...modal}
|
||||
onClose={() => this.remove(modal.key!)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const modalController = new ModalController<Modal>({
|
||||
/**
|
||||
* Modal controller with additional helpers.
|
||||
*/
|
||||
class ModalControllerExtended extends ModalController<Modal> {
|
||||
/**
|
||||
* Perform MFA flow
|
||||
* @param client Client
|
||||
*/
|
||||
mfaFlow(client: Client) {
|
||||
return runInAction(
|
||||
() =>
|
||||
new Promise((callback: (ticket: API.MFATicket) => void) =>
|
||||
this.push({
|
||||
type: "mfa_flow",
|
||||
state: "known",
|
||||
client,
|
||||
callback,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const modalController = new ModalControllerExtended({
|
||||
mfa_flow: MFAFlow,
|
||||
mfa_recovery: MFARecovery,
|
||||
test: Test,
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@ export type Modal = {
|
||||
callback: (response: API.MFAResponse) => void;
|
||||
}
|
||||
))
|
||||
| { type: "mfa_recovery"; codes: string[]; client: Client }
|
||||
| {
|
||||
type: "test";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user