2026-02-17 12:22:36 +00:00

160 lines
4.8 KiB
TypeScript

/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import FormField from '@app/components/auth/FormField';
import styles from '@app/components/auth/MfaScreen.module.css';
import {Button} from '@app/components/uikit/button/Button';
import {useMfaController} from '@app/hooks/useLoginFlow';
import type {LoginSuccessPayload, MfaChallenge} from '@app/viewmodels/auth/AuthFlow';
import {Trans, useLingui} from '@lingui/react/macro';
import {useId} from 'react';
interface MfaScreenProps {
challenge: MfaChallenge;
inviteCode?: string;
onSuccess: (payload: LoginSuccessPayload) => Promise<void> | void;
onCancel: () => void;
}
const MfaScreen = ({challenge, inviteCode, onSuccess, onCancel}: MfaScreenProps) => {
const {t} = useLingui();
const codeId = useId();
const {
form,
isLoading,
fieldErrors,
selectedMethod,
setSelectedMethod,
smsSent,
handleSendSms,
handleWebAuthn,
isWebAuthnLoading,
supports,
} = useMfaController({
ticket: challenge.ticket,
methods: {sms: challenge.sms, totp: challenge.totp, webauthn: challenge.webauthn},
inviteCode,
onLoginSuccess: onSuccess,
});
if (!selectedMethod && (supports.sms || supports.webauthn || supports.totp)) {
return (
<div className={styles.container}>
<h1 className={styles.title}>
<Trans>Two-factor authentication</Trans>
</h1>
<p className={styles.description}>
<Trans>Choose a verification method</Trans>
</p>
<div className={styles.buttons}>
{supports.totp && (
<Button type="button" fitContainer onClick={() => setSelectedMethod('totp')}>
<Trans>Authenticator App</Trans>
</Button>
)}
{supports.sms && (
<Button type="button" fitContainer variant="secondary" onClick={() => setSelectedMethod('sms')}>
<Trans>SMS Code</Trans>
</Button>
)}
{supports.webauthn && (
<Button
type="button"
fitContainer
variant="secondary"
onClick={handleWebAuthn}
disabled={isWebAuthnLoading}
>
<Trans>Security Key / Passkey</Trans>
</Button>
)}
</div>
<div className={styles.footer}>
<Button type="button" variant="secondary" onClick={onCancel} className={styles.footerButton}>
<Trans>Back to login</Trans>
</Button>
</div>
</div>
);
}
return (
<div className={styles.container}>
<h1 className={styles.title}>
<Trans>Two-factor authentication</Trans>
</h1>
<p className={styles.description}>
{selectedMethod === 'sms' ? (
<Trans>Enter the 6-digit code sent to your phone.</Trans>
) : (
<Trans>Enter the 6-digit code from your authenticator app or one of your backup codes.</Trans>
)}
</p>
{selectedMethod === 'sms' && !smsSent && supports.sms && (
<div className={styles.smsSection}>
<Button type="button" fitContainer onClick={handleSendSms}>
<Trans>Send SMS Code</Trans>
</Button>
</div>
)}
{supports.webauthn && (
<div className={styles.webauthnSection}>
<Button type="button" fitContainer variant="secondary" onClick={handleWebAuthn} disabled={isWebAuthnLoading}>
<Trans>Try security key / passkey instead</Trans>
</Button>
</div>
)}
<form className={styles.form} onSubmit={form.handleSubmit}>
<FormField
id={codeId}
name="code"
type="text"
autoComplete="one-time-code"
required
label={t`Code`}
value={form.getValue('code')}
onChange={(value) => form.setValue('code', value)}
error={form.getError('code') || fieldErrors?.code}
/>
<Button type="submit" fitContainer disabled={isLoading || form.isSubmitting}>
<Trans>Log in</Trans>
</Button>
</form>
<div className={styles.footerButtons}>
{(supports.sms || supports.webauthn || supports.totp) && (
<Button
type="button"
variant="secondary"
onClick={() => setSelectedMethod(null)}
className={styles.footerButton}
>
<Trans>Try another method</Trans>
</Button>
)}
<Button type="button" variant="secondary" onClick={onCancel} className={styles.footerButton}>
<Trans>Back to login</Trans>
</Button>
</div>
</div>
);
};
export default MfaScreen;