From c79de38d5489d1de3c5c6e11e68c1538341b1206 Mon Sep 17 00:00:00 2001 From: speakeloudest Date: Tue, 9 Dec 2025 21:10:05 -0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E7=BA=BF=E8=B7=AF?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=B3=A8=E5=86=8C=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/(main)/(content)/register/page.tsx | 10 + apps/user/app/auth/email2/auth-form.tsx | 111 ++++++++ apps/user/app/auth/email2/login-form.tsx | 147 ++++++++++ apps/user/app/auth/email2/register-form.tsx | 262 ++++++++++++++++++ apps/user/app/auth/email2/reset-form.tsx | 174 ++++++++++++ apps/user/utils/common.ts | 2 +- 6 files changed, 705 insertions(+), 1 deletion(-) create mode 100644 apps/user/app/(main)/(content)/register/page.tsx create mode 100644 apps/user/app/auth/email2/auth-form.tsx create mode 100644 apps/user/app/auth/email2/login-form.tsx create mode 100644 apps/user/app/auth/email2/register-form.tsx create mode 100644 apps/user/app/auth/email2/reset-form.tsx diff --git a/apps/user/app/(main)/(content)/register/page.tsx b/apps/user/app/(main)/(content)/register/page.tsx new file mode 100644 index 0000000..29d4b87 --- /dev/null +++ b/apps/user/app/(main)/(content)/register/page.tsx @@ -0,0 +1,10 @@ +'use client'; +import EmailAuthForm2 from '@/app/auth/email2/auth-form'; + +export default function RegisterPage() { + return ( +
+ +
+ ); +} diff --git a/apps/user/app/auth/email2/auth-form.tsx b/apps/user/app/auth/email2/auth-form.tsx new file mode 100644 index 0000000..01431c2 --- /dev/null +++ b/apps/user/app/auth/email2/auth-form.tsx @@ -0,0 +1,111 @@ +'use client'; + +import { resetPassword, userLogin, userRegister } from '@/services/common/auth'; +import { useTranslations } from 'next-intl'; +import { useRouter } from 'next/navigation'; +import { ReactNode, useState, useTransition } from 'react'; +import { toast } from 'sonner'; + +import { + NEXT_PUBLIC_DEFAULT_USER_EMAIL, + NEXT_PUBLIC_DEFAULT_USER_PASSWORD, +} from '@/config/constants'; +import useGlobalStore from '@/config/use-global'; +import { getRedirectUrl, setAuthorization } from '@/utils/common'; +import LoginForm from './login-form'; +import RegisterForm from './register-form'; +import ResetForm from './reset-form'; + +export default function EmailAuthForm(props: { isRedirect: boolean }) { + const t = useTranslations('auth'); + const router = useRouter(); + const [type, setType] = useState<'login' | 'register' | 'reset'>('register'); + const [loading, startTransition] = useTransition(); + const [initialValues, setInitialValues] = useState<{ + email?: string; + password?: string; + }>({ + email: NEXT_PUBLIC_DEFAULT_USER_EMAIL, + password: NEXT_PUBLIC_DEFAULT_USER_PASSWORD, + }); + const { getUserInfo } = useGlobalStore(); + const handleFormSubmit = async (params: any) => { + const onLogin = async (token?: string) => { + if (!token) return; + setAuthorization(token); + console.log('props.isRedirect', token); + console.log('props.isRedirect', props.isRedirect); + console.log('props.isRedirect ', getRedirectUrl()); + if (props.isRedirect) { + router.replace(getRedirectUrl()); + router.refresh(); + } else { + await getUserInfo(); + } + }; + startTransition(async () => { + try { + switch (type) { + case 'login': { + const login = await userLogin(params); + toast.success(t('login.success')); + onLogin(login.data.data?.token); + break; + } + case 'register': { + const create = await userRegister(params); + toast.success(t('register.success')); + onLogin(create.data.data?.token); + break; + } + case 'reset': + await resetPassword(params); + toast.success(t('reset.success')); + setType('login'); + break; + } + } catch (error) { + /* empty */ + } + }); + }; + + let UserForm: ReactNode = null; + switch (type) { + case 'login': + UserForm = ( + + ); + break; + case 'register': + UserForm = ( + + ); + break; + case 'reset': + UserForm = ( + + ); + break; + } + + return UserForm; +} diff --git a/apps/user/app/auth/email2/login-form.tsx b/apps/user/app/auth/email2/login-form.tsx new file mode 100644 index 0000000..859f1a6 --- /dev/null +++ b/apps/user/app/auth/email2/login-form.tsx @@ -0,0 +1,147 @@ +import useGlobalStore from '@/config/use-global'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { AiroButton } from '@workspace/airo-ui/components/AiroButton'; +import { Button } from '@workspace/airo-ui/components/button'; +import { + Form, + FormControl, + FormField, + FormItem, + FormMessage, +} from '@workspace/airo-ui/components/form'; +import { Input } from '@workspace/airo-ui/components/input'; +import { Icon } from '@workspace/airo-ui/custom-components/icon'; +import { useTranslations } from 'next-intl'; +import { Dispatch, SetStateAction, useRef } from 'react'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; +import CloudFlareTurnstile, { TurnstileRef } from '../turnstile'; + +export default function LoginForm({ + loading, + onSubmit, + initialValues, + setInitialValues, + onSwitchForm, +}: { + loading?: boolean; + onSubmit: (data: any) => void; + initialValues: any; + setInitialValues: Dispatch>; + onSwitchForm: Dispatch>; +}) { + const t = useTranslations('auth.login'); + const { common } = useGlobalStore(); + const { verify } = common; + + const formSchema = z.object({ + email: z.string().email(t('email')), + password: z.string(), + cf_token: + verify.enable_login_verify && verify.turnstile_site_key ? z.string() : z.string().optional(), + }); + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: initialValues, + }); + + const turnstile = useRef(null); + const handleSubmit = form.handleSubmit((data) => { + try { + onSubmit(data); + } catch (error) { + turnstile.current?.reset(); + } + }); + + return ( + <> +
账户验证
+
+ + ( + + + + + + + )} + /> + ( + + + + + + + )} + /> + {verify.enable_login_verify && ( + ( + + + + + + + )} + /> + )} +
+ + +
+ +
+ + {loading && } + {t('title')} + +
+ + + + ); +} diff --git a/apps/user/app/auth/email2/register-form.tsx b/apps/user/app/auth/email2/register-form.tsx new file mode 100644 index 0000000..80d8b01 --- /dev/null +++ b/apps/user/app/auth/email2/register-form.tsx @@ -0,0 +1,262 @@ +'use client'; +import useGlobalStore from '@/config/use-global'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { AiroButton } from '@workspace/airo-ui/components/AiroButton'; +import { Button } from '@workspace/airo-ui/components/button'; + +import { + Form, + FormControl, + FormField, + FormItem, + FormMessage, +} from '@workspace/airo-ui/components/form'; +import { Input } from '@workspace/airo-ui/components/input'; +import { Icon } from '@workspace/airo-ui/custom-components/icon'; +import { Markdown } from '@workspace/airo-ui/custom-components/markdown'; +import { useTranslations } from 'next-intl'; +import { useRouter } from 'next/navigation'; +import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; +import SendCode from '../send-code'; +import CloudFlareTurnstile, { TurnstileRef } from '../turnstile'; + +export default function RegisterForm({ + loading, + onSubmit, + initialValues, + setInitialValues, + onSwitchForm, +}: { + loading?: boolean; + onSubmit: (data: any) => void; + initialValues: any; + setInitialValues: Dispatch>; + onSwitchForm: Dispatch>; +}) { + const t = useTranslations('auth.register'); + const { common } = useGlobalStore(); + const { verify, auth, invite } = common; + const router = useRouter(); + const handleCheckUser = async (email: string) => { + try { + if (!auth.email.enable_domain_suffix) return true; + const domain = email.split('@')[1]; + const isValid = auth.email?.domain_suffix_list.split('\n').includes(domain || ''); + return isValid; + } catch (error) { + console.log('Error checking user:', error); + return false; + } + }; + + const formSchema = z + .object({ + email: z + .string() + .email(t('email')) + .refine(handleCheckUser, { + message: t('whitelist'), + }), + password: z.string().min(1, '请输入密码'), // 必填提示 + repeat_password: z.string().min(1, '请重复输入密码'), // 必填 + code: auth.email.enable_verify + ? z.string().min(1, '请输入验证码') // 必填 + : z.string().nullish(), + invite: invite.forced_invite ? z.string().min(1, '请输入邀请码') : z.string().nullish(), + cf_token: + verify.enable_register_verify && verify.turnstile_site_key + ? z.string() + : z.string().nullish(), + }) + .superRefine(({ password, repeat_password }, ctx) => { + if (password !== repeat_password) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: t('passwordMismatch'), + path: ['repeat_password'], + }); + } + }); + + const [inviteDefault, setInviteDefault] = useState(''); + + useEffect(() => { + const invite = localStorage.getItem('invite') || ''; + setInviteDefault(invite); + }, []); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + ...initialValues, + invite: inviteDefault, + }, + }); + + const turnstile = useRef(null); + const handleSubmit = form.handleSubmit((data) => { + try { + onSubmit(data); + } catch (error) { + turnstile.current?.reset(); + } + }); + + return ( + <> +
线路优化
+ + {auth.register.stop_register ? ( + {t('message')} + ) : ( +
+ +
+ ( + + + + + + + )} + /> + ( + + + + + + + )} + /> + ( + + + + + + + )} + /> + {auth.email.enable_verify && ( + ( + + +
+ + +
+
+ +
+ )} + /> + )} + ( + + + + + + + )} + /> + {verify.enable_register_verify && ( + ( + + + + + + + )} + /> + )} +
+ +
+ {t('existingAccount')}  + +
+
+ + {loading && } + {t('title')} + +
+
+ + )} + + ); +} diff --git a/apps/user/app/auth/email2/reset-form.tsx b/apps/user/app/auth/email2/reset-form.tsx new file mode 100644 index 0000000..5cadaa8 --- /dev/null +++ b/apps/user/app/auth/email2/reset-form.tsx @@ -0,0 +1,174 @@ +import useGlobalStore from '@/config/use-global'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { AiroButton } from '@workspace/airo-ui/components/AiroButton'; +import { Button } from '@workspace/airo-ui/components/button'; +import { + Form, + FormControl, + FormField, + FormItem, + FormMessage, +} from '@workspace/airo-ui/components/form'; +import { Input } from '@workspace/airo-ui/components/input'; +import { Icon } from '@workspace/airo-ui/custom-components/icon'; +import { useTranslations } from 'next-intl'; +import { Dispatch, SetStateAction, useRef } from 'react'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; +import SendCode from '../send-code'; +import CloudFlareTurnstile, { TurnstileRef } from '../turnstile'; + +export default function ResetForm({ + loading, + onSubmit, + initialValues, + setInitialValues, + onSwitchForm, +}: { + loading?: boolean; + onSubmit: (data: any) => void; + initialValues: any; + setInitialValues: Dispatch>; + onSwitchForm: Dispatch>; +}) { + const t = useTranslations('auth.reset'); + + const { common } = useGlobalStore(); + const { verify, auth } = common; + + const formSchema = z.object({ + email: z.string().email(t('email')), + password: z.string(), + code: auth?.email?.enable_verify ? z.string() : z.string().nullish(), + cf_token: + verify.enable_register_verify && verify.turnstile_site_key + ? z.string() + : z.string().nullish(), + }); + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: initialValues, + }); + + const turnstile = useRef(null); + const handleSubmit = form.handleSubmit((data) => { + try { + onSubmit(data); + } catch (error) { + turnstile.current?.reset(); + } + }); + + return ( + <> +
找回账户
+
+ +
+ ( + + + + + + + )} + /> + ( + + +
+ + +
+
+ +
+ )} + /> + ( + + + + + + + )} + /> + {verify.enable_reset_password_verify && ( + ( + + + + + + + )} + /> + )} +
+
+ {t('existingAccount')}  + +
+
+ + {loading && } + {t('title')} + +
+
+ + + ); +} diff --git a/apps/user/utils/common.ts b/apps/user/utils/common.ts index 4a7a362..d017e05 100644 --- a/apps/user/utils/common.ts +++ b/apps/user/utils/common.ts @@ -53,7 +53,7 @@ export function Logout() { Crisp.session.reset(); // 2. Unbind the current session const pathname = location.pathname; if ( - !['', '/', '/auth', '/tos', '/privacy-policy'].includes(pathname) && + !['', '/', '/auth', '/tos', '/privacy-policy', '/register'].includes(pathname) && !pathname.startsWith('/purchasing') && !pathname.startsWith('/oauth/') ) {