@lobehub/lobehub 2.0.0-next.124 → 2.0.0-next.126

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/.cursor/rules/db-migrations.mdc +16 -1
  2. package/.cursor/rules/project-introduce.mdc +1 -1
  3. package/.cursor/rules/project-structure.mdc +20 -2
  4. package/.env.example +148 -65
  5. package/.env.example.development +6 -8
  6. package/AGENTS.md +1 -3
  7. package/CHANGELOG.md +50 -0
  8. package/Dockerfile +7 -5
  9. package/GEMINI.md +63 -0
  10. package/changelog/v1.json +18 -0
  11. package/docs/development/database-schema.dbml +37 -0
  12. package/docs/self-hosting/advanced/auth.mdx +82 -2
  13. package/docs/self-hosting/advanced/auth.zh-CN.mdx +82 -2
  14. package/docs/self-hosting/environment-variables/auth.mdx +187 -1
  15. package/docs/self-hosting/environment-variables/auth.zh-CN.mdx +187 -1
  16. package/locales/en-US/auth.json +93 -0
  17. package/locales/zh-CN/auth.json +107 -1
  18. package/package.json +5 -2
  19. package/packages/const/src/auth.ts +2 -1
  20. package/packages/database/migrations/0049_better_auth.sql +49 -0
  21. package/packages/database/migrations/meta/0048_snapshot.json +312 -932
  22. package/packages/database/migrations/meta/0049_snapshot.json +8151 -0
  23. package/packages/database/migrations/meta/_journal.json +8 -1
  24. package/packages/database/src/core/migrations.json +13 -0
  25. package/packages/database/src/index.ts +1 -0
  26. package/packages/database/src/models/__tests__/session.test.ts +1 -2
  27. package/packages/database/src/models/user.ts +9 -8
  28. package/packages/database/src/repositories/tableViewer/index.test.ts +2 -2
  29. package/packages/database/src/schemas/betterAuth.ts +63 -0
  30. package/packages/database/src/schemas/index.ts +1 -0
  31. package/packages/database/src/schemas/ragEvals.ts +1 -2
  32. package/packages/database/src/schemas/user.ts +3 -2
  33. package/packages/database/src/server/models/__tests__/user.test.ts +1 -4
  34. package/packages/types/src/user/preference.ts +11 -0
  35. package/packages/utils/src/server/__tests__/auth.test.ts +52 -0
  36. package/packages/utils/src/server/auth.ts +18 -1
  37. package/src/app/(backend)/api/auth/[...all]/route.ts +19 -0
  38. package/src/app/(backend)/api/auth/check-user/route.ts +62 -0
  39. package/src/app/(backend)/middleware/auth/index.ts +14 -0
  40. package/src/app/(backend)/middleware/auth/utils.test.ts +16 -0
  41. package/src/app/(backend)/middleware/auth/utils.ts +13 -10
  42. package/src/app/(backend)/webapi/chat/[provider]/route.test.ts +1 -0
  43. package/src/app/[variants]/(auth)/reset-password/layout.tsx +12 -0
  44. package/src/app/[variants]/(auth)/reset-password/page.tsx +209 -0
  45. package/src/app/[variants]/(auth)/signin/layout.tsx +12 -0
  46. package/src/app/[variants]/(auth)/signin/page.tsx +448 -0
  47. package/src/app/[variants]/(auth)/signup/[[...signup]]/BetterAuthSignUpForm.tsx +192 -0
  48. package/src/app/[variants]/(auth)/signup/[[...signup]]/page.tsx +31 -6
  49. package/src/app/[variants]/(auth)/verify-email/layout.tsx +12 -0
  50. package/src/app/[variants]/(auth)/verify-email/page.tsx +164 -0
  51. package/src/app/[variants]/(main)/(mobile)/me/(home)/__tests__/UserBanner.test.tsx +12 -10
  52. package/src/app/[variants]/(main)/(mobile)/me/(home)/__tests__/useCategory.test.tsx +13 -11
  53. package/src/app/[variants]/(main)/profile/(home)/Client.tsx +306 -52
  54. package/src/app/[variants]/(main)/profile/(home)/features/SSOProvidersList/index.tsx +89 -47
  55. package/src/auth.ts +118 -0
  56. package/src/components/NextAuth/AuthIcons.tsx +3 -1
  57. package/src/envs/auth.ts +260 -13
  58. package/src/envs/email.ts +37 -0
  59. package/src/features/User/UserPanel/PanelContent.tsx +6 -5
  60. package/src/features/User/__tests__/PanelContent.test.tsx +15 -6
  61. package/src/features/User/__tests__/UserAvatar.test.tsx +17 -6
  62. package/src/features/User/__tests__/useMenu.test.tsx +14 -12
  63. package/src/layout/AuthProvider/BetterAuth/UserUpdater.tsx +51 -0
  64. package/src/layout/AuthProvider/BetterAuth/index.tsx +14 -0
  65. package/src/layout/AuthProvider/index.tsx +3 -0
  66. package/src/libs/better-auth/auth-client.ts +34 -0
  67. package/src/libs/better-auth/constants.ts +13 -0
  68. package/src/libs/better-auth/email-templates/index.ts +3 -0
  69. package/src/libs/better-auth/email-templates/magic-link.ts +98 -0
  70. package/src/libs/better-auth/email-templates/reset-password.ts +91 -0
  71. package/src/libs/better-auth/email-templates/verification.ts +108 -0
  72. package/src/libs/better-auth/sso/helpers.ts +61 -0
  73. package/src/libs/better-auth/sso/index.ts +113 -0
  74. package/src/libs/better-auth/sso/providers/auth0.ts +33 -0
  75. package/src/libs/better-auth/sso/providers/authelia.ts +35 -0
  76. package/src/libs/better-auth/sso/providers/authentik.ts +35 -0
  77. package/src/libs/better-auth/sso/providers/casdoor.ts +48 -0
  78. package/src/libs/better-auth/sso/providers/cloudflare-zero-trust.ts +41 -0
  79. package/src/libs/better-auth/sso/providers/cognito.ts +45 -0
  80. package/src/libs/better-auth/sso/providers/feishu.ts +181 -0
  81. package/src/libs/better-auth/sso/providers/generic-oidc.ts +44 -0
  82. package/src/libs/better-auth/sso/providers/github.ts +30 -0
  83. package/src/libs/better-auth/sso/providers/google.ts +30 -0
  84. package/src/libs/better-auth/sso/providers/keycloak.ts +35 -0
  85. package/src/libs/better-auth/sso/providers/logto.ts +38 -0
  86. package/src/libs/better-auth/sso/providers/microsoft.ts +65 -0
  87. package/src/libs/better-auth/sso/providers/okta.ts +37 -0
  88. package/src/libs/better-auth/sso/providers/wechat.ts +140 -0
  89. package/src/libs/better-auth/sso/providers/zitadel.ts +54 -0
  90. package/src/libs/better-auth/sso/types.ts +25 -0
  91. package/src/libs/better-auth/utils/client.ts +1 -0
  92. package/src/libs/better-auth/utils/common.ts +20 -0
  93. package/src/libs/better-auth/utils/server.test.ts +61 -0
  94. package/src/libs/better-auth/utils/server.ts +18 -0
  95. package/src/libs/trpc/lambda/context.test.ts +116 -0
  96. package/src/libs/trpc/lambda/context.ts +27 -0
  97. package/src/libs/trpc/middleware/userAuth.ts +4 -2
  98. package/src/locales/default/auth.ts +114 -1
  99. package/src/proxy.ts +71 -7
  100. package/src/server/globalConfig/index.ts +12 -1
  101. package/src/server/routers/lambda/user.ts +4 -0
  102. package/src/server/services/email/README.md +241 -0
  103. package/src/server/services/email/impls/index.test.ts +39 -0
  104. package/src/server/services/email/impls/index.ts +32 -0
  105. package/src/server/services/email/impls/nodemailer/index.ts +108 -0
  106. package/src/server/services/email/impls/nodemailer/type.ts +31 -0
  107. package/src/server/services/email/impls/type.ts +61 -0
  108. package/src/server/services/email/index.test.ts +144 -0
  109. package/src/server/services/email/index.ts +40 -0
  110. package/src/services/user/index.test.ts +162 -2
  111. package/src/services/user/index.ts +6 -3
  112. package/src/store/user/slices/auth/action.test.ts +213 -16
  113. package/src/store/user/slices/auth/action.ts +86 -1
  114. package/src/store/user/slices/auth/initialState.ts +13 -2
  115. package/src/store/user/slices/auth/selectors.ts +6 -2
  116. package/src/store/user/slices/common/action.ts +5 -1
  117. package/src/app/(backend)/api/auth/[...nextauth]/route.ts +0 -3
