@lobehub/lobehub 2.0.0-next.355 → 2.0.0-next.357

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 (156) hide show
  1. package/.env.desktop +0 -1
  2. package/.env.example +16 -20
  3. package/.env.example.development +1 -4
  4. package/.github/workflows/e2e.yml +10 -11
  5. package/CHANGELOG.md +60 -0
  6. package/Dockerfile +28 -4
  7. package/changelog/v1.json +18 -0
  8. package/docker-compose/local/docker-compose.yml +2 -2
  9. package/docker-compose/local/grafana/docker-compose.yml +2 -2
  10. package/docker-compose/local/logto/docker-compose.yml +2 -2
  11. package/docker-compose/local/zitadel/.env.example +2 -2
  12. package/docker-compose/local/zitadel/.env.zh-CN.example +2 -2
  13. package/docker-compose/production/grafana/docker-compose.yml +2 -2
  14. package/docker-compose/production/logto/.env.example +2 -2
  15. package/docker-compose/production/logto/.env.zh-CN.example +2 -2
  16. package/docker-compose/production/zitadel/.env.example +2 -2
  17. package/docker-compose/production/zitadel/.env.zh-CN.example +2 -2
  18. package/docs/development/basic/add-new-authentication-providers.mdx +144 -136
  19. package/docs/development/basic/add-new-authentication-providers.zh-CN.mdx +146 -136
  20. package/docs/self-hosting/advanced/auth/legacy.mdx +4 -0
  21. package/docs/self-hosting/advanced/auth/legacy.zh-CN.mdx +4 -0
  22. package/docs/self-hosting/advanced/auth/nextauth-to-betterauth.mdx +326 -0
  23. package/docs/self-hosting/advanced/auth/nextauth-to-betterauth.zh-CN.mdx +323 -0
  24. package/docs/self-hosting/advanced/auth.mdx +43 -16
  25. package/docs/self-hosting/advanced/auth.zh-CN.mdx +44 -16
  26. package/docs/self-hosting/advanced/redis/upstash.mdx +69 -0
  27. package/docs/self-hosting/advanced/redis/upstash.zh-CN.mdx +69 -0
  28. package/docs/self-hosting/advanced/redis.mdx +128 -0
  29. package/docs/self-hosting/advanced/redis.zh-CN.mdx +126 -0
  30. package/docs/self-hosting/environment-variables/auth.mdx +15 -1
  31. package/docs/self-hosting/environment-variables/auth.zh-CN.mdx +15 -1
  32. package/docs/self-hosting/environment-variables/basic.mdx +13 -0
  33. package/docs/self-hosting/environment-variables/basic.zh-CN.mdx +13 -0
  34. package/docs/self-hosting/environment-variables/redis.mdx +68 -0
  35. package/docs/self-hosting/environment-variables/redis.zh-CN.mdx +67 -0
  36. package/docs/self-hosting/migration/v2/breaking-changes.mdx +23 -23
  37. package/docs/self-hosting/migration/v2/breaking-changes.zh-CN.mdx +23 -23
  38. package/docs/self-hosting/server-database/docker-compose.mdx +4 -4
  39. package/docs/self-hosting/server-database/docker-compose.zh-CN.mdx +4 -4
  40. package/e2e/CLAUDE.md +5 -6
  41. package/e2e/docs/local-setup.md +9 -12
  42. package/e2e/scripts/setup.ts +9 -15
  43. package/e2e/src/support/webServer.ts +6 -5
  44. package/package.json +4 -6
  45. package/packages/database/src/schemas/nextauth.ts +7 -2
  46. package/packages/model-runtime/src/core/contextBuilders/anthropic.test.ts +370 -0
  47. package/packages/model-runtime/src/core/contextBuilders/anthropic.ts +18 -5
  48. package/packages/utils/src/server/__tests__/auth.test.ts +1 -63
  49. package/packages/utils/src/server/auth.ts +8 -24
  50. package/scripts/_shared/checkDeprecatedAuth.js +99 -0
  51. package/scripts/clerk-to-betterauth/index.ts +8 -3
  52. package/scripts/nextauth-to-betterauth/_internal/config.ts +41 -0
  53. package/scripts/nextauth-to-betterauth/_internal/db.ts +32 -0
  54. package/scripts/nextauth-to-betterauth/_internal/env.ts +6 -0
  55. package/scripts/nextauth-to-betterauth/index.ts +226 -0
  56. package/scripts/nextauth-to-betterauth/verify.ts +188 -0
  57. package/scripts/prebuild.mts +66 -13
  58. package/scripts/serverLauncher/startServer.js +5 -5
  59. package/src/app/(backend)/api/auth/[...all]/route.ts +5 -23
  60. package/src/app/(backend)/api/webhooks/casdoor/route.ts +5 -5
  61. package/src/app/(backend)/api/webhooks/logto/route.ts +8 -8
  62. package/src/app/(backend)/middleware/auth/index.test.ts +8 -1
  63. package/src/app/(backend)/middleware/auth/index.ts +6 -15
  64. package/src/app/(backend)/middleware/auth/utils.test.ts +0 -32
  65. package/src/app/(backend)/middleware/auth/utils.ts +3 -8
  66. package/src/app/(backend)/webapi/chat/[provider]/route.test.ts +8 -1
  67. package/src/app/(backend)/webapi/create-image/comfyui/route.ts +0 -1
  68. package/src/app/(backend)/webapi/models/[provider]/route.test.ts +8 -1
  69. package/src/app/[variants]/(auth)/signin/SignInEmailStep.tsx +1 -1
  70. package/src/app/[variants]/(auth)/signup/[[...signup]]/page.tsx +4 -17
  71. package/src/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobContentEditor.tsx +34 -21
  72. package/src/app/[variants]/(main)/settings/profile/features/SSOProvidersList/index.tsx +12 -19
  73. package/src/app/[variants]/(main)/settings/profile/index.tsx +8 -14
  74. package/src/components/{NextAuth/AuthIcons.tsx → AuthIcons.tsx} +8 -10
  75. package/src/envs/auth.ts +12 -51
  76. package/src/envs/email.ts +3 -0
  77. package/src/envs/redis.ts +12 -54
  78. package/src/features/ChatInput/ChatInputProvider.tsx +22 -2
  79. package/src/features/ChatInput/InputEditor/index.tsx +14 -3
  80. package/src/features/ChatInput/store/initialState.ts +2 -0
  81. package/src/features/EditorCanvas/DiffAllToolbar.tsx +4 -5
  82. package/src/features/EditorCanvas/DocumentIdMode.tsx +21 -1
  83. package/src/features/User/__tests__/PanelContent.test.tsx +0 -11
  84. package/src/features/User/__tests__/UserAvatar.test.tsx +1 -16
  85. package/src/layout/AuthProvider/index.tsx +1 -6
  86. package/src/layout/GlobalProvider/StoreInitialization.tsx +2 -4
  87. package/src/libs/better-auth/define-config.ts +2 -0
  88. package/src/libs/better-auth/plugins/email-whitelist.test.ts +120 -0
  89. package/src/libs/better-auth/plugins/email-whitelist.ts +62 -0
  90. package/src/libs/next/config/define-config.ts +13 -1
  91. package/src/libs/next/proxy/define-config.ts +2 -75
  92. package/src/libs/oidc-provider/provider.test.ts +0 -4
  93. package/src/libs/redis/index.ts +0 -1
  94. package/src/libs/redis/manager.test.ts +9 -45
  95. package/src/libs/redis/manager.ts +2 -16
  96. package/src/libs/redis/redis.test.ts +2 -4
  97. package/src/libs/redis/redis.ts +2 -4
  98. package/src/libs/redis/types.ts +2 -24
  99. package/src/libs/redis/utils.test.ts +0 -10
  100. package/src/libs/redis/utils.ts +0 -19
  101. package/src/libs/trpc/lambda/context.test.ts +0 -13
  102. package/src/libs/trpc/lambda/context.ts +21 -59
  103. package/src/libs/trpc/middleware/userAuth.ts +1 -7
  104. package/src/libs/trusted-client/getSessionUser.ts +15 -35
  105. package/src/server/globalConfig/index.ts +1 -3
  106. package/src/server/routers/lambda/__tests__/user.test.ts +0 -48
  107. package/src/server/routers/lambda/user.ts +1 -12
  108. package/src/server/services/email/impls/nodemailer/index.ts +2 -2
  109. package/src/server/services/webhookUser/index.ts +88 -0
  110. package/src/services/user/index.test.ts +0 -14
  111. package/src/services/user/index.ts +0 -4
  112. package/src/store/document/slices/document/action.ts +1 -0
  113. package/src/store/user/slices/auth/action.test.ts +22 -126
  114. package/src/store/user/slices/auth/action.ts +32 -65
  115. package/src/store/user/slices/auth/initialState.ts +0 -3
  116. package/src/store/user/slices/auth/selectors.ts +0 -3
  117. package/tests/setup.ts +10 -0
  118. package/scripts/_shared/checkDeprecatedClerkEnv.js +0 -42
  119. package/src/app/(backend)/api/auth/adapter/route.ts +0 -137
  120. package/src/app/[variants]/(auth)/next-auth/error/AuthErrorPage.tsx +0 -40
  121. package/src/app/[variants]/(auth)/next-auth/error/page.tsx +0 -11
  122. package/src/app/[variants]/(auth)/next-auth/signin/AuthSignInBox.tsx +0 -167
  123. package/src/app/[variants]/(auth)/next-auth/signin/page.tsx +0 -11
  124. package/src/app/[variants]/(auth)/reset-password/layout.tsx +0 -12
  125. package/src/app/[variants]/(auth)/signin/layout.tsx +0 -12
  126. package/src/app/[variants]/(auth)/verify-email/layout.tsx +0 -12
  127. package/src/envs/auth.test.ts +0 -47
  128. package/src/layout/AuthProvider/NextAuth/UserUpdater.tsx +0 -44
  129. package/src/layout/AuthProvider/NextAuth/index.tsx +0 -17
  130. package/src/libs/next-auth/adapter/index.ts +0 -177
  131. package/src/libs/next-auth/auth.config.ts +0 -64
  132. package/src/libs/next-auth/index.ts +0 -20
  133. package/src/libs/next-auth/sso-providers/auth0.ts +0 -24
  134. package/src/libs/next-auth/sso-providers/authelia.ts +0 -39
  135. package/src/libs/next-auth/sso-providers/authentik.ts +0 -25
  136. package/src/libs/next-auth/sso-providers/casdoor.ts +0 -50
  137. package/src/libs/next-auth/sso-providers/cloudflare-zero-trust.ts +0 -34
  138. package/src/libs/next-auth/sso-providers/cognito.ts +0 -8
  139. package/src/libs/next-auth/sso-providers/feishu.ts +0 -83
  140. package/src/libs/next-auth/sso-providers/generic-oidc.ts +0 -38
  141. package/src/libs/next-auth/sso-providers/github.ts +0 -23
  142. package/src/libs/next-auth/sso-providers/google.ts +0 -18
  143. package/src/libs/next-auth/sso-providers/index.ts +0 -35
  144. package/src/libs/next-auth/sso-providers/keycloak.ts +0 -22
  145. package/src/libs/next-auth/sso-providers/logto.ts +0 -48
  146. package/src/libs/next-auth/sso-providers/microsoft-entra-id-helper.ts +0 -29
  147. package/src/libs/next-auth/sso-providers/microsoft-entra-id.ts +0 -19
  148. package/src/libs/next-auth/sso-providers/okta.ts +0 -22
  149. package/src/libs/next-auth/sso-providers/sso.config.ts +0 -8
  150. package/src/libs/next-auth/sso-providers/wechat.ts +0 -36
  151. package/src/libs/next-auth/sso-providers/zitadel.ts +0 -21
  152. package/src/libs/redis/upstash.test.ts +0 -158
  153. package/src/libs/redis/upstash.ts +0 -136
  154. package/src/server/services/nextAuthUser/index.ts +0 -318
  155. package/src/server/services/nextAuthUser/utils.ts +0 -62
  156. package/src/types/next-auth.d.ts +0 -26
