@lobehub/chat 1.36.8 → 1.36.9

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 (46) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/changelog/v1.json +12 -0
  3. package/locales/ar/models.json +78 -0
  4. package/locales/ar/providers.json +3 -0
  5. package/locales/bg-BG/models.json +78 -0
  6. package/locales/bg-BG/providers.json +3 -0
  7. package/locales/de-DE/models.json +78 -0
  8. package/locales/de-DE/providers.json +3 -0
  9. package/locales/en-US/models.json +78 -0
  10. package/locales/en-US/providers.json +3 -0
  11. package/locales/es-ES/models.json +78 -0
  12. package/locales/es-ES/providers.json +3 -0
  13. package/locales/fa-IR/models.json +78 -0
  14. package/locales/fa-IR/providers.json +3 -0
  15. package/locales/fr-FR/models.json +78 -0
  16. package/locales/fr-FR/providers.json +3 -0
  17. package/locales/it-IT/models.json +78 -0
  18. package/locales/it-IT/providers.json +3 -0
  19. package/locales/ja-JP/models.json +78 -0
  20. package/locales/ja-JP/providers.json +3 -0
  21. package/locales/ko-KR/models.json +78 -0
  22. package/locales/ko-KR/providers.json +3 -0
  23. package/locales/nl-NL/models.json +78 -0
  24. package/locales/nl-NL/providers.json +3 -0
  25. package/locales/pl-PL/modelProvider.json +9 -9
  26. package/locales/pl-PL/models.json +78 -0
  27. package/locales/pl-PL/providers.json +3 -0
  28. package/locales/pt-BR/models.json +78 -0
  29. package/locales/pt-BR/providers.json +3 -0
  30. package/locales/ru-RU/models.json +78 -0
  31. package/locales/ru-RU/providers.json +3 -0
  32. package/locales/tr-TR/models.json +78 -0
  33. package/locales/tr-TR/providers.json +3 -0
  34. package/locales/vi-VN/models.json +78 -0
  35. package/locales/vi-VN/providers.json +3 -0
  36. package/locales/zh-CN/models.json +88 -10
  37. package/locales/zh-CN/providers.json +3 -0
  38. package/locales/zh-TW/models.json +78 -0
  39. package/locales/zh-TW/providers.json +3 -0
  40. package/package.json +1 -1
  41. package/src/app/(backend)/api/webhooks/clerk/route.ts +18 -3
  42. package/src/database/server/models/__tests__/nextauth.test.ts +33 -0
  43. package/src/libs/next-auth/adapter/index.ts +8 -2
  44. package/src/server/services/user/index.test.ts +200 -0
  45. package/src/server/services/user/index.ts +24 -32
  46. package/vitest.config.ts +1 -1