@@ -0,0 +1,192 @@
1
+ 'use client';
2
+
3
+ import { Button } from '@lobehub/ui';
4
+ import { LobeHub } from '@lobehub/ui/brand';
5
+ import { Form, Input } from 'antd';
6
+ import { createStyles } from 'antd-style';
7
+ import { ChevronRight, Lock, Mail } from 'lucide-react';
8
+ import Link from 'next/link';
9
+ import { useRouter, useSearchParams } from 'next/navigation';
10
+ import { useEffect, useState } from 'react';
11
+ import { useTranslation } from 'react-i18next';
12
+ import { Flexbox } from 'react-layout-kit';
13
+
14
+ import { message } from '@/components/AntdStaticMethods';
15
+ import { authEnv } from '@/envs/auth';
16
+ import { signUp } from '@/libs/better-auth/auth-client';
17
+
18
+ const useStyles = createStyles(({ css, token }) => ({
19
+ card: css`
20
+ padding-block: 2.5rem;
21
+ padding-inline: 2rem;
22
+ `,
23
+ container: css`
24
+ width: 360px;
25
+ border: 1px solid ${token.colorBorder};
26
+ border-radius: ${token.borderRadiusLG}px;
27
+ `,
28
+ footer: css`
29
+ padding: 1rem;
30
+ border-block-start: 1px solid ${token.colorBorder};
31
+
32
+ font-size: 14px;
33
+ color: ${token.colorTextDescription};
34
+ text-align: center;
35
+
36
+ background: ${token.colorBgElevated};
37
+ `,
38
+ subtitle: css`
39
+ margin-block-start: 0.5rem;
40
+ font-size: 14px;
41
+ color: ${token.colorTextSecondary};
42
+ text-align: center;
43
+ `,
44
+ title: css`
45
+ margin-block-start: 1rem;
46
+
47
+ font-size: 24px;
48
+ font-weight: 600;
49
+ color: ${token.colorTextHeading};
50
+ text-align: center;
51
+ `,
52
+ }));
53
+
54
+ interface SignUpFormValues {
55
+ email: string;
56
+ password: string;
57
+ }
58
+
59
+ export default function BetterAuthSignUpForm() {
60
+ const { styles } = useStyles();
61
+ const { t } = useTranslation('auth');
62
+ const router = useRouter();
63
+ const searchParams = useSearchParams();
64
+ const [form] = Form.useForm();
65
+ const [loading, setLoading] = useState(false);
66
+
67
+ // Pre-fill email from query params (from signin page redirect)
68
+ useEffect(() => {
69
+ const email = searchParams.get('email');
70
+ if (email) {
71
+ form.setFieldsValue({ email });
72
+ }
73
+ }, [searchParams, form]);
74
+
75
+ const handleSignUp = async (values: SignUpFormValues) => {
76
+ setLoading(true);
77
+ try {
78
+ const callbackUrl = searchParams.get('callbackUrl') || '/';
79
+
80
+ // Generate username from email (use the part before @)
81
+ const username = values.email.split('@')[0];
82
+
83
+ const { error } = await signUp.email({
84
+ callbackURL: callbackUrl,
85
+ email: values.email,
86
+ name: username,
87
+ password: values.password,
88
+ });
89
+
90
+ if (error) {
91
+ message.error(error.message || t('betterAuth.signup.error'));
92
+ return;
93
+ }
94
+
95
+ // Redirect based on email verification requirement
96
+ if (authEnv.NEXT_PUBLIC_AUTH_EMAIL_VERIFICATION) {
97
+ // Email verification required, redirect to verification notice page
98
+ // callbackURL is already passed to signUp.email for verification link
99
+ router.push(
100
+ `/verify-email?email=${encodeURIComponent(values.email)}&callbackUrl=${encodeURIComponent(callbackUrl)}`,
101
+ );
102
+ } else {
103
+ // Email verification not required, user is already logged in (autoSignIn: true)
104
+ // Redirect to callback URL or home
105
+ router.push(callbackUrl);
106
+ }
107
+ } catch {
108
+ message.error(t('betterAuth.signup.error'));
109
+ } finally {
110
+ setLoading(false);
111
+ }
112
+ };
113
+
114
+ return (
115
+ <Flexbox align="center" justify="center" style={{ minHeight: '100vh' }}>
116
+ <div className={styles.container}>
117
+ <div className={styles.card}>
118
+ <Flexbox align="center" gap={8} justify="center">
119
+ <LobeHub size={48} />
120
+ </Flexbox>
121
+
122
+ <h1 className={styles.title}>{t('betterAuth.signup.title')}</h1>
123
+ <p className={styles.subtitle}>{t('betterAuth.signup.subtitle')}</p>
124
+
125
+ <Form form={form} layout="vertical" onFinish={handleSignUp} style={{ marginTop: '2rem' }}>
126
+ <Form.Item
127
+ name="email"
128
+ rules={[
129
+ { message: t('betterAuth.errors.emailRequired'), required: true },
130
+ { message: t('betterAuth.errors.emailInvalid'), type: 'email' },
131
+ ]}
132
+ >
133
+ <Input
134
+ placeholder={t('betterAuth.signup.emailPlaceholder')}
135
+ prefix={<Mail size={16} />}
136
+ size="large"
137
+ />
138
+ </Form.Item>
139
+
140
+ <Form.Item
141
+ name="password"
142
+ rules={[
143
+ { message: t('betterAuth.errors.passwordRequired'), required: true },
144
+ { message: t('betterAuth.errors.passwordMinLength'), min: 8 },
145
+ { max: 64, message: t('betterAuth.errors.passwordMaxLength') },
146
+ {
147
+ message: t('betterAuth.errors.passwordFormat'),
148
+ validator: (_, value) => {
149
+ if (!value) return Promise.resolve();
150
+ const hasLetter = /[A-Za-z]/.test(value);
151
+ const hasNumber = /\d/.test(value);
152
+ if (hasLetter && hasNumber) {
153
+ return Promise.resolve();
154
+ }
155
+ return Promise.reject();
156
+ },
157
+ },
158
+ ]}
159
+ >
160
+ <Input.Password
161
+ placeholder={t('betterAuth.signup.passwordPlaceholder')}
162
+ prefix={<Lock size={16} />}
163
+ size="large"
164
+ />
165
+ </Form.Item>
166
+
167
+ <Form.Item>
168
+ <Button
169
+ block
170
+ htmlType="submit"
171
+ icon={<ChevronRight size={16} />}
172
+ iconPosition="end"
173
+ loading={loading}
174
+ size="large"
175
+ type="primary"
176
+ >
177
+ {t('betterAuth.signup.submit')}
178
+ </Button>
179
+ </Form.Item>
180
+ </Form>
181
+ </div>
182
+
183
+ <div className={styles.footer}>
184
+ {t('betterAuth.signup.hasAccount')}{' '}
185
+ <Link href={`/signin?${searchParams.toString()}`}>
186
+ {t('betterAuth.signup.signinLink')}
187
+ </Link>
188
+ </div>
189
+ </div>
190
+ </Flexbox>
191
+ );
192
+ }
@@ -1,26 +1,51 @@
1
1
  import { SignUp } from '@clerk/nextjs';