@@ -1,7 +1,5 @@
1
1
  import { TRPCError } from '@trpc/server';
2
2
 
3
- import { enableBetterAuth, enableNextAuth } from '@/envs/auth';
4
-
5
3
  import { trpc } from '../lambda/init';
6
4
 
7
5
  export const userAuth = trpc.middleware(async (opts) => {
@@ -9,11 +7,7 @@ export const userAuth = trpc.middleware(async (opts) => {
9
7
 
10
8
  // `ctx.user` is nullable
11
9
  if (!ctx.userId) {
12
- if (enableBetterAuth) {
13
- console.log('better auth: no session found in context');
14
- } else if (enableNextAuth) {
15
- console.log('next auth:', ctx.nextAuth);
16
- }
10
+ console.log('better auth: no session found in context');
17
11
  throw new TRPCError({ code: 'UNAUTHORIZED' });
18
12
  }
19
13
 
@@ -1,50 +1,30 @@
1
- import { enableBetterAuth, enableNextAuth } from '@/envs/auth';
1
+ import { headers } from 'next/headers';
2
2
 
3
3
  import type { TrustedClientUserInfo } from './index';
4
4
 
5
5
  /**
6
6
  * Get user info from the current session for trusted client authentication
7
- * This works with different authentication providers (BetterAuth, NextAuth)
8
7
  *
9
8
  * @returns User info or undefined if not authenticated
10
9
  */
11
10
  export const getSessionUser = async (): Promise<TrustedClientUserInfo | undefined> => {
12
11
  try {
13
- if (enableBetterAuth) {
14
- const { headers } = await import('next/headers');
15
- const { auth } = await import('@/auth');
16
- const headersList = await headers();
17
- const session = await auth.api.getSession({
18
- headers: headersList,
19
- });
20
-
21
- if (!session?.user?.id || !session?.user?.email) {
22
- return undefined;
23
- }
24
-
25
- return {
26
- email: session.user.email,
27
- name: session.user.name || undefined,
28
- userId: session.user.id,
29
- };
12
+ // Dynamic import to avoid validator ESM/CJS issue during sitemap generation
13
+ const { auth } = await import('@/auth');
14
+ const headersList = await headers();
15
+ const session = await auth.api.getSession({
16
+ headers: headersList,
17
+ });
18
+
19
+ if (!session?.user?.id || !session?.user?.email) {
20
+ return undefined;
30
21
  }
31
22
 
32
- if (enableNextAuth) {
33
- const { default: NextAuth } = await import('@/libs/next-auth');
34
- const session = await NextAuth.auth();
35
-
36
- if (!session?.user?.id || !session?.user?.email) {
37
- return undefined;
38
- }
39
-
40
- return {
41
- email: session.user.email,
42
- name: session.user.name || undefined,
43
- userId: session.user.id,
44
- };
45
- }
46
-
47
- return undefined;
23
+ return {
24
+ email: session.user.email,
25
+ name: session.user.name || undefined,
26
+ userId: session.user.id,
27
+ };
48
28
  } catch {
49
29
  return undefined;
50
30
  }
@@ -90,9 +90,7 @@ export const getServerGlobalConfig = async () => {
90
90
  memory: {
91
91
  userMemory: cleanObject(getPublicMemoryExtractionConfig()),
92
92
  },
93
- oAuthSSOProviders: authEnv.NEXT_PUBLIC_ENABLE_BETTER_AUTH
94
- ? getBetterAuthSSOProviders()
95
- : authEnv.NEXT_AUTH_SSO_PROVIDERS.trim().split(/[,,]/),
93
+ oAuthSSOProviders: getBetterAuthSSOProviders(),
96
94
  systemAgent: parseSystemAgent(appEnv.SYSTEM_AGENT),
97
95
  telemetry: {
98
96
  langfuse: langfuseEnv.ENABLE_LANGFUSE,
@@ -6,7 +6,6 @@ import { SessionModel } from '@/database/models/session';
6
6
  import { UserModel, UserNotFoundError } from '@/database/models/user';
7
7
  import { serverDB } from '@/database/server';
8
8
  import { KeyVaultsGateKeeper } from '@/server/modules/KeyVaultsEncrypt';
9
- import { NextAuthUserService } from '@/server/services/nextAuthUser';
10
9
  import { UserService } from '@/server/services/user';
11
10
 
12
11
  import { userRouter } from '../user';
@@ -22,11 +21,6 @@ vi.mock('@/database/models/user');
22
21
  vi.mock('@/server/modules/KeyVaultsEncrypt');
23
22
  vi.mock('@/server/modules/S3');
24
23
  vi.mock('@/server/services/user');
25
- vi.mock('@/server/services/nextAuthUser');
26
- vi.mock('@/envs/auth', () => ({
27
- enableBetterAuth: false,
28
- enableNextAuth: false,
29
- }));
30
24
 
31
25
  describe('userRouter', () => {
32
26
  const mockUserId = 'test-user-id';
@@ -122,7 +116,6 @@ describe('userRouter', () => {
122
116
  userId: mockUserId,
123
117
  });
124
118
  });
125
-
126
119
  });
127
120
 
128
121
  describe('makeUserOnboarded', () => {
@@ -140,47 +133,6 @@ describe('userRouter', () => {
140
133
  });
141
134
  });
142
135
 
143
- describe('unlinkSSOProvider', () => {
144
- it('should unlink SSO provider successfully', async () => {
145
- const mockInput = {
146
- provider: 'google',
147
- providerAccountId: '123',
148
- };
149
-
150
- const mockAccount = {
151
- userId: mockUserId,
152
- provider: 'google',
153
- providerAccountId: '123',
154
- type: 'oauth',
155
- };
156
-
157
- vi.mocked(NextAuthUserService).mockReturnValue({
158
- getAccount: vi.fn().mockResolvedValue(mockAccount),
159
- unlinkAccount: vi.fn().mockResolvedValue(undefined),
160
- } as any);
161
-
162
- await expect(
163
- userRouter.createCaller({ ...mockCtx }).unlinkSSOProvider(mockInput),
164
- ).resolves.not.toThrow();
165
- });
166
-
167
- it('should throw error if account does not exist', async () => {
168
- const mockInput = {
169
- provider: 'google',
170
- providerAccountId: '123',
171
- };
172
-
173
- vi.mocked(NextAuthUserService).mockReturnValue({
174
- getAccount: vi.fn().mockResolvedValue(null),
175
- unlinkAccount: vi.fn(),
176
- } as any);
177
-
178
- await expect(
179
- userRouter.createCaller({ ...mockCtx }).unlinkSSOProvider(mockInput),
180
- ).rejects.toThrow('The account does not exist');
181
- });
182
- });
183
-
184
136
  describe('updateSettings', () => {
185
137
  it('should update settings with encrypted key vaults', async () => {
186
138
  const mockSettings = {
@@ -1,6 +1,5 @@
1
1
  import { isDesktop } from '@lobechat/const';
2
2
  import {
3
- NextAuthAccountSchame,
4
3
  Plans,
5
4
  UserGuideSchema,
6
5
  type UserInitializationState,
@@ -16,8 +15,8 @@ import { v4 as uuidv4 } from 'uuid';
16
15
  import { z } from 'zod';
17
16
 
18
17
  import {
19
- getIsInviteCodeRequired,
20
18
  getIsInWaitList,
19
+ getIsInviteCodeRequired,
21
20
  getReferralStatus,
22
21
  getSubscriptionPlan,
23
22
  } from '@/business/server/user';
@@ -29,7 +28,6 @@ import { serverDatabase } from '@/libs/trpc/lambda/middleware';
29
28
  import { KeyVaultsGateKeeper } from '@/server/modules/KeyVaultsEncrypt';
30
29
  import { FileS3 } from '@/server/modules/S3';
31
30
  import { FileService } from '@/server/services/file';
32
- import { NextAuthUserService } from '@/server/services/nextAuthUser';
33
31
 
34
32
  const usernameSchema = z
35
33
  .string()
@@ -42,7 +40,6 @@ const userProcedure = authedProcedure.use(serverDatabase).use(async ({ ctx, next
42
40
  ctx: {
43
41
  fileService: new FileService(ctx.serverDB, ctx.userId),
44
42
  messageModel: new MessageModel(ctx.serverDB, ctx.userId),
45
- nextAuthUserService: new NextAuthUserService(ctx.serverDB),
46
43
  sessionModel: new SessionModel(ctx.serverDB, ctx.userId),
47
44
  userModel: new UserModel(ctx.serverDB, ctx.userId),
48
45
  },
@@ -138,14 +135,6 @@ export const userRouter = router({
138
135
  return ctx.userModel.deleteSetting();
139
136
  }),
140
137
 
141
- unlinkSSOProvider: userProcedure.input(NextAuthAccountSchame).mutation(async ({ ctx, input }) => {
142
- const { provider, providerAccountId } = input;
143
- const account = await ctx.nextAuthUserService.getAccount(providerAccountId, provider);
144
- // The userId can either get from ctx.nextAuth?.id or ctx.userId
145
- if (!account || account.userId !== ctx.userId) throw new Error('The account does not exist');
146
- await ctx.nextAuthUserService.unlinkAccount({ provider, providerAccountId });
147
- }),
148
-
149
138
  updateAvatar: userProcedure.input(z.string()).mutation(async ({ ctx, input }) => {
150
139
  // If it's Base64 data, need to upload to S3
151
140
  if (input.startsWith('data:image')) {
@@ -49,8 +49,8 @@ export class NodemailerImpl implements EmailServiceImpl {
49
49
  }
50
50
 
51
51
  async sendMail(payload: EmailPayload): Promise<EmailResponse> {
52
- // Use SMTP_USER as default sender if not provided
53
- const from = payload.from ?? emailEnv.SMTP_USER!;
52
+ // Use SMTP_FROM as default sender, fallback to SMTP_USER for backward compatibility
53
+ const from = payload.from ?? emailEnv.SMTP_FROM ?? emailEnv.SMTP_USER!;
54
54
 
55
55
  log('Sending email with payload: %o', {
56
56
  from,
@@ -0,0 +1,88 @@
1
+ import { type LobeChatDatabase } from '@lobechat/database';
2
+ import { and, eq } from 'drizzle-orm';
3
+ import { NextResponse } from 'next/server';
4
+
5
+ import { UserModel } from '@/database/models/user';
6
+ import { type UserItem, account, session } from '@/database/schemas';
7
+ import { pino } from '@/libs/logger';
8
+
9
+ export class WebhookUserService {
10
+ private db: LobeChatDatabase;
11
+
12
+ constructor(db: LobeChatDatabase) {
13
+ this.db = db;
14
+ }
15
+
16
+ /**
17
+ * Find user by provider account info
18
+ */
19
+ private getUserByAccount = async ({
20
+ providerId,
21
+ accountId,
22
+ }: {
23
+ accountId: string;
24
+ providerId: string;
25
+ }) => {
26
+ const result = await this.db.query.account.findFirst({
27
+ where: and(eq(account.providerId, providerId), eq(account.accountId, accountId)),
28
+ });
29
+
30
+ if (!result) return null;
31
+
32
+ return this.db.query.users.findFirst({
33
+ where: eq(account.userId, result.userId),
34
+ });
35
+ };
36
+
37
+ /**
38
+ * Safely update user data from webhook
39
+ */
40
+ safeUpdateUser = async (
41
+ { accountId, providerId }: { accountId: string; providerId: string },
42
+ data: Partial<UserItem>,
43
+ ) => {
44
+ pino.info(`updating user "${JSON.stringify({ accountId, providerId })}" due to webhook`);
45
+
46
+ const user = await this.getUserByAccount({ accountId, providerId });
47
+
48
+ if (user?.id) {
49
+ const userModel = new UserModel(this.db, user.id);
50
+ await userModel.updateUser({
51
+ avatar: data?.avatar,
52
+ email: data?.email,
53
+ fullName: data?.fullName,
54
+ });
55
+ } else {
56
+ pino.warn(
57
+ `[${providerId}]: Webhook user "${JSON.stringify({ accountId, providerId })}" update for "${JSON.stringify(data)}", but no user was found.`,
58
+ );
59
+ }
60
+
61
+ return NextResponse.json({ message: 'user updated', success: true }, { status: 200 });
62
+ };
63
+
64
+ /**
65
+ * Safely sign out user (delete all sessions)
66
+ */
67
+ safeSignOutUser = async ({
68
+ accountId,
69
+ providerId,
70
+ }: {
71
+ accountId: string;
72
+ providerId: string;
73
+ }) => {
74
+ pino.info(`Signing out user "${JSON.stringify({ accountId, providerId })}"`);
75
+
76
+ const user = await this.getUserByAccount({ accountId, providerId });
77
+
78
+ if (user?.id) {
79
+ await this.db.delete(session).where(eq(session.userId, user.id));
80
+ } else {
81
+ pino.warn(
82
+ `[${providerId}]: Webhook user "${JSON.stringify({ accountId, providerId })}" signout, but no user was found.`,
83
+ );
84
+ }
85
+
86
+ return NextResponse.json({ message: 'user signed out', success: true }, { status: 200 });
87
+ };
88
+ }
@@ -8,7 +8,6 @@ const mockLambdaClient = vi.hoisted(() => ({
8
8
  getUserRegistrationDuration: { query: vi.fn() },
9
9
  getUserState: { query: vi.fn() },
10
10
  getUserSSOProviders: { query: vi.fn() },
11
- unlinkSSOProvider: { mutate: vi.fn() },
12
11
  makeUserOnboarded: { mutate: vi.fn() },
13
12
  updateAvatar: { mutate: vi.fn() },
14
13
  updateFullName: { mutate: vi.fn() },
@@ -64,19 +63,6 @@ describe('UserService', () => {
64
63
  });
65
64
  });
66
65
 
67
- describe('unlinkSSOProvider', () => {
68
- it('should call lambdaClient.user.unlinkSSOProvider.mutate with correct params', async () => {
69
- mockLambdaClient.user.unlinkSSOProvider.mutate.mockResolvedValueOnce({ success: true });
70
-
71
- await userService.unlinkSSOProvider('github', 'account-123');
72
-
73
- expect(mockLambdaClient.user.unlinkSSOProvider.mutate).toHaveBeenCalledWith({
74
- provider: 'github',
75
- providerAccountId: 'account-123',
76
- });
77
- });
78
- });
79
-
80
66
  describe('makeUserOnboarded', () => {
81
67
  it('should call lambdaClient.user.makeUserOnboarded.mutate', async () => {
82
68
  mockLambdaClient.user.makeUserOnboarded.mutate.mockResolvedValueOnce({ success: true });
@@ -27,10 +27,6 @@ export class UserService {
27
27
  return lambdaClient.user.getUserSSOProviders.query();
28
28
  };
29
29
 
30
- unlinkSSOProvider = async (provider: string, providerAccountId: string) => {
31
- return lambdaClient.user.unlinkSSOProvider.mutate({ provider, providerAccountId });
32
- };
33
-
34
30
  makeUserOnboarded = async () => {
35
31
  return lambdaClient.user.makeUserOnboarded.mutate();
36
32
  };
@@ -194,6 +194,7 @@ export const createDocumentSlice: StateCreator<
194
194
  // Check if this response is still for the current active document
195
195
  // This prevents race conditions when quickly switching between documents
196
196
  const currentActiveId = get().activeDocumentId;
197
+
197
198
  if (currentActiveId && currentActiveId !== documentId) {
198
199
  // User has already switched to another document, discard this stale response
199
200
  return;
@@ -15,29 +15,6 @@ vi.mock('@/libs/swr', async () => {
15
15
  };
16
16
  });
17
17
 
18
- // Use vi.hoisted to ensure variables exist before vi.mock factory executes
19
- const { enableNextAuth, enableBetterAuth } = vi.hoisted(() => ({
20
- enableNextAuth: { value: false },
21
- enableBetterAuth: { value: false },
22
- }));
23
-
24
- vi.mock('@/envs/auth', () => ({
25
- get enableNextAuth() {
26
- return enableNextAuth.value;
27
- },
28
- get enableBetterAuth() {
29
- return enableBetterAuth.value;
30
- },
31
- }));
32
-
33
- const mockUserService = vi.hoisted(() => ({
34
- getUserSSOProviders: vi.fn().mockResolvedValue([]),
35
- }));
36
-
37
- vi.mock('@/services/user', () => ({
38
- userService: mockUserService,
39
- }));
40
-
41
18
  const mockBetterAuthClient = vi.hoisted(() => ({
42
19
  listAccounts: vi.fn().mockResolvedValue({ data: [] }),
43
20
  accountInfo: vi.fn().mockResolvedValue({ data: { user: {} } }),
@@ -50,9 +27,6 @@ afterEach(() => {
50
27
  vi.restoreAllMocks();
51
28
  vi.clearAllMocks();
52
29
 
53
- enableNextAuth.value = false;
54
- enableBetterAuth.value = false;
55
-
56
30
  // Reset store state
57
31
  useUserStore.setState({
58
32
  isLoadedAuthProviders: false,
@@ -61,16 +35,6 @@ afterEach(() => {
61
35
  });
62
36
  });
63
37
 
64
- /**
65
- * Mock nextauth 库相关方法
66
- */
67
- vi.mock('next-auth/react', async () => {
68
- return {
69
- signIn: vi.fn(),
70
- signOut: vi.fn(),
71
- };
72
- });
73
-
74
38
  describe('createAuthSlice', () => {
75
39
  describe('refreshUserState', () => {
76
40
  it('should refresh user config', async () => {
@@ -85,64 +49,19 @@ describe('createAuthSlice', () => {
85
49
  });
86
50
 
87
51
  describe('logout', () => {
88
- it('should call next-auth signOut when NextAuth is enabled', async () => {
89
- enableNextAuth.value = true;
90
-
91
- const { result } = renderHook(() => useUserStore());
92
-
93
- await act(async () => {
94
- await result.current.logout();
95
- });
96
-
97
- const { signOut } = await import('next-auth/react');
98
-
99
- expect(signOut).toHaveBeenCalled();
100
- enableNextAuth.value = false;
101
- });
102
-
103
- it('should not call next-auth signOut when NextAuth is disabled', async () => {
52
+ it('should call better-auth signOut', async () => {
104
53
  const { result } = renderHook(() => useUserStore());
105
54
 
106
55
  await act(async () => {
107
56
  await result.current.logout();
108
57
  });
109
58
 
110
- const { signOut } = await import('next-auth/react');
111
-
112
- expect(signOut).not.toHaveBeenCalled();
59
+ expect(mockBetterAuthClient.signOut).toHaveBeenCalled();
113
60
  });
114
61
  });
115
62
 
116
63
  describe('openLogin', () => {
117
- it('should call next-auth signIn when NextAuth is enabled', async () => {
118
- enableNextAuth.value = true;
119
-
120
- const { result } = renderHook(() => useUserStore());
121
-
122
- await act(async () => {
123
- await result.current.openLogin();
124
- });
125
-
126
- const { signIn } = await import('next-auth/react');
127
-
128
- expect(signIn).toHaveBeenCalled();
129
- enableNextAuth.value = false;
130
- });
131
- it('should not call next-auth signIn when NextAuth is disabled', async () => {
132
- const { result } = renderHook(() => useUserStore());
133
-
134
- await act(async () => {
135
- await result.current.openLogin();
136
- });
137
-
138
- const { signIn } = await import('next-auth/react');
139
-
140
- expect(signIn).not.toHaveBeenCalled();
141
- });
142
-
143
- it('should redirect to signin page when BetterAuth is enabled', async () => {
144
- enableBetterAuth.value = true;
145
-
64
+ it('should redirect to signin page', async () => {
146
65
  const originalLocation = window.location;
147
66
  Object.defineProperty(window, 'location', {
148
67
  configurable: true,
@@ -171,18 +90,15 @@ describe('createAuthSlice', () => {
171
90
  });
172
91
  });
173
92
 
174
- it('should call signIn with single provider when only one OAuth provider available', async () => {
175
- enableNextAuth.value = true;
176
- useUserStore.setState({ oAuthSSOProviders: ['github'] });
177
-
93
+ it('should not redirect when already on signin page', async () => {
178
94
  const originalLocation = window.location;
179
95
  Object.defineProperty(window, 'location', {
180
96
  configurable: true,
181
97
  value: {
182
98
  ...originalLocation,
183
99
  href: '',
184
- pathname: '/chat',
185
- toString: () => 'http://localhost/chat',
100
+ pathname: '/signin',
101
+ toString: () => 'http://localhost/signin',
186
102
  },
187
103
  writable: true,
188
104
  });
@@ -193,9 +109,7 @@ describe('createAuthSlice', () => {
193
109
  await result.current.openLogin();
194
110
  });
195
111
 
196
- const { signIn } = await import('next-auth/react');
197
-
198
- expect(signIn).toHaveBeenCalledWith('github');
112
+ expect(window.location.href).toBe('');
199
113
 
200
114
  Object.defineProperty(window, 'location', {
201
115
  configurable: true,
@@ -215,29 +129,10 @@ describe('createAuthSlice', () => {
215
129
  await result.current.fetchAuthProviders();
216
130
  });
217
131
 
218
- expect(mockUserService.getUserSSOProviders).not.toHaveBeenCalled();
219
- });
220
-
221
- it('should fetch providers from NextAuth when BetterAuth is disabled', async () => {
222
- enableBetterAuth.value = false;
223
- const mockProviders = [
224
- { provider: 'github', email: 'test@example.com', providerAccountId: '123' },
225
- ];
226
- mockUserService.getUserSSOProviders.mockResolvedValueOnce(mockProviders);
227
-
228
- const { result } = renderHook(() => useUserStore());
229
-
230
- await act(async () => {
231
- await result.current.fetchAuthProviders();
232
- });
233
-
234
- expect(mockUserService.getUserSSOProviders).toHaveBeenCalled();
235
- expect(result.current.isLoadedAuthProviders).toBe(true);
236
- expect(result.current.authProviders).toEqual(mockProviders);
132
+ expect(mockBetterAuthClient.listAccounts).not.toHaveBeenCalled();
237
133
  });
238
134
 
239
- it('should fetch providers from BetterAuth when enabled', async () => {
240
- enableBetterAuth.value = true;
135
+ it('should fetch providers from BetterAuth', async () => {
241
136
  mockBetterAuthClient.listAccounts.mockResolvedValueOnce({
242
137
  data: [
243
138
  { providerId: 'github', accountId: 'gh-123' },
@@ -260,8 +155,7 @@ describe('createAuthSlice', () => {
260
155
  });
261
156
 
262
157
  it('should handle fetch error gracefully', async () => {
263
- enableBetterAuth.value = false;
264
- mockUserService.getUserSSOProviders.mockRejectedValueOnce(new Error('Network error'));
158
+ mockBetterAuthClient.listAccounts.mockRejectedValueOnce(new Error('Network error'));
265
159
 
266
160
  const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
267
161
 
@@ -277,12 +171,13 @@ describe('createAuthSlice', () => {
277
171
  });
278
172
 
279
173
  describe('refreshAuthProviders', () => {
280
- it('should refresh providers from NextAuth', async () => {
281
- enableBetterAuth.value = false;
282
- const mockProviders = [
283
- { provider: 'google', email: 'user@gmail.com', providerAccountId: 'g-1' },
284
- ];
285
- mockUserService.getUserSSOProviders.mockResolvedValueOnce(mockProviders);
174
+ it('should refresh providers from BetterAuth', async () => {
175
+ mockBetterAuthClient.listAccounts.mockResolvedValueOnce({
176
+ data: [{ providerId: 'google', accountId: 'g-1' }],
177
+ });
178
+ mockBetterAuthClient.accountInfo.mockResolvedValueOnce({
179
+ data: { user: { email: 'user@gmail.com' } },
180
+ });
286
181
 
287
182
  const { result } = renderHook(() => useUserStore());
288
183
 
@@ -290,13 +185,14 @@ describe('createAuthSlice', () => {
290
185
  await result.current.refreshAuthProviders();
291
186
  });
292
187
 
293
- expect(mockUserService.getUserSSOProviders).toHaveBeenCalled();
294
- expect(result.current.authProviders).toEqual(mockProviders);
188
+ expect(mockBetterAuthClient.listAccounts).toHaveBeenCalled();
189
+ expect(result.current.authProviders).toEqual([
190
+ { provider: 'google', email: 'user@gmail.com', providerAccountId: 'g-1' },
191
+ ]);
295
192
  });
296
193
 
297
194
  it('should handle refresh error gracefully', async () => {
298
- enableBetterAuth.value = false;
299
- mockUserService.getUserSSOProviders.mockRejectedValueOnce(new Error('Refresh failed'));
195
+ mockBetterAuthClient.listAccounts.mockRejectedValueOnce(new Error('Refresh failed'));
300
196
 
301
197
  const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
302
198