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

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 (156) hide show
  1. package/.env.desktop +0 -1
  2. package/.env.example +16 -20
  3. package/.env.example.development +1 -4
  4. package/.github/workflows/e2e.yml +10 -11
  5. package/CHANGELOG.md +60 -0
  6. package/Dockerfile +28 -4
  7. package/changelog/v1.json +18 -0
  8. package/docker-compose/local/docker-compose.yml +2 -2
  9. package/docker-compose/local/grafana/docker-compose.yml +2 -2
  10. package/docker-compose/local/logto/docker-compose.yml +2 -2
  11. package/docker-compose/local/zitadel/.env.example +2 -2
  12. package/docker-compose/local/zitadel/.env.zh-CN.example +2 -2
  13. package/docker-compose/production/grafana/docker-compose.yml +2 -2
  14. package/docker-compose/production/logto/.env.example +2 -2
  15. package/docker-compose/production/logto/.env.zh-CN.example +2 -2
  16. package/docker-compose/production/zitadel/.env.example +2 -2
  17. package/docker-compose/production/zitadel/.env.zh-CN.example +2 -2
  18. package/docs/development/basic/add-new-authentication-providers.mdx +144 -136
  19. package/docs/development/basic/add-new-authentication-providers.zh-CN.mdx +146 -136
  20. package/docs/self-hosting/advanced/auth/legacy.mdx +4 -0
  21. package/docs/self-hosting/advanced/auth/legacy.zh-CN.mdx +4 -0
  22. package/docs/self-hosting/advanced/auth/nextauth-to-betterauth.mdx +326 -0
  23. package/docs/self-hosting/advanced/auth/nextauth-to-betterauth.zh-CN.mdx +323 -0
  24. package/docs/self-hosting/advanced/auth.mdx +43 -16
  25. package/docs/self-hosting/advanced/auth.zh-CN.mdx +44 -16
  26. package/docs/self-hosting/advanced/redis/upstash.mdx +69 -0
  27. package/docs/self-hosting/advanced/redis/upstash.zh-CN.mdx +69 -0
  28. package/docs/self-hosting/advanced/redis.mdx +128 -0
  29. package/docs/self-hosting/advanced/redis.zh-CN.mdx +126 -0
  30. package/docs/self-hosting/environment-variables/auth.mdx +15 -1
  31. package/docs/self-hosting/environment-variables/auth.zh-CN.mdx +15 -1
  32. package/docs/self-hosting/environment-variables/basic.mdx +13 -0
  33. package/docs/self-hosting/environment-variables/basic.zh-CN.mdx +13 -0
  34. package/docs/self-hosting/environment-variables/redis.mdx +68 -0
  35. package/docs/self-hosting/environment-variables/redis.zh-CN.mdx +67 -0
  36. package/docs/self-hosting/migration/v2/breaking-changes.mdx +23 -23
  37. package/docs/self-hosting/migration/v2/breaking-changes.zh-CN.mdx +23 -23
  38. package/docs/self-hosting/server-database/docker-compose.mdx +4 -4
  39. package/docs/self-hosting/server-database/docker-compose.zh-CN.mdx +4 -4
  40. package/e2e/CLAUDE.md +5 -6
  41. package/e2e/docs/local-setup.md +9 -12
  42. package/e2e/scripts/setup.ts +9 -15
  43. package/e2e/src/support/webServer.ts +6 -5
  44. package/package.json +4 -6
  45. package/packages/database/src/schemas/nextauth.ts +7 -2
  46. package/packages/model-runtime/src/core/contextBuilders/anthropic.test.ts +370 -0
  47. package/packages/model-runtime/src/core/contextBuilders/anthropic.ts +18 -5
  48. package/packages/utils/src/server/__tests__/auth.test.ts +1 -63
  49. package/packages/utils/src/server/auth.ts +8 -24
  50. package/scripts/_shared/checkDeprecatedAuth.js +99 -0
  51. package/scripts/clerk-to-betterauth/index.ts +8 -3
  52. package/scripts/nextauth-to-betterauth/_internal/config.ts +41 -0
  53. package/scripts/nextauth-to-betterauth/_internal/db.ts +32 -0
  54. package/scripts/nextauth-to-betterauth/_internal/env.ts +6 -0
  55. package/scripts/nextauth-to-betterauth/index.ts +226 -0
  56. package/scripts/nextauth-to-betterauth/verify.ts +188 -0
  57. package/scripts/prebuild.mts +66 -13
  58. package/scripts/serverLauncher/startServer.js +5 -5
  59. package/src/app/(backend)/api/auth/[...all]/route.ts +5 -23
  60. package/src/app/(backend)/api/webhooks/casdoor/route.ts +5 -5
  61. package/src/app/(backend)/api/webhooks/logto/route.ts +8 -8
  62. package/src/app/(backend)/middleware/auth/index.test.ts +8 -1
  63. package/src/app/(backend)/middleware/auth/index.ts +6 -15
  64. package/src/app/(backend)/middleware/auth/utils.test.ts +0 -32
  65. package/src/app/(backend)/middleware/auth/utils.ts +3 -8
  66. package/src/app/(backend)/webapi/chat/[provider]/route.test.ts +8 -1
  67. package/src/app/(backend)/webapi/create-image/comfyui/route.ts +0 -1
  68. package/src/app/(backend)/webapi/models/[provider]/route.test.ts +8 -1
  69. package/src/app/[variants]/(auth)/signin/SignInEmailStep.tsx +1 -1
  70. package/src/app/[variants]/(auth)/signup/[[...signup]]/page.tsx +4 -17
  71. package/src/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobContentEditor.tsx +34 -21
  72. package/src/app/[variants]/(main)/settings/profile/features/SSOProvidersList/index.tsx +12 -19
  73. package/src/app/[variants]/(main)/settings/profile/index.tsx +8 -14
  74. package/src/components/{NextAuth/AuthIcons.tsx → AuthIcons.tsx} +8 -10
  75. package/src/envs/auth.ts +12 -51
  76. package/src/envs/email.ts +3 -0
  77. package/src/envs/redis.ts +12 -54
  78. package/src/features/ChatInput/ChatInputProvider.tsx +22 -2
  79. package/src/features/ChatInput/InputEditor/index.tsx +14 -3
  80. package/src/features/ChatInput/store/initialState.ts +2 -0
  81. package/src/features/EditorCanvas/DiffAllToolbar.tsx +4 -5
  82. package/src/features/EditorCanvas/DocumentIdMode.tsx +21 -1
  83. package/src/features/User/__tests__/PanelContent.test.tsx +0 -11
  84. package/src/features/User/__tests__/UserAvatar.test.tsx +1 -16
  85. package/src/layout/AuthProvider/index.tsx +1 -6
  86. package/src/layout/GlobalProvider/StoreInitialization.tsx +2 -4
  87. package/src/libs/better-auth/define-config.ts +2 -0
  88. package/src/libs/better-auth/plugins/email-whitelist.test.ts +120 -0
  89. package/src/libs/better-auth/plugins/email-whitelist.ts +62 -0
  90. package/src/libs/next/config/define-config.ts +13 -1
  91. package/src/libs/next/proxy/define-config.ts +2 -75
  92. package/src/libs/oidc-provider/provider.test.ts +0 -4
  93. package/src/libs/redis/index.ts +0 -1
  94. package/src/libs/redis/manager.test.ts +9 -45
  95. package/src/libs/redis/manager.ts +2 -16
  96. package/src/libs/redis/redis.test.ts +2 -4
  97. package/src/libs/redis/redis.ts +2 -4
  98. package/src/libs/redis/types.ts +2 -24
  99. package/src/libs/redis/utils.test.ts +0 -10
  100. package/src/libs/redis/utils.ts +0 -19
  101. package/src/libs/trpc/lambda/context.test.ts +0 -13
  102. package/src/libs/trpc/lambda/context.ts +21 -59
  103. package/src/libs/trpc/middleware/userAuth.ts +1 -7
  104. package/src/libs/trusted-client/getSessionUser.ts +15 -35
  105. package/src/server/globalConfig/index.ts +1 -3
  106. package/src/server/routers/lambda/__tests__/user.test.ts +0 -48
  107. package/src/server/routers/lambda/user.ts +1 -12
  108. package/src/server/services/email/impls/nodemailer/index.ts +2 -2
  109. package/src/server/services/webhookUser/index.ts +88 -0
  110. package/src/services/user/index.test.ts +0 -14
  111. package/src/services/user/index.ts +0 -4
  112. package/src/store/document/slices/document/action.ts +1 -0
  113. package/src/store/user/slices/auth/action.test.ts +22 -126
  114. package/src/store/user/slices/auth/action.ts +32 -65
  115. package/src/store/user/slices/auth/initialState.ts +0 -3
  116. package/src/store/user/slices/auth/selectors.ts +0 -3
  117. package/tests/setup.ts +10 -0
  118. package/scripts/_shared/checkDeprecatedClerkEnv.js +0 -42
  119. package/src/app/(backend)/api/auth/adapter/route.ts +0 -137
  120. package/src/app/[variants]/(auth)/next-auth/error/AuthErrorPage.tsx +0 -40
  121. package/src/app/[variants]/(auth)/next-auth/error/page.tsx +0 -11
  122. package/src/app/[variants]/(auth)/next-auth/signin/AuthSignInBox.tsx +0 -167
  123. package/src/app/[variants]/(auth)/next-auth/signin/page.tsx +0 -11
  124. package/src/app/[variants]/(auth)/reset-password/layout.tsx +0 -12
  125. package/src/app/[variants]/(auth)/signin/layout.tsx +0 -12
  126. package/src/app/[variants]/(auth)/verify-email/layout.tsx +0 -12
  127. package/src/envs/auth.test.ts +0 -47
  128. package/src/layout/AuthProvider/NextAuth/UserUpdater.tsx +0 -44
  129. package/src/layout/AuthProvider/NextAuth/index.tsx +0 -17
  130. package/src/libs/next-auth/adapter/index.ts +0 -177
  131. package/src/libs/next-auth/auth.config.ts +0 -64
  132. package/src/libs/next-auth/index.ts +0 -20
  133. package/src/libs/next-auth/sso-providers/auth0.ts +0 -24
  134. package/src/libs/next-auth/sso-providers/authelia.ts +0 -39
  135. package/src/libs/next-auth/sso-providers/authentik.ts +0 -25
  136. package/src/libs/next-auth/sso-providers/casdoor.ts +0 -50
  137. package/src/libs/next-auth/sso-providers/cloudflare-zero-trust.ts +0 -34
  138. package/src/libs/next-auth/sso-providers/cognito.ts +0 -8
  139. package/src/libs/next-auth/sso-providers/feishu.ts +0 -83
  140. package/src/libs/next-auth/sso-providers/generic-oidc.ts +0 -38
  141. package/src/libs/next-auth/sso-providers/github.ts +0 -23
  142. package/src/libs/next-auth/sso-providers/google.ts +0 -18
  143. package/src/libs/next-auth/sso-providers/index.ts +0 -35
  144. package/src/libs/next-auth/sso-providers/keycloak.ts +0 -22
  145. package/src/libs/next-auth/sso-providers/logto.ts +0 -48
  146. package/src/libs/next-auth/sso-providers/microsoft-entra-id-helper.ts +0 -29
  147. package/src/libs/next-auth/sso-providers/microsoft-entra-id.ts +0 -19
  148. package/src/libs/next-auth/sso-providers/okta.ts +0 -22
  149. package/src/libs/next-auth/sso-providers/sso.config.ts +0 -8
  150. package/src/libs/next-auth/sso-providers/wechat.ts +0 -36
  151. package/src/libs/next-auth/sso-providers/zitadel.ts +0 -21
  152. package/src/libs/redis/upstash.test.ts +0 -158
  153. package/src/libs/redis/upstash.ts +0 -136
  154. package/src/server/services/nextAuthUser/index.ts +0 -318
  155. package/src/server/services/nextAuthUser/utils.ts +0 -62
  156. package/src/types/next-auth.d.ts +0 -26
