Captcha Refactoring (#397)
* refactor: replace useCaptchaToken with useCaptcha hook and integrate CaptchaField across forms
This commit is contained in:
committed by
GitHub
parent
9eccb319af
commit
ea0c1dde80
@@ -46,7 +46,11 @@ async function SignInPage({ searchParams }: SignInPageProps) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SignInMethodsContainer paths={paths} providers={authConfig.providers} />
|
<SignInMethodsContainer
|
||||||
|
paths={paths}
|
||||||
|
providers={authConfig.providers}
|
||||||
|
captchaSiteKey={authConfig.captchaTokenSiteKey}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className={'flex justify-center'}>
|
<div className={'flex justify-center'}>
|
||||||
<Button asChild variant={'link'} size={'sm'}>
|
<Button asChild variant={'link'} size={'sm'}>
|
||||||
|
|||||||
@@ -2,11 +2,8 @@
|
|||||||
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import dynamic from 'next/dynamic';
|
|
||||||
|
|
||||||
import { ThemeProvider } from 'next-themes';
|
import { ThemeProvider } from 'next-themes';
|
||||||
|
|
||||||
import { CaptchaProvider } from '@kit/auth/captcha/client';
|
|
||||||
import { I18nProvider } from '@kit/i18n/provider';
|
import { I18nProvider } from '@kit/i18n/provider';
|
||||||
import { MonitoringProvider } from '@kit/monitoring/components';
|
import { MonitoringProvider } from '@kit/monitoring/components';
|
||||||
import { AppEventsProvider } from '@kit/shared/events';
|
import { AppEventsProvider } from '@kit/shared/events';
|
||||||
@@ -16,27 +13,12 @@ import { VersionUpdater } from '@kit/ui/version-updater';
|
|||||||
import { AnalyticsProvider } from '~/components/analytics-provider';
|
import { AnalyticsProvider } from '~/components/analytics-provider';
|
||||||
import { AuthProvider } from '~/components/auth-provider';
|
import { AuthProvider } from '~/components/auth-provider';
|
||||||
import appConfig from '~/config/app.config';
|
import appConfig from '~/config/app.config';
|
||||||
import authConfig from '~/config/auth.config';
|
|
||||||
import featuresFlagConfig from '~/config/feature-flags.config';
|
import featuresFlagConfig from '~/config/feature-flags.config';
|
||||||
import { i18nResolver } from '~/lib/i18n/i18n.resolver';
|
import { i18nResolver } from '~/lib/i18n/i18n.resolver';
|
||||||
import { getI18nSettings } from '~/lib/i18n/i18n.settings';
|
import { getI18nSettings } from '~/lib/i18n/i18n.settings';
|
||||||
|
|
||||||
import { ReactQueryProvider } from './react-query-provider';
|
import { ReactQueryProvider } from './react-query-provider';
|
||||||
|
|
||||||
const captchaSiteKey = authConfig.captchaTokenSiteKey;
|
|
||||||
|
|
||||||
const CaptchaTokenSetter = dynamic(async () => {
|
|
||||||
if (!captchaSiteKey) {
|
|
||||||
return Promise.resolve(() => null);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { CaptchaTokenSetter } = await import('@kit/auth/captcha/client');
|
|
||||||
|
|
||||||
return {
|
|
||||||
default: CaptchaTokenSetter,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
type RootProvidersProps = React.PropsWithChildren<{
|
type RootProvidersProps = React.PropsWithChildren<{
|
||||||
// The language to use for the app (optional)
|
// The language to use for the app (optional)
|
||||||
lang?: string;
|
lang?: string;
|
||||||
@@ -60,22 +42,18 @@ export function RootProviders({
|
|||||||
<AnalyticsProvider>
|
<AnalyticsProvider>
|
||||||
<ReactQueryProvider>
|
<ReactQueryProvider>
|
||||||
<I18nProvider settings={i18nSettings} resolver={i18nResolver}>
|
<I18nProvider settings={i18nSettings} resolver={i18nResolver}>
|
||||||
<CaptchaProvider>
|
<AuthProvider>
|
||||||
<CaptchaTokenSetter siteKey={captchaSiteKey} nonce={nonce} />
|
<ThemeProvider
|
||||||
|
attribute="class"
|
||||||
<AuthProvider>
|
enableSystem
|
||||||
<ThemeProvider
|
disableTransitionOnChange
|
||||||
attribute="class"
|
defaultTheme={theme}
|
||||||
enableSystem
|
enableColorScheme={false}
|
||||||
disableTransitionOnChange
|
nonce={nonce}
|
||||||
defaultTheme={theme}
|
>
|
||||||
enableColorScheme={false}
|
{children}
|
||||||
nonce={nonce}
|
</ThemeProvider>
|
||||||
>
|
</AuthProvider>
|
||||||
{children}
|
|
||||||
</ThemeProvider>
|
|
||||||
</AuthProvider>
|
|
||||||
</CaptchaProvider>
|
|
||||||
|
|
||||||
<If condition={featuresFlagConfig.enableVersionUpdater}>
|
<If condition={featuresFlagConfig.enableVersionUpdater}>
|
||||||
<VersionUpdater />
|
<VersionUpdater />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "next-supabase-saas-kit-turbo",
|
"name": "next-supabase-saas-kit-turbo",
|
||||||
"version": "2.18.2",
|
"version": "2.18.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
125
packages/features/auth/src/captcha/client/captcha-field.tsx
Normal file
125
packages/features/auth/src/captcha/client/captcha-field.tsx
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useRef } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Turnstile,
|
||||||
|
TurnstileInstance,
|
||||||
|
TurnstileProps,
|
||||||
|
} from '@marsidev/react-turnstile';
|
||||||
|
import type { Control, FieldPath, FieldValues } from 'react-hook-form';
|
||||||
|
import { useController } from 'react-hook-form';
|
||||||
|
|
||||||
|
interface BaseCaptchaFieldProps {
|
||||||
|
siteKey: string | undefined;
|
||||||
|
options?: TurnstileProps;
|
||||||
|
nonce?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StandaloneCaptchaFieldProps extends BaseCaptchaFieldProps {
|
||||||
|
onTokenChange: (token: string) => void;
|
||||||
|
onInstanceChange?: (instance: TurnstileInstance | null) => void;
|
||||||
|
control?: never;
|
||||||
|
name?: never;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ReactHookFormCaptchaFieldProps<
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
> extends BaseCaptchaFieldProps {
|
||||||
|
control: Control<TFieldValues>;
|
||||||
|
name: TName;
|
||||||
|
onTokenChange?: never;
|
||||||
|
onInstanceChange?: never;
|
||||||
|
}
|
||||||
|
|
||||||
|
type CaptchaFieldProps<
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
> =
|
||||||
|
| StandaloneCaptchaFieldProps
|
||||||
|
| ReactHookFormCaptchaFieldProps<TFieldValues, TName>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name CaptchaField
|
||||||
|
* @description Self-contained captcha component with two modes:
|
||||||
|
*
|
||||||
|
* **Standalone mode** - For use outside react-hook-form:
|
||||||
|
* ```tsx
|
||||||
|
* <CaptchaField
|
||||||
|
* siteKey={siteKey}
|
||||||
|
* onTokenChange={setToken}
|
||||||
|
* />
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* **React Hook Form mode** - Automatic form integration:
|
||||||
|
* ```tsx
|
||||||
|
* <CaptchaField
|
||||||
|
* siteKey={siteKey}
|
||||||
|
* control={form.control}
|
||||||
|
* name="captchaToken"
|
||||||
|
* />
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function CaptchaField<
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
>(props: CaptchaFieldProps<TFieldValues, TName>) {
|
||||||
|
const { siteKey, options, nonce } = props;
|
||||||
|
const instanceRef = useRef<TurnstileInstance | null>(null);
|
||||||
|
|
||||||
|
// React Hook Form integration
|
||||||
|
const controller =
|
||||||
|
'control' in props && props.control
|
||||||
|
? // eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
useController({
|
||||||
|
control: props.control,
|
||||||
|
name: props.name,
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (!siteKey) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultOptions: Partial<TurnstileProps> = {
|
||||||
|
options: {
|
||||||
|
size: 'invisible',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSuccess = (token: string) => {
|
||||||
|
if (controller) {
|
||||||
|
// React Hook Form mode - use setValue from controller
|
||||||
|
controller.field.onChange(token);
|
||||||
|
} else if ('onTokenChange' in props && props.onTokenChange) {
|
||||||
|
// Standalone mode
|
||||||
|
props.onTokenChange(token);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInstanceChange = (instance: TurnstileInstance | null) => {
|
||||||
|
instanceRef.current = instance;
|
||||||
|
|
||||||
|
if ('onInstanceChange' in props && props.onInstanceChange) {
|
||||||
|
props.onInstanceChange(instance);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Turnstile
|
||||||
|
ref={(instance) => {
|
||||||
|
if (instance) {
|
||||||
|
handleInstanceChange(instance);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
siteKey={siteKey}
|
||||||
|
onSuccess={handleSuccess}
|
||||||
|
scriptOptions={{
|
||||||
|
nonce,
|
||||||
|
}}
|
||||||
|
{...defaultOptions}
|
||||||
|
{...options}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { createContext, useCallback, useRef, useState } from 'react';
|
|
||||||
|
|
||||||
import { TurnstileInstance } from '@marsidev/react-turnstile';
|
|
||||||
|
|
||||||
export const Captcha = createContext<{
|
|
||||||
token: string;
|
|
||||||
setToken: (token: string) => void;
|
|
||||||
instance: TurnstileInstance | null;
|
|
||||||
setInstance: (ref: TurnstileInstance) => void;
|
|
||||||
}>({
|
|
||||||
token: '',
|
|
||||||
instance: null,
|
|
||||||
setToken: (_: string) => {
|
|
||||||
// do nothing
|
|
||||||
return '';
|
|
||||||
},
|
|
||||||
setInstance: () => {
|
|
||||||
// do nothing
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export function CaptchaProvider(props: { children: React.ReactNode }) {
|
|
||||||
const [token, setToken] = useState<string>('');
|
|
||||||
const instanceRef = useRef<TurnstileInstance | null>(null);
|
|
||||||
|
|
||||||
const setInstance = useCallback((ref: TurnstileInstance) => {
|
|
||||||
instanceRef.current = ref;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Captcha.Provider
|
|
||||||
value={{ token, setToken, instance: instanceRef.current, setInstance }}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</Captcha.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useContext } from 'react';
|
|
||||||
|
|
||||||
import { Turnstile, TurnstileProps } from '@marsidev/react-turnstile';
|
|
||||||
|
|
||||||
import { Captcha } from './captcha-provider';
|
|
||||||
|
|
||||||
export function CaptchaTokenSetter(props: {
|
|
||||||
siteKey: string | undefined;
|
|
||||||
options?: TurnstileProps;
|
|
||||||
nonce?: string;
|
|
||||||
}) {
|
|
||||||
const { setToken, setInstance } = useContext(Captcha);
|
|
||||||
|
|
||||||
if (!props.siteKey) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = props.options ?? {
|
|
||||||
options: {
|
|
||||||
size: 'invisible',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Turnstile
|
|
||||||
ref={(instance) => {
|
|
||||||
if (instance) {
|
|
||||||
setInstance(instance);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
siteKey={props.siteKey}
|
|
||||||
onSuccess={setToken}
|
|
||||||
scriptOptions={{
|
|
||||||
nonce: props.nonce,
|
|
||||||
}}
|
|
||||||
{...options}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,2 @@
|
|||||||
export * from './captcha-token-setter';
|
export * from './captcha-field';
|
||||||
export * from './use-captcha-token';
|
export * from './use-captcha';
|
||||||
export * from './captcha-provider';
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { useContext, useMemo } from 'react';
|
|
||||||
|
|
||||||
import { Captcha } from './captcha-provider';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @name useCaptchaToken
|
|
||||||
* @description A hook to get the captcha token and reset function
|
|
||||||
* @returns The captcha token and reset function
|
|
||||||
*/
|
|
||||||
export function useCaptchaToken() {
|
|
||||||
const context = useContext(Captcha);
|
|
||||||
|
|
||||||
if (!context) {
|
|
||||||
throw new Error(`useCaptchaToken must be used within a CaptchaProvider`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return useMemo(() => {
|
|
||||||
return {
|
|
||||||
captchaToken: context.token,
|
|
||||||
resetCaptchaToken: () => context.instance?.reset(),
|
|
||||||
};
|
|
||||||
}, [context]);
|
|
||||||
}
|
|
||||||
81
packages/features/auth/src/captcha/client/use-captcha.tsx
Normal file
81
packages/features/auth/src/captcha/client/use-captcha.tsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import type { TurnstileInstance } from '@marsidev/react-turnstile';
|
||||||
|
|
||||||
|
import { CaptchaField } from './captcha-field';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name useCaptcha
|
||||||
|
* @description Zero-boilerplate hook for captcha integration.
|
||||||
|
* Manages token state and instance internally, exposing a clean API.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```tsx
|
||||||
|
* function SignInForm({ captchaSiteKey }) {
|
||||||
|
* const captcha = useCaptcha({ siteKey: captchaSiteKey });
|
||||||
|
*
|
||||||
|
* const handleSubmit = async (data) => {
|
||||||
|
* await signIn({ ...data, captchaToken: captcha.token });
|
||||||
|
* captcha.reset();
|
||||||
|
* };
|
||||||
|
*
|
||||||
|
* return (
|
||||||
|
* <form onSubmit={handleSubmit}>
|
||||||
|
* {captcha.field}
|
||||||
|
* <button>Submit</button>
|
||||||
|
* </form>
|
||||||
|
* );
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function useCaptcha(
|
||||||
|
{ siteKey, nonce }: { siteKey?: string; nonce?: string } = {
|
||||||
|
siteKey: undefined,
|
||||||
|
nonce: undefined,
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const [token, setToken] = useState('');
|
||||||
|
const instanceRef = useRef<TurnstileInstance | null>(null);
|
||||||
|
|
||||||
|
const reset = useCallback(() => {
|
||||||
|
instanceRef.current?.reset();
|
||||||
|
setToken('');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleTokenChange = useCallback((newToken: string) => {
|
||||||
|
setToken(newToken);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleInstanceChange = useCallback(
|
||||||
|
(instance: TurnstileInstance | null) => {
|
||||||
|
instanceRef.current = instance;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const field = useMemo(
|
||||||
|
() => (
|
||||||
|
<CaptchaField
|
||||||
|
siteKey={siteKey}
|
||||||
|
onTokenChange={handleTokenChange}
|
||||||
|
onInstanceChange={handleInstanceChange}
|
||||||
|
nonce={nonce}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[siteKey, nonce, handleTokenChange, handleInstanceChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => ({
|
||||||
|
/** The current captcha token */
|
||||||
|
token,
|
||||||
|
/** Reset the captcha (clears token and resets widget) */
|
||||||
|
reset,
|
||||||
|
/** The captcha field component to render */
|
||||||
|
field,
|
||||||
|
}),
|
||||||
|
[token, reset, field],
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -23,7 +23,7 @@ import { Input } from '@kit/ui/input';
|
|||||||
import { toast } from '@kit/ui/sonner';
|
import { toast } from '@kit/ui/sonner';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import { useCaptchaToken } from '../captcha/client';
|
import { useCaptcha } from '../captcha/client';
|
||||||
import { useLastAuthMethod } from '../hooks/use-last-auth-method';
|
import { useLastAuthMethod } from '../hooks/use-last-auth-method';
|
||||||
import { TermsAndConditionsFormField } from './terms-and-conditions-form-field';
|
import { TermsAndConditionsFormField } from './terms-and-conditions-form-field';
|
||||||
|
|
||||||
@@ -32,16 +32,18 @@ export function MagicLinkAuthContainer({
|
|||||||
shouldCreateUser,
|
shouldCreateUser,
|
||||||
defaultValues,
|
defaultValues,
|
||||||
displayTermsCheckbox,
|
displayTermsCheckbox,
|
||||||
|
captchaSiteKey,
|
||||||
}: {
|
}: {
|
||||||
redirectUrl: string;
|
redirectUrl: string;
|
||||||
shouldCreateUser: boolean;
|
shouldCreateUser: boolean;
|
||||||
displayTermsCheckbox?: boolean;
|
displayTermsCheckbox?: boolean;
|
||||||
|
captchaSiteKey?: string;
|
||||||
|
|
||||||
defaultValues?: {
|
defaultValues?: {
|
||||||
email: string;
|
email: string;
|
||||||
};
|
};
|
||||||
}) {
|
}) {
|
||||||
const { captchaToken, resetCaptchaToken } = useCaptchaToken();
|
const captcha = useCaptcha({ siteKey: captchaSiteKey });
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const signInWithOtpMutation = useSignInWithOtp();
|
const signInWithOtpMutation = useSignInWithOtp();
|
||||||
const appEvents = useAppEvents();
|
const appEvents = useAppEvents();
|
||||||
@@ -68,7 +70,7 @@ export function MagicLinkAuthContainer({
|
|||||||
email,
|
email,
|
||||||
options: {
|
options: {
|
||||||
emailRedirectTo,
|
emailRedirectTo,
|
||||||
captchaToken,
|
captchaToken: captcha.token,
|
||||||
shouldCreateUser,
|
shouldCreateUser,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -91,7 +93,7 @@ export function MagicLinkAuthContainer({
|
|||||||
error: t(`auth:errors.linkTitle`),
|
error: t(`auth:errors.linkTitle`),
|
||||||
});
|
});
|
||||||
|
|
||||||
resetCaptchaToken();
|
captcha.reset();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (signInWithOtpMutation.data) {
|
if (signInWithOtpMutation.data) {
|
||||||
@@ -106,6 +108,8 @@ export function MagicLinkAuthContainer({
|
|||||||
<ErrorAlert />
|
<ErrorAlert />
|
||||||
</If>
|
</If>
|
||||||
|
|
||||||
|
{captcha.field}
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import {
|
|||||||
import { Spinner } from '@kit/ui/spinner';
|
import { Spinner } from '@kit/ui/spinner';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import { useCaptchaToken } from '../captcha/client';
|
import { useCaptcha } from '../captcha/client';
|
||||||
import { useLastAuthMethod } from '../hooks/use-last-auth-method';
|
import { useLastAuthMethod } from '../hooks/use-last-auth-method';
|
||||||
import { AuthErrorAlert } from './auth-error-alert';
|
import { AuthErrorAlert } from './auth-error-alert';
|
||||||
|
|
||||||
@@ -36,6 +36,7 @@ const OtpSchema = z.object({ token: z.string().min(6).max(6) });
|
|||||||
|
|
||||||
type OtpSignInContainerProps = {
|
type OtpSignInContainerProps = {
|
||||||
shouldCreateUser: boolean;
|
shouldCreateUser: boolean;
|
||||||
|
captchaSiteKey?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function OtpSignInContainer(props: OtpSignInContainerProps) {
|
export function OtpSignInContainer(props: OtpSignInContainerProps) {
|
||||||
@@ -88,6 +89,7 @@ export function OtpSignInContainer(props: OtpSignInContainerProps) {
|
|||||||
return (
|
return (
|
||||||
<OtpEmailForm
|
<OtpEmailForm
|
||||||
shouldCreateUser={shouldCreateUser}
|
shouldCreateUser={shouldCreateUser}
|
||||||
|
captchaSiteKey={props.captchaSiteKey}
|
||||||
onSendOtp={(email) => {
|
onSendOtp={(email) => {
|
||||||
otpForm.setValue('email', email, {
|
otpForm.setValue('email', email, {
|
||||||
shouldValidate: true,
|
shouldValidate: true,
|
||||||
@@ -174,12 +176,14 @@ export function OtpSignInContainer(props: OtpSignInContainerProps) {
|
|||||||
|
|
||||||
function OtpEmailForm({
|
function OtpEmailForm({
|
||||||
shouldCreateUser,
|
shouldCreateUser,
|
||||||
|
captchaSiteKey,
|
||||||
onSendOtp,
|
onSendOtp,
|
||||||
}: {
|
}: {
|
||||||
shouldCreateUser: boolean;
|
shouldCreateUser: boolean;
|
||||||
|
captchaSiteKey?: string;
|
||||||
onSendOtp: (email: string) => void;
|
onSendOtp: (email: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const { captchaToken, resetCaptchaToken } = useCaptchaToken();
|
const captcha = useCaptcha({ siteKey: captchaSiteKey });
|
||||||
const signInMutation = useSignInWithOtp();
|
const signInMutation = useSignInWithOtp();
|
||||||
|
|
||||||
const emailForm = useForm({
|
const emailForm = useForm({
|
||||||
@@ -190,10 +194,10 @@ function OtpEmailForm({
|
|||||||
const handleSendOtp = async ({ email }: z.infer<typeof EmailSchema>) => {
|
const handleSendOtp = async ({ email }: z.infer<typeof EmailSchema>) => {
|
||||||
await signInMutation.mutateAsync({
|
await signInMutation.mutateAsync({
|
||||||
email,
|
email,
|
||||||
options: { captchaToken, shouldCreateUser },
|
options: { captchaToken: captcha.token, shouldCreateUser },
|
||||||
});
|
});
|
||||||
|
|
||||||
resetCaptchaToken();
|
captcha.reset();
|
||||||
onSendOtp(email);
|
onSendOtp(email);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -205,6 +209,8 @@ function OtpEmailForm({
|
|||||||
>
|
>
|
||||||
<AuthErrorAlert error={signInMutation.error} />
|
<AuthErrorAlert error={signInMutation.error} />
|
||||||
|
|
||||||
|
{captcha.field}
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
name="email"
|
name="email"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { If } from '@kit/ui/if';
|
|||||||
import { Input } from '@kit/ui/input';
|
import { Input } from '@kit/ui/input';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import { useCaptchaToken } from '../captcha/client';
|
import { useCaptcha } from '../captcha/client';
|
||||||
import { AuthErrorAlert } from './auth-error-alert';
|
import { AuthErrorAlert } from './auth-error-alert';
|
||||||
|
|
||||||
const PasswordResetSchema = z.object({
|
const PasswordResetSchema = z.object({
|
||||||
@@ -29,10 +29,11 @@ const PasswordResetSchema = z.object({
|
|||||||
|
|
||||||
export function PasswordResetRequestContainer(params: {
|
export function PasswordResetRequestContainer(params: {
|
||||||
redirectPath: string;
|
redirectPath: string;
|
||||||
|
captchaSiteKey?: string;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation('auth');
|
const { t } = useTranslation('auth');
|
||||||
const resetPasswordMutation = useRequestResetPassword();
|
const resetPasswordMutation = useRequestResetPassword();
|
||||||
const { captchaToken, resetCaptchaToken } = useCaptchaToken();
|
const captcha = useCaptcha({ siteKey: params.captchaSiteKey });
|
||||||
|
|
||||||
const error = resetPasswordMutation.error;
|
const error = resetPasswordMutation.error;
|
||||||
const success = resetPasswordMutation.data;
|
const success = resetPasswordMutation.data;
|
||||||
@@ -67,10 +68,10 @@ export function PasswordResetRequestContainer(params: {
|
|||||||
.mutateAsync({
|
.mutateAsync({
|
||||||
email,
|
email,
|
||||||
redirectTo,
|
redirectTo,
|
||||||
captchaToken,
|
captchaToken: captcha.token,
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
resetCaptchaToken();
|
captcha.reset();
|
||||||
});
|
});
|
||||||
})}
|
})}
|
||||||
className={'w-full'}
|
className={'w-full'}
|
||||||
@@ -78,6 +79,8 @@ export function PasswordResetRequestContainer(params: {
|
|||||||
<div className={'flex flex-col gap-y-4'}>
|
<div className={'flex flex-col gap-y-4'}>
|
||||||
<AuthErrorAlert error={error} />
|
<AuthErrorAlert error={error} />
|
||||||
|
|
||||||
|
{captcha.field}
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
name={'email'}
|
name={'email'}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type { z } from 'zod';
|
|||||||
|
|
||||||
import { useSignInWithEmailPassword } from '@kit/supabase/hooks/use-sign-in-with-email-password';
|
import { useSignInWithEmailPassword } from '@kit/supabase/hooks/use-sign-in-with-email-password';
|
||||||
|
|
||||||
import { useCaptchaToken } from '../captcha/client';
|
import { useCaptcha } from '../captcha/client';
|
||||||
import { useLastAuthMethod } from '../hooks/use-last-auth-method';
|
import { useLastAuthMethod } from '../hooks/use-last-auth-method';
|
||||||
import type { PasswordSignInSchema } from '../schemas/password-sign-in.schema';
|
import type { PasswordSignInSchema } from '../schemas/password-sign-in.schema';
|
||||||
import { AuthErrorAlert } from './auth-error-alert';
|
import { AuthErrorAlert } from './auth-error-alert';
|
||||||
@@ -14,10 +14,12 @@ import { PasswordSignInForm } from './password-sign-in-form';
|
|||||||
|
|
||||||
export function PasswordSignInContainer({
|
export function PasswordSignInContainer({
|
||||||
onSignIn,
|
onSignIn,
|
||||||
|
captchaSiteKey,
|
||||||
}: {
|
}: {
|
||||||
onSignIn?: (userId?: string) => unknown;
|
onSignIn?: (userId?: string) => unknown;
|
||||||
|
captchaSiteKey?: string;
|
||||||
}) {
|
}) {
|
||||||
const { captchaToken, resetCaptchaToken } = useCaptchaToken();
|
const captcha = useCaptcha({ siteKey: captchaSiteKey });
|
||||||
const signInMutation = useSignInWithEmailPassword();
|
const signInMutation = useSignInWithEmailPassword();
|
||||||
const { recordAuthMethod } = useLastAuthMethod();
|
const { recordAuthMethod } = useLastAuthMethod();
|
||||||
const isLoading = signInMutation.isPending;
|
const isLoading = signInMutation.isPending;
|
||||||
@@ -28,7 +30,7 @@ export function PasswordSignInContainer({
|
|||||||
try {
|
try {
|
||||||
const data = await signInMutation.mutateAsync({
|
const data = await signInMutation.mutateAsync({
|
||||||
...credentials,
|
...credentials,
|
||||||
options: { captchaToken },
|
options: { captchaToken: captcha.token },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Record successful password sign-in
|
// Record successful password sign-in
|
||||||
@@ -42,22 +44,18 @@ export function PasswordSignInContainer({
|
|||||||
} catch {
|
} catch {
|
||||||
// wrong credentials, do nothing
|
// wrong credentials, do nothing
|
||||||
} finally {
|
} finally {
|
||||||
resetCaptchaToken();
|
captcha.reset();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[captcha, onSignIn, signInMutation, recordAuthMethod],
|
||||||
captchaToken,
|
|
||||||
onSignIn,
|
|
||||||
resetCaptchaToken,
|
|
||||||
signInMutation,
|
|
||||||
recordAuthMethod,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AuthErrorAlert error={signInMutation.error} />
|
<AuthErrorAlert error={signInMutation.error} />
|
||||||
|
|
||||||
|
{captcha.field}
|
||||||
|
|
||||||
<PasswordSignInForm
|
<PasswordSignInForm
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
|||||||
import { If } from '@kit/ui/if';
|
import { If } from '@kit/ui/if';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import { useCaptchaToken } from '../captcha/client';
|
import { useCaptcha } from '../captcha/client';
|
||||||
import { usePasswordSignUpFlow } from '../hooks/use-sign-up-flow';
|
import { usePasswordSignUpFlow } from '../hooks/use-sign-up-flow';
|
||||||
import { AuthErrorAlert } from './auth-error-alert';
|
import { AuthErrorAlert } from './auth-error-alert';
|
||||||
import { PasswordSignUpForm } from './password-sign-up-form';
|
import { PasswordSignUpForm } from './password-sign-up-form';
|
||||||
@@ -18,6 +18,7 @@ interface EmailPasswordSignUpContainerProps {
|
|||||||
};
|
};
|
||||||
onSignUp?: (userId?: string) => unknown;
|
onSignUp?: (userId?: string) => unknown;
|
||||||
emailRedirectTo: string;
|
emailRedirectTo: string;
|
||||||
|
captchaSiteKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EmailPasswordSignUpContainer({
|
export function EmailPasswordSignUpContainer({
|
||||||
@@ -25,8 +26,9 @@ export function EmailPasswordSignUpContainer({
|
|||||||
onSignUp,
|
onSignUp,
|
||||||
emailRedirectTo,
|
emailRedirectTo,
|
||||||
displayTermsCheckbox,
|
displayTermsCheckbox,
|
||||||
|
captchaSiteKey,
|
||||||
}: EmailPasswordSignUpContainerProps) {
|
}: EmailPasswordSignUpContainerProps) {
|
||||||
const { captchaToken, resetCaptchaToken } = useCaptchaToken();
|
const captcha = useCaptcha({ siteKey: captchaSiteKey });
|
||||||
|
|
||||||
const {
|
const {
|
||||||
signUp: onSignupRequested,
|
signUp: onSignupRequested,
|
||||||
@@ -36,8 +38,8 @@ export function EmailPasswordSignUpContainer({
|
|||||||
} = usePasswordSignUpFlow({
|
} = usePasswordSignUpFlow({
|
||||||
emailRedirectTo,
|
emailRedirectTo,
|
||||||
onSignUp,
|
onSignUp,
|
||||||
captchaToken,
|
captchaToken: captcha.token,
|
||||||
resetCaptchaToken,
|
resetCaptchaToken: captcha.reset,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -49,6 +51,8 @@ export function EmailPasswordSignUpContainer({
|
|||||||
<If condition={!showVerifyEmailAlert}>
|
<If condition={!showVerifyEmailAlert}>
|
||||||
<AuthErrorAlert error={error} />
|
<AuthErrorAlert error={error} />
|
||||||
|
|
||||||
|
{captcha.field}
|
||||||
|
|
||||||
<PasswordSignUpForm
|
<PasswordSignUpForm
|
||||||
onSubmit={onSignupRequested}
|
onSubmit={onSignupRequested}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
|||||||
@@ -18,10 +18,14 @@ import {
|
|||||||
import { Input } from '@kit/ui/input';
|
import { Input } from '@kit/ui/input';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import { useCaptchaToken } from '../captcha/client';
|
import { useCaptcha } from '../captcha/client';
|
||||||
|
|
||||||
export function ResendAuthLinkForm(props: { redirectPath?: string }) {
|
export function ResendAuthLinkForm(props: {
|
||||||
const resendLink = useResendLink();
|
redirectPath?: string;
|
||||||
|
captchaSiteKey?: string;
|
||||||
|
}) {
|
||||||
|
const captcha = useCaptcha({ siteKey: props.captchaSiteKey });
|
||||||
|
const resendLink = useResendLink(captcha.token);
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
resolver: zodResolver(z.object({ email: z.string().email() })),
|
resolver: zodResolver(z.object({ email: z.string().email() })),
|
||||||
@@ -52,12 +56,20 @@ export function ResendAuthLinkForm(props: { redirectPath?: string }) {
|
|||||||
<form
|
<form
|
||||||
className={'flex flex-col space-y-2'}
|
className={'flex flex-col space-y-2'}
|
||||||
onSubmit={form.handleSubmit((data) => {
|
onSubmit={form.handleSubmit((data) => {
|
||||||
return resendLink.mutate({
|
const promise = resendLink.mutateAsync({
|
||||||
email: data.email,
|
email: data.email,
|
||||||
redirectPath: props.redirectPath,
|
redirectPath: props.redirectPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
promise.finally(() => {
|
||||||
|
captcha.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
return promise;
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
{captcha.field}
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
@@ -83,9 +95,8 @@ export function ResendAuthLinkForm(props: { redirectPath?: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function useResendLink() {
|
function useResendLink(captchaToken: string) {
|
||||||
const supabase = useSupabase();
|
const supabase = useSupabase();
|
||||||
const { captchaToken } = useCaptchaToken();
|
|
||||||
|
|
||||||
const mutationFn = async (props: {
|
const mutationFn = async (props: {
|
||||||
email: string;
|
email: string;
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ export function SignInMethodsContainer(props: {
|
|||||||
otp: boolean;
|
otp: boolean;
|
||||||
oAuth: Provider[];
|
oAuth: Provider[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
captchaSiteKey?: string;
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@@ -48,18 +50,25 @@ export function SignInMethodsContainer(props: {
|
|||||||
<LastAuthMethodHint />
|
<LastAuthMethodHint />
|
||||||
|
|
||||||
<If condition={props.providers.password}>
|
<If condition={props.providers.password}>
|
||||||
<PasswordSignInContainer onSignIn={onSignIn} />
|
<PasswordSignInContainer
|
||||||
|
onSignIn={onSignIn}
|
||||||
|
captchaSiteKey={props.captchaSiteKey}
|
||||||
|
/>
|
||||||
</If>
|
</If>
|
||||||
|
|
||||||
<If condition={props.providers.magicLink}>
|
<If condition={props.providers.magicLink}>
|
||||||
<MagicLinkAuthContainer
|
<MagicLinkAuthContainer
|
||||||
redirectUrl={redirectUrl}
|
redirectUrl={redirectUrl}
|
||||||
shouldCreateUser={false}
|
shouldCreateUser={false}
|
||||||
|
captchaSiteKey={props.captchaSiteKey}
|
||||||
/>
|
/>
|
||||||
</If>
|
</If>
|
||||||
|
|
||||||
<If condition={props.providers.otp}>
|
<If condition={props.providers.otp}>
|
||||||
<OtpSignInContainer shouldCreateUser={false} />
|
<OtpSignInContainer
|
||||||
|
shouldCreateUser={false}
|
||||||
|
captchaSiteKey={props.captchaSiteKey}
|
||||||
|
/>
|
||||||
</If>
|
</If>
|
||||||
|
|
||||||
<If condition={props.providers.oAuth.length}>
|
<If condition={props.providers.oAuth.length}>
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export function SignUpMethodsContainer(props: {
|
|||||||
};
|
};
|
||||||
|
|
||||||
displayTermsCheckbox?: boolean;
|
displayTermsCheckbox?: boolean;
|
||||||
|
captchaSiteKey?: string;
|
||||||
}) {
|
}) {
|
||||||
const redirectUrl = getCallbackUrl(props);
|
const redirectUrl = getCallbackUrl(props);
|
||||||
const defaultValues = getDefaultValues();
|
const defaultValues = getDefaultValues();
|
||||||
@@ -41,11 +42,15 @@ export function SignUpMethodsContainer(props: {
|
|||||||
emailRedirectTo={redirectUrl}
|
emailRedirectTo={redirectUrl}
|
||||||
defaultValues={defaultValues}
|
defaultValues={defaultValues}
|
||||||
displayTermsCheckbox={props.displayTermsCheckbox}
|
displayTermsCheckbox={props.displayTermsCheckbox}
|
||||||
|
captchaSiteKey={props.captchaSiteKey}
|
||||||
/>
|
/>
|
||||||
</If>
|
</If>
|
||||||
|
|
||||||
<If condition={props.providers.otp}>
|
<If condition={props.providers.otp}>
|
||||||
<OtpSignInContainer shouldCreateUser={true} />
|
<OtpSignInContainer
|
||||||
|
shouldCreateUser={true}
|
||||||
|
captchaSiteKey={props.captchaSiteKey}
|
||||||
|
/>
|
||||||
</If>
|
</If>
|
||||||
|
|
||||||
<If condition={props.providers.magicLink}>
|
<If condition={props.providers.magicLink}>
|
||||||
@@ -54,6 +59,7 @@ export function SignUpMethodsContainer(props: {
|
|||||||
shouldCreateUser={true}
|
shouldCreateUser={true}
|
||||||
defaultValues={defaultValues}
|
defaultValues={defaultValues}
|
||||||
displayTermsCheckbox={props.displayTermsCheckbox}
|
displayTermsCheckbox={props.displayTermsCheckbox}
|
||||||
|
captchaSiteKey={props.captchaSiteKey}
|
||||||
/>
|
/>
|
||||||
</If>
|
</If>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user