Fixed hardcoded sitekey, fixed hasCaptcha being ignored, fixed setState while unmounted

This commit is contained in:
mrjvs 2023-11-17 19:10:02 +01:00
parent a5512b95e5
commit 328414ab06
6 changed files with 40 additions and 19 deletions

View File

@ -5,6 +5,7 @@ export interface MetaResponse {
name: string; name: string;
description?: string; description?: string;
hasCaptcha: boolean; hasCaptcha: boolean;
captchaClientKey?: string;
} }
export async function getBackendMeta(url: string): Promise<MetaResponse> { export async function getBackendMeta(url: string): Promise<MetaResponse> {

View File

@ -1,5 +1,5 @@
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { useCopyToClipboard } from "react-use"; import { useCopyToClipboard, useMountedState } from "react-use";
import { Icon, Icons } from "./Icon"; import { Icon, Icons } from "./Icon";
@ -9,6 +9,7 @@ export function PassphaseDisplay(props: { mnemonic: string }) {
const [, copy] = useCopyToClipboard(); const [, copy] = useCopyToClipboard();
const [hasCopied, setHasCopied] = useState(false); const [hasCopied, setHasCopied] = useState(false);
const isMounted = useMountedState();
const timeout = useRef<ReturnType<typeof setTimeout>>(); const timeout = useRef<ReturnType<typeof setTimeout>>();
@ -16,7 +17,7 @@ export function PassphaseDisplay(props: { mnemonic: string }) {
copy(props.mnemonic); copy(props.mnemonic);
setHasCopied(true); setHasCopied(true);
if (timeout.current) clearTimeout(timeout.current); if (timeout.current) clearTimeout(timeout.current);
timeout.current = setTimeout(() => setHasCopied(false), 500); timeout.current = setTimeout(() => isMounted() && setHasCopied(false), 500);
} }
return ( return (

View File

@ -19,7 +19,7 @@ import { useBackendUrl } from "@/hooks/auth/useBackendUrl";
import { useAuthStore } from "@/stores/auth"; import { useAuthStore } from "@/stores/auth";
export interface RegistrationData { export interface RegistrationData {
recaptchaToken: string; recaptchaToken?: string;
mnemonic: string; mnemonic: string;
userData: { userData: {
device: string; device: string;

View File

@ -1,6 +1,7 @@
import { useState } from "react"; import { useState } from "react";
import { GoogleReCaptchaProvider } from "react-google-recaptcha-v3"; import { GoogleReCaptchaProvider } from "react-google-recaptcha-v3";
import { MetaResponse } from "@/backend/accounts/meta";
import { SubPageLayout } from "@/pages/layouts/SubPageLayout"; import { SubPageLayout } from "@/pages/layouts/SubPageLayout";
import { import {
AccountCreatePart, AccountCreatePart,
@ -9,20 +10,38 @@ import {
import { PassphraseGeneratePart } from "@/pages/parts/auth/PassphraseGeneratePart"; import { PassphraseGeneratePart } from "@/pages/parts/auth/PassphraseGeneratePart";
import { TrustBackendPart } from "@/pages/parts/auth/TrustBackendPart"; import { TrustBackendPart } from "@/pages/parts/auth/TrustBackendPart";
import { VerifyPassphrase } from "@/pages/parts/auth/VerifyPassphrasePart"; import { VerifyPassphrase } from "@/pages/parts/auth/VerifyPassphrasePart";
import { conf } from "@/setup/config";
function CaptchaProvider(props: {
siteKey: string | null;
children: JSX.Element;
}) {
if (!props.siteKey) return props.children;
return (
<GoogleReCaptchaProvider reCaptchaKey={props.siteKey}>
{props.children}
</GoogleReCaptchaProvider>
);
}
export function RegisterPage() { export function RegisterPage() {
const [step, setStep] = useState(0); const [step, setStep] = useState(0);
const [mnemonic, setMnemonic] = useState<null | string>(null); const [mnemonic, setMnemonic] = useState<null | string>(null);
const [account, setAccount] = useState<null | AccountProfile>(null); const [account, setAccount] = useState<null | AccountProfile>(null);
const reCaptchaKey = conf().RECAPTCHA_SITE_KEY; const [siteKey, setSiteKey] = useState<string | null>(null);
// TODO because of user data loading (in useAuthRestore()), the register page gets unmounted before finishing the register flow
return ( return (
<GoogleReCaptchaProvider reCaptchaKey={reCaptchaKey}> <CaptchaProvider siteKey={siteKey}>
<SubPageLayout> <SubPageLayout>
{step === 0 ? ( {step === 0 ? (
<TrustBackendPart <TrustBackendPart
onNext={() => { onNext={(meta: MetaResponse) => {
setSiteKey(
meta.hasCaptcha && meta.captchaClientKey
? meta.captchaClientKey
: null
);
setStep(1); setStep(1);
}} }}
/> />
@ -45,6 +64,7 @@ export function RegisterPage() {
) : null} ) : null}
{step === 3 ? ( {step === 3 ? (
<VerifyPassphrase <VerifyPassphrase
hasCaptcha={!!siteKey}
mnemonic={mnemonic} mnemonic={mnemonic}
userData={account} userData={account}
onNext={() => { onNext={() => {
@ -54,6 +74,6 @@ export function RegisterPage() {
) : null} ) : null}
{step === 4 ? <p>Success, account now exists</p> : null} {step === 4 ? <p>Success, account now exists</p> : null}
</SubPageLayout> </SubPageLayout>
</GoogleReCaptchaProvider> </CaptchaProvider>
); );
} }

View File

@ -15,6 +15,7 @@ import { AccountProfile } from "@/pages/parts/auth/AccountCreatePart";
interface VerifyPassphraseProps { interface VerifyPassphraseProps {
mnemonic: string | null; mnemonic: string | null;
hasCaptcha?: boolean;
userData: AccountProfile | null; userData: AccountProfile | null;
onNext?: () => void; onNext?: () => void;
} }
@ -27,18 +28,20 @@ export function VerifyPassphrase(props: VerifyPassphraseProps) {
const [result, execute] = useAsyncFn( const [result, execute] = useAsyncFn(
async (inputMnemonic: string) => { async (inputMnemonic: string) => {
const recaptchaToken = executeRecaptcha
? await executeRecaptcha()
: undefined;
if (!props.mnemonic || !props.userData) if (!props.mnemonic || !props.userData)
throw new Error("Data is not valid"); throw new Error("Data is not valid");
let recaptchaToken: string | undefined;
if (props.hasCaptcha) {
recaptchaToken = executeRecaptcha
? await executeRecaptcha()
: undefined;
if (!recaptchaToken) throw new Error("ReCaptcha validation failed"); if (!recaptchaToken) throw new Error("ReCaptcha validation failed");
}
if (inputMnemonic !== props.mnemonic) if (inputMnemonic !== props.mnemonic)
throw new Error("Passphrase doesn't match"); throw new Error("Passphrase doesn't match");
// TODO captcha?
await register({ await register({
mnemonic: inputMnemonic, mnemonic: inputMnemonic,
userData: props.userData, userData: props.userData,

View File

@ -8,7 +8,6 @@ interface Config {
CORS_PROXY_URL: string; CORS_PROXY_URL: string;
NORMAL_ROUTER: boolean; NORMAL_ROUTER: boolean;
BACKEND_URL: string; BACKEND_URL: string;
RECAPTCHA_SITE_KEY: string;
} }
export interface RuntimeConfig { export interface RuntimeConfig {
@ -19,7 +18,6 @@ export interface RuntimeConfig {
NORMAL_ROUTER: boolean; NORMAL_ROUTER: boolean;
PROXY_URLS: string[]; PROXY_URLS: string[];
BACKEND_URL: string; BACKEND_URL: string;
RECAPTCHA_SITE_KEY: string;
} }
const env: Record<keyof Config, undefined | string> = { const env: Record<keyof Config, undefined | string> = {
@ -30,7 +28,6 @@ const env: Record<keyof Config, undefined | string> = {
CORS_PROXY_URL: import.meta.env.VITE_CORS_PROXY_URL, CORS_PROXY_URL: import.meta.env.VITE_CORS_PROXY_URL,
NORMAL_ROUTER: import.meta.env.VITE_NORMAL_ROUTER, NORMAL_ROUTER: import.meta.env.VITE_NORMAL_ROUTER,
BACKEND_URL: import.meta.env.VITE_BACKEND_URL, BACKEND_URL: import.meta.env.VITE_BACKEND_URL,
RECAPTCHA_SITE_KEY: import.meta.env.VITE_RECAPTCHA_SITE_KEY,
}; };
// loads from different locations, in order: environment (VITE_{KEY}), window (public/config.js) // loads from different locations, in order: environment (VITE_{KEY}), window (public/config.js)
@ -56,6 +53,5 @@ export function conf(): RuntimeConfig {
.split(",") .split(",")
.map((v) => v.trim()), .map((v) => v.trim()),
NORMAL_ROUTER: getKey("NORMAL_ROUTER", "false") === "true", NORMAL_ROUTER: getKey("NORMAL_ROUTER", "false") === "true",
RECAPTCHA_SITE_KEY: getKey("RECAPTCHA_SITE_KEY"),
}; };
} }