mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-14 03:30:19 +00:00
🐛 fix: handle auth captcha retries (#14346)
This commit is contained in:
@@ -33,6 +33,10 @@
|
||||
"authModal.signIn": "重新登录",
|
||||
"authModal.signingIn": "登录中...",
|
||||
"authModal.title": "登录已过期",
|
||||
"betterAuth.captcha.continue": "继续",
|
||||
"betterAuth.captcha.description": "请先完成下方安全验证。验证通过后,我们会自动继续注册或登录。",
|
||||
"betterAuth.captcha.pendingDescription": "请先完成人机验证,然后继续。",
|
||||
"betterAuth.captcha.title": "需要完成安全验证",
|
||||
"betterAuth.errors.confirmPasswordRequired": "请确认密码",
|
||||
"betterAuth.errors.emailExists": "该邮箱已注册,请直接登录",
|
||||
"betterAuth.errors.emailInvalid": "请输入有效的邮箱地址或用户名",
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
"types": "./src/index.ts",
|
||||
"default": "./src/index.ts"
|
||||
},
|
||||
"./headers": {
|
||||
"types": "./src/headers.ts",
|
||||
"default": "./src/headers.ts"
|
||||
},
|
||||
"./parseError": {
|
||||
"types": "./src/parseError.ts",
|
||||
"default": "./src/parseError.ts"
|
||||
|
||||
@@ -5,6 +5,7 @@ import AnalyticsRSCProvider from '@/layout/AnalyticsRSCProvider';
|
||||
import AuthProvider from '@/layout/AuthProvider';
|
||||
import NextThemeProvider from '@/layout/GlobalProvider/NextThemeProvider';
|
||||
import StyleRegistry from '@/layout/GlobalProvider/StyleRegistry';
|
||||
import { getServerFeatureFlagsStateFromRuntimeConfig } from '@/server/featureFlags';
|
||||
import { getServerAuthConfig } from '@/server/globalConfig/getServerAuthConfig';
|
||||
import { RouteVariants } from '@/utils/server/routeVariants';
|
||||
|
||||
@@ -20,6 +21,7 @@ interface AuthGlobalProviderProps {
|
||||
const AuthGlobalProvider = async ({ children, variants }: AuthGlobalProviderProps) => {
|
||||
const { locale, isMobile } = RouteVariants.deserializeVariants(variants);
|
||||
const serverConfig = getServerAuthConfig();
|
||||
const featureFlags = await getServerFeatureFlagsStateFromRuntimeConfig();
|
||||
|
||||
return (
|
||||
<StyleRegistry>
|
||||
@@ -27,6 +29,7 @@ const AuthGlobalProvider = async ({ children, variants }: AuthGlobalProviderProp
|
||||
<NextThemeProvider>
|
||||
<AuthThemeLite globalCDN={appEnv.CDN_USE_GLOBAL}>
|
||||
<AuthServerConfigProvider
|
||||
featureFlags={featureFlags}
|
||||
isMobile={isMobile}
|
||||
segmentVariants={variants}
|
||||
serverConfig={serverConfig}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { createContext, memo, type ReactNode, use } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { createContext, memo, use } from 'react';
|
||||
|
||||
import { type GlobalServerConfig } from '@/types/serverConfig';
|
||||
import type { IFeatureFlagsState } from '@/config/featureFlags';
|
||||
import type { GlobalServerConfig } from '@/types/serverConfig';
|
||||
|
||||
interface AuthServerConfigState {
|
||||
featureFlags: Partial<IFeatureFlagsState>;
|
||||
isMobile?: boolean;
|
||||
segmentVariants?: string;
|
||||
serverConfig: GlobalServerConfig;
|
||||
@@ -15,15 +18,17 @@ const AuthServerConfigContext = createContext<AuthServerConfigState | null>(null
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
featureFlags?: Partial<IFeatureFlagsState>;
|
||||
isMobile?: boolean;
|
||||
segmentVariants?: string;
|
||||
serverConfig?: GlobalServerConfig;
|
||||
}
|
||||
|
||||
export const AuthServerConfigProvider = memo<Props>(
|
||||
({ children, serverConfig, isMobile, segmentVariants }) => (
|
||||
({ children, featureFlags, serverConfig, isMobile, segmentVariants }) => (
|
||||
<AuthServerConfigContext
|
||||
value={{
|
||||
featureFlags: featureFlags || {},
|
||||
isMobile,
|
||||
segmentVariants,
|
||||
serverConfig: serverConfig || { aiProvider: {}, telemetry: {} },
|
||||
|
||||
@@ -14,6 +14,7 @@ const mockSignInOauth2 = vi.hoisted(() => vi.fn());
|
||||
const mockSignInEmail = vi.hoisted(() => vi.fn());
|
||||
const mockSignInMagicLink = vi.hoisted(() => vi.fn());
|
||||
const mockRequestPasswordReset = vi.hoisted(() => vi.fn());
|
||||
const mockGetCaptchaTokenOnError = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('next/navigation', () => ({
|
||||
useRouter: () => ({ push: mockPush }),
|
||||
@@ -48,6 +49,7 @@ vi.mock('@/business/client/hooks/useBusinessSignin', () => ({
|
||||
useBusinessSignin: () => ({
|
||||
businessElement: null,
|
||||
getAdditionalData: async () => ({}),
|
||||
getCaptchaTokenOnError: mockGetCaptchaTokenOnError,
|
||||
getFetchOptions: async () => undefined,
|
||||
preSocialSigninCheck: async () => true,
|
||||
ssoProviders: [],
|
||||
@@ -97,6 +99,7 @@ describe('useSignIn', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockSearchParamsGet.mockReturnValue(null);
|
||||
mockGetCaptchaTokenOnError.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -346,6 +349,46 @@ describe('useSignIn', () => {
|
||||
expect(mockMessageError).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should retry social sign in with captcha token when captcha is required', async () => {
|
||||
mockGetCaptchaTokenOnError.mockResolvedValue('captcha-token');
|
||||
mockSignInSocial
|
||||
.mockResolvedValueOnce({
|
||||
error: { code: 'CAPTCHA_REQUIRED', message: 'Missing CAPTCHA response', status: 400 },
|
||||
})
|
||||
.mockResolvedValueOnce({ url: 'https://google.com/auth' });
|
||||
|
||||
const { result } = renderHook(() => useSignIn());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleSocialSignIn('google');
|
||||
});
|
||||
|
||||
expect(mockSignInSocial).toHaveBeenCalledTimes(2);
|
||||
expect(mockSignInSocial).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
fetchOptions: { headers: { 'x-captcha-response': 'captcha-token' } },
|
||||
provider: 'google',
|
||||
}),
|
||||
);
|
||||
expect(mockMessageError).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should stop social sign in when captcha modal is cancelled', async () => {
|
||||
mockGetCaptchaTokenOnError.mockResolvedValue(null);
|
||||
mockSignInSocial.mockResolvedValue({
|
||||
error: { code: 'CAPTCHA_REQUIRED', message: 'Missing CAPTCHA response', status: 400 },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useSignIn());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleSocialSignIn('google');
|
||||
});
|
||||
|
||||
expect(mockSignInSocial).toHaveBeenCalledTimes(1);
|
||||
expect(mockMessageError).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should save last auth provider to localStorage', async () => {
|
||||
mockSignInSocial.mockResolvedValue({ url: 'https://google.com/auth' });
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { type CheckUserResponseData } from '@/app/(backend)/api/auth/check-user/route';
|
||||
import { type ResolveUsernameResponseData } from '@/app/(backend)/api/auth/resolve-username/route';
|
||||
import type { CheckUserResponseData } from '@/app/(backend)/api/auth/check-user/route';
|
||||
import type { ResolveUsernameResponseData } from '@/app/(backend)/api/auth/resolve-username/route';
|
||||
import { useBusinessSignin } from '@/business/client/hooks/useBusinessSignin';
|
||||
import { message } from '@/components/AntdStaticMethods';
|
||||
import { trackLoginOrSignupClicked } from '@/features/User/UserLoginOrSignup/trackLoginOrSignupClicked';
|
||||
@@ -13,6 +13,8 @@ import { requestPasswordReset, signIn } from '@/libs/better-auth/auth-client';
|
||||
import { isBuiltinProvider, normalizeProviderId } from '@/libs/better-auth/utils/client';
|
||||
|
||||
import { useAuthServerConfigStore } from '../_layout/AuthServerConfigProvider';
|
||||
import type { AuthFetchOptions } from '../utils/authFetchOptions';
|
||||
import { withCaptchaToken } from '../utils/authFetchOptions';
|
||||
import { EMAIL_REGEX, USERNAME_REGEX } from './SignInEmailStep';
|
||||
|
||||
const LAST_AUTH_PROVIDER_KEY = 'lobehub:auth:last-provider:v1';
|
||||
@@ -57,6 +59,7 @@ export const useSignIn = () => {
|
||||
ssoProviders,
|
||||
preSocialSigninCheck,
|
||||
getAdditionalData,
|
||||
getCaptchaTokenOnError,
|
||||
getFetchOptions,
|
||||
} = useBusinessSignin();
|
||||
|
||||
@@ -228,19 +231,30 @@ export const useSignIn = () => {
|
||||
const callbackUrl = searchParams.get('callbackUrl') || '/';
|
||||
const additionalData = await getAdditionalData();
|
||||
const fetchOptions = await getFetchOptions();
|
||||
const result = isBuiltinProvider(normalizedProvider)
|
||||
? await signIn.social({
|
||||
additionalData,
|
||||
callbackURL: callbackUrl,
|
||||
fetchOptions,
|
||||
provider: normalizedProvider,
|
||||
})
|
||||
: await signIn.oauth2({
|
||||
additionalData,
|
||||
callbackURL: callbackUrl,
|
||||
fetchOptions,
|
||||
providerId: normalizedProvider,
|
||||
});
|
||||
const signInWithFetchOptions = async (nextFetchOptions?: AuthFetchOptions) =>
|
||||
isBuiltinProvider(normalizedProvider)
|
||||
? await signIn.social({
|
||||
additionalData,
|
||||
callbackURL: callbackUrl,
|
||||
fetchOptions: nextFetchOptions,
|
||||
provider: normalizedProvider,
|
||||
})
|
||||
: await signIn.oauth2({
|
||||
additionalData,
|
||||
callbackURL: callbackUrl,
|
||||
fetchOptions: nextFetchOptions,
|
||||
providerId: normalizedProvider,
|
||||
});
|
||||
|
||||
let result = await signInWithFetchOptions(fetchOptions);
|
||||
if (result && 'error' in result && result.error) {
|
||||
const captchaToken = await getCaptchaTokenOnError(result.error);
|
||||
if (captchaToken === null) return;
|
||||
if (captchaToken) {
|
||||
result = await signInWithFetchOptions(withCaptchaToken(fetchOptions, captchaToken));
|
||||
}
|
||||
}
|
||||
|
||||
if (result && 'error' in result && result.error) throw result.error;
|
||||
} catch (error) {
|
||||
console.error(`${normalizedProvider} sign in error:`, error);
|
||||
|
||||
@@ -9,6 +9,7 @@ const mockPush = vi.hoisted(() => vi.fn());
|
||||
const mockSearchParamsGet = vi.hoisted(() => vi.fn().mockReturnValue(null));
|
||||
const mockMessageError = vi.hoisted(() => vi.fn());
|
||||
const mockSignUpEmail = vi.hoisted(() => vi.fn());
|
||||
const mockGetCaptchaTokenOnError = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('next/navigation', () => ({
|
||||
useRouter: () => ({ push: mockPush }),
|
||||
@@ -31,6 +32,7 @@ vi.mock('@lobechat/business-const', () => ({
|
||||
vi.mock('@/business/client/hooks/useBusinessSignup', () => ({
|
||||
useBusinessSignup: () => ({
|
||||
businessElement: null,
|
||||
getCaptchaTokenOnError: mockGetCaptchaTokenOnError,
|
||||
getFetchOptions: async () => undefined,
|
||||
preSocialSignupCheck: async () => true,
|
||||
}),
|
||||
@@ -53,6 +55,7 @@ describe('useSignUp', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockSearchParamsGet.mockReturnValue(null);
|
||||
mockGetCaptchaTokenOnError.mockResolvedValue(undefined);
|
||||
mockEnableEmailVerification = false;
|
||||
});
|
||||
|
||||
@@ -199,6 +202,47 @@ describe('useSignUp', () => {
|
||||
expect(mockPush).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should retry sign up with captcha token when captcha is required', async () => {
|
||||
mockGetCaptchaTokenOnError.mockResolvedValue('captcha-token');
|
||||
mockSignUpEmail
|
||||
.mockResolvedValueOnce({
|
||||
error: { code: 'CAPTCHA_REQUIRED', message: 'Missing CAPTCHA response' },
|
||||
})
|
||||
.mockResolvedValueOnce({ error: null });
|
||||
|
||||
const { result } = renderHook(() => useSignUp());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.onSubmit(validValues);
|
||||
});
|
||||
|
||||
expect(mockSignUpEmail).toHaveBeenCalledTimes(2);
|
||||
expect(mockSignUpEmail).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
fetchOptions: { headers: { 'x-captcha-response': 'captcha-token' } },
|
||||
}),
|
||||
);
|
||||
expect(mockMessageError).not.toHaveBeenCalled();
|
||||
expect(mockPush).toHaveBeenCalledWith('/');
|
||||
});
|
||||
|
||||
it('should stop sign up when captcha modal is cancelled', async () => {
|
||||
mockGetCaptchaTokenOnError.mockResolvedValue(null);
|
||||
mockSignUpEmail.mockResolvedValue({
|
||||
error: { code: 'CAPTCHA_REQUIRED', message: 'Missing CAPTCHA response' },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useSignUp());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.onSubmit(validValues);
|
||||
});
|
||||
|
||||
expect(mockSignUpEmail).toHaveBeenCalledTimes(1);
|
||||
expect(mockMessageError).not.toHaveBeenCalled();
|
||||
expect(mockPush).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should show generic error on unexpected exception', async () => {
|
||||
mockSignUpEmail.mockRejectedValue(new Error('network error'));
|
||||
|
||||
|
||||
@@ -4,23 +4,36 @@ import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { type BusinessSignupFomData } from '@/business/client/hooks/useBusinessSignup';
|
||||
import type { BusinessSignupFomData } from '@/business/client/hooks/useBusinessSignup';
|
||||
import { useBusinessSignup } from '@/business/client/hooks/useBusinessSignup';
|
||||
import { message } from '@/components/AntdStaticMethods';
|
||||
import { trackLoginOrSignupClicked } from '@/features/User/UserLoginOrSignup/trackLoginOrSignupClicked';
|
||||
import { signUp } from '@/libs/better-auth/auth-client';
|
||||
|
||||
import { useAuthServerConfigStore } from '../../_layout/AuthServerConfigProvider';
|
||||
import { type BaseSignUpFormValues } from './types';
|
||||
import type { AuthFetchOptions } from '../../utils/authFetchOptions';
|
||||
import { withCaptchaToken } from '../../utils/authFetchOptions';
|
||||
import type { BaseSignUpFormValues } from './types';
|
||||
|
||||
export type SignUpFormValues = BaseSignUpFormValues & BusinessSignupFomData;
|
||||
|
||||
interface SignUpErrorLike {
|
||||
code?: string;
|
||||
details?: {
|
||||
cause?: {
|
||||
code?: string;
|
||||
};
|
||||
};
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export const useSignUp = () => {
|
||||
const { t } = useTranslation(['auth', 'authError']);
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { getFetchOptions, preSocialSignupCheck, businessElement } = useBusinessSignup(form);
|
||||
const { getCaptchaTokenOnError, getFetchOptions, preSocialSignupCheck, businessElement } =
|
||||
useBusinessSignup(form);
|
||||
const enableEmailVerification = useAuthServerConfigStore(
|
||||
(s) => s.serverConfig.enableEmailVerification || false,
|
||||
);
|
||||
@@ -37,34 +50,47 @@ export const useSignUp = () => {
|
||||
|
||||
const callbackUrl = searchParams.get('callbackUrl') || '/';
|
||||
const username = values.email.split('@')[0];
|
||||
const fetchOptions = await getFetchOptions();
|
||||
|
||||
const { error } = await signUp.email({
|
||||
callbackURL: callbackUrl,
|
||||
email: values.email,
|
||||
fetchOptions: await getFetchOptions(),
|
||||
name: username,
|
||||
password: values.password,
|
||||
});
|
||||
const submit = async (nextFetchOptions?: AuthFetchOptions) =>
|
||||
signUp.email({
|
||||
callbackURL: callbackUrl,
|
||||
email: values.email,
|
||||
fetchOptions: nextFetchOptions,
|
||||
name: username,
|
||||
password: values.password,
|
||||
});
|
||||
|
||||
let { error } = await submit(fetchOptions);
|
||||
|
||||
if (error) {
|
||||
const captchaToken = await getCaptchaTokenOnError(error);
|
||||
if (captchaToken === null) return;
|
||||
if (captchaToken) {
|
||||
({ error } = await submit(withCaptchaToken(fetchOptions, captchaToken)));
|
||||
}
|
||||
}
|
||||
|
||||
if (error) {
|
||||
const signUpError = error as SignUpErrorLike;
|
||||
const isEmailDuplicate =
|
||||
error.code === 'FAILED_TO_CREATE_USER' &&
|
||||
(error as any)?.details?.cause?.code === '23505';
|
||||
signUpError.code === 'FAILED_TO_CREATE_USER' &&
|
||||
signUpError.details?.cause?.code === '23505';
|
||||
|
||||
if (isEmailDuplicate) {
|
||||
message.error(t('betterAuth.errors.emailExists'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (error.code === 'INVALID_EMAIL' || error.message === 'Invalid email') {
|
||||
if (signUpError.code === 'INVALID_EMAIL' || signUpError.message === 'Invalid email') {
|
||||
message.error(t('betterAuth.errors.emailInvalid'));
|
||||
return;
|
||||
}
|
||||
|
||||
const translated = error.code
|
||||
? t(`authError:codes.${error.code}`, { defaultValue: '' })
|
||||
const translated = signUpError.code
|
||||
? t(`authError:codes.${signUpError.code}`, { defaultValue: '' })
|
||||
: '';
|
||||
message.error(translated || error.message || t('betterAuth.signup.error'));
|
||||
message.error(translated || signUpError.message || t('betterAuth.signup.error'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { headersToRecord } from '@lobechat/fetch-sse/headers';
|
||||
|
||||
import { CAPTCHA_RESPONSE_HEADER } from '@/libs/better-auth/captcha';
|
||||
|
||||
export interface AuthFetchOptions {
|
||||
headers?: HeadersInit;
|
||||
query?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export const withCaptchaToken = (
|
||||
fetchOptions: AuthFetchOptions | undefined,
|
||||
captchaToken: string,
|
||||
) => ({
|
||||
...fetchOptions,
|
||||
headers: {
|
||||
...headersToRecord(fetchOptions?.headers),
|
||||
[CAPTCHA_RESPONSE_HEADER]: captchaToken,
|
||||
},
|
||||
});
|
||||
@@ -6,7 +6,9 @@ export const useBusinessSignin = () => {
|
||||
getAdditionalData: async () => {
|
||||
return {};
|
||||
},
|
||||
getFetchOptions: async () => undefined as Record<string, any> | undefined,
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
getCaptchaTokenOnError: async (error: unknown) => undefined as string | null | undefined,
|
||||
getFetchOptions: async () => undefined as Record<string, unknown> | undefined,
|
||||
preSocialSigninCheck: async () => {
|
||||
return true;
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type BaseSignUpFormValues } from '@/app/[variants]/(auth)/signup/[[...signup]]/types';
|
||||
import type { BaseSignUpFormValues } from '@/app/[variants]/(auth)/signup/[[...signup]]/types';
|
||||
|
||||
export interface BusinessSignupFomData {}
|
||||
|
||||
@@ -6,6 +6,8 @@ export interface BusinessSignupFomData {}
|
||||
export const useBusinessSignup = (form: any) => {
|
||||
return {
|
||||
businessElement: null,
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
getCaptchaTokenOnError: async (error: unknown) => undefined as string | null | undefined,
|
||||
getFetchOptions: async () => {
|
||||
return {};
|
||||
},
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { evaluateFeatureFlag, FeatureFlagsSchema, mapFeatureFlagsEnvToState } from './schema';
|
||||
import {
|
||||
DEFAULT_FEATURE_FLAGS,
|
||||
evaluateFeatureFlag,
|
||||
FeatureFlagsSchema,
|
||||
mapFeatureFlagsEnvToState,
|
||||
} from './schema';
|
||||
|
||||
describe('FeatureFlagsSchema', () => {
|
||||
it('should validate correct feature flags with boolean values', () => {
|
||||
@@ -96,6 +101,12 @@ describe('evaluateFeatureFlag', () => {
|
||||
});
|
||||
|
||||
describe('mapFeatureFlagsEnvToState', () => {
|
||||
it('should enable auth captcha by default', () => {
|
||||
const mappedState = mapFeatureFlagsEnvToState(DEFAULT_FEATURE_FLAGS);
|
||||
|
||||
expect(mappedState.enableAuthCaptcha).toBe(true);
|
||||
});
|
||||
|
||||
it('should correctly map boolean feature flags to state', () => {
|
||||
const config = {
|
||||
provider_settings: true,
|
||||
@@ -110,6 +121,7 @@ describe('mapFeatureFlagsEnvToState', () => {
|
||||
agent_self_iteration: true,
|
||||
agent_onboarding: true,
|
||||
agent_task: true,
|
||||
auth_captcha: true,
|
||||
market: true,
|
||||
speech_to_text: true,
|
||||
changelog: false,
|
||||
@@ -136,6 +148,7 @@ describe('mapFeatureFlagsEnvToState', () => {
|
||||
enableAgentSelfIteration: true,
|
||||
enableAgentOnboarding: true,
|
||||
enableAgentTask: true,
|
||||
enableAuthCaptcha: true,
|
||||
showMarket: true,
|
||||
enableSTT: true,
|
||||
showCloudPromotion: true,
|
||||
@@ -151,6 +164,7 @@ describe('mapFeatureFlagsEnvToState', () => {
|
||||
agent_self_iteration: ['user-123'],
|
||||
agent_onboarding: ['user-123'],
|
||||
agent_task: ['user-123'],
|
||||
auth_captcha: ['user-123'],
|
||||
create_session: ['user-789'],
|
||||
dalle: true,
|
||||
knowledge_base: ['user-123'],
|
||||
@@ -163,6 +177,7 @@ describe('mapFeatureFlagsEnvToState', () => {
|
||||
expect(mappedState.enableAgentSelfIteration).toBe(true); // user-123 is in allowlist
|
||||
expect(mappedState.enableAgentOnboarding).toBe(true); // user-123 is in allowlist
|
||||
expect(mappedState.enableAgentTask).toBe(true); // user-123 is in allowlist
|
||||
expect(mappedState.enableAuthCaptcha).toBe(true); // user-123 is in allowlist
|
||||
expect(mappedState.enableKnowledgeBase).toBe(true); // user-123 is in allowlist
|
||||
});
|
||||
|
||||
|
||||
@@ -33,6 +33,8 @@ export const FeatureFlagsSchema = z.object({
|
||||
agent_self_iteration: FeatureFlagValue.optional(),
|
||||
agent_onboarding: FeatureFlagValue.optional(),
|
||||
agent_task: FeatureFlagValue.optional(),
|
||||
// Cloud feature flag. Keep here until cloud owns a separate runtime flag domain.
|
||||
auth_captcha: FeatureFlagValue.optional(),
|
||||
cloud_promotion: FeatureFlagValue.optional(),
|
||||
|
||||
// the flags below can only be used with commercial license
|
||||
@@ -82,6 +84,7 @@ export const DEFAULT_FEATURE_FLAGS: IFeatureFlags = {
|
||||
agent_self_iteration: isDev,
|
||||
agent_onboarding: isDev,
|
||||
agent_task: isDev,
|
||||
auth_captcha: true,
|
||||
cloud_promotion: false,
|
||||
|
||||
market: true,
|
||||
@@ -116,6 +119,7 @@ export const mapFeatureFlagsEnvToState = (config: IFeatureFlags, userId?: string
|
||||
enableAgentSelfIteration: evaluateFeatureFlag(config.agent_self_iteration, userId),
|
||||
enableAgentOnboarding: evaluateFeatureFlag(config.agent_onboarding, userId),
|
||||
enableAgentTask: evaluateFeatureFlag(config.agent_task, userId),
|
||||
enableAuthCaptcha: evaluateFeatureFlag(config.auth_captcha, userId),
|
||||
|
||||
showCloudPromotion: evaluateFeatureFlag(config.cloud_promotion, userId),
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
export const CAPTCHA_RESPONSE_HEADER = 'x-captcha-response';
|
||||
|
||||
export const CAPTCHA_REQUIRED_CODE = 'CAPTCHA_REQUIRED';
|
||||
export const CAPTCHA_FAILED_CODE = 'CAPTCHA_FAILED';
|
||||
export const CAPTCHA_UNAVAILABLE_CODE = 'CAPTCHA_UNAVAILABLE';
|
||||
|
||||
export const CAPTCHA_REQUIRED_MESSAGE = 'Missing CAPTCHA response';
|
||||
export const CAPTCHA_FAILED_MESSAGE = 'Captcha verification failed';
|
||||
export const CAPTCHA_UNAVAILABLE_MESSAGE = 'CAPTCHA service unavailable';
|
||||
@@ -62,6 +62,11 @@ export default {
|
||||
'betterAuth.resetPassword.success':
|
||||
'Password reset successful, please sign in with your new password',
|
||||
'betterAuth.resetPassword.title': 'Reset Password',
|
||||
'betterAuth.captcha.continue': 'Continue',
|
||||
'betterAuth.captcha.description':
|
||||
'Complete the security verification below. We will continue your sign up or sign in automatically.',
|
||||
'betterAuth.captcha.pendingDescription': 'Please complete the verification first, then continue.',
|
||||
'betterAuth.captcha.title': 'Security verification required',
|
||||
'betterAuth.signin.backToEmail': 'Back to change email',
|
||||
'betterAuth.signin.continueWithApple': 'Continue with Apple',
|
||||
'betterAuth.signin.continueWithAuth0': 'Sign in with Auth0',
|
||||
|
||||
Reference in New Issue
Block a user