@@ -29,13 +29,28 @@ export const POST = async (req: Request): Promise<NextResponse> => {
29
29
  switch (type) {
30
30
  case 'user.created': {
31
31
  pino.info('creating user due to clerk webhook');
32
- return userService.createUser(data.id, data);
32
+ const result = await userService.createUser(data.id, data);
33
+
34
+ return NextResponse.json(result, { status: 200 });
33
35
  }
36
+
34
37
  case 'user.deleted': {
35
- return userService.deleteUser(data.id);
38
+ if (!data.id) {
39
+ pino.warn('clerk sent a delete user request, but no user ID was included in the payload');
40
+ return NextResponse.json({ message: 'ok' }, { status: 200 });
41
+ }
42
+
43
+ pino.info('delete user due to clerk webhook');
44
+
45
+ await userService.deleteUser(data.id);
46
+
47
+ return NextResponse.json({ message: 'user deleted' }, { status: 200 });
36
48
  }
49
+
37
50
  case 'user.updated': {
38
- return userService.updateUser(data.id, data);
51
+ const result = await userService.updateUser(data.id, data);
52
+
53
+ return NextResponse.json(result, { status: 200 });
39
54
  }
40
55
 
41
56
  default: {
@@ -146,6 +146,39 @@ describe('LobeNextAuthDbAdapter', () => {
146
146
  await serverDB.query.users.findMany({ where: eq(users.id, anotherUserId) }),
147
147
  ).toHaveLength(1);
148
148
  });
149
+
150
+ it('should create a user if id not exist even email is invalid type', async () => {
151
+ // In previous version, it will link the account to the existing user if the email is null
152
+ // issue: https://github.com/lobehub/lobe-chat/issues/4918
153
+ expect(nextAuthAdapter).toBeDefined();
154
+ expect(nextAuthAdapter.createUser).toBeDefined();
155
+
156
+ const existUserId = 'user-db-1';
157
+ const existUserName = 'John Doe 1';
158
+ // @ts-expect-error: createUser is defined
159
+ await nextAuthAdapter.createUser({
160
+ ...user,
161
+ id: existUserId,
162
+ name: existUserName,
163
+ email: Object({}), // assign a non-string value
164
+ });
165
+
166
+ const anotherUserId = 'user-db-2';
167
+ const anotherUserName = 'John Doe 2';
168
+ // @ts-expect-error: createUser is defined
169
+ await nextAuthAdapter.createUser({
170
+ ...user,
171
+ id: anotherUserId,
172
+ name: anotherUserName,
173
+ // @ts-expect-error: try to assign undefined value
174
+ email: undefined,
175
+ });
176
+
177
+ // Should create a new user if id not exists and email is null
178
+ expect(
179
+ await serverDB.query.users.findMany({ where: eq(users.id, anotherUserId) }),
180
+ ).toHaveLength(1);
181
+ });
149
182
  });
150
183
 