@@ -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,6 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { loadEnvFile } from 'node:process';
3
+
4
+ if (existsSync('.env')) {
5
+ loadEnvFile();
6
+ }
@@ -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();
@@ -0,0 +1,188 @@
1
+ /* eslint-disable unicorn/prefer-top-level-await */
2
+ import { getMigrationMode } from './_internal/config';
3
+ import { db, pool, schema } from './_internal/db';
4
+
5
+ type ExpectedAccount = {
6
+ accountId: string;
7
+ providerId: string;
8
+ scope?: string;
9
+ userId: string;
10
+ };
11
+
12
+ type ActualAccount = {
13
+ accountId: string;
14
+ providerId: string;
15
+ scope: string | null;
16
+ userId: string;
17
+ };
18
+
19
+ const MAX_SAMPLES = 5;
20
+
21
+ const formatDuration = (ms: number) => `${(ms / 1000).toFixed(1)}s`;
22
+
23
+ function buildAccountKey(account: { accountId: string; providerId: string; userId: string }) {
24
+ return `${account.userId}__${account.providerId}__${account.accountId}`;
25
+ }
26
+
27
+ async function loadNextAuthAccounts() {
28
+ const rows = await db.select().from(schema.nextauthAccounts);
29
+ return rows;
30
+ }
31
+
32
+ async function loadActualAccounts() {
33
+ const rows = await db
34
+ .select({
35
+ accountId: schema.account.accountId,
36
+ providerId: schema.account.providerId,
37
+ scope: schema.account.scope,
38
+ userId: schema.account.userId,
39
+ })
40
+ .from(schema.account);
41
+
42
+ return rows as ActualAccount[];
43
+ }
44
+
45
+ function buildExpectedAccounts(nextauthAccounts: Awaited<ReturnType<typeof loadNextAuthAccounts>>) {
46
+ const expectedAccounts: ExpectedAccount[] = [];
47
+
48
+ for (const nextauthAccount of nextauthAccounts) {
49
+ expectedAccounts.push({
50
+ accountId: nextauthAccount.providerAccountId,
51
+ providerId: nextauthAccount.provider,
52
+ scope: nextauthAccount.scope ?? undefined,
53
+ userId: nextauthAccount.userId,
54
+ });
55
+ }
56
+
57
+ return { expectedAccounts };
58
+ }
59
+
60
+ async function main() {
61
+ const startedAt = Date.now();
62
+ const mode = getMigrationMode();
63
+
64
+ console.log('');
65
+ console.log('╔════════════════════════════════════════════════════════════╗');
66
+ console.log('║ NextAuth to Better Auth Verification Script ║');
67
+ console.log('╠════════════════════════════════════════════════════════════╣');
68
+ console.log(`║ Mode: ${mode.padEnd(48)}║`);
69
+ console.log('╚════════════════════════════════════════════════════════════╝');
70
+ console.log('');
71
+
72
+ const [nextauthAccounts, actualAccounts] = await Promise.all([
73
+ loadNextAuthAccounts(),
74
+ loadActualAccounts(),
75
+ ]);
76
+
77
+ console.log(`📦 [verify] Loaded nextauth_accounts=${nextauthAccounts.length}`);
78
+
79
+ const { expectedAccounts } = buildExpectedAccounts(nextauthAccounts);
80
+
81
+ console.log(`🧮 [verify] Expected accounts=${expectedAccounts.length}`);
82
+ console.log(`🗄️ [verify] DB snapshot: accounts=${actualAccounts.length}`);
83
+
84
+ const expectedAccountSet = new Set(expectedAccounts.map(buildAccountKey));
85
+ const actualAccountSet = new Set(actualAccounts.map(buildAccountKey));
86
+
87
+ let missingAccounts = 0;
88
+ const missingAccountSamples: string[] = [];
89
+ for (const account of expectedAccounts) {
90
+ const key = buildAccountKey(account);
91
+ if (!actualAccountSet.has(key)) {
92
+ missingAccounts += 1;
93
+ if (missingAccountSamples.length < MAX_SAMPLES) {
94
+ missingAccountSamples.push(`${account.providerId}/${account.accountId}`);
95
+ }
96
+ }
97
+ }
98
+
99
+ let unexpectedAccounts = 0;
100
+ const unexpectedAccountSamples: string[] = [];
101
+ for (const account of actualAccounts) {
102
+ const key = buildAccountKey(account);
103
+ if (!expectedAccountSet.has(key)) {
104
+ unexpectedAccounts += 1;
105
+ if (unexpectedAccountSamples.length < MAX_SAMPLES) {
106
+ unexpectedAccountSamples.push(`${account.providerId}/${account.accountId}`);
107
+ }
108
+ }
109
+ }
110
+
111
+ // Provider counts
112
+ const expectedProviderCounts: Record<string, number> = {};
113
+ const actualProviderCounts: Record<string, number> = {};
114
+
115
+ for (const account of expectedAccounts) {
116
+ expectedProviderCounts[account.providerId] =
117
+ (expectedProviderCounts[account.providerId] ?? 0) + 1;
118
+ }
119
+
120
+ for (const account of actualAccounts) {
121
+ actualProviderCounts[account.providerId] = (actualProviderCounts[account.providerId] ?? 0) + 1;
122
+ }
123
+
124
+ const formatCounts = (counts: Record<string, number>) =>
125
+ Object.entries(counts)
126
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
127
+ .map(([providerId, count]) => `${providerId}=${count}`)
128
+ .join(', ');
129
+
130
+ console.log(
131
+ `📊 [verify] Expected provider counts: ${formatCounts(expectedProviderCounts) || 'n/a'}`,
132
+ );
133
+ console.log(
134
+ `📊 [verify] Actual provider counts: ${formatCounts(actualProviderCounts) || 'n/a'}`,
135
+ );
136
+
137
+ // Check for missing scope in actual accounts
138
+ let missingScopeNonCredential = 0;
139
+ const sampleMissingScope: string[] = [];
140
+
141
+ for (const account of actualAccounts) {
142
+ if (account.providerId !== 'credential' && !account.scope) {
143
+ // Find corresponding nextauth account to check if it had scope
144
+ const nextauthAccount = nextauthAccounts.find(
145
+ (na) => na.provider === account.providerId && na.providerAccountId === account.accountId,
146
+ );
147
+ if (nextauthAccount?.scope) {
148
+ missingScopeNonCredential += 1;
149
+ if (sampleMissingScope.length < MAX_SAMPLES) {
150
+ sampleMissingScope.push(`${account.providerId}/${account.accountId}`);
151
+ }
152
+ }
153
+ }
154
+ }
155
+
156
+ console.log('');
157
+ console.log('📋 [verify] Summary:');
158
+ console.log(
159
+ ` - Missing accounts: ${missingAccounts} ${missingAccountSamples.length > 0 ? `(samples: ${missingAccountSamples.join(', ')})` : ''}`,
160
+ );
161
+ console.log(
162
+ ` - Unexpected accounts: ${unexpectedAccounts} ${unexpectedAccountSamples.length > 0 ? `(samples: ${unexpectedAccountSamples.join(', ')})` : '(accounts not from nextauth)'}`,
163
+ );
164
+ console.log(
165
+ ` - Missing scope (had in nextauth): ${missingScopeNonCredential} ${sampleMissingScope.length > 0 ? `(samples: ${sampleMissingScope.join(', ')})` : ''}`,
166
+ );
167
+
168
+ console.log('');
169
+ if (missingAccounts === 0) {
170
+ console.log(
171
+ `✅ Verification success! All nextauth accounts migrated. (${formatDuration(Date.now() - startedAt)})`,
172
+ );
173
+ } else {
174
+ console.log(
175
+ `⚠️ Verification completed with ${missingAccounts} missing accounts. (${formatDuration(Date.now() - startedAt)})`,
176
+ );
177
+ }
178
+ }
179
+
180
+ void main()
181
+ .catch((error) => {
182
+ console.log('');
183
+ console.error('❌ Verification failed:', error);
184
+ process.exitCode = 1;
185
+ })
186
+ .finally(async () => {
187
+ await pool.end();
188
+ });
@@ -9,10 +9,11 @@ import { fileURLToPath } from 'node:url';
9
9
 