2
2
  import { notFound } from 'next/navigation';
3
3
 
4
- import { enableClerk } from '@/const/auth';
4
+ import { enableBetterAuth, enableClerk } from '@/const/auth';
5
5
  import { metadataModule } from '@/server/metadata';
6
6
  import { translation } from '@/server/translation';
7
7
  import { DynamicLayoutProps } from '@/types/next';
8
8
  import { RouteVariants } from '@/utils/server/routeVariants';
9
9
 
10
+ import BetterAuthSignUpForm from './BetterAuthSignUpForm';
11
+
10
12
  export const generateMetadata = async (props: DynamicLayoutProps) => {
11
13
  const locale = await RouteVariants.getLocale(props);
12
- const { t } = await translation('clerk', locale);
14
+
15
+ if (enableClerk) {
16
+ const { t } = await translation('clerk', locale);
17
+ return metadataModule.generate({
18
+ description: t('signUp.start.subtitle'),
19
+ title: t('signUp.start.title'),
20
+ url: '/signup',
21
+ });
22
+ }
23
+
24
+ if (enableBetterAuth) {
25
+ const { t } = await translation('auth', locale);
26
+ return metadataModule.generate({
27
+ description: t('betterAuth.signup.subtitle'),
28
+ title: t('betterAuth.signup.title'),
29
+ url: '/signup',
30
+ });
31
+ }
32
+
13
33
  return metadataModule.generate({
14
- description: t('signUp.start.subtitle'),
15
- title: t('signUp.start.title'),
34
+ title: 'Sign Up',
16
35
  url: '/signup',
17
36
  });
18
37
  };
