/* GoToSocial Copyright (C) GoToSocial Authors admin@gotosocial.org SPDX-License-Identifier: AGPL-3.0-or-later This program 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. This program 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 this program. If not, see . */ import React, { ReactNode, useEffect, useMemo, useState } from "react"; import { TextInput } from "../../../components/form/inputs"; import MutationButton from "../../../components/form/mutation-button"; import useFormSubmit from "../../../lib/form/submit"; import { useTwoFactorQRCodeURIMutation, useTwoFactorDisableMutation, useTwoFactorEnableMutation, useTwoFactorQRCodePngMutation, } from "../../../lib/query/user/twofactor"; import { useTextInput } from "../../../lib/form"; import Loading from "../../../components/loading"; import { Error } from "../../../components/error"; import { HighlightedCode } from "../../../components/highlightedcode"; import { useDispatch } from "react-redux"; import { gtsApi } from "../../../lib/query/gts-api"; interface TwoFactorProps { twoFactorEnabledAt?: string, oidcEnabled?: boolean, } export default function TwoFactor({ twoFactorEnabledAt, oidcEnabled }: TwoFactorProps) { switch (true) { case oidcEnabled: // Can't enable if OIDC is in place. return ; case twoFactorEnabledAt !== undefined: // Already enabled. Show the disable form. return ; default: // Not enabled. Show the enable form. return ; } } function CannotEnable() { return (
OIDC is enabled for your instance. To enable 2FA, you must use your instance's OIDC provider instead. Poke your admin for more information.

} /> ); } function EnableForm() { const form = { code: useTextInput("code") }; const [ recoveryCodes, setRecoveryCodes ] = useState(); const dispatch = useDispatch(); // Prepare trigger to submit the code and enable 2FA. // If the enable call is a success, set the recovery // codes state to a nice newline-separated text. const [submitForm, result] = useFormSubmit(form, useTwoFactorEnableMutation(), { changedOnly: true, onFinish: (res) => { const codes = res.data as string[]; if (!codes) { return; } setRecoveryCodes(codes.join("\n")); }, }); // When the component is unmounted, clear the user // cache if 2FA was just enabled. This will prevent // the recovery codes from being shown again. useEffect(() => { return () => { if (recoveryCodes) { dispatch(gtsApi.util.invalidateTags(["User"])); } }; }, [recoveryCodes, dispatch]); return (
You can use this form to enable 2FA for your account.
In your authenticator app, either scan the QR code, or copy the 2FA secret manually, and then enter a 2FA code to verify.

} /> {/* If the enable call was successful then recovery codes will now be set. Display these to the user. If the call hasn't been made yet, show the form to enable 2FA as normal. */} { recoveryCodes ? <>

Two-factor authentication is now enabled for your account!
From now on, you will need to provide a code from your authenticator app whenever you want to sign in.
If you lose access to your authenticator app, you may also sign in by providing one of the below one-time recovery codes instead of a 2FA code.
Once you have used a recovery code once, you will not be able to use it again!
You will not be shown these codes again, so copy them now into a safe place! Treat them like passwords!

Show / hide codes
: <> } ); } // Load and show QR code png only when // the "Show QR Code" button is clicked. function CodePng() { const [ getPng, { isUninitialized, isLoading, isSuccess, data, error, reset, } ] = useTwoFactorQRCodePngMutation(); const [ content, setContent ] = useState(); useEffect(() => { if (isLoading) { setContent(); } else if (isSuccess && data) { setContent(); } else { setContent(); } }, [isLoading, isSuccess, data, error]); return ( <> { isUninitialized ? : } { content } ); } // Get 2fa secret from server and // load it into clipboard on click. function Secret() { const [ getURI, { isUninitialized, isSuccess, data, error, reset, }, ] = useTwoFactorQRCodeURIMutation(); const [ buttonContents, setButtonContents ] = useState(); useEffect(() => { if (isUninitialized) { setButtonContents("Copy 2FA secret to clipboard"); } else if (isSuccess && data) { const url = new URL(data); const secret = url.searchParams.get("secret"); if (!secret) { throw "null secret"; } navigator.clipboard.writeText(secret); setButtonContents("Copied!"); setTimeout(() => { reset(); }, 3000); } else { setButtonContents(); } }, [isUninitialized, isSuccess, data, reset, error]); return ( ); } function DisableForm({ twoFactorEnabledAt }: { twoFactorEnabledAt: string }) { const enabledAt = useMemo(() => { const enabledAt = new Date(twoFactorEnabledAt); return ; }, [twoFactorEnabledAt]); const form = { password: useTextInput("password"), }; const [submitForm, result] = useFormSubmit(form, useTwoFactorDisableMutation()); return (
Two-factor auth is enabled for your account, since {enabledAt}.
To disable 2FA, supply your password for verification and click "Disable 2FA".

} /> ); } function TwoFactorHeader({ blurb }: { blurb: ReactNode }) { return (

Two-Factor Authentication

{blurb} Learn more about this (opens in a new tab)
); }