10
10
  // Use createRequire for CommonJS module compatibility
11
11
  const require = createRequire(import.meta.url);
12
- const { checkDeprecatedClerkEnv } = require('./_shared/checkDeprecatedClerkEnv.js');
12
+ const { checkDeprecatedAuth } = require('./_shared/checkDeprecatedAuth.js');
13
13
 
14
14
  const isDesktop = process.env.NEXT_PUBLIC_IS_DESKTOP_APP === '1';
15
15
  const isBundleAnalyzer = process.env.ANALYZE === 'true' && process.env.CI === 'true';
16
+ const isServerDB = !!process.env.DATABASE_URL;
16
17
 
17
18
  if (isDesktop) {
18
19
  dotenvExpand.expand(dotenv.config({ path: '.env.desktop' }));
@@ -21,10 +22,40 @@ if (isDesktop) {
21
22
  dotenvExpand.expand(dotenv.config());
22
23
  }
23
24
 
24
- // Auth flags - use process.env directly for build-time dead code elimination
25
- // Better Auth is the default auth solution when NextAuth is not explicitly enabled
26
- const enableNextAuth = process.env.NEXT_PUBLIC_ENABLE_NEXT_AUTH === '1';
27
- const enableBetterAuth = !enableNextAuth;
25
+ const AUTH_SECRET_DOC_URL = 'https://lobehub.com/docs/self-hosting/environment-variables/auth#auth-secret';
26
+ const KEY_VAULTS_SECRET_DOC_URL = 'https://lobehub.com/docs/self-hosting/environment-variables/basic#key-vaults-secret';
27
+
28
+ /**
29
+ * Check for required environment variables in server database mode
30
+ */
31
+ const checkRequiredEnvVars = () => {
32
+ if (isDesktop || !isServerDB) return;
33
+
34
+ const missingVars: { docUrl: string; name: string }[] = [];
35
+
36
+ if (!process.env.AUTH_SECRET) {
37
+ missingVars.push({ docUrl: AUTH_SECRET_DOC_URL, name: 'AUTH_SECRET' });
38
+ }
39
+
40
+ if (!process.env.KEY_VAULTS_SECRET) {
41
+ missingVars.push({ docUrl: KEY_VAULTS_SECRET_DOC_URL, name: 'KEY_VAULTS_SECRET' });
42
+ }
43
+
44
+ if (missingVars.length > 0) {
45
+ console.error('\n' + '═'.repeat(70));
46
+ console.error('❌ ERROR: Missing required environment variables!');
47
+ console.error('═'.repeat(70));
48
+ console.error('\nThe following environment variables are required for server database mode:\n');
49
+ for (const { name, docUrl } of missingVars) {
50
+ console.error(` • ${name}`);
51
+ console.error(` 📖 Documentation: ${docUrl}\n`);
52
+ }
53
+ console.error('Please configure these environment variables and redeploy.');
54
+ console.error('═'.repeat(70) + '\n');
55
+ process.exit(1);
56
+ }
57
+ };
58
+
28
59
 
29
60
  const getCommandVersion = (command: string): string | null => {
30
61
  try {
@@ -58,13 +89,32 @@ const printEnvInfo = () => {
58
89
  console.log(` VERCEL_PROJECT_PRODUCTION_URL: ${process.env.VERCEL_PROJECT_PRODUCTION_URL ?? '(not set)'}`);
59
90
  console.log(` AUTH_EMAIL_VERIFICATION: ${process.env.AUTH_EMAIL_VERIFICATION ?? '(not set)'}`);
60
91
  console.log(` ENABLE_MAGIC_LINK: ${process.env.ENABLE_MAGIC_LINK ?? '(not set)'}`);
61
- console.log(` AUTH_SECRET: ${process.env.AUTH_SECRET ? '✓ set' : '✗ not set'}`);
62
- console.log(` KEY_VAULTS_SECRET: ${process.env.KEY_VAULTS_SECRET ? '✓ set' : '✗ not set'}`);
63
92
 
64
- // Auth flags
65
- console.log('\n Auth Flags:');
66
- console.log(` enableBetterAuth: ${enableBetterAuth}`);
67
- console.log(` enableNextAuth: ${enableNextAuth}`);
93
+ // Check SSO providers configuration
94
+ const ssoProviders = process.env.AUTH_SSO_PROVIDERS;
95
+ console.log(` AUTH_SSO_PROVIDERS: ${ssoProviders ?? '(not set)'}`);
96
+
97
+ if (ssoProviders) {
98
+ const getEnvPrefix = (provider: string) => `AUTH_${provider.toUpperCase().replaceAll('-', '_')}`;
99
+
100
+ const providers = ssoProviders.split(/[,,]/).map(p => p.trim()).filter(Boolean);
101
+ const missingProviders: string[] = [];
102
+
103
+ for (const provider of providers) {
104
+ const envPrefix = getEnvPrefix(provider);
105
+ const hasEnvVar = Object.keys(process.env).some(key => key.startsWith(envPrefix));
106
+ if (!hasEnvVar) {
107
+ missingProviders.push(provider);
108
+ }
109
+ }
110
+
111
+ if (missingProviders.length > 0) {
112
+ console.log('\n ⚠️ SSO Provider Configuration Warning:');
113
+ for (const provider of missingProviders) {
114
+ console.log(` - "${provider}" is configured but no ${getEnvPrefix(provider)}_* env vars found`);
115
+ }
116
+ }
117
+ }
68
118
 
69
119
  console.log('─'.repeat(50));
70
120
  };
@@ -160,8 +210,11 @@ export const runPrebuild = async (targetDir: string = 'src') => {
160
210
  const isMainModule = process.argv[1] === fileURLToPath(import.meta.url);
161
211
 
162
212
  if (isMainModule) {
163
- // Check for deprecated Clerk env vars first - fail fast if found
164
- checkDeprecatedClerkEnv();
213
+ // Check for deprecated auth env vars first - fail fast if found
214
+ checkDeprecatedAuth();
215
+
216
+ // Check for required env vars in server database mode
217
+ checkRequiredEnvVars();
165
218
 
166
219
  printEnvInfo();
167
220
  // 执行删除操作
@@ -7,11 +7,11 @@ const { existsSync } = require('node:fs');
7
7
  // Resolve shared module path for both local dev and Docker environments
8
8
  // Local: scripts/serverLauncher/startServer.js -> scripts/_shared/...
9
9
  // Docker: /app/startServer.js -> /app/scripts/_shared/...
10
- const localPath = path.join(__dirname, '..', '_shared', 'checkDeprecatedClerkEnv.js');
11
- const dockerPath = '/app/scripts/_shared/checkDeprecatedClerkEnv.js';
10
+ const localPath = path.join(__dirname, '..', '_shared', 'checkDeprecatedAuth.js');
11
+ const dockerPath = '/app/scripts/_shared/checkDeprecatedAuth.js';
12
12
  const sharedModulePath = existsSync(localPath) ? localPath : dockerPath;
13
13
 
14
- const { checkDeprecatedClerkEnv } = require(sharedModulePath);
14
+ const { checkDeprecatedAuth } = require(sharedModulePath);
15
15
 
16
16
  // Set file paths
17
17
  const DB_MIGRATION_SCRIPT_PATH = '/app/docker.cjs';
@@ -139,8 +139,8 @@ const runServer = async () => {
139
139
 
140
140
  // Main execution block
141
141
  (async () => {
142
- // Check for deprecated Clerk env vars first - fail fast if found
143
- checkDeprecatedClerkEnv({ action: 'restart' });
142
+ // Check for deprecated auth env vars first - fail fast if found
143
+ checkDeprecatedAuth({ action: 'restart' });
144
144
 
145
145
  console.log('🌐 DNS Server:', dns.getServers());
146
146
  console.log('-------------------------------------');
@@ -1,32 +1,14 @@
1
+ import { toNextJsHandler } from 'better-auth/next-js';
1
2
  import type { NextRequest } from 'next/server';
2
3
 
3
- import { enableBetterAuth, enableNextAuth } from '@/envs/auth';
4
+ import { auth } from '@/auth';
4
5
 
5
- const createHandler = async () => {
6
- if (enableBetterAuth) {
7
- const [{ toNextJsHandler }, { auth }] = await Promise.all([
8
- import('better-auth/next-js'),
9
- import('@/auth'),
10
- ]);
11
- return toNextJsHandler(auth);
12
- }
13
-
14
- if (enableNextAuth) {
15
- const NextAuthNode = await import('@/libs/next-auth');
16
- return NextAuthNode.default.handlers;
17
- }
18
-
19
- return { GET: undefined, POST: undefined };
20
- };
21
-
22
- const handler = createHandler();
6
+ const handler = toNextJsHandler(auth);
23
7
 
24
8
  export const GET = async (req: NextRequest) => {
25
- const { GET } = await handler;
26
- return GET?.(req);
9
+ return handler.GET(req);
27
10
  };
28
11
 
29
12
  export const POST = async (req: NextRequest) => {
30
- const { POST } = await handler;
31
- return POST?.(req);
13
+ return handler.POST(req);
32
14
  };