@lobehub/lobehub 2.0.0-next.124 → 2.0.0-next.126
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor/rules/db-migrations.mdc +16 -1
- package/.cursor/rules/project-introduce.mdc +1 -1
- package/.cursor/rules/project-structure.mdc +20 -2
- package/.env.example +148 -65
- package/.env.example.development +6 -8
- package/AGENTS.md +1 -3
- package/CHANGELOG.md +50 -0
- package/Dockerfile +7 -5
- package/GEMINI.md +63 -0
- package/changelog/v1.json +18 -0
- package/docs/development/database-schema.dbml +37 -0
- package/docs/self-hosting/advanced/auth.mdx +82 -2
- package/docs/self-hosting/advanced/auth.zh-CN.mdx +82 -2
- package/docs/self-hosting/environment-variables/auth.mdx +187 -1
- package/docs/self-hosting/environment-variables/auth.zh-CN.mdx +187 -1
- package/locales/en-US/auth.json +93 -0
- package/locales/zh-CN/auth.json +107 -1
- package/package.json +5 -2
- package/packages/const/src/auth.ts +2 -1
- package/packages/database/migrations/0049_better_auth.sql +49 -0
- package/packages/database/migrations/meta/0048_snapshot.json +312 -932
- package/packages/database/migrations/meta/0049_snapshot.json +8151 -0
- package/packages/database/migrations/meta/_journal.json +8 -1
- package/packages/database/src/core/migrations.json +13 -0
- package/packages/database/src/index.ts +1 -0
- package/packages/database/src/models/__tests__/session.test.ts +1 -2
- package/packages/database/src/models/user.ts +9 -8
- package/packages/database/src/repositories/tableViewer/index.test.ts +2 -2
- package/packages/database/src/schemas/betterAuth.ts +63 -0
- package/packages/database/src/schemas/index.ts +1 -0
- package/packages/database/src/schemas/ragEvals.ts +1 -2
- package/packages/database/src/schemas/user.ts +3 -2
- package/packages/database/src/server/models/__tests__/user.test.ts +1 -4
- package/packages/types/src/user/preference.ts +11 -0
- package/packages/utils/src/server/__tests__/auth.test.ts +52 -0
- package/packages/utils/src/server/auth.ts +18 -1
- package/src/app/(backend)/api/auth/[...all]/route.ts +19 -0
- package/src/app/(backend)/api/auth/check-user/route.ts +62 -0
- package/src/app/(backend)/middleware/auth/index.ts +14 -0
- package/src/app/(backend)/middleware/auth/utils.test.ts +16 -0
- package/src/app/(backend)/middleware/auth/utils.ts +13 -10
- package/src/app/(backend)/webapi/chat/[provider]/route.test.ts +1 -0
- package/src/app/[variants]/(auth)/reset-password/layout.tsx +12 -0
- package/src/app/[variants]/(auth)/reset-password/page.tsx +209 -0
- package/src/app/[variants]/(auth)/signin/layout.tsx +12 -0
- package/src/app/[variants]/(auth)/signin/page.tsx +448 -0
- package/src/app/[variants]/(auth)/signup/[[...signup]]/BetterAuthSignUpForm.tsx +192 -0
- package/src/app/[variants]/(auth)/signup/[[...signup]]/page.tsx +31 -6
- package/src/app/[variants]/(auth)/verify-email/layout.tsx +12 -0
- package/src/app/[variants]/(auth)/verify-email/page.tsx +164 -0
- package/src/app/[variants]/(main)/(mobile)/me/(home)/__tests__/UserBanner.test.tsx +12 -10
- package/src/app/[variants]/(main)/(mobile)/me/(home)/__tests__/useCategory.test.tsx +13 -11
- package/src/app/[variants]/(main)/profile/(home)/Client.tsx +306 -52
- package/src/app/[variants]/(main)/profile/(home)/features/SSOProvidersList/index.tsx +89 -47
- package/src/auth.ts +118 -0
- package/src/components/NextAuth/AuthIcons.tsx +3 -1
- package/src/envs/auth.ts +260 -13
- package/src/envs/email.ts +37 -0
- package/src/features/User/UserPanel/PanelContent.tsx +6 -5
- package/src/features/User/__tests__/PanelContent.test.tsx +15 -6
- package/src/features/User/__tests__/UserAvatar.test.tsx +17 -6
- package/src/features/User/__tests__/useMenu.test.tsx +14 -12
- package/src/layout/AuthProvider/BetterAuth/UserUpdater.tsx +51 -0
- package/src/layout/AuthProvider/BetterAuth/index.tsx +14 -0
- package/src/layout/AuthProvider/index.tsx +3 -0
- package/src/libs/better-auth/auth-client.ts +34 -0
- package/src/libs/better-auth/constants.ts +13 -0
- package/src/libs/better-auth/email-templates/index.ts +3 -0
- package/src/libs/better-auth/email-templates/magic-link.ts +98 -0
- package/src/libs/better-auth/email-templates/reset-password.ts +91 -0
- package/src/libs/better-auth/email-templates/verification.ts +108 -0
- package/src/libs/better-auth/sso/helpers.ts +61 -0
- package/src/libs/better-auth/sso/index.ts +113 -0
- package/src/libs/better-auth/sso/providers/auth0.ts +33 -0
- package/src/libs/better-auth/sso/providers/authelia.ts +35 -0
- package/src/libs/better-auth/sso/providers/authentik.ts +35 -0
- package/src/libs/better-auth/sso/providers/casdoor.ts +48 -0
- package/src/libs/better-auth/sso/providers/cloudflare-zero-trust.ts +41 -0
- package/src/libs/better-auth/sso/providers/cognito.ts +45 -0
- package/src/libs/better-auth/sso/providers/feishu.ts +181 -0
- package/src/libs/better-auth/sso/providers/generic-oidc.ts +44 -0
- package/src/libs/better-auth/sso/providers/github.ts +30 -0
- package/src/libs/better-auth/sso/providers/google.ts +30 -0
- package/src/libs/better-auth/sso/providers/keycloak.ts +35 -0
- package/src/libs/better-auth/sso/providers/logto.ts +38 -0
- package/src/libs/better-auth/sso/providers/microsoft.ts +65 -0
- package/src/libs/better-auth/sso/providers/okta.ts +37 -0
- package/src/libs/better-auth/sso/providers/wechat.ts +140 -0
- package/src/libs/better-auth/sso/providers/zitadel.ts +54 -0
- package/src/libs/better-auth/sso/types.ts +25 -0
- package/src/libs/better-auth/utils/client.ts +1 -0
- package/src/libs/better-auth/utils/common.ts +20 -0
- package/src/libs/better-auth/utils/server.test.ts +61 -0
- package/src/libs/better-auth/utils/server.ts +18 -0
- package/src/libs/trpc/lambda/context.test.ts +116 -0
- package/src/libs/trpc/lambda/context.ts +27 -0
- package/src/libs/trpc/middleware/userAuth.ts +4 -2
- package/src/locales/default/auth.ts +114 -1
- package/src/proxy.ts +71 -7
- package/src/server/globalConfig/index.ts +12 -1
- package/src/server/routers/lambda/user.ts +4 -0
- package/src/server/services/email/README.md +241 -0
- package/src/server/services/email/impls/index.test.ts +39 -0
- package/src/server/services/email/impls/index.ts +32 -0
- package/src/server/services/email/impls/nodemailer/index.ts +108 -0
- package/src/server/services/email/impls/nodemailer/type.ts +31 -0
- package/src/server/services/email/impls/type.ts +61 -0
- package/src/server/services/email/index.test.ts +144 -0
- package/src/server/services/email/index.ts +40 -0
- package/src/services/user/index.test.ts +162 -2
- package/src/services/user/index.ts +6 -3
- package/src/store/user/slices/auth/action.test.ts +213 -16
- package/src/store/user/slices/auth/action.ts +86 -1
- package/src/store/user/slices/auth/initialState.ts +13 -2
- package/src/store/user/slices/auth/selectors.ts +6 -2
- package/src/store/user/slices/common/action.ts +5 -1
- package/src/app/(backend)/api/auth/[...nextauth]/route.ts +0 -3
|
@@ -343,7 +343,14 @@
|
|
|
343
343
|
"when": 1764215503726,
|
|
344
344
|
"tag": "0048_add_editor_data",
|
|
345
345
|
"breakpoints": true
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
"idx": 49,
|
|
349
|
+
"version": "7",
|
|
350
|
+
"when": 1764229953081,
|
|
351
|
+
"tag": "0049_better_auth",
|
|
352
|
+
"breakpoints": true
|
|
346
353
|
}
|
|
347
354
|
],
|
|
348
355
|
"version": "6"
|
|
349
|
-
}
|
|
356
|
+
}
|
|
@@ -803,5 +803,18 @@
|
|
|
803
803
|
"bps": true,
|
|
804
804
|
"folderMillis": 1764215503726,
|
|
805
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"
|
|
806
819
|
}
|
|
807
820
|
]
|
|
@@ -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 {
|
|
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
|
-
|
|
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(
|
|
27
|
-
expect(result[0]).toEqual({ name: '
|
|
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 () => {
|
|
@@ -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
|
+
});
|
|
@@ -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 {
|
|
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 {
|
|
18
|
-
* @param {string}
|
|
19
|
-
* @param {
|
|
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
|
|
|
@@ -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;
|