@lobehub/lobehub 2.0.0-next.123 → 2.0.0-next.125

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 (126) 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 +51 -0
  8. package/Dockerfile +6 -6
  9. package/GEMINI.md +63 -0
  10. package/README.md +8 -8
  11. package/README.zh-CN.md +8 -8
  12. package/changelog/v1.json +18 -0
  13. package/docs/development/database-schema.dbml +38 -0
  14. package/docs/self-hosting/advanced/auth.mdx +75 -2
  15. package/docs/self-hosting/advanced/auth.zh-CN.mdx +75 -2
  16. package/docs/self-hosting/environment-variables/auth.mdx +187 -1
  17. package/docs/self-hosting/environment-variables/auth.zh-CN.mdx +187 -1
  18. package/locales/en-US/auth.json +93 -0
  19. package/locales/zh-CN/auth.json +107 -1
  20. package/package.json +5 -2
  21. package/packages/const/src/auth.ts +2 -1
  22. package/packages/database/migrations/0048_add_editor_data.sql +1 -0
  23. package/packages/database/migrations/0049_better_auth.sql +49 -0
  24. package/packages/database/migrations/meta/0048_snapshot.json +7913 -0
  25. package/packages/database/migrations/meta/0049_snapshot.json +8151 -0
  26. package/packages/database/migrations/meta/_journal.json +14 -0
  27. package/packages/database/src/core/migrations.json +19 -0
  28. package/packages/database/src/index.ts +1 -0
  29. package/packages/database/src/models/__tests__/session.test.ts +1 -2
  30. package/packages/database/src/models/user.ts +9 -8
  31. package/packages/database/src/repositories/tableViewer/index.test.ts +2 -2
  32. package/packages/database/src/schemas/agent.ts +1 -0
  33. package/packages/database/src/schemas/betterAuth.ts +63 -0
  34. package/packages/database/src/schemas/index.ts +1 -0
  35. package/packages/database/src/schemas/ragEvals.ts +1 -2
  36. package/packages/database/src/schemas/user.ts +3 -2
  37. package/packages/database/src/server/models/__tests__/user.test.ts +1 -4
  38. package/packages/types/src/user/preference.ts +11 -0
  39. package/packages/utils/src/server/__tests__/auth.test.ts +52 -0
  40. package/packages/utils/src/server/auth.ts +18 -1
  41. package/src/app/(backend)/api/auth/[...all]/route.ts +19 -0
  42. package/src/app/(backend)/api/auth/check-user/route.ts +62 -0
  43. package/src/app/(backend)/middleware/auth/index.ts +14 -0
  44. package/src/app/(backend)/middleware/auth/utils.test.ts +16 -0
  45. package/src/app/(backend)/middleware/auth/utils.ts +13 -10
  46. package/src/app/(backend)/webapi/chat/[provider]/route.test.ts +1 -0
  47. package/src/app/[variants]/(auth)/reset-password/layout.tsx +12 -0
  48. package/src/app/[variants]/(auth)/reset-password/page.tsx +209 -0
  49. package/src/app/[variants]/(auth)/signin/layout.tsx +12 -0
  50. package/src/app/[variants]/(auth)/signin/page.tsx +448 -0
  51. package/src/app/[variants]/(auth)/signup/[[...signup]]/BetterAuthSignUpForm.tsx +192 -0
  52. package/src/app/[variants]/(auth)/signup/[[...signup]]/page.tsx +31 -6
  53. package/src/app/[variants]/(auth)/verify-email/layout.tsx +12 -0
  54. package/src/app/[variants]/(auth)/verify-email/page.tsx +164 -0
  55. package/src/app/[variants]/(main)/(mobile)/me/(home)/__tests__/UserBanner.test.tsx +12 -10
  56. package/src/app/[variants]/(main)/(mobile)/me/(home)/__tests__/useCategory.test.tsx +13 -11
  57. package/src/app/[variants]/(main)/chat/components/topic/features/Topic/TopicListContent/TopicItem/TopicContent.tsx +15 -8
  58. package/src/app/[variants]/(main)/chat/components/topic/features/Topic/TopicListContent/TopicItem/index.tsx +27 -30
  59. package/src/app/[variants]/(main)/profile/(home)/Client.tsx +306 -52
  60. package/src/app/[variants]/(main)/profile/(home)/features/SSOProvidersList/index.tsx +89 -47
  61. package/src/auth.ts +118 -0
  62. package/src/components/NextAuth/AuthIcons.tsx +3 -1
  63. package/src/envs/auth.ts +260 -13
  64. package/src/envs/email.ts +37 -0
  65. package/src/features/AgentSetting/AgentPlugin/index.tsx +6 -2
  66. package/src/features/User/UserPanel/PanelContent.tsx +6 -5
  67. package/src/features/User/__tests__/PanelContent.test.tsx +15 -6
  68. package/src/features/User/__tests__/UserAvatar.test.tsx +17 -6
  69. package/src/features/User/__tests__/useMenu.test.tsx +14 -12
  70. package/src/layout/AuthProvider/BetterAuth/UserUpdater.tsx +51 -0
  71. package/src/layout/AuthProvider/BetterAuth/index.tsx +14 -0
  72. package/src/layout/AuthProvider/index.tsx +3 -0
  73. package/src/layout/GlobalProvider/StoreInitialization.tsx +3 -3
  74. package/src/libs/better-auth/auth-client.ts +34 -0
  75. package/src/libs/better-auth/constants.ts +13 -0
  76. package/src/libs/better-auth/email-templates/index.ts +3 -0
  77. package/src/libs/better-auth/email-templates/magic-link.ts +98 -0
  78. package/src/libs/better-auth/email-templates/reset-password.ts +91 -0
  79. package/src/libs/better-auth/email-templates/verification.ts +108 -0
  80. package/src/libs/better-auth/sso/helpers.ts +61 -0
  81. package/src/libs/better-auth/sso/index.ts +113 -0
  82. package/src/libs/better-auth/sso/providers/auth0.ts +33 -0
  83. package/src/libs/better-auth/sso/providers/authelia.ts +35 -0
  84. package/src/libs/better-auth/sso/providers/authentik.ts +35 -0
  85. package/src/libs/better-auth/sso/providers/casdoor.ts +48 -0
  86. package/src/libs/better-auth/sso/providers/cloudflare-zero-trust.ts +41 -0
  87. package/src/libs/better-auth/sso/providers/cognito.ts +45 -0
  88. package/src/libs/better-auth/sso/providers/feishu.ts +181 -0
  89. package/src/libs/better-auth/sso/providers/generic-oidc.ts +44 -0
  90. package/src/libs/better-auth/sso/providers/github.ts +30 -0
  91. package/src/libs/better-auth/sso/providers/google.ts +30 -0
  92. package/src/libs/better-auth/sso/providers/keycloak.ts +35 -0
  93. package/src/libs/better-auth/sso/providers/logto.ts +38 -0
  94. package/src/libs/better-auth/sso/providers/microsoft.ts +65 -0
  95. package/src/libs/better-auth/sso/providers/okta.ts +37 -0
  96. package/src/libs/better-auth/sso/providers/wechat.ts +140 -0
  97. package/src/libs/better-auth/sso/providers/zitadel.ts +54 -0
  98. package/src/libs/better-auth/sso/types.ts +25 -0
  99. package/src/libs/better-auth/utils/client.ts +1 -0
  100. package/src/libs/better-auth/utils/common.ts +20 -0
  101. package/src/libs/better-auth/utils/server.test.ts +61 -0
  102. package/src/libs/better-auth/utils/server.ts +18 -0
  103. package/src/libs/trpc/lambda/context.test.ts +116 -0
  104. package/src/libs/trpc/lambda/context.ts +27 -0
  105. package/src/libs/trpc/middleware/userAuth.ts +4 -2
  106. package/src/locales/default/auth.ts +114 -1
  107. package/src/proxy.ts +71 -7
  108. package/src/server/globalConfig/index.ts +12 -1
  109. package/src/server/routers/lambda/user.ts +4 -0
  110. package/src/server/services/email/README.md +241 -0
  111. package/src/server/services/email/impls/index.test.ts +39 -0
  112. package/src/server/services/email/impls/index.ts +32 -0
  113. package/src/server/services/email/impls/nodemailer/index.ts +108 -0
  114. package/src/server/services/email/impls/nodemailer/type.ts +31 -0
  115. package/src/server/services/email/impls/type.ts +61 -0
  116. package/src/server/services/email/index.test.ts +144 -0
  117. package/src/server/services/email/index.ts +40 -0
  118. package/src/services/user/index.test.ts +162 -2
  119. package/src/services/user/index.ts +6 -3
  120. package/src/store/aiInfra/slices/aiProvider/action.ts +4 -4
  121. package/src/store/user/slices/auth/action.test.ts +213 -16
  122. package/src/store/user/slices/auth/action.ts +86 -1
  123. package/src/store/user/slices/auth/initialState.ts +13 -2
  124. package/src/store/user/slices/auth/selectors.ts +6 -2
  125. package/src/store/user/slices/common/action.ts +5 -1
  126. package/src/app/(backend)/api/auth/[...nextauth]/route.ts +0 -3