19
38
 
20
39
  const Page = () => {
21
- if (!enableClerk) return notFound();
40
+ if (enableClerk) {
41
+ return <SignUp path="/signup" />;
42
+ }
43
+
44
+ if (enableBetterAuth) {
45
+ return <BetterAuthSignUpForm />;
46
+ }
22
47
 
23
- return <SignUp path="/signup" />;
48
+ return notFound();
24
49
  };
25
50
 
26
51
  Page.displayName = 'SignUp';
@@ -0,0 +1,12 @@
1
+ import { notFound } from 'next/navigation';
2
+ import { PropsWithChildren } from 'react';
3
+
4
+ import { enableBetterAuth } from '@/const/auth';
5
+
6
+ const Layout = ({ children }: PropsWithChildren) => {
7
+ if (!enableBetterAuth) return notFound();
8
+
9
+ return children;
10
+ };
11
+
12
+ export default Layout;
@@ -0,0 +1,164 @@
1
+ 'use client';
2
+
3
+ import { Button } from '@lobehub/ui';
4
+ import { LobeHub } from '@lobehub/ui/brand';
5
+ import { createStyles, useTheme } from 'antd-style';
6
+ import { ArrowLeft, Mail, RefreshCw } from 'lucide-react';
7
+ import Link from 'next/link';
8
+ import { useSearchParams } from 'next/navigation';
9
+ import { useState } from 'react';
10
+ import { useTranslation } from 'react-i18next';
11
+ import { Center, Flexbox } from 'react-layout-kit';
12
+
13
+ import { message } from '@/components/AntdStaticMethods';
14
+ import { sendVerificationEmail } from '@/libs/better-auth/auth-client';
15
+
16
+ const useStyles = createStyles(({ css, token }) => ({
17
+ backLink: css`
18
+ display: inline-flex;
19
+ gap: 6px;
20
+ align-items: center;
21
+
22
+ font-size: 14px;
23
+ color: ${token.colorTextSecondary};
24
+ text-decoration: none;
25
+
26
+ transition: color 0.2s ease;
27
+
28
+ &:hover {
29
+ color: ${token.colorText};
30
+ }
31
+ `,
32
+ container: css`
33
+ max-width: 480px;
34
+ padding: 2rem;
35
+ text-align: center;
36
+ `,
37
+ description: css`
38
+ font-size: 16px;
39
+ line-height: 1.6;
40
+ color: ${token.colorText};
41
+ `,
42
+ hint: css`
43
+ margin-block-start: 0.5rem;
44
+ font-size: 14px;
45
+ color: ${token.colorTextTertiary};
46
+ `,
47
+ iconWrapper: css`
48
+ display: inline-flex;
49
+ align-items: center;
50
+ justify-content: center;
51
+
52
+ width: 96px;
53
+ height: 96px;
54
+ border-radius: 50%;
55
+
56
+ background: linear-gradient(
57
+ 135deg,
58
+ ${token.colorPrimaryBg} 0%,
59
+ ${token.colorPrimaryBgHover} 100%
60
+ );
61
+ `,
62
+ mailLink: css`
63
+ font-weight: 500;
64
+ color: ${token.colorPrimary};
65
+ text-decoration: none;
66
+
67
+ &:hover {
68
+ text-decoration: underline;
69
+ }
70
+ `,
71
+ resendButton: css`
72
+ margin-block-start: 0.5rem;
73
+ `,
74
+ textGroup: css`
75
+ display: flex;
76
+ flex-direction: column;
77
+ gap: 0.5rem;
78
+ `,
79
+ title: css`
80
+ margin-block: 0;
81
+ font-size: 28px;
82
+ font-weight: 600;
83
+ color: ${token.colorTextHeading};
84
+ `,
85
+ }));
86
+
87
+ export default function VerifyEmailPage() {
88
+ const { styles } = useStyles();
89
+ const theme = useTheme();
90
+ const { t } = useTranslation('auth');
91
+ const searchParams = useSearchParams();
92
+ const email = searchParams.get('email');
93
+ const [resending, setResending] = useState(false);
94
+
95
+ const handleResendEmail = async () => {
96
+ if (!email) {
97
+ message.error(t('betterAuth.verifyEmail.resend.noEmail'));
98
+ return;
99
+ }
100
+
101
+ setResending(true);
102
+ try {
103
+ const callbackUrl = searchParams.get('callbackUrl') || '/';
104
+
105
+ const result = await sendVerificationEmail({
106
+ callbackURL: callbackUrl,
107
+ email,
108
+ });
109
+
110
+ if (result.error) {
111
+ message.error(result.error.message || t('betterAuth.verifyEmail.resend.error'));
112
+ return;
113
+ }
114
+
115
+ message.success(t('betterAuth.verifyEmail.resend.success'));
116
+ } catch (error) {
117
+ console.error('Error resending verification email:', error);
118
+ message.error(t('betterAuth.verifyEmail.resend.error'));
119
+ } finally {
120
+ setResending(false);
121
+ }
122
+ };
123
+
124
+ return (
125
+ <Center style={{ minHeight: '100vh' }}>
126
+ <Flexbox align="center" className={styles.container} gap={24}>
127
+ <LobeHub size={56} />
128
+
129
+ <h1 className={styles.title}>{t('betterAuth.verifyEmail.title')}</h1>
130
+
131
+ <div className={styles.iconWrapper}>
132
+ <Mail color={theme.colorPrimary} size={40} strokeWidth={1.5} />
133
+ </div>
134
+
135
+ <div className={styles.textGroup}>
136
+ <p className={styles.description}>
137
+ {t('betterAuth.verifyEmail.descriptionPrefix')}{' '}
138
+ <a className={styles.mailLink} href={`mailto:${email}`}>
139
+ {email}
140
+ </a>{' '}
141
+ {t('betterAuth.verifyEmail.descriptionSuffix')}
142
+ </p>
143
+ <p className={styles.hint}>{t('betterAuth.verifyEmail.checkSpam')}</p>
144
+ </div>
145
+
146
+ <Button
147
+ className={styles.resendButton}
148
+ icon={<RefreshCw size={16} />}
149
+ loading={resending}
150
+ onClick={handleResendEmail}
151
+ size="middle"
152
+ type="default"
153
+ >
154
+ {t('betterAuth.verifyEmail.resend.button')}
155
+ </Button>
156
+
157
+ <Link className={styles.backLink} href="/signin">
158
+ <ArrowLeft size={16} />
159
+ {t('betterAuth.verifyEmail.backToSignIn')}
160
+ </Link>
161
+ </Flexbox>
162
+ </Center>
163
+ );
164
+ }
@@ -31,22 +31,24 @@ vi.mock('@/const/version', () => ({
31
31
  isDesktop: false,
32
32
  }));
