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

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 (151) 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 +33 -0
  6. package/Dockerfile +28 -4
  7. package/changelog/v1.json +9 -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/utils/src/server/__tests__/auth.test.ts +1 -63
  47. package/packages/utils/src/server/auth.ts +8 -24
  48. package/scripts/_shared/checkDeprecatedAuth.js +99 -0
  49. package/scripts/clerk-to-betterauth/index.ts +8 -3
  50. package/scripts/nextauth-to-betterauth/_internal/config.ts +41 -0
  51. package/scripts/nextauth-to-betterauth/_internal/db.ts +32 -0
  52. package/scripts/nextauth-to-betterauth/_internal/env.ts +6 -0
  53. package/scripts/nextauth-to-betterauth/index.ts +226 -0
  54. package/scripts/nextauth-to-betterauth/verify.ts +188 -0
  55. package/scripts/prebuild.mts +66 -13
  56. package/scripts/serverLauncher/startServer.js +5 -5
  57. package/src/app/(backend)/api/auth/[...all]/route.ts +5 -23
  58. package/src/app/(backend)/api/webhooks/casdoor/route.ts +5 -5
  59. package/src/app/(backend)/api/webhooks/logto/route.ts +8 -8
  60. package/src/app/(backend)/middleware/auth/index.test.ts +8 -1
  61. package/src/app/(backend)/middleware/auth/index.ts +6 -15
  62. package/src/app/(backend)/middleware/auth/utils.test.ts +0 -32
  63. package/src/app/(backend)/middleware/auth/utils.ts +3 -8
  64. package/src/app/(backend)/webapi/chat/[provider]/route.test.ts +8 -1
  65. package/src/app/(backend)/webapi/create-image/comfyui/route.ts +0 -1
  66. package/src/app/(backend)/webapi/models/[provider]/route.test.ts +8 -1
  67. package/src/app/[variants]/(auth)/signin/SignInEmailStep.tsx +1 -1
  68. package/src/app/[variants]/(auth)/signup/[[...signup]]/page.tsx +4 -17
  69. package/src/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobContentEditor.tsx +34 -21
  70. package/src/app/[variants]/(main)/settings/profile/features/SSOProvidersList/index.tsx +12 -19
  71. package/src/app/[variants]/(main)/settings/profile/index.tsx +8 -14
  72. package/src/components/{NextAuth/AuthIcons.tsx → AuthIcons.tsx} +8 -10
  73. package/src/envs/auth.ts +12 -51
  74. package/src/envs/email.ts +3 -0
  75. package/src/envs/redis.ts +12 -54
  76. package/src/features/ChatInput/ChatInputProvider.tsx +22 -2
  77. package/src/features/ChatInput/InputEditor/index.tsx +14 -3
  78. package/src/features/ChatInput/store/initialState.ts +2 -0
  79. package/src/features/User/__tests__/PanelContent.test.tsx +0 -11
  80. package/src/features/User/__tests__/UserAvatar.test.tsx +1 -16
  81. package/src/layout/AuthProvider/index.tsx +1 -6
  82. package/src/layout/GlobalProvider/StoreInitialization.tsx +2 -4
  83. package/src/libs/better-auth/define-config.ts +2 -0
  84. package/src/libs/better-auth/plugins/email-whitelist.test.ts +120 -0
  85. package/src/libs/better-auth/plugins/email-whitelist.ts +62 -0
  86. package/src/libs/next/config/define-config.ts +13 -1
  87. package/src/libs/next/proxy/define-config.ts +2 -75
  88. package/src/libs/oidc-provider/provider.test.ts +0 -4
  89. package/src/libs/redis/index.ts +0 -1
  90. package/src/libs/redis/manager.test.ts +9 -45
  91. package/src/libs/redis/manager.ts +2 -16
  92. package/src/libs/redis/redis.test.ts +2 -4
  93. package/src/libs/redis/redis.ts +2 -4
  94. package/src/libs/redis/types.ts +2 -24
  95. package/src/libs/redis/utils.test.ts +0 -10
  96. package/src/libs/redis/utils.ts +0 -19
  97. package/src/libs/trpc/lambda/context.test.ts +0 -13
  98. package/src/libs/trpc/lambda/context.ts +21 -59
  99. package/src/libs/trpc/middleware/userAuth.ts +1 -7
  100. package/src/libs/trusted-client/getSessionUser.ts +15 -35
  101. package/src/server/globalConfig/index.ts +1 -3
  102. package/src/server/routers/lambda/__tests__/user.test.ts +0 -48
  103. package/src/server/routers/lambda/user.ts +1 -12
  104. package/src/server/services/email/impls/nodemailer/index.ts +2 -2
  105. package/src/server/services/webhookUser/index.ts +88 -0
  106. package/src/services/user/index.test.ts +0 -14
  107. package/src/services/user/index.ts +0 -4
  108. package/src/store/user/slices/auth/action.test.ts +22 -126
  109. package/src/store/user/slices/auth/action.ts +32 -65
  110. package/src/store/user/slices/auth/initialState.ts +0 -3
  111. package/src/store/user/slices/auth/selectors.ts +0 -3
  112. package/tests/setup.ts +10 -0
  113. package/scripts/_shared/checkDeprecatedClerkEnv.js +0 -42
  114. package/src/app/(backend)/api/auth/adapter/route.ts +0 -137
  115. package/src/app/[variants]/(auth)/next-auth/error/AuthErrorPage.tsx +0 -40
  116. package/src/app/[variants]/(auth)/next-auth/error/page.tsx +0 -11
  117. package/src/app/[variants]/(auth)/next-auth/signin/AuthSignInBox.tsx +0 -167
  118. package/src/app/[variants]/(auth)/next-auth/signin/page.tsx +0 -11
  119. package/src/app/[variants]/(auth)/reset-password/layout.tsx +0 -12
  120. package/src/app/[variants]/(auth)/signin/layout.tsx +0 -12
  121. package/src/app/[variants]/(auth)/verify-email/layout.tsx +0 -12
  122. package/src/envs/auth.test.ts +0 -47
  123. package/src/layout/AuthProvider/NextAuth/UserUpdater.tsx +0 -44
  124. package/src/layout/AuthProvider/NextAuth/index.tsx +0 -17
  125. package/src/libs/next-auth/adapter/index.ts +0 -177
  126. package/src/libs/next-auth/auth.config.ts +0 -64
  127. package/src/libs/next-auth/index.ts +0 -20
  128. package/src/libs/next-auth/sso-providers/auth0.ts +0 -24
  129. package/src/libs/next-auth/sso-providers/authelia.ts +0 -39
  130. package/src/libs/next-auth/sso-providers/authentik.ts +0 -25
  131. package/src/libs/next-auth/sso-providers/casdoor.ts +0 -50
  132. package/src/libs/next-auth/sso-providers/cloudflare-zero-trust.ts +0 -34
  133. package/src/libs/next-auth/sso-providers/cognito.ts +0 -8
  134. package/src/libs/next-auth/sso-providers/feishu.ts +0 -83
  135. package/src/libs/next-auth/sso-providers/generic-oidc.ts +0 -38
  136. package/src/libs/next-auth/sso-providers/github.ts +0 -23
  137. package/src/libs/next-auth/sso-providers/google.ts +0 -18
  138. package/src/libs/next-auth/sso-providers/index.ts +0 -35
  139. package/src/libs/next-auth/sso-providers/keycloak.ts +0 -22
  140. package/src/libs/next-auth/sso-providers/logto.ts +0 -48
  141. package/src/libs/next-auth/sso-providers/microsoft-entra-id-helper.ts +0 -29
  142. package/src/libs/next-auth/sso-providers/microsoft-entra-id.ts +0 -19
  143. package/src/libs/next-auth/sso-providers/okta.ts +0 -22
  144. package/src/libs/next-auth/sso-providers/sso.config.ts +0 -8
  145. package/src/libs/next-auth/sso-providers/wechat.ts +0 -36
  146. package/src/libs/next-auth/sso-providers/zitadel.ts +0 -21
  147. package/src/libs/redis/upstash.test.ts +0 -158
  148. package/src/libs/redis/upstash.ts +0 -136
  149. package/src/server/services/nextAuthUser/index.ts +0 -318
  150. package/src/server/services/nextAuthUser/utils.ts +0 -62
  151. package/src/types/next-auth.d.ts +0 -26
@@ -1,16 +1,10 @@
1
1
  import { type ClientSecretPayload } from '@lobechat/types';
2
2
  import { parse } from 'cookie';
3
3
  import debug from 'debug';
4
- import { type User } from 'next-auth';
5
4
  import { type NextRequest } from 'next/server';
6
5
 
7
- import {
8
- LOBE_CHAT_AUTH_HEADER,
9
- LOBE_CHAT_OIDC_AUTH_HEADER,
10
- authEnv,
11
- enableBetterAuth,
12
- enableNextAuth,
13
- } from '@/envs/auth';
6
+ import { auth } from '@/auth';
7
+ import { LOBE_CHAT_AUTH_HEADER, LOBE_CHAT_OIDC_AUTH_HEADER, authEnv } from '@/envs/auth';
14
8
  import { validateOIDCJWT } from '@/libs/oidc-provider/jwt';
15
9
 
16
10
  // Create context logger namespace
@@ -43,7 +37,6 @@ export interface AuthContext {
43
37
  clientIp?: string | null;
44
38
  jwtPayload?: ClientSecretPayload | null;
45
39
  marketAccessToken?: string;
46
- nextAuth?: User;
47
40
  // Add OIDC authentication information
48
41
  oidcAuth?: OIDCAuth | null;
49
42
  resHeaders?: Headers;
@@ -59,7 +52,6 @@ export const createContextInner = async (params?: {
59
52
  authorizationHeader?: string | null;
60
53
  clientIp?: string | null;
61
54
  marketAccessToken?: string;
62
- nextAuth?: User;
63
55
  oidcAuth?: OIDCAuth | null;
64
56
  userAgent?: string;
65
57
  userId?: string | null;
@@ -71,7 +63,6 @@ export const createContextInner = async (params?: {
71
63
  authorizationHeader: params?.authorizationHeader,
72
64
  clientIp: params?.clientIp,
73
65
  marketAccessToken: params?.marketAccessToken,
74
- nextAuth: params?.nextAuth,
75
66
  oidcAuth: params?.oidcAuth,
76
67
  resHeaders: responseHeaders,
77
68
  userAgent: params?.userAgent,
@@ -120,7 +111,6 @@ export const createLambdaContext = async (request: NextRequest): Promise<LambdaC
120
111
  log('LobeChat Authorization header: %s', authorization ? 'exists' : 'not found');
121
112
 
122
113
  let userId;
123
- let auth;
124
114
  let oidcAuth = null;
125
115
 
126
116
  // Prioritize checking for OIDC authentication (both standard Authorization and custom Oidc-Auth headers)
@@ -159,55 +149,27 @@ export const createLambdaContext = async (request: NextRequest): Promise<LambdaC
159
149
  }
160
150
  }
161
151
 
162
- // If OIDC is not enabled or validation fails, try other authentication methods
163
- if (enableBetterAuth) {
164
- log('Attempting Better Auth authentication');
165
- try {
166
- const { auth: betterAuth } = await import('@/auth');
167
-
168
- const session = await betterAuth.api.getSession({
169
- headers: request.headers,
170
- });
171
-
172
- if (session && session?.user?.id) {
173
- userId = session.user.id;
174
- log('Better Auth authentication successful, userId: %s', userId);
175
- } else {
176
- log('Better Auth authentication failed, no valid session');
177
- }
178
-
179
- return createContextInner({
180
- ...commonContext,
181
- userId,
182
- });
183
- } catch (e) {
184
- log('Better Auth authentication error: %O', e);
185
- console.error('better auth err', e);
152
+ // If OIDC is not enabled or validation fails, try Better Auth authentication
153
+ log('Attempting Better Auth authentication');
154
+ try {
155
+ const session = await auth.api.getSession({
156
+ headers: request.headers,
157
+ });
158
+
159
+ if (session && session?.user?.id) {
160
+ userId = session.user.id;
161
+ log('Better Auth authentication successful, userId: %s', userId);
162
+ } else {
163
+ log('Better Auth authentication failed, no valid session');
186
164
  }
187
- }
188
165
 
189
- if (enableNextAuth) {
190
- log('Attempting NextAuth authentication');
191
- try {
192
- const { default: NextAuth } = await import('@/libs/next-auth');
193
-
194
- const session = await NextAuth.auth();
195
- if (session && session?.user?.id) {
196
- auth = session.user;
197
- userId = session.user.id;
198
- log('NextAuth authentication successful, userId: %s', userId);
199
- } else {
200
- log('NextAuth authentication failed, no valid session');
201
- }
202
- return createContextInner({
203
- nextAuth: auth,
204
- ...commonContext,
205
- userId,
206
- });
207
- } catch (e) {
208
- log('NextAuth authentication error: %O', e);
209
- console.error('next auth err', e);
210
- }
166
+ return createContextInner({
167
+ ...commonContext,
168
+ userId,
169
+ });
170
+ } catch (e) {
171
+ log('Better Auth authentication error: %O', e);
172
+ console.error('better auth err', e);
211
173
  }
212
174
 
213
175
  // Final return, userId may be undefined
@@ -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
  };