@@ -336,6 +336,20 @@
336
336
  "when": 1763987922211,
337
337
  "tag": "0047_add_slug_document",
338
338
  "breakpoints": true
339
+ },
340
+ {
341
+ "idx": 48,
342
+ "version": "7",
343
+ "when": 1764215503726,
344
+ "tag": "0048_add_editor_data",
345
+ "breakpoints": true
346
+ },
347
+ {
348
+ "idx": 49,
349
+ "version": "7",
350
+ "when": 1764229953081,
351
+ "tag": "0049_better_auth",
352
+ "breakpoints": true
339
353
  }
340
354
  ],
341
355
  "version": "6"
@@ -797,5 +797,24 @@
797
797
  "bps": true,
798
798
  "folderMillis": 1763987922211,
799
799
  "hash": "f823b521f4d25e5dc5ab238b372727d2d2d7f0aed27b5eabc8a9608ce4e50568"
800
+ },
801
+ {
802
+ "sql": ["ALTER TABLE \"agents\" ADD COLUMN IF NOT EXISTS \"editor_data\" jsonb;"],
803
+ "bps": true,
804
+ "folderMillis": 1764215503726,
805
+ "hash": "4188893a9083b3c7baebdbad0dd3f9d9400ede7584ca2394f5c64305dc9ec7b0"
806
+ },
807
+ {
808
+ "sql": [
809
+ "CREATE TABLE IF NOT EXISTS \"accounts\" (\n\t\"access_token\" text,\n\t\"access_token_expires_at\" timestamp,\n\t\"account_id\" text NOT NULL,\n\t\"created_at\" timestamp DEFAULT now() NOT NULL,\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"id_token\" text,\n\t\"password\" text,\n\t\"provider_id\" text NOT NULL,\n\t\"refresh_token\" text,\n\t\"refresh_token_expires_at\" timestamp,\n\t\"scope\" text,\n\t\"updated_at\" timestamp NOT NULL,\n\t\"user_id\" text NOT NULL\n);\n",
810
+ "\nCREATE TABLE IF NOT EXISTS \"auth_sessions\" (\n\t\"created_at\" timestamp DEFAULT now() NOT NULL,\n\t\"expires_at\" timestamp NOT NULL,\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"ip_address\" text,\n\t\"token\" text NOT NULL,\n\t\"updated_at\" timestamp NOT NULL,\n\t\"user_agent\" text,\n\t\"user_id\" text NOT NULL,\n\tCONSTRAINT \"auth_sessions_token_unique\" UNIQUE(\"token\")\n);\n",
811
+ "\nCREATE TABLE IF NOT EXISTS \"verifications\" (\n\t\"created_at\" timestamp DEFAULT now() NOT NULL,\n\t\"expires_at\" timestamp NOT NULL,\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"identifier\" text NOT NULL,\n\t\"updated_at\" timestamp DEFAULT now() NOT NULL,\n\t\"value\" text NOT NULL\n);\n",
812
+ "\nALTER TABLE \"users\" ADD COLUMN IF NOT EXISTS \"email_verified\" boolean DEFAULT false NOT NULL;",
813
+ "\nDO $$ BEGIN\n ALTER TABLE \"accounts\" ADD CONSTRAINT \"accounts_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n",
814
+ "\nDO $$ BEGIN\n ALTER TABLE \"auth_sessions\" ADD CONSTRAINT \"auth_sessions_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n"
815
+ ],
816
+ "bps": true,
817
+ "folderMillis": 1764229953081,
818
+ "hash": "1532ebceae7b70550bc9c230fb0a65090aaa773bc7b873eefbc2ce2a815997e2"
800
819
  }