151
184
  describe('deleteUser', () => {
@@ -53,7 +53,10 @@ export function LobeNextAuthDbAdapter(serverDB: NeonDatabase<typeof schema>): Ad
53
53
  async createUser(user): Promise<AdapterUser> {
54
54
  const { id, name, email, emailVerified, image, providerAccountId } = user;
55
55
  // return the user if it already exists
56
- let existingUser = email.trim() ? await UserModel.findByEmail(serverDB, email) : undefined;
56
+ let existingUser =
57
+ email && typeof email === 'string' && email.trim()
58
+ ? await UserModel.findByEmail(serverDB, email)
59
+ : undefined;
57
60
  // If the user is not found by email, try to find by providerAccountId
58
61
  if (!existingUser && providerAccountId) {
59
62
  existingUser = await UserModel.findById(serverDB, providerAccountId);
@@ -169,7 +172,10 @@ export function LobeNextAuthDbAdapter(serverDB: NeonDatabase<typeof schema>): Ad
169
172
  },
170
173
 
171
174
  async getUserByEmail(email): Promise<AdapterUser | null> {
172
- const lobeUser = email.trim() ? await UserModel.findByEmail(serverDB, email) : undefined;
175
+ const lobeUser =
176
+ email && typeof email === 'string' && email.trim()
177
+ ? await UserModel.findByEmail(serverDB, email)
178
+ : undefined;
173
179
  return lobeUser ? mapLobeUserToAdapterUser(lobeUser) : null;
174
180
  },
175
181
 
@@ -0,0 +1,200 @@
1
+ import { UserJSON } from '@clerk/backend';
2
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ import { UserItem } from '@/database/schemas';
5
+ import { UserModel } from '@/database/server/models/user';
6
+ import { pino } from '@/libs/logger';
7
+
8
+ import { UserService } from './index';
9
+
10
+ // Mock dependencies
11
+ vi.mock('@/database/server/models/user', () => {
12
+ const MockUserModel = vi.fn();
13
+ // @ts-ignore
14
+ MockUserModel.findById = vi.fn();
15
+ // @ts-ignore
16
+ MockUserModel.createUser = vi.fn();
17
+ // @ts-ignore
18
+ MockUserModel.deleteUser = vi.fn();
19
+
20
+ // Mock instance methods
21
+ MockUserModel.prototype.updateUser = vi.fn();
22
+
23
+ return { UserModel: MockUserModel };
24
+ });
25
+
26
+ vi.mock('@/libs/logger', () => ({
27
+ pino: {
28
+ info: vi.fn(),
29
+ },
30
+ }));
31
+
32
+ let service: UserService;
33
+ const mockUserId = 'test-user-id';
34
+
35
+ // Mock user data
36
+ const mockUserJSON: UserJSON = {
37
+ id: mockUserId,
38
+ email_addresses: [{ id: 'email-1', email_address: 'test@example.com' }],
39
+ phone_numbers: [{ id: 'phone-1', phone_number: '+1234567890' }],
40
+ primary_email_address_id: 'email-1',
41
+ primary_phone_number_id: 'phone-1',
42
+ image_url: 'https://example.com/avatar.jpg',
43
+ first_name: 'Test',
44
+ last_name: 'User',
45
+ username: 'testuser',
46
+ created_at: '2023-01-01T00:00:00Z',
47
+ } as unknown as UserJSON;
48
+
49
+ beforeEach(() => {
50
+ service = new UserService();
51
+ vi.clearAllMocks();
52
+ });
53
+
54
+ describe('UserService', () => {
55
+ describe('createUser', () => {
56
+ it('should create a new user when user does not exist', async () => {
57
+ // Mock user not found
58
+ vi.mocked(UserModel.findById).mockResolvedValue(null as any);
59
+
60
+ await service.createUser(mockUserId, mockUserJSON);
61
+
62
+ expect(UserModel.findById).toHaveBeenCalledWith(expect.anything(), mockUserId);
63
+ expect(UserModel.createUser).toHaveBeenCalledWith(
64
+ expect.anything(),
65
+ expect.objectContaining({
66
+ id: mockUserId,
67
+ email: 'test@example.com',
68
+ phone: '+1234567890',
69
+ firstName: 'Test',
70
+ lastName: 'User',
71
+ username: 'testuser',
72
+ avatar: 'https://example.com/avatar.jpg',
73
+ clerkCreatedAt: new Date('2023-01-01T00:00:00Z'),
74
+ }),
75
+ );
76
+ });
77
+
78
+ it('should not create user if already exists', async () => {
79
+ // Mock user found
80
+ vi.mocked(UserModel.findById).mockResolvedValue({ id: mockUserId } as UserItem);
81
+
82
+ const result = await service.createUser(mockUserId, mockUserJSON);
83
+
84
+ expect(UserModel.findById).toHaveBeenCalledWith(expect.anything(), mockUserId);
85
+ expect(UserModel.createUser).not.toHaveBeenCalled();
86
+ expect(result).toEqual({
87
+ message: 'user not created due to user already existing in the database',
88
+ success: false,
89
+ });
90
+ });
91
+
92
+ it('should handle user without primary phone number', async () => {
93
+ vi.mocked(UserModel.findById).mockResolvedValue(null as any);
94
+
95
+ const userWithoutPrimaryPhone = {
96
+ ...mockUserJSON,
97
+ primary_phone_number_id: null,
98
+ phone_numbers: [{ id: 'phone-1', phone_number: '+1234567890' }],
99
+ } as UserJSON;
100
+
101
+ await service.createUser(mockUserId, userWithoutPrimaryPhone);
102
+
103
+ expect(UserModel.createUser).toHaveBeenCalledWith(
104
+ expect.anything(),
105
+ expect.objectContaining({
106
+ phone: '+1234567890', // Should use first phone number
107
+ }),
108
+ );
109
+ });
110
+ });
111
+
112
+ describe('deleteUser', () => {
113
+ it('should delete user', async () => {
114
+ await service.deleteUser(mockUserId);
115
+
116
+ expect(UserModel.deleteUser).toHaveBeenCalledWith(expect.anything(), mockUserId);
117
+ });
118
+
119
+ it('should throw error if deletion fails', async () => {
120
+ const error = new Error('Deletion failed');
121
+ vi.mocked(UserModel.deleteUser).mockRejectedValue(error);
122
+
123
+ await expect(service.deleteUser(mockUserId)).rejects.toThrow('Deletion failed');
124
+ });
125
+ });
126
+
127
+ describe('updateUser', () => {
128
+ it('should update user when user exists', async () => {
129
+ // Mock user found
130
+ vi.mocked(UserModel.findById).mockResolvedValue({ id: mockUserId } as UserItem);
131
+ const mockUpdateUser = vi.mocked(UserModel.prototype.updateUser);
132
+
133
+ const result = await service.updateUser(mockUserId, mockUserJSON);
134
+
135
+ expect(UserModel.findById).toHaveBeenCalledWith(expect.anything(), mockUserId);
136
+ expect(pino.info).toHaveBeenCalledWith('updating user due to clerk webhook');
137
+ expect(mockUpdateUser).toHaveBeenCalledWith(
138
+ expect.objectContaining({
139
+ id: mockUserId,
140
+ email: 'test@example.com',
141
+ phone: '+1234567890',
142
+ firstName: 'Test',
143
+ lastName: 'User',
144
+ username: 'testuser',
145
+ avatar: 'https://example.com/avatar.jpg',
146
+ }),
147
+ );
148
+ expect(result).toEqual({
149
+ message: 'user updated',
150
+ success: true,
151
+ });
152
+ });
153
+
154
+ it('should not update user when user does not exist', async () => {
155
+ // Mock user not found
156
+ vi.mocked(UserModel.findById).mockResolvedValue(null as any);
157
+ const mockUpdateUser = vi.mocked(UserModel.prototype.updateUser);
158
+
159
+ const result = await service.updateUser(mockUserId, mockUserJSON);
160
+
161
+ expect(UserModel.findById).toHaveBeenCalledWith(expect.anything(), mockUserId);
162
+ expect(mockUpdateUser).not.toHaveBeenCalled();
163
+ expect(result).toEqual({
164
+ message: "user not updated due to the user don't existing in the database",
165
+ success: false,
166
+ });
167
+ });
168
+
169
+ it('should handle user without primary email and phone', async () => {
170
+ vi.mocked(UserModel.findById).mockResolvedValue({ id: mockUserId } as UserItem);
171
+ const mockUpdateUser = vi.mocked(UserModel.prototype.updateUser);
172
+
173
+ const userWithoutPrimaryContacts = {
174
+ ...mockUserJSON,
175
+ primary_email_address_id: null,
176
+ primary_phone_number_id: null,
177
+ email_addresses: [{ id: 'email-1', email_address: 'test@example.com' }],
178
+ phone_numbers: [{ id: 'phone-1', phone_number: '+1234567890' }],
179
+ } as UserJSON;
180
+
181
+ await service.updateUser(mockUserId, userWithoutPrimaryContacts);
182
+
183
+ // Verify that the first email and phone are used when primary is not specified
184
+ expect(mockUpdateUser).toHaveBeenCalledWith(
185
+ expect.objectContaining({
186
+ phone: '+1234567890',
187
+ }),
188
+ );
189
+ });
190
+
191
+ it('should handle update failure', async () => {
192
+ vi.mocked(UserModel.findById).mockResolvedValue({ id: mockUserId } as UserItem);
193
+ const mockUpdateUser = vi.mocked(UserModel.prototype.updateUser);
194
+ const error = new Error('Update failed');
195
+ mockUpdateUser.mockRejectedValue(error);
196
+
197
+ await expect(service.updateUser(mockUserId, mockUserJSON)).rejects.toThrow('Update failed');
198
+ });
199
+ });
200
+ });
@@ -1,5 +1,4 @@
1
1
  import { UserJSON } from '@clerk/backend';
2
- import { NextResponse } from 'next/server';
3
2
 
4
3
  import { serverDB } from '@/database/server';
5
4
  import { UserModel } from '@/database/server/models/user';
@@ -12,16 +11,18 @@ export class UserService {
12
11
 
13
12
  // If user already exists, skip creating a new user
14
13
  if (res)
15
- return NextResponse.json(
16
- {
17
- message: 'user not created due to user already existing in the database',
18
- success: false,
19
- },
20
- { status: 200 },
21
- );
14
+ return {
15
+ message: 'user not created due to user already existing in the database',
16
+ success: false,
17
+ };
22
18
 
23
19
  const email = params.email_addresses.find((e) => e.id === params.primary_email_address_id);
24
- const phone = params.phone_numbers.find((e) => e.id === params.primary_phone_number_id);
20
+
21
+ const phone = params.phone_numbers.find((e, index) => {
22
+ if (!!params.primary_phone_number_id) return e.id === params.primary_phone_number_id;
23
+
24
+ return index === 0;
25
+ });
25
26
 
26
27
  /* ↓ cloud slot ↓ */
27
28
 
@@ -43,25 +44,14 @@ export class UserService {
43
44
 
44
45
  /* ↑ cloud slot ↑ */
45
46
 
46
- return NextResponse.json({ message: 'user created', success: true }, { status: 200 });
47
+ return { message: 'user created', success: true };
47
48
  };
48
49
 
49
- deleteUser = async (id?: string) => {
50
- if (id) {
51
- pino.info('delete user due to clerk webhook');
52
-
53
- await UserModel.deleteUser(serverDB, id);
54
-
55
- return NextResponse.json({ message: 'user deleted' }, { status: 200 });
56
- } else {
57
- pino.warn('clerk sent a delete user request, but no user ID was included in the payload');
58
- return NextResponse.json({ message: 'ok' }, { status: 200 });
59
- }
50
+ deleteUser = async (id: string) => {
51
+ await UserModel.deleteUser(serverDB, id);
60
52
  };
61
53
 
62
54
  updateUser = async (id: string, params: UserJSON) => {
63
- pino.info('updating user due to clerk webhook');
64
-
65
55
  const userModel = new UserModel(serverDB, id);
66
56
 
67
57
  // Check if user already exists
@@ -69,16 +59,18 @@ export class UserService {
69
59
 
70
60
  // If user not exists, skip update the user
71
61
  if (!res)
72
- return NextResponse.json(
73
- {
74
- message: "user not updated due to the user don't existing in the database",
75
- success: false,
76
- },
77
- { status: 200 },
78
- );
62
+ return {
63
+ message: "user not updated due to the user don't existing in the database",
64
+ success: false,
65
+ };
66
+
67
+ pino.info('updating user due to clerk webhook');
79
68
 
80
69
  const email = params.email_addresses.find((e) => e.id === params.primary_email_address_id);
81
- const phone = params.phone_numbers.find((e) => e.id === params.primary_phone_number_id);
70
+ const phone = params.phone_numbers.find((e, index) => {
71
+ if (params.primary_phone_number_id) return e.id === params.primary_phone_number_id;
72
+ return index === 0;
73
+ });
82
74
 
83
75
  await userModel.updateUser({
84
76
  avatar: params.image_url,
@@ -90,6 +82,6 @@ export class UserService {
90
82
  username: params.username,
91
83
  });
92
84
 
93
- return NextResponse.json({ message: 'user updated', success: true }, { status: 200 });
85
+ return { message: 'user updated', success: true };
94
86
  };
95
87
  }
package/vitest.config.ts CHANGED
@@ -31,7 +31,7 @@ export default defineConfig({
31
31
  '**/dist/**',
32
32
  '**/build/**',
33
33
  'src/database/server/**/**',
34
- 'src/server/services/!(discover)/**/**',
34
+ 'src/server/services/dataImporter/**/**',
35
35
  ],
36
36
  globals: true,
37
37
  server: {