33
33
 
34
- // 定义一个变量来存储 enableAuth 的值
35
- let enableAuth = true;
36
- let enableClerk = false;
34
+ // Use vi.hoisted to ensure variables exist before vi.mock factory executes
35
+ const { enableAuth, enableClerk } = vi.hoisted(() => ({
36
+ enableAuth: { value: true },
37
+ enableClerk: { value: false },
38
+ }));
37
39
 
38
- // 模拟 @/const/auth 模块
39
40
  vi.mock('@/const/auth', () => ({
40
41
  get enableAuth() {
41
- return enableAuth;
42
+ return enableAuth.value;
42
43
  },
43
44
  get enableClerk() {
44
- return enableClerk;
45
+ return enableClerk.value;
45
46
  },
46
47
  }));
47
48
 
48
49
  afterEach(() => {
49
- enableAuth = true;
50
+ enableAuth.value = true;
51
+ enableClerk.value = false;
50
52
  mockNavigate.mockReset();
51
53
  });
52
54
 
@@ -55,7 +57,7 @@ describe('UserBanner', () => {
55
57
  act(() => {
56
58
  useUserStore.setState({ isSignedIn: false });
57
59
  });
58
- enableAuth = false;
60
+ enableAuth.value = false;
59
61
 
60
62
  render(<UserBanner />);
61
63
 
@@ -69,7 +71,7 @@ describe('UserBanner', () => {
69
71
  useUserStore.setState({ isSignedIn: true });
70
72
  });
71
73
 
72
- enableClerk = true;
74
+ enableClerk.value = true;
73
75
 
74
76
  render(<UserBanner />);
75
77
 
@@ -82,7 +84,7 @@ describe('UserBanner', () => {
82
84
  act(() => {
83
85
  useUserStore.setState({ isSignedIn: false });
84
86
  });
85
- enableClerk = true;
87
+ enableClerk.value = true;
86
88
 
87
89
  render(<UserBanner />);
88
90
 
@@ -22,16 +22,18 @@ vi.mock('react-i18next', () => ({
22
22
  })),
23
23
  }));
24
24
 
25
- // 定义一个变量来存储 enableAuth 的值
26
- let enableAuth = true;
27
- let enableClerk = true;
28
- // 模拟 @/const/auth 模块
25
+ // Use vi.hoisted to ensure variables exist before vi.mock factory executes
26
+ const { enableAuth, enableClerk } = vi.hoisted(() => ({
27
+ enableAuth: { value: true },
28
+ enableClerk: { value: true },
29
+ }));
30
+
29
31
  vi.mock('@/const/auth', () => ({
30
32
  get enableAuth() {
31
- return enableAuth;
33
+ return enableAuth.value;
32
34
  },
33
35
  get enableClerk() {
34
- return enableClerk;
36
+ return enableClerk.value;
35
37
  },
36
38
  }));
37
39
 
@@ -45,8 +47,8 @@ vi.mock('@/const/version', async (importOriginal) => {
45
47
  });
46
48
 
47
49
  afterEach(() => {
48
- enableAuth = true;
49
- enableClerk = true;
50
+ enableAuth.value = true;
51
+ enableClerk.value = true;
50
52
  mockNavigate.mockReset();
51
53
  });
52
54
 
@@ -55,8 +57,8 @@ describe('useCategory', () => {
55
57
  act(() => {
56
58
  useUserStore.setState({ isSignedIn: true });
57
59
  });
58
- enableAuth = true;
59
- enableClerk = false;
60
+ enableAuth.value = true;
61
+ enableClerk.value = false;
60
62
 
61
63
  const { result } = renderHook(() => useCategory(), { wrapper });
62
64
 
@@ -74,7 +76,7 @@ describe('useCategory', () => {
74
76
  act(() => {
75
77
  useUserStore.setState({ isSignedIn: false });
76
78
  });
77
- enableAuth = true;
79
+ enableAuth.value = true;
78
80
 
79
81
  const { result } = renderHook(() => useCategory(), { wrapper });
80
82