801
820
  ]
@@ -1 +1,2 @@
1
+ export * from './core/db-adaptor';
1
2
  export * from './type';
@@ -1,9 +1,8 @@
1
+ import { DEFAULT_AGENT_CONFIG } from '@lobechat/const';
1
2
  import { and, eq, inArray } from 'drizzle-orm';
2
3
  import { LLMParams } from 'model-bank';
3
4
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
5
 
5
- import { DEFAULT_AGENT_CONFIG } from '@/const/settings';
6
-
7
6
  import {
8
7
  NewSession,
9
8
  SessionItem,
@@ -1,8 +1,13 @@
1
- import { UserGuide, UserKeyVaults, UserPreference, UserSettings } from '@lobechat/types';
1
+ import {
2
+ SSOProvider,
3
+ UserGuide,
4
+ UserKeyVaults,
5
+ UserPreference,
6
+ UserSettings,
7
+ } from '@lobechat/types';
2
8
  import { TRPCError } from '@trpc/server';
3
9
  import dayjs from 'dayjs';
4
10
  import { eq } from 'drizzle-orm';
5
- import type { AdapterAccount } from 'next-auth/adapters';
6
11
  import type { PartialDeep } from 'type-fest';
7
12
 
8
13
  import { merge } from '@/utils/merge';
@@ -126,19 +131,15 @@ export class UserModel {
126
131
  };
127
132
  };
128
133
 
129
- getUserSSOProviders = async () => {
130
- const result = await this.db
134
+ getUserSSOProviders = async (): Promise<SSOProvider[]> => {
135
+ return this.db
131
136
  .select({
132
137
  expiresAt: nextauthAccounts.expires_at,
133
138
  provider: nextauthAccounts.provider,
134
139
  providerAccountId: nextauthAccounts.providerAccountId,
135
- scope: nextauthAccounts.scope,
136
- type: nextauthAccounts.type,
137
- userId: nextauthAccounts.userId,
138
140
  })
139
141
  .from(nextauthAccounts)
140
142
  .where(eq(nextauthAccounts.userId, this.userId));
141
- return result as unknown as AdapterAccount[];
142
143
  };
143
144
 
144
145
  getUserSettings = async () => {
@@ -23,8 +23,8 @@ describe('TableViewerRepo', () => {
23
23
  it('should return all tables with counts', async () => {
24
24
  const result = await repo.getAllTables();
25
25
 
26
- expect(result.length).toEqual(68);
27
- expect(result[0]).toEqual({ name: 'agents', count: 0, type: 'BASE TABLE' });
26
+ expect(result.length).toEqual(71);
27
+ expect(result[0]).toEqual({ name: 'accounts', count: 0, type: 'BASE TABLE' });
28
28
  });
29
29
 
30
30
  it('should handle custom schema', async () => {
@@ -34,6 +34,7 @@ export const agents = pgTable(
34
34
  title: varchar('title', { length: 255 }),
35
35
  description: varchar('description', { length: 1000 }),
36
36
  tags: jsonb('tags').$type<string[]>().default([]),
37
+ editorData: jsonb('editor_data'),
37
38
  avatar: text('avatar'),
38
39
  backgroundColor: text('background_color'),
39
40
  marketIdentifier: text('market_identifier'),
@@ -0,0 +1,63 @@
1
+ import { pgTable, text, timestamp } from 'drizzle-orm/pg-core';
2
+
3
+ import { users } from './user';
4
+
5
+ // export const user = pgTable('betterauth_user', {
6
+ // createdAt: timestamp('created_at').defaultNow().notNull(),
7
+ // email: text('email').notNull().unique(),
8
+ // emailVerified: boolean('email_verified').default(false).notNull(),
9
+ // id: text('id').primaryKey(),
10
+ // image: text('image'),
11
+ // name: text('name').notNull(),
12
+ // updatedAt: timestamp('updated_at')
13
+ // .defaultNow()
14
+ // .$onUpdate(() => /* @__PURE__ */ new Date())
15
+ // .notNull(),
16
+ // });
17
+
18
+ export const session = pgTable('auth_sessions', {
19
+ createdAt: timestamp('created_at').defaultNow().notNull(),
20
+ expiresAt: timestamp('expires_at').notNull(),
21
+ id: text('id').primaryKey(),
22
+ ipAddress: text('ip_address'),
23
+ token: text('token').notNull().unique(),
24
+ updatedAt: timestamp('updated_at')
25
+ .$onUpdate(() => /* @__PURE__ */ new Date())
26
+ .notNull(),
27
+ userAgent: text('user_agent'),
28
+ userId: text('user_id')
29
+ .notNull()
30
+ .references(() => users.id, { onDelete: 'cascade' }),
31
+ });
32
+
33
+ export const account = pgTable('accounts', {
34
+ accessToken: text('access_token'),
35
+ accessTokenExpiresAt: timestamp('access_token_expires_at'),
36
+ accountId: text('account_id').notNull(),
37
+ createdAt: timestamp('created_at').defaultNow().notNull(),
38
+ id: text('id').primaryKey(),
39
+ idToken: text('id_token'),
40
+ password: text('password'),
41
+ providerId: text('provider_id').notNull(),
42
+ refreshToken: text('refresh_token'),
43
+ refreshTokenExpiresAt: timestamp('refresh_token_expires_at'),
44
+ scope: text('scope'),
45
+ updatedAt: timestamp('updated_at')
46
+ .$onUpdate(() => /* @__PURE__ */ new Date())
47
+ .notNull(),
48
+ userId: text('user_id')
49
+ .notNull()
50
+ .references(() => users.id, { onDelete: 'cascade' }),
51
+ });
52
+
53
+ export const verification = pgTable('verifications', {
54
+ createdAt: timestamp('created_at').defaultNow().notNull(),
55
+ expiresAt: timestamp('expires_at').notNull(),
56
+ id: text('id').primaryKey(),
57
+ identifier: text('identifier').notNull(),
58
+ updatedAt: timestamp('updated_at')
59
+ .defaultNow()
60
+ .$onUpdate(() => /* @__PURE__ */ new Date())
61
+ .notNull(),
62
+ value: text('value').notNull(),
63
+ });
@@ -2,6 +2,7 @@ export * from './agent';
2
2
  export * from './aiInfra';
3
3
  export * from './apiKey';
4
4
  export * from './asyncTask';
5
+ export * from './betterAuth';
5
6
  export * from './chatGroup';
6
7
  export * from './file';
7
8
  export * from './generation';
@@ -1,9 +1,8 @@
1
1
  /* eslint-disable sort-keys-fix/sort-keys-fix */
2
+ import { DEFAULT_MODEL } from '@lobechat/const';
2
3
  import { EvalEvaluationStatus } from '@lobechat/types';
3
4
  import { integer, jsonb, pgTable, text, uuid } from 'drizzle-orm/pg-core';
4
5
 
5
- import { DEFAULT_MODEL } from '@/const/settings';
6
-
7
6
  import { timestamps } from './_helpers';
8
7
  import { knowledgeBases } from './file';
9
8
  import { embeddings } from './rag';
@@ -1,10 +1,9 @@
1
1
  /* eslint-disable sort-keys-fix/sort-keys-fix */
2
+ import { DEFAULT_PREFERENCE } from '@lobechat/const';
2
3
  import type { CustomPluginParams } from '@lobechat/types';
3
4
  import { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
4
5
  import { boolean, jsonb, pgTable, primaryKey, text } from 'drizzle-orm/pg-core';
5
6
 
6
- import { DEFAULT_PREFERENCE } from '@/const/user';
7
-
8
7
  import { timestamps, timestamptz } from './_helpers';
9
8
 
10
9
  export const users = pgTable('users', {
@@ -22,6 +21,8 @@ export const users = pgTable('users', {
22
21
  // Time user was created in Clerk
23
22
  clerkCreatedAt: timestamptz('clerk_created_at'),
24
23
 
24
+ // Required by better-auth
25
+ emailVerified: boolean('email_verified').default(false).notNull(),
25
26
  // Required by nextauth, all null allowed
26
27
  emailVerifiedAt: timestamptz('email_verified_at'),
27
28
 
@@ -1,10 +1,10 @@
1
+ import { INBOX_SESSION_ID } from '@lobechat/const';
1
2
  import type { UserGuide, UserPreference } from '@lobechat/types';
2
3
  import { TRPCError } from '@trpc/server';
3
4
  import dayjs from 'dayjs';
4
5
  import { count, eq } from 'drizzle-orm';
5
6
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
6
7
 
7
- import { INBOX_SESSION_ID } from '@/const/session';
8
8
  import { KeyVaultsGateKeeper } from '@/server/modules/KeyVaultsEncrypt';
9
9
 
10
10
  import { getTestDBInstance } from '../../../core/dbForTest';
@@ -428,9 +428,6 @@ describe('UserModel', () => {
428
428
  expect(result[0]).toMatchObject({
429
429
  provider: 'github',
430
430
  providerAccountId: '123456',
431
- type: 'oauth',
432
- userId,
433
- scope: 'user:email',
434
431
  });
435
432
  expect(result[0].expiresAt).toBeDefined();
436
433
  });
@@ -89,6 +89,17 @@ export const NextAuthAccountSchame = z.object({
89
89
  providerAccountId: z.string(),
90
90
  });
91
91
 
92
+ /**
93
+ * SSO Provider info displayed in profile page
94
+ */
95
+ export interface SSOProvider {
96
+ email?: string;
97
+ /** Expiration time - Date for better-auth, number (Unix timestamp) for next-auth */
98
+ expiresAt?: Date | number | null;
99
+ provider: string;
100
+ providerAccountId: string;
101
+ }
102
+
92
103
  export const UserPreferenceSchema = z
93
104
  .object({
94
105
  guide: UserGuideSchema.optional(),
@@ -3,10 +3,14 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
3
3
  import { extractBearerToken, getUserAuth } from '../auth';
4
4
 
5
5
  // Mock auth constants
6
+ let mockEnableBetterAuth = false;
6
7
  let mockEnableClerk = false;
7
8
  let mockEnableNextAuth = false;
8
9
 
9
10
  vi.mock('@/const/auth', () => ({
11
+ get enableBetterAuth() {
12
+ return mockEnableBetterAuth;
13
+ },
10
14
  get enableClerk() {
11
15
  return mockEnableClerk;
12
16
  },
@@ -38,9 +42,26 @@ vi.mock('@/libs/next-auth', () => ({
38
42
  },
39
43
  }));
40
44
 
45
+ vi.mock('next/headers', () => ({
46
+ headers: vi.fn(() => new Headers()),
47
+ }));
48
+
49
+ vi.mock('@/auth', () => ({
50
+ auth: {
51
+ api: {
52
+ getSession: vi.fn().mockResolvedValue({
53
+ user: {
54
+ id: 'better-auth-user-id',
55
+ },
56
+ }),
57
+ },
58
+ },
59
+ }));
60
+
41
61
  describe('getUserAuth', () => {
42
62
  beforeEach(() => {
43
63
  vi.clearAllMocks();
64
+ mockEnableBetterAuth = false;
44
65
  mockEnableClerk = false;
45
66
  mockEnableNextAuth = false;
46
67
  });
@@ -92,6 +113,37 @@ describe('getUserAuth', () => {
92
113
  userId: 'clerk-user-id',
93
114
  });
94
115
  });
116
+
117
+ it('should return better auth when better auth is enabled', async () => {
118
+ mockEnableBetterAuth = true;
119
+
120
+ const auth = await getUserAuth();
121
+
122
+ expect(auth).toEqual({
123
+ betterAuth: {
124
+ user: {
125
+ id: 'better-auth-user-id',
126
+ },
127
+ },
128
+ userId: 'better-auth-user-id',
129
+ });
130
+ });
131
+
132
+ it('should prioritize better auth over next auth when both are enabled', async () => {
133
+ mockEnableBetterAuth = true;
134
+ mockEnableNextAuth = true;
135
+
136
+ const auth = await getUserAuth();
137
+
138
+ expect(auth).toEqual({
139
+ betterAuth: {
140
+ user: {
141
+ id: 'better-auth-user-id',
142
+ },
143
+ },
144
+ userId: 'better-auth-user-id',
145
+ });
146
+ });
95
147
  });
96
148
 
97
149
  describe('extractBearerToken', () => {
@@ -1,4 +1,6 @@
1
- import { enableClerk, enableNextAuth } from '@/const/auth';
1
+ import { headers } from 'next/headers';
2
+
3
+ import { enableBetterAuth, enableClerk, enableNextAuth } from '@/const/auth';
2
4
  import { DESKTOP_USER_ID } from '@/const/desktop';
3
5
  import { isDesktop } from '@/const/version';
4
6
 
@@ -11,6 +13,21 @@ export const getUserAuth = async () => {
11
13
  return await clerkAuth.getAuth();
12
14
  }
13
15
 
16
+ if (enableBetterAuth) {
17
+ const { auth: betterAuth } = await import('@/auth');
18
+
19
+ const currentHeaders = await headers();
20
+ const requestHeaders = Object.fromEntries(currentHeaders.entries());
21
+
22
+ const session = await betterAuth.api.getSession({
23
+ headers: requestHeaders,
24
+ });
25
+
26
+ const userId = session?.user?.id;
27
+
28
+ return { betterAuth: session, userId };
29
+ }
30
+
14
31
  if (enableNextAuth) {
15
32
  const { default: NextAuth } = await import('@/libs/next-auth');
16
33
 
@@ -0,0 +1,19 @@
1
+ import { enableBetterAuth, enableNextAuth } from '@lobechat/const';
2
+ import { toNextJsHandler } from 'better-auth/next-js';
3
+
4
+ import { auth } from '@/auth';
5
+ import NextAuthNode from '@/libs/next-auth';
6
+
7
+ const betterAuthHandler = toNextJsHandler(auth);
8
+
9
+ export const GET = enableBetterAuth
10
+ ? betterAuthHandler.GET
11
+ : enableNextAuth
12
+ ? NextAuthNode.handlers.GET
13
+ : undefined;
14
+
15
+ export const POST = enableBetterAuth
16
+ ? betterAuthHandler.POST
17
+ : enableNextAuth
18
+ ? NextAuthNode.handlers.POST
19
+ : undefined;
@@ -0,0 +1,62 @@
1
+ import { and, eq } from 'drizzle-orm';
2
+ import { NextRequest, NextResponse } from 'next/server';
3
+
4
+ import { account } from '@/database/schemas/betterAuth';
5
+ import { users } from '@/database/schemas/user';
6
+ import { serverDB } from '@/database/server';
7
+
8
+ /**
9
+ * Check if a user exists by email
10
+ * @param req - POST request with { email: string }
11
+ * @returns { exists: boolean, emailVerified?: boolean }
12
+ */
13
+ export async function POST(req: NextRequest) {
14
+ try {
15
+ const body = await req.json();
16
+ const { email } = body;
17
+
18
+ if (!email || typeof email !== 'string') {
19
+ return NextResponse.json({ error: 'Email is required', exists: false }, { status: 400 });
20
+ }
21
+
22
+ // Query database for user with this email
23
+ const [user] = await serverDB
24
+ .select({
25
+ emailVerified: users.emailVerified,
26
+ id: users.id,
27
+ })
28
+ .from(users)
29
+ .where(eq(users.email, email.toLowerCase().trim()))
30
+ .limit(1);
31
+
32
+ if (!user) {
33
+ return NextResponse.json({ exists: false });
34
+ }
35
+
36
+ const accounts = await serverDB
37
+ .select({
38
+ password: account.password,
39
+ providerId: account.providerId,
40
+ })
41
+ .from(account)
42
+ .where(and(eq(account.userId, user.id)));
43
+
44
+ const providers = Array.from(new Set(accounts.map((a) => a.providerId).filter(Boolean)));
45
+ const hasPassword = accounts.some(
46
+ (a) =>
47
+ a.providerId === 'credential' && typeof a.password === 'string' && a.password.length > 0,
48
+ );
49
+
50
+ return NextResponse.json({
51
+ emailVerified: user.emailVerified,
52
+ exists: true,
53
+ hasPassword,
54
+ providers,
55
+ });
56
+ } catch (error) {
57
+ console.error('Error checking user existence:', error);
58
+ return NextResponse.json({ error: 'Internal server error', exists: false }, { status: 500 });
59
+ }
60
+ }
61
+
62
+ export const runtime = 'nodejs';
@@ -12,6 +12,7 @@ import {
12
12
  LOBE_CHAT_AUTH_HEADER,
13
13
  LOBE_CHAT_OIDC_AUTH_HEADER,
14
14
  OAUTH_AUTHORIZED,
15
+ enableBetterAuth,
15
16
  enableClerk,
16
17
  } from '@/const/auth';
17
18
  import { ClerkAuth } from '@/libs/clerk-auth';
@@ -49,6 +50,18 @@ export const checkAuth =
49
50
  // get Authorization from header
50
51
  const authorization = req.headers.get(LOBE_CHAT_AUTH_HEADER);
51
52
  const oauthAuthorized = !!req.headers.get(OAUTH_AUTHORIZED);
53
+ let betterAuthAuthorized = false;
54
+
55
+ // better auth handler
56
+ if (enableBetterAuth) {
57
+ const { auth: betterAuth } = await import('@/auth');
58
+
59
+ const session = await betterAuth.api.getSession({
60
+ headers: req.headers,
61
+ });
62
+
63
+ betterAuthAuthorized = !!session?.user?.id;
64
+ }
52
65
 
53
66
  if (!authorization) throw AgentRuntimeError.createError(ChatErrorType.Unauthorized);
54
67
 
@@ -81,6 +94,7 @@ export const checkAuth =
81
94
  checkAuthMethod({
82
95
  accessCode: jwtPayload.accessCode,
83
96
  apiKey: jwtPayload.apiKey,
97
+ betterAuthAuthorized,
84
98
  clerkAuth,
85
99
  nextAuthAuthorized: oauthAuthorized,
86
100
  });
@@ -7,6 +7,7 @@ import { checkAuthMethod } from './utils';
7
7
 
8
8
  let enableClerkMock = false;
9
9
  let enableNextAuthMock = false;
10
+ let enableBetterAuthMock = false;
10
11
 
11
12
  vi.mock('@/const/auth', async (importOriginal) => {
12
13
  const data = await importOriginal();
@@ -16,6 +17,9 @@ vi.mock('@/const/auth', async (importOriginal) => {
16
17
  get enableClerk() {
17
18
  return enableClerkMock;
18
19
  },
20
+ get enableBetterAuth() {
21
+ return enableBetterAuthMock;
22
+ },
19
23
  get enableNextAuth() {
20
24
  return enableNextAuthMock;
21
25
  },
@@ -67,6 +71,18 @@ describe('checkAuthMethod', () => {
67
71
  enableNextAuthMock = false;
68
72
  });
69
73
 
74
+ it('should pass with valid Better Auth session', () => {
75
+ enableBetterAuthMock = true;
76
+
77
+ expect(() =>
78
+ checkAuthMethod({
79
+ betterAuthAuthorized: true,
80
+ }),
81
+ ).not.toThrow();
82
+
83
+ enableBetterAuthMock = false;
84
+ });
85
+
70
86
  it('should pass with valid API key', () => {
71
87
  expect(() =>
72
88
  checkAuthMethod({
@@ -2,29 +2,29 @@ import { type AuthObject } from '@clerk/backend';
2
2
  import { AgentRuntimeError } from '@lobechat/model-runtime';
3
3
  import { ChatErrorType } from '@lobechat/types';
4
4
 
5
- import { enableClerk, enableNextAuth } from '@/const/auth';
5
+ import { enableBetterAuth, enableClerk, enableNextAuth } from '@/const/auth';
6
6
  import { getAppConfig } from '@/envs/app';
7
7
 
8
8
  interface CheckAuthParams {
9
9
  accessCode?: string;
10
10
  apiKey?: string;
11
+ betterAuthAuthorized?: boolean;
11
12
  clerkAuth?: AuthObject;
12
13
  nextAuthAuthorized?: boolean;
13
14
  }
14
15
  /**
15
16
  * Check if the provided access code is valid, a user API key should be used or the OAuth 2 header is provided.
16
17
  *
17
- * @param {string} accessCode - The access code to check.
18
- * @param {string} apiKey - The user API key.
19
- * @param {boolean} oauthAuthorized - Whether the OAuth 2 header is provided.
18
+ * @param {CheckAuthParams} params - Authentication parameters extracted from headers.
19
+ * @param {string} [params.accessCode] - The access code to check.
20
+ * @param {string} [params.apiKey] - The user API key.
21
+ * @param {boolean} [params.betterAuthAuthorized] - Whether the Better Auth session exists.
22
+ * @param {AuthObject} [params.clerkAuth] - Clerk authentication payload from middleware.
23
+ * @param {boolean} [params.nextAuthAuthorized] - Whether the OAuth 2 header is provided.
20
24
  * @throws {AgentRuntimeError} If the access code is invalid and no user API key is provided.
21
25
  */
22
- export const checkAuthMethod = ({
23
- apiKey,
24
- nextAuthAuthorized,
25
- accessCode,
26
- clerkAuth,
27
- }: CheckAuthParams) => {
26
+ export const checkAuthMethod = (params: CheckAuthParams) => {
27
+ const { apiKey, betterAuthAuthorized, nextAuthAuthorized, accessCode, clerkAuth } = params;
28
28
  // clerk auth handler
29
29
  if (enableClerk) {
30
30
  // if there is no userId, means the use is not login, just throw error
@@ -34,6 +34,9 @@ export const checkAuthMethod = ({
34
34
  else return;
35
35
  }
36
36
 
37
+ // if better auth session exists
38
+ if (enableBetterAuth && betterAuthAuthorized) return;
39
+
37
40
  // if next auth handler is provided
38
41
  if (enableNextAuth && nextAuthAuthorized) return;
39
42
 
@@ -135,6 +135,7 @@ describe('POST handler', () => {
135
135
  expect(checkAuthMethod).toBeCalledWith({
136
136
  accessCode: 'test-access-code',
137
137
  apiKey: 'test-api-key',
138
+ betterAuthAuthorized: false,
138
139
  clerkAuth: {},
139
140
  nextAuthAuthorized: true,
140
141
  });
@@ -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;