@lobehub/lobehub 2.0.0-next.354 → 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.
- package/.env.desktop +0 -1
- package/.env.example +16 -20
- package/.env.example.development +1 -4
- package/.github/workflows/e2e.yml +10 -11
- package/CHANGELOG.md +60 -0
- package/Dockerfile +28 -4
- package/changelog/v1.json +18 -0
- package/docker-compose/local/docker-compose.yml +2 -2
- package/docker-compose/local/grafana/docker-compose.yml +2 -2
- package/docker-compose/local/logto/docker-compose.yml +2 -2
- package/docker-compose/local/zitadel/.env.example +2 -2
- package/docker-compose/local/zitadel/.env.zh-CN.example +2 -2
- package/docker-compose/production/grafana/docker-compose.yml +2 -2
- package/docker-compose/production/logto/.env.example +2 -2
- package/docker-compose/production/logto/.env.zh-CN.example +2 -2
- package/docker-compose/production/zitadel/.env.example +2 -2
- package/docker-compose/production/zitadel/.env.zh-CN.example +2 -2
- package/docs/development/basic/add-new-authentication-providers.mdx +144 -136
- package/docs/development/basic/add-new-authentication-providers.zh-CN.mdx +146 -136
- package/docs/self-hosting/advanced/auth/legacy.mdx +4 -0
- package/docs/self-hosting/advanced/auth/legacy.zh-CN.mdx +4 -0
- package/docs/self-hosting/advanced/auth/nextauth-to-betterauth.mdx +326 -0
- package/docs/self-hosting/advanced/auth/nextauth-to-betterauth.zh-CN.mdx +323 -0
- package/docs/self-hosting/advanced/auth.mdx +43 -16
- package/docs/self-hosting/advanced/auth.zh-CN.mdx +44 -16
- package/docs/self-hosting/advanced/redis/upstash.mdx +69 -0
- package/docs/self-hosting/advanced/redis/upstash.zh-CN.mdx +69 -0
- package/docs/self-hosting/advanced/redis.mdx +128 -0
- package/docs/self-hosting/advanced/redis.zh-CN.mdx +126 -0
- package/docs/self-hosting/environment-variables/auth.mdx +15 -1
- package/docs/self-hosting/environment-variables/auth.zh-CN.mdx +15 -1
- package/docs/self-hosting/environment-variables/basic.mdx +13 -0
- package/docs/self-hosting/environment-variables/basic.zh-CN.mdx +13 -0
- package/docs/self-hosting/environment-variables/redis.mdx +68 -0
- package/docs/self-hosting/environment-variables/redis.zh-CN.mdx +67 -0
- package/docs/self-hosting/migration/v2/breaking-changes.mdx +23 -23
- package/docs/self-hosting/migration/v2/breaking-changes.zh-CN.mdx +23 -23
- package/docs/self-hosting/server-database/docker-compose.mdx +4 -4
- package/docs/self-hosting/server-database/docker-compose.zh-CN.mdx +4 -4
- package/e2e/CLAUDE.md +5 -6
- package/e2e/docs/local-setup.md +9 -12
- package/e2e/scripts/setup.ts +9 -15
- package/e2e/src/support/webServer.ts +6 -5
- package/locales/en-US/plugin.json +3 -0
- package/locales/zh-CN/plugin.json +3 -0
- package/package.json +4 -6
- package/packages/builtin-tool-memory/src/client/Render/SearchUserMemory/index.tsx +3 -11
- package/packages/context-engine/src/engine/messages/MessagesEngine.ts +0 -13
- package/packages/context-engine/src/engine/messages/__tests__/MessagesEngine.test.ts +0 -25
- package/packages/database/src/models/__tests__/topics/topic.create.test.ts +3 -3
- package/packages/database/src/schemas/nextauth.ts +7 -2
- package/packages/utils/src/server/__tests__/auth.test.ts +1 -63
- package/packages/utils/src/server/auth.ts +8 -24
- package/scripts/_shared/checkDeprecatedAuth.js +99 -0
- package/scripts/clerk-to-betterauth/index.ts +8 -3
- package/scripts/nextauth-to-betterauth/_internal/config.ts +41 -0
- package/scripts/nextauth-to-betterauth/_internal/db.ts +32 -0
- package/scripts/nextauth-to-betterauth/_internal/env.ts +6 -0
- package/scripts/nextauth-to-betterauth/index.ts +226 -0
- package/scripts/nextauth-to-betterauth/verify.ts +188 -0
- package/scripts/prebuild.mts +66 -13
- package/scripts/serverLauncher/startServer.js +5 -5
- package/src/app/(backend)/api/auth/[...all]/route.ts +5 -23
- package/src/app/(backend)/api/webhooks/casdoor/route.ts +5 -5
- package/src/app/(backend)/api/webhooks/logto/route.ts +8 -8
- package/src/app/(backend)/middleware/auth/index.test.ts +8 -1
- package/src/app/(backend)/middleware/auth/index.ts +6 -15
- package/src/app/(backend)/middleware/auth/utils.test.ts +0 -32
- package/src/app/(backend)/middleware/auth/utils.ts +3 -8
- package/src/app/(backend)/webapi/chat/[provider]/route.test.ts +8 -1
- package/src/app/(backend)/webapi/create-image/comfyui/route.ts +0 -1
- package/src/app/(backend)/webapi/models/[provider]/route.test.ts +8 -1
- package/src/app/[variants]/(auth)/signin/SignInEmailStep.tsx +1 -1
- package/src/app/[variants]/(auth)/signup/[[...signup]]/page.tsx +4 -17
- package/src/app/[variants]/(main)/agent/_layout/Sidebar/Topic/List/Item/index.tsx +1 -0
- package/src/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobContentEditor.tsx +34 -21
- package/src/app/[variants]/(main)/agent/features/Conversation/ConversationArea.tsx +4 -0
- package/src/app/[variants]/(main)/group/_layout/Sidebar/Topic/List/Item/index.tsx +1 -0
- package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentItem/index.tsx +1 -1
- package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/InboxItem.tsx +19 -29
- package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/List.tsx +1 -1
- package/src/app/[variants]/(main)/home/_layout/Body/Agent/ModalProvider.tsx +1 -1
- package/src/app/[variants]/(main)/settings/profile/features/SSOProvidersList/index.tsx +12 -19
- package/src/app/[variants]/(main)/settings/profile/index.tsx +8 -14
- package/src/components/{NextAuth/AuthIcons.tsx → AuthIcons.tsx} +8 -10
- package/src/envs/auth.ts +12 -51
- package/src/envs/email.ts +3 -0
- package/src/envs/redis.ts +12 -54
- package/src/features/ChatInput/ChatInputProvider.tsx +22 -2
- package/src/features/ChatInput/InputEditor/index.tsx +14 -3
- package/src/features/ChatInput/store/initialState.ts +2 -0
- package/src/features/User/__tests__/PanelContent.test.tsx +0 -11
- package/src/features/User/__tests__/UserAvatar.test.tsx +1 -16
- package/src/layout/AuthProvider/index.tsx +1 -6
- package/src/layout/GlobalProvider/StoreInitialization.tsx +2 -4
- package/src/libs/better-auth/define-config.ts +2 -0
- package/src/libs/better-auth/plugins/email-whitelist.test.ts +120 -0
- package/src/libs/better-auth/plugins/email-whitelist.ts +62 -0
- package/src/libs/next/config/define-config.ts +13 -1
- package/src/libs/next/proxy/define-config.ts +2 -75
- package/src/libs/oidc-provider/provider.test.ts +0 -4
- package/src/libs/redis/index.ts +0 -1
- package/src/libs/redis/manager.test.ts +9 -45
- package/src/libs/redis/manager.ts +2 -16
- package/src/libs/redis/redis.test.ts +2 -4
- package/src/libs/redis/redis.ts +2 -4
- package/src/libs/redis/types.ts +2 -24
- package/src/libs/redis/utils.test.ts +0 -10
- package/src/libs/redis/utils.ts +0 -19
- package/src/libs/trpc/lambda/context.test.ts +0 -13
- package/src/libs/trpc/lambda/context.ts +21 -59
- package/src/libs/trpc/middleware/userAuth.ts +1 -7
- package/src/libs/trusted-client/getSessionUser.ts +15 -35
- package/src/locales/default/plugin.ts +3 -0
- package/src/server/globalConfig/index.ts +1 -3
- package/src/server/modules/Mecha/ContextEngineering/__tests__/serverMessagesEngine.test.ts +0 -25
- package/src/server/routers/lambda/__tests__/user.test.ts +0 -48
- package/src/server/routers/lambda/user.ts +1 -12
- package/src/server/services/email/impls/nodemailer/index.ts +2 -2
- package/src/server/services/webhookUser/index.ts +88 -0
- package/src/services/chat/chat.test.ts +19 -19
- package/src/services/chat/index.ts +8 -3
- package/src/services/chat/mecha/agentConfigResolver.test.ts +72 -55
- package/src/services/chat/mecha/agentConfigResolver.ts +28 -4
- package/src/services/chat/mecha/contextEngineering.test.ts +21 -14
- package/src/services/chat/mecha/contextEngineering.ts +12 -0
- package/src/services/chat/types.ts +7 -1
- package/src/services/user/index.test.ts +0 -14
- package/src/services/user/index.ts +0 -4
- package/src/store/chat/agents/createAgentExecutors.ts +15 -4
- package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +1 -0
- package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +6 -2
- package/src/store/user/slices/auth/action.test.ts +22 -126
- package/src/store/user/slices/auth/action.ts +32 -65
- package/src/store/user/slices/auth/initialState.ts +0 -3
- package/src/store/user/slices/auth/selectors.ts +0 -3
- package/tests/setup.ts +10 -0
- package/scripts/_shared/checkDeprecatedClerkEnv.js +0 -42
- package/src/app/(backend)/api/auth/adapter/route.ts +0 -137
- package/src/app/[variants]/(auth)/next-auth/error/AuthErrorPage.tsx +0 -40
- package/src/app/[variants]/(auth)/next-auth/error/page.tsx +0 -11
- package/src/app/[variants]/(auth)/next-auth/signin/AuthSignInBox.tsx +0 -167
- package/src/app/[variants]/(auth)/next-auth/signin/page.tsx +0 -11
- package/src/app/[variants]/(auth)/reset-password/layout.tsx +0 -12
- package/src/app/[variants]/(auth)/signin/layout.tsx +0 -12
- package/src/app/[variants]/(auth)/verify-email/layout.tsx +0 -12
- package/src/envs/auth.test.ts +0 -47
- package/src/layout/AuthProvider/NextAuth/UserUpdater.tsx +0 -44
- package/src/layout/AuthProvider/NextAuth/index.tsx +0 -17
- package/src/libs/next-auth/adapter/index.ts +0 -177
- package/src/libs/next-auth/auth.config.ts +0 -64
- package/src/libs/next-auth/index.ts +0 -20
- package/src/libs/next-auth/sso-providers/auth0.ts +0 -24
- package/src/libs/next-auth/sso-providers/authelia.ts +0 -39
- package/src/libs/next-auth/sso-providers/authentik.ts +0 -25
- package/src/libs/next-auth/sso-providers/casdoor.ts +0 -50
- package/src/libs/next-auth/sso-providers/cloudflare-zero-trust.ts +0 -34
- package/src/libs/next-auth/sso-providers/cognito.ts +0 -8
- package/src/libs/next-auth/sso-providers/feishu.ts +0 -83
- package/src/libs/next-auth/sso-providers/generic-oidc.ts +0 -38
- package/src/libs/next-auth/sso-providers/github.ts +0 -23
- package/src/libs/next-auth/sso-providers/google.ts +0 -18
- package/src/libs/next-auth/sso-providers/index.ts +0 -35
- package/src/libs/next-auth/sso-providers/keycloak.ts +0 -22
- package/src/libs/next-auth/sso-providers/logto.ts +0 -48
- package/src/libs/next-auth/sso-providers/microsoft-entra-id-helper.ts +0 -29
- package/src/libs/next-auth/sso-providers/microsoft-entra-id.ts +0 -19
- package/src/libs/next-auth/sso-providers/okta.ts +0 -22
- package/src/libs/next-auth/sso-providers/sso.config.ts +0 -8
- package/src/libs/next-auth/sso-providers/wechat.ts +0 -36
- package/src/libs/next-auth/sso-providers/zitadel.ts +0 -21
- package/src/libs/redis/upstash.test.ts +0 -158
- package/src/libs/redis/upstash.ts +0 -136
- package/src/server/services/nextAuthUser/index.ts +0 -318
- package/src/server/services/nextAuthUser/utils.ts +0 -62
- package/src/types/next-auth.d.ts +0 -26
|
@@ -113,31 +113,6 @@ describe('MessagesEngine', () => {
|
|
|
113
113
|
expect(result.messages[0].content).toBe(systemRole);
|
|
114
114
|
});
|
|
115
115
|
|
|
116
|
-
it('should truncate history when enabled', async () => {
|
|
117
|
-
const messages: UIChatMessage[] = [];
|
|
118
|
-
for (let i = 0; i < 20; i++) {
|
|
119
|
-
messages.push({
|
|
120
|
-
content: `Message ${i}`,
|
|
121
|
-
createdAt: Date.now(),
|
|
122
|
-
id: `msg-${i}`,
|
|
123
|
-
role: i % 2 === 0 ? 'user' : 'assistant',
|
|
124
|
-
updatedAt: Date.now(),
|
|
125
|
-
} as UIChatMessage);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const params = createBasicParams({
|
|
129
|
-
enableHistoryCount: true,
|
|
130
|
-
historyCount: 5,
|
|
131
|
-
messages,
|
|
132
|
-
});
|
|
133
|
-
const engine = new MessagesEngine(params);
|
|
134
|
-
|
|
135
|
-
const result = await engine.process();
|
|
136
|
-
|
|
137
|
-
// Should have truncated to 5 messages
|
|
138
|
-
expect(result.messages.length).toBeLessThanOrEqual(5);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
116
|
it('should inject history summary when provided', async () => {
|
|
142
117
|
const historySummary = 'We discussed AI and machine learning';
|
|
143
118
|
const params = createBasicParams({ historySummary });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { eq, inArray } from 'drizzle-orm';
|
|
1
|
+
import { asc, eq, inArray } from 'drizzle-orm';
|
|
2
2
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
3
3
|
|
|
4
4
|
import { getTestDB } from '../../../core/getTestDB';
|
|
@@ -188,12 +188,12 @@ describe('TopicModel - Create', () => {
|
|
|
188
188
|
userId,
|
|
189
189
|
});
|
|
190
190
|
|
|
191
|
-
const items = await serverDB.select().from(topics);
|
|
191
|
+
const items = await serverDB.select().from(topics).orderBy(asc(topics.title));
|
|
192
192
|
expect(items).toHaveLength(2);
|
|
193
193
|
expect(items[0]).toMatchObject({ title: 'Topic 1', favorite: true, sessionId, userId });
|
|
194
194
|
expect(items[1]).toMatchObject({ title: 'Topic 2', favorite: false, sessionId, userId });
|
|
195
195
|
|
|
196
|
-
const updatedMessages = await serverDB.select().from(messages);
|
|
196
|
+
const updatedMessages = await serverDB.select().from(messages).orderBy(asc(messages.id));
|
|
197
197
|
expect(updatedMessages).toHaveLength(3);
|
|
198
198
|
expect(updatedMessages[0].topicId).toBe(createdTopics[0].id);
|
|
199
199
|
expect(updatedMessages[1].topicId).toBe(createdTopics[0].id);
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { boolean, integer, pgTable, primaryKey, text, timestamp } from 'drizzle-orm/pg-core';
|
|
2
|
-
import { AdapterAccount } from 'next-auth/adapters';
|
|
3
2
|
|
|
4
3
|
import { users } from './user';
|
|
5
4
|
|
|
5
|
+
/**
|
|
6
|
+
* NextAuth account type (oauth, email, credentials, etc.)
|
|
7
|
+
* Previously imported from next-auth/adapters, now defined locally to remove dependency
|
|
8
|
+
*/
|
|
9
|
+
type AccountType = 'credentials' | 'email' | 'oauth' | 'oidc' | 'webauthn';
|
|
10
|
+
|
|
6
11
|
/**
|
|
7
12
|
* This table stores nextauth accounts. This is used to link users to their sso profiles.
|
|
8
13
|
* @see {@link https://authjs.dev/guides/creating-a-database-adapter#database-session-management | NextAuth Doc}
|
|
@@ -19,7 +24,7 @@ export const nextauthAccounts = pgTable(
|
|
|
19
24
|
scope: text('scope'),
|
|
20
25
|
session_state: text('session_state'),
|
|
21
26
|
token_type: text('token_type'),
|
|
22
|
-
type: text('type').$type<
|
|
27
|
+
type: text('type').$type<AccountType>().notNull(),
|
|
23
28
|
userId: text('user_id')
|
|
24
29
|
.notNull()
|
|
25
30
|
.references(() => users.id, { onDelete: 'cascade' }),
|
|
@@ -2,29 +2,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
2
2
|
|
|
3
3
|
import { extractBearerToken, getUserAuth } from '../auth';
|
|
4
4
|
|
|
5
|
-
// Mock auth constants
|
|
6
|
-
let mockEnableBetterAuth = false;
|
|
7
|
-
let mockEnableNextAuth = false;
|
|
8
|
-
|
|
9
|
-
vi.mock('@/envs/auth', () => ({
|
|
10
|
-
get enableBetterAuth() {
|
|
11
|
-
return mockEnableBetterAuth;
|
|
12
|
-
},
|
|
13
|
-
get enableNextAuth() {
|
|
14
|
-
return mockEnableNextAuth;
|
|
15
|
-
},
|
|
16
|
-
}));
|
|
17
|
-
|
|
18
|
-
vi.mock('@/libs/next-auth', () => ({
|
|
19
|
-
default: {
|
|
20
|
-
auth: vi.fn().mockResolvedValue({
|
|
21
|
-
user: {
|
|
22
|
-
id: 'next-auth-user-id',
|
|
23
|
-
},
|
|
24
|
-
}),
|
|
25
|
-
},
|
|
26
|
-
}));
|
|
27
|
-
|
|
28
5
|
vi.mock('next/headers', () => ({
|
|
29
6
|
headers: vi.fn(() => new Headers()),
|
|
30
7
|
}));
|
|
@@ -44,48 +21,9 @@ vi.mock('@/auth', () => ({
|
|
|
44
21
|
describe('getUserAuth', () => {
|
|
45
22
|
beforeEach(() => {
|
|
46
23
|
vi.clearAllMocks();
|
|
47
|
-
mockEnableBetterAuth = false;
|
|
48
|
-
mockEnableNextAuth = false;
|
|
49
24
|
});
|
|
50
25
|
|
|
51
|
-
it('should
|
|
52
|
-
await expect(getUserAuth()).rejects.toThrow('Auth method is not enabled');
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('should return next auth when next auth is enabled', async () => {
|
|
56
|
-
mockEnableNextAuth = true;
|
|
57
|
-
|
|
58
|
-
const auth = await getUserAuth();
|
|
59
|
-
|
|
60
|
-
expect(auth).toEqual({
|
|
61
|
-
nextAuth: {
|
|
62
|
-
user: {
|
|
63
|
-
id: 'next-auth-user-id',
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
userId: 'next-auth-user-id',
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('should return better auth when better auth is enabled', async () => {
|
|
71
|
-
mockEnableBetterAuth = true;
|
|
72
|
-
|
|
73
|
-
const auth = await getUserAuth();
|
|
74
|
-
|
|
75
|
-
expect(auth).toEqual({
|
|
76
|
-
betterAuth: {
|
|
77
|
-
user: {
|
|
78
|
-
id: 'better-auth-user-id',
|
|
79
|
-
},
|
|
80
|
-
},
|
|
81
|
-
userId: 'better-auth-user-id',
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('should prioritize better auth over next auth when both are enabled', async () => {
|
|
86
|
-
mockEnableBetterAuth = true;
|
|
87
|
-
mockEnableNextAuth = true;
|
|
88
|
-
|
|
26
|
+
it('should return better auth session', async () => {
|
|
89
27
|
const auth = await getUserAuth();
|
|
90
28
|
|
|
91
29
|
expect(auth).toEqual({
|
|
@@ -1,34 +1,18 @@
|
|
|
1
1
|
import { headers } from 'next/headers';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { auth } from '@/auth';
|
|
4
4
|
|
|
5
5
|
export const getUserAuth = async () => {
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
const currentHeaders = await headers();
|
|
7
|
+
const requestHeaders = Object.fromEntries(currentHeaders.entries());
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
const session = await auth.api.getSession({
|
|
10
|
+
headers: requestHeaders,
|
|
11
|
+
});
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
headers: requestHeaders,
|
|
14
|
-
});
|
|
13
|
+
const userId = session?.user?.id;
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return { betterAuth: session, userId };
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
if (enableNextAuth) {
|
|
22
|
-
const { default: NextAuth } = await import('@/libs/next-auth');
|
|
23
|
-
|
|
24
|
-
const session = await NextAuth.auth();
|
|
25
|
-
|
|
26
|
-
const userId = session?.user.id;
|
|
27
|
-
|
|
28
|
-
return { nextAuth: session, userId };
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
throw new Error('Auth method is not enabled');
|
|
15
|
+
return { betterAuth: session, userId };
|
|
32
16
|
};
|
|
33
17
|
|
|
34
18
|
/**
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utility to check for deprecated authentication environment variables.
|
|
3
|
+
* Used by both prebuild.mts (build time) and startServer.js (Docker runtime).
|
|
4
|
+
*
|
|
5
|
+
* IMPORTANT: Keep this file as CommonJS (.js) for compatibility with startServer.js
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const MIGRATION_DOC_BASE = 'https://lobehub.com/docs/self-hosting/advanced/auth';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Deprecated environment variable checks configuration
|
|
12
|
+
* @type {Array<{
|
|
13
|
+
* name: string;
|
|
14
|
+
* getVars: () => string[];
|
|
15
|
+
* message: string;
|
|
16
|
+
* docUrl?: string;
|
|
17
|
+
* formatVar?: (envVar: string) => string;
|
|
18
|
+
* }>}
|
|
19
|
+
*/
|
|
20
|
+
const DEPRECATED_CHECKS = [
|
|
21
|
+
{
|
|
22
|
+
docUrl: `${MIGRATION_DOC_BASE}/nextauth-to-betterauth`,
|
|
23
|
+
getVars: () =>
|
|
24
|
+
Object.keys(process.env).filter(
|
|
25
|
+
(key) => key.startsWith('NEXT_AUTH') || key.startsWith('NEXTAUTH'),
|
|
26
|
+
),
|
|
27
|
+
message: 'NextAuth has been removed from LobeChat. Please migrate to Better Auth.',
|
|
28
|
+
name: 'NextAuth',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
docUrl: `${MIGRATION_DOC_BASE}/clerk-to-betterauth`,
|
|
32
|
+
getVars: () =>
|
|
33
|
+
['NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY', 'CLERK_SECRET_KEY', 'CLERK_WEBHOOK_SECRET'].filter(
|
|
34
|
+
(key) => process.env[key],
|
|
35
|
+
),
|
|
36
|
+
message: 'Clerk has been removed from LobeChat. Please migrate to Better Auth.',
|
|
37
|
+
name: 'Clerk',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
formatVar: (envVar) => {
|
|
41
|
+
const mapping = {
|
|
42
|
+
NEXT_PUBLIC_AUTH_EMAIL_VERIFICATION: 'AUTH_EMAIL_VERIFICATION',
|
|
43
|
+
NEXT_PUBLIC_ENABLE_MAGIC_LINK: 'ENABLE_MAGIC_LINK',
|
|
44
|
+
};
|
|
45
|
+
return `${envVar} → Please use ${mapping[envVar]} instead`;
|
|
46
|
+
},
|
|
47
|
+
getVars: () =>
|
|
48
|
+
['NEXT_PUBLIC_AUTH_EMAIL_VERIFICATION', 'NEXT_PUBLIC_ENABLE_MAGIC_LINK'].filter(
|
|
49
|
+
(key) => process.env[key],
|
|
50
|
+
),
|
|
51
|
+
message: 'Please update to the new environment variable names.',
|
|
52
|
+
name: 'Deprecated Auth',
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Print error message and exit
|
|
58
|
+
*/
|
|
59
|
+
function printErrorAndExit(name, vars, message, action, docUrl, formatVar) {
|
|
60
|
+
console.error('\n' + '═'.repeat(70));
|
|
61
|
+
console.error(`❌ ERROR: ${name} environment variables are deprecated!`);
|
|
62
|
+
console.error('═'.repeat(70));
|
|
63
|
+
console.error('\nDetected deprecated environment variables:');
|
|
64
|
+
for (const envVar of vars) {
|
|
65
|
+
console.error(` • ${formatVar ? formatVar(envVar) : envVar}`);
|
|
66
|
+
}
|
|
67
|
+
console.error(`\n${message}`);
|
|
68
|
+
if (docUrl) {
|
|
69
|
+
console.error(`\n📖 Migration guide: ${docUrl}`);
|
|
70
|
+
}
|
|
71
|
+
console.error(`\nPlease update your environment variables and ${action}.`);
|
|
72
|
+
console.error('═'.repeat(70) + '\n');
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Check for deprecated authentication environment variables and exit if found
|
|
78
|
+
* @param {object} options
|
|
79
|
+
* @param {string} [options.action='redeploy'] - Action hint in error message ('redeploy' or 'restart')
|
|
80
|
+
*/
|
|
81
|
+
function checkDeprecatedAuth(options = {}) {
|
|
82
|
+
const { action = 'redeploy' } = options;
|
|
83
|
+
|
|
84
|
+
for (const check of DEPRECATED_CHECKS) {
|
|
85
|
+
const foundVars = check.getVars();
|
|
86
|
+
if (foundVars.length > 0) {
|
|
87
|
+
printErrorAndExit(
|
|
88
|
+
check.name,
|
|
89
|
+
foundVars,
|
|
90
|
+
check.message,
|
|
91
|
+
action,
|
|
92
|
+
check.docUrl,
|
|
93
|
+
check.formatVar,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = { checkDeprecatedAuth };
|
|
@@ -13,6 +13,11 @@ const IS_DRY_RUN =
|
|
|
13
13
|
process.argv.includes('--dry-run') || process.env.CLERK_TO_BETTERAUTH_DRY_RUN === '1';
|
|
14
14
|
const formatDuration = (ms: number) => `${(ms / 1000).toFixed(1)}s`;
|
|
15
15
|
|
|
16
|
+
// ANSI color codes
|
|
17
|
+
const GREEN_BOLD = '\u001B[1;32m';
|
|
18
|
+
const RED_BOLD = '\u001B[1;31m';
|
|
19
|
+
const RESET = '\u001B[0m';
|
|
20
|
+
|
|
16
21
|
function chunk<T>(items: T[], size: number): T[][] {
|
|
17
22
|
if (!Number.isFinite(size) || size <= 0) return [items];
|
|
18
23
|
const result: T[][] = [];
|
|
@@ -241,7 +246,7 @@ async function migrateFromClerk() {
|
|
|
241
246
|
}
|
|
242
247
|
|
|
243
248
|
console.log(
|
|
244
|
-
`[clerk-to-betterauth] completed users=${processed}, skipped=${skipped}, accounts attempted=${accountAttempts}, 2fa attempted=${twoFactorAttempts}, dryRun=${IS_DRY_RUN}, elapsed=${formatDuration(Date.now() - startedAt)}`,
|
|
249
|
+
`[clerk-to-betterauth] completed users=${GREEN_BOLD}${processed}${RESET}, skipped=${skipped}, accounts attempted=${accountAttempts}, 2fa attempted=${twoFactorAttempts}, dryRun=${IS_DRY_RUN}, elapsed=${formatDuration(Date.now() - startedAt)}`,
|
|
245
250
|
);
|
|
246
251
|
|
|
247
252
|
const accountCountsText = Object.entries(accountCounts)
|
|
@@ -301,10 +306,10 @@ async function main() {
|
|
|
301
306
|
try {
|
|
302
307
|
await migrateFromClerk();
|
|
303
308
|
console.log('');
|
|
304
|
-
console.log(
|
|
309
|
+
console.log(`${GREEN_BOLD}✅ Migration success!${RESET} (${formatDuration(Date.now() - startedAt)})`);
|
|
305
310
|
} catch (error) {
|
|
306
311
|
console.log('');
|
|
307
|
-
console.error(
|
|
312
|
+
console.error(`${RED_BOLD}❌ Migration failed${RESET} (${formatDuration(Date.now() - startedAt)}):`, error);
|
|
308
313
|
process.exitCode = 1;
|
|
309
314
|
} finally {
|
|
310
315
|
await pool.end();
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import './env';
|
|
2
|
+
|
|
3
|
+
export type MigrationMode = 'test' | 'prod';
|
|
4
|
+
export type DatabaseDriver = 'neon' | 'node';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_MODE: MigrationMode = 'test';
|
|
7
|
+
const DEFAULT_DATABASE_DRIVER: DatabaseDriver = 'neon';
|
|
8
|
+
|
|
9
|
+
export function getMigrationMode(): MigrationMode {
|
|
10
|
+
const mode = process.env.NEXTAUTH_TO_BETTERAUTH_MODE;
|
|
11
|
+
if (mode === 'test' || mode === 'prod') return mode;
|
|
12
|
+
return DEFAULT_MODE;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getDatabaseUrl(mode = getMigrationMode()): string {
|
|
16
|
+
const key =
|
|
17
|
+
mode === 'test'
|
|
18
|
+
? 'TEST_NEXTAUTH_TO_BETTERAUTH_DATABASE_URL'
|
|
19
|
+
: 'PROD_NEXTAUTH_TO_BETTERAUTH_DATABASE_URL';
|
|
20
|
+
const value = process.env[key];
|
|
21
|
+
|
|
22
|
+
if (!value) {
|
|
23
|
+
throw new Error(`${key} is not set`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getDatabaseDriver(): DatabaseDriver {
|
|
30
|
+
const driver = process.env.NEXTAUTH_TO_BETTERAUTH_DATABASE_DRIVER;
|
|
31
|
+
if (driver === 'neon' || driver === 'node') return driver;
|
|
32
|
+
return DEFAULT_DATABASE_DRIVER;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getBatchSize(): number {
|
|
36
|
+
return Number(process.env.NEXTAUTH_TO_BETTERAUTH_BATCH_SIZE) || 300;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function isDryRun(): boolean {
|
|
40
|
+
return process.argv.includes('--dry-run') || process.env.NEXTAUTH_TO_BETTERAUTH_DRY_RUN === '1';
|
|
41
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Pool as NeonPool, neonConfig } from '@neondatabase/serverless';
|
|
2
|
+
import { drizzle as neonDrizzle } from 'drizzle-orm/neon-serverless';
|
|
3
|
+
import { drizzle as nodeDrizzle } from 'drizzle-orm/node-postgres';
|
|
4
|
+
import { Pool as NodePool } from 'pg';
|
|
5
|
+
import ws from 'ws';
|
|
6
|
+
|
|
7
|
+
// schema is the only dependency on project code, required for type-safe migrations
|
|
8
|
+
import * as schemaModule from '../../../packages/database/src/schemas';
|
|
9
|
+
import { getDatabaseDriver, getDatabaseUrl } from './config';
|
|
10
|
+
|
|
11
|
+
function createDatabase() {
|
|
12
|
+
const databaseUrl = getDatabaseUrl();
|
|
13
|
+
const driver = getDatabaseDriver();
|
|
14
|
+
|
|
15
|
+
if (driver === 'node') {
|
|
16
|
+
const pool = new NodePool({ connectionString: databaseUrl });
|
|
17
|
+
const db = nodeDrizzle(pool, { schema: schemaModule });
|
|
18
|
+
return { db, pool };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// neon driver (default)
|
|
22
|
+
// https://github.com/neondatabase/serverless/blob/main/CONFIG.md#websocketconstructor-typeof-websocket--undefined
|
|
23
|
+
neonConfig.webSocketConstructor = ws;
|
|
24
|
+
const pool = new NeonPool({ connectionString: databaseUrl });
|
|
25
|
+
const db = neonDrizzle(pool, { schema: schemaModule });
|
|
26
|
+
return { db, pool };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const { db, pool } = createDatabase();
|
|
30
|
+
|
|
31
|
+
export { db, pool };
|
|
32
|
+
export * as schema from '../../../packages/database/src/schemas';
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/* eslint-disable unicorn/prefer-top-level-await */
|
|
2
|
+
import { sql } from 'drizzle-orm';
|
|
3
|
+
|
|
4
|
+
import { getBatchSize, getMigrationMode, isDryRun } from './_internal/config';
|
|
5
|
+
import { db, pool, schema } from './_internal/db';
|
|
6
|
+
|
|
7
|
+
const BATCH_SIZE = getBatchSize();
|
|
8
|
+
const PROGRESS_TABLE = sql.identifier('nextauth_migration_progress');
|
|
9
|
+
const IS_DRY_RUN = isDryRun();
|
|
10
|
+
const formatDuration = (ms: number) => `${(ms / 1000).toFixed(1)}s`;
|
|
11
|
+
|
|
12
|
+
// ANSI color codes
|
|
13
|
+
const GREEN_BOLD = '\u001B[1;32m';
|
|
14
|
+
const RED_BOLD = '\u001B[1;31m';
|
|
15
|
+
const RESET = '\u001B[0m';
|
|
16
|
+
|
|
17
|
+
function chunk<T>(items: T[], size: number): T[][] {
|
|
18
|
+
if (!Number.isFinite(size) || size <= 0) return [items];
|
|
19
|
+
const result: T[][] = [];
|
|
20
|
+
for (let i = 0; i < items.length; i += size) {
|
|
21
|
+
result.push(items.slice(i, i + size));
|
|
22
|
+
}
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Convert expires_at (seconds since epoch) to Date
|
|
28
|
+
*/
|
|
29
|
+
function convertExpiresAt(expiresAt: number | null): Date | undefined {
|
|
30
|
+
if (expiresAt === null || expiresAt === undefined) return undefined;
|
|
31
|
+
return new Date(expiresAt * 1000);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Convert scope format from NextAuth (space-separated) to Better Auth (comma-separated)
|
|
36
|
+
* e.g., "openid profile email" -> "openid,profile,email"
|
|
37
|
+
*/
|
|
38
|
+
function convertScope(scope: string | null): string | undefined {
|
|
39
|
+
if (!scope) return undefined;
|
|
40
|
+
return scope.trim().split(/\s+/).join(',');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Create a composite key for nextauth_accounts (provider + providerAccountId)
|
|
45
|
+
*/
|
|
46
|
+
function createAccountKey(provider: string, providerAccountId: string): string {
|
|
47
|
+
return `${provider}__${providerAccountId}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function loadNextAuthAccounts() {
|
|
51
|
+
const rows = await db.select().from(schema.nextauthAccounts);
|
|
52
|
+
return rows;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function migrateFromNextAuth() {
|
|
56
|
+
const mode = getMigrationMode();
|
|
57
|
+
const nextauthAccounts = await loadNextAuthAccounts();
|
|
58
|
+
|
|
59
|
+
if (!IS_DRY_RUN) {
|
|
60
|
+
await db.execute(sql`
|
|
61
|
+
CREATE TABLE IF NOT EXISTS ${PROGRESS_TABLE} (
|
|
62
|
+
account_key TEXT PRIMARY KEY,
|
|
63
|
+
processed_at TIMESTAMPTZ DEFAULT NOW()
|
|
64
|
+
);
|
|
65
|
+
`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const processedAccounts = new Set<string>();
|
|
69
|
+
|
|
70
|
+
if (!IS_DRY_RUN) {
|
|
71
|
+
try {
|
|
72
|
+
const processedResult = await db.execute<{ account_key: string }>(
|
|
73
|
+
sql`SELECT account_key FROM ${PROGRESS_TABLE};`,
|
|
74
|
+
);
|
|
75
|
+
const rows = (processedResult as { rows?: { account_key: string }[] }).rows ?? [];
|
|
76
|
+
|
|
77
|
+
for (const row of rows) {
|
|
78
|
+
const accountKey = row?.account_key;
|
|
79
|
+
if (typeof accountKey === 'string') {
|
|
80
|
+
processedAccounts.add(accountKey);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.warn(
|
|
85
|
+
'[nextauth-to-betterauth] failed to read progress table, treating as empty',
|
|
86
|
+
error,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
console.log(`[nextauth-to-betterauth] mode: ${mode} (dryRun=${IS_DRY_RUN})`);
|
|
92
|
+
console.log(`[nextauth-to-betterauth] nextauth accounts: ${nextauthAccounts.length}`);
|
|
93
|
+
console.log(`[nextauth-to-betterauth] already processed: ${processedAccounts.size}`);
|
|
94
|
+
|
|
95
|
+
const unprocessedAccounts = nextauthAccounts.filter(
|
|
96
|
+
(acc) => !processedAccounts.has(createAccountKey(acc.provider, acc.providerAccountId)),
|
|
97
|
+
);
|
|
98
|
+
const batches = chunk(unprocessedAccounts, BATCH_SIZE);
|
|
99
|
+
console.log(
|
|
100
|
+
`[nextauth-to-betterauth] batches: ${batches.length} (batchSize=${BATCH_SIZE}, toProcess=${unprocessedAccounts.length})`,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
let processed = 0;
|
|
104
|
+
const skipped = nextauthAccounts.length - unprocessedAccounts.length;
|
|
105
|
+
const startedAt = Date.now();
|
|
106
|
+
const providerCounts: Record<string, number> = {};
|
|
107
|
+
|
|
108
|
+
const bumpProviderCount = (providerId: string) => {
|
|
109
|
+
providerCounts[providerId] = (providerCounts[providerId] ?? 0) + 1;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
for (let batchIndex = 0; batchIndex < batches.length; batchIndex += 1) {
|
|
113
|
+
const batch = batches[batchIndex];
|
|
114
|
+
const accountRows: (typeof schema.account.$inferInsert)[] = [];
|
|
115
|
+
const accountKeys: string[] = [];
|
|
116
|
+
|
|
117
|
+
for (const nextauthAccount of batch) {
|
|
118
|
+
const accountKey = createAccountKey(
|
|
119
|
+
nextauthAccount.provider,
|
|
120
|
+
nextauthAccount.providerAccountId,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const accountRow: typeof schema.account.$inferInsert = {
|
|
124
|
+
accessToken: nextauthAccount.access_token ?? undefined,
|
|
125
|
+
accessTokenExpiresAt: convertExpiresAt(nextauthAccount.expires_at),
|
|
126
|
+
accountId: nextauthAccount.providerAccountId,
|
|
127
|
+
// id and createdAt/updatedAt use database defaults
|
|
128
|
+
id: accountKey, // deterministic id based on provider + providerAccountId
|
|
129
|
+
idToken: nextauthAccount.id_token ?? undefined,
|
|
130
|
+
providerId: nextauthAccount.provider,
|
|
131
|
+
refreshToken: nextauthAccount.refresh_token ?? undefined,
|
|
132
|
+
scope: convertScope(nextauthAccount.scope),
|
|
133
|
+
userId: nextauthAccount.userId,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
accountRows.push(accountRow);
|
|
137
|
+
accountKeys.push(accountKey);
|
|
138
|
+
bumpProviderCount(nextauthAccount.provider);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!IS_DRY_RUN) {
|
|
142
|
+
await db.transaction(async (tx) => {
|
|
143
|
+
if (accountRows.length > 0) {
|
|
144
|
+
await tx.insert(schema.account).values(accountRows).onConflictDoNothing();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const accountKeyValues = accountKeys.map((key) => sql`(${key})`);
|
|
148
|
+
if (accountKeyValues.length > 0) {
|
|
149
|
+
await tx.execute(sql`
|
|
150
|
+
INSERT INTO ${PROGRESS_TABLE} (account_key)
|
|
151
|
+
VALUES ${sql.join(accountKeyValues, sql`, `)}
|
|
152
|
+
ON CONFLICT (account_key) DO NOTHING;
|
|
153
|
+
`);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
processed += batch.length;
|
|
159
|
+
console.log(
|
|
160
|
+
`[nextauth-to-betterauth] batch ${batchIndex + 1}/${batches.length} done, accounts ${processed}/${unprocessedAccounts.length}, dryRun=${IS_DRY_RUN}`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
console.log(
|
|
165
|
+
`[nextauth-to-betterauth] completed accounts=${GREEN_BOLD}${processed}${RESET}, skipped=${skipped}, dryRun=${IS_DRY_RUN}, elapsed=${formatDuration(Date.now() - startedAt)}`,
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const providerCountsText = Object.entries(providerCounts)
|
|
169
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
170
|
+
.map(([providerId, count]) => `${providerId}=${count}`)
|
|
171
|
+
.join(', ');
|
|
172
|
+
|
|
173
|
+
console.log(`[nextauth-to-betterauth] provider counts: ${providerCountsText || 'none recorded'}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function main() {
|
|
177
|
+
const startedAt = Date.now();
|
|
178
|
+
const mode = getMigrationMode();
|
|
179
|
+
|
|
180
|
+
console.log('');
|
|
181
|
+
console.log('╔════════════════════════════════════════════════════════════╗');
|
|
182
|
+
console.log('║ NextAuth to Better Auth Migration Script ║');
|
|
183
|
+
console.log('╠════════════════════════════════════════════════════════════╣');
|
|
184
|
+
console.log(`║ Mode: ${mode.padEnd(48)}║`);
|
|
185
|
+
console.log(`║ Dry Run: ${(IS_DRY_RUN ? 'YES (no changes will be made)' : 'NO').padEnd(48)}║`);
|
|
186
|
+
console.log(`║ Batch: ${String(BATCH_SIZE).padEnd(48)}║`);
|
|
187
|
+
console.log('╚════════════════════════════════════════════════════════════╝');
|
|
188
|
+
console.log('');
|
|
189
|
+
|
|
190
|
+
if (mode === 'prod' && !IS_DRY_RUN) {
|
|
191
|
+
console.log('⚠️ WARNING: Running in PRODUCTION mode. Data will be modified!');
|
|
192
|
+
console.log(' Type "yes" to continue or press Ctrl+C to abort.');
|
|
193
|
+
console.log('');
|
|
194
|
+
|
|
195
|
+
const readline = await import('node:readline');
|
|
196
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
197
|
+
const answer = await new Promise<string>((resolve) => {
|
|
198
|
+
rl.question(' Confirm (yes/no): ', (ans) => {
|
|
199
|
+
resolve(ans);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
rl.close();
|
|
203
|
+
|
|
204
|
+
if (answer.toLowerCase() !== 'yes') {
|
|
205
|
+
console.log('❌ Aborted by user.');
|
|
206
|
+
process.exitCode = 0;
|
|
207
|
+
await pool.end();
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
console.log('');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
await migrateFromNextAuth();
|
|
215
|
+
console.log('');
|
|
216
|
+
console.log(`${GREEN_BOLD}✅ Migration success!${RESET} (${formatDuration(Date.now() - startedAt)})`);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
console.log('');
|
|
219
|
+
console.error(`${RED_BOLD}❌ Migration failed${RESET} (${formatDuration(Date.now() - startedAt)}):`, error);
|
|
220
|
+
process.exitCode = 1;
|
|
221
|
+
} finally {
|
|
222
|
+
await pool.end();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
void main();
|