@lobehub/lobehub 2.0.0-next.355 → 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.
Files changed (151) 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 +33 -0
  6. package/Dockerfile +28 -4
  7. package/changelog/v1.json +9 -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/utils/src/server/__tests__/auth.test.ts +1 -63
  47. package/packages/utils/src/server/auth.ts +8 -24
  48. package/scripts/_shared/checkDeprecatedAuth.js +99 -0
  49. package/scripts/clerk-to-betterauth/index.ts +8 -3
  50. package/scripts/nextauth-to-betterauth/_internal/config.ts +41 -0
  51. package/scripts/nextauth-to-betterauth/_internal/db.ts +32 -0
  52. package/scripts/nextauth-to-betterauth/_internal/env.ts +6 -0
  53. package/scripts/nextauth-to-betterauth/index.ts +226 -0
  54. package/scripts/nextauth-to-betterauth/verify.ts +188 -0
  55. package/scripts/prebuild.mts +66 -13
  56. package/scripts/serverLauncher/startServer.js +5 -5
  57. package/src/app/(backend)/api/auth/[...all]/route.ts +5 -23
  58. package/src/app/(backend)/api/webhooks/casdoor/route.ts +5 -5
  59. package/src/app/(backend)/api/webhooks/logto/route.ts +8 -8
  60. package/src/app/(backend)/middleware/auth/index.test.ts +8 -1
  61. package/src/app/(backend)/middleware/auth/index.ts +6 -15
  62. package/src/app/(backend)/middleware/auth/utils.test.ts +0 -32
  63. package/src/app/(backend)/middleware/auth/utils.ts +3 -8
  64. package/src/app/(backend)/webapi/chat/[provider]/route.test.ts +8 -1
  65. package/src/app/(backend)/webapi/create-image/comfyui/route.ts +0 -1
  66. package/src/app/(backend)/webapi/models/[provider]/route.test.ts +8 -1
  67. package/src/app/[variants]/(auth)/signin/SignInEmailStep.tsx +1 -1
  68. package/src/app/[variants]/(auth)/signup/[[...signup]]/page.tsx +4 -17
  69. package/src/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobContentEditor.tsx +34 -21
  70. package/src/app/[variants]/(main)/settings/profile/features/SSOProvidersList/index.tsx +12 -19
  71. package/src/app/[variants]/(main)/settings/profile/index.tsx +8 -14
  72. package/src/components/{NextAuth/AuthIcons.tsx → AuthIcons.tsx} +8 -10
  73. package/src/envs/auth.ts +12 -51
  74. package/src/envs/email.ts +3 -0
  75. package/src/envs/redis.ts +12 -54
  76. package/src/features/ChatInput/ChatInputProvider.tsx +22 -2
  77. package/src/features/ChatInput/InputEditor/index.tsx +14 -3
  78. package/src/features/ChatInput/store/initialState.ts +2 -0
  79. package/src/features/User/__tests__/PanelContent.test.tsx +0 -11
  80. package/src/features/User/__tests__/UserAvatar.test.tsx +1 -16
  81. package/src/layout/AuthProvider/index.tsx +1 -6
  82. package/src/layout/GlobalProvider/StoreInitialization.tsx +2 -4
  83. package/src/libs/better-auth/define-config.ts +2 -0
  84. package/src/libs/better-auth/plugins/email-whitelist.test.ts +120 -0
  85. package/src/libs/better-auth/plugins/email-whitelist.ts +62 -0
  86. package/src/libs/next/config/define-config.ts +13 -1
  87. package/src/libs/next/proxy/define-config.ts +2 -75
  88. package/src/libs/oidc-provider/provider.test.ts +0 -4
  89. package/src/libs/redis/index.ts +0 -1
  90. package/src/libs/redis/manager.test.ts +9 -45
  91. package/src/libs/redis/manager.ts +2 -16
  92. package/src/libs/redis/redis.test.ts +2 -4
  93. package/src/libs/redis/redis.ts +2 -4
  94. package/src/libs/redis/types.ts +2 -24
  95. package/src/libs/redis/utils.test.ts +0 -10
  96. package/src/libs/redis/utils.ts +0 -19
  97. package/src/libs/trpc/lambda/context.test.ts +0 -13
  98. package/src/libs/trpc/lambda/context.ts +21 -59
  99. package/src/libs/trpc/middleware/userAuth.ts +1 -7
  100. package/src/libs/trusted-client/getSessionUser.ts +15 -35
  101. package/src/server/globalConfig/index.ts +1 -3
  102. package/src/server/routers/lambda/__tests__/user.test.ts +0 -48
  103. package/src/server/routers/lambda/user.ts +1 -12
  104. package/src/server/services/email/impls/nodemailer/index.ts +2 -2
  105. package/src/server/services/webhookUser/index.ts +88 -0
  106. package/src/services/user/index.test.ts +0 -14
  107. package/src/services/user/index.ts +0 -4
  108. package/src/store/user/slices/auth/action.test.ts +22 -126
  109. package/src/store/user/slices/auth/action.ts +32 -65
  110. package/src/store/user/slices/auth/initialState.ts +0 -3
  111. package/src/store/user/slices/auth/selectors.ts +0 -3
  112. package/tests/setup.ts +10 -0
  113. package/scripts/_shared/checkDeprecatedClerkEnv.js +0 -42
  114. package/src/app/(backend)/api/auth/adapter/route.ts +0 -137
  115. package/src/app/[variants]/(auth)/next-auth/error/AuthErrorPage.tsx +0 -40
  116. package/src/app/[variants]/(auth)/next-auth/error/page.tsx +0 -11
  117. package/src/app/[variants]/(auth)/next-auth/signin/AuthSignInBox.tsx +0 -167
  118. package/src/app/[variants]/(auth)/next-auth/signin/page.tsx +0 -11
  119. package/src/app/[variants]/(auth)/reset-password/layout.tsx +0 -12
  120. package/src/app/[variants]/(auth)/signin/layout.tsx +0 -12
  121. package/src/app/[variants]/(auth)/verify-email/layout.tsx +0 -12
  122. package/src/envs/auth.test.ts +0 -47
  123. package/src/layout/AuthProvider/NextAuth/UserUpdater.tsx +0 -44
  124. package/src/layout/AuthProvider/NextAuth/index.tsx +0 -17
  125. package/src/libs/next-auth/adapter/index.ts +0 -177
  126. package/src/libs/next-auth/auth.config.ts +0 -64
  127. package/src/libs/next-auth/index.ts +0 -20
  128. package/src/libs/next-auth/sso-providers/auth0.ts +0 -24
  129. package/src/libs/next-auth/sso-providers/authelia.ts +0 -39
  130. package/src/libs/next-auth/sso-providers/authentik.ts +0 -25
  131. package/src/libs/next-auth/sso-providers/casdoor.ts +0 -50
  132. package/src/libs/next-auth/sso-providers/cloudflare-zero-trust.ts +0 -34
  133. package/src/libs/next-auth/sso-providers/cognito.ts +0 -8
  134. package/src/libs/next-auth/sso-providers/feishu.ts +0 -83
  135. package/src/libs/next-auth/sso-providers/generic-oidc.ts +0 -38
  136. package/src/libs/next-auth/sso-providers/github.ts +0 -23
  137. package/src/libs/next-auth/sso-providers/google.ts +0 -18
  138. package/src/libs/next-auth/sso-providers/index.ts +0 -35
  139. package/src/libs/next-auth/sso-providers/keycloak.ts +0 -22
  140. package/src/libs/next-auth/sso-providers/logto.ts +0 -48
  141. package/src/libs/next-auth/sso-providers/microsoft-entra-id-helper.ts +0 -29
  142. package/src/libs/next-auth/sso-providers/microsoft-entra-id.ts +0 -19
  143. package/src/libs/next-auth/sso-providers/okta.ts +0 -22
  144. package/src/libs/next-auth/sso-providers/sso.config.ts +0 -8
  145. package/src/libs/next-auth/sso-providers/wechat.ts +0 -36
  146. package/src/libs/next-auth/sso-providers/zitadel.ts +0 -21
  147. package/src/libs/redis/upstash.test.ts +0 -158
  148. package/src/libs/redis/upstash.ts +0 -136
  149. package/src/server/services/nextAuthUser/index.ts +0 -318
  150. package/src/server/services/nextAuthUser/utils.ts +0 -62
  151. package/src/types/next-auth.d.ts +0 -26
@@ -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
  };
@@ -3,7 +3,7 @@ import { NextResponse } from 'next/server';
3
3
  import { serverDB } from '@/database/server';
4
4
  import { authEnv } from '@/envs/auth';
5
5
  import { pino } from '@/libs/logger';
6
- import { NextAuthUserService } from '@/server/services/nextAuthUser';
6
+ import { WebhookUserService } from '@/server/services/webhookUser';
7
7
 
8
8
  import { validateRequest } from './validateRequest';
9
9
 
@@ -19,13 +19,13 @@ export const POST = async (req: Request): Promise<NextResponse> => {
19
19
 
20
20
  const { action, object } = payload;
21
21
 
22
- const nextAuthUserService = new NextAuthUserService(serverDB);
22
+ const webhookUserService = new WebhookUserService(serverDB);
23
23
  switch (action) {
24
24
  case 'update-user': {
25
- return nextAuthUserService.safeUpdateUser(
25
+ return webhookUserService.safeUpdateUser(
26
26
  {
27
- provider: 'casdoor',
28
- providerAccountId: object.id,
27
+ accountId: object.id,
28
+ providerId: 'casdoor',
29
29
  },
30
30
  {
31
31
  avatar: object?.avatar,
@@ -3,7 +3,7 @@ import { NextResponse } from 'next/server';
3
3
  import { serverDB } from '@/database/server';
4
4
  import { authEnv } from '@/envs/auth';
5
5
  import { pino } from '@/libs/logger';
6
- import { NextAuthUserService } from '@/server/services/nextAuthUser';
6
+ import { WebhookUserService } from '@/server/services/webhookUser';
7
7
 
8
8
  import { validateRequest } from './validateRequest';
9
9
 
@@ -21,13 +21,13 @@ export const POST = async (req: Request): Promise<NextResponse> => {
21
21
 
22
22
  pino.trace(`logto webhook payload: ${{ data, event }}`);
23
23
 
24
- const nextAuthUserService = new NextAuthUserService(serverDB);
24
+ const webhookUserService = new WebhookUserService(serverDB);
25
25
  switch (event) {
26
26
  case 'User.Data.Updated': {
27
- return nextAuthUserService.safeUpdateUser(
27
+ return webhookUserService.safeUpdateUser(
28
28
  {
29
- provider: 'logto',
30
- providerAccountId: data.id,
29
+ accountId: data.id,
30
+ providerId: 'logto',
31
31
  },
32
32
  {
33
33
  avatar: data?.avatar,
@@ -38,9 +38,9 @@ export const POST = async (req: Request): Promise<NextResponse> => {
38
38
  }
39
39
  case 'User.SuspensionStatus.Updated': {
40
40
  if (data.isSuspended) {
41
- return nextAuthUserService.safeSignOutUser({
42
- provider: 'logto',
43
- providerAccountId: data.id,
41
+ return webhookUserService.safeSignOutUser({
42
+ accountId: data.id,
43
+ providerId: 'logto',
44
44
  });
45
45
  }
46
46
  return NextResponse.json({ message: 'user reactivated', success: true }, { status: 200 });
@@ -24,10 +24,17 @@ vi.mock('@/envs/auth', async (importOriginal) => {
24
24
  const actual = await importOriginal<typeof import('@/envs/auth')>();
25
25
  return {
26
26
  ...actual,
27
- enableBetterAuth: false,
28
27
  };
29
28
  });
30
29
 
30
+ vi.mock('@/auth', () => ({
31
+ auth: {
32
+ api: {
33
+ getSession: vi.fn().mockResolvedValue(null),
34
+ },
35
+ },
36
+ }));
37
+
31
38
  describe('checkAuth', () => {
32
39
  const mockHandler: RequestHandler = vi.fn();
33
40
  const mockRequest = new Request('https://example.com');
@@ -6,14 +6,10 @@ import {
6
6
  import { ChatErrorType, type ClientSecretPayload } from '@lobechat/types';
7
7
  import { getXorPayload } from '@lobechat/utils/server';
8
8
 
9
+ import { auth } from '@/auth';
9
10
  import { getServerDB } from '@/database/core/db-adaptor';
10
11
  import { type LobeChatDatabase } from '@/database/type';
11
- import {
12
- LOBE_CHAT_AUTH_HEADER,
13
- LOBE_CHAT_OIDC_AUTH_HEADER,
14
- OAUTH_AUTHORIZED,
15
- enableBetterAuth,
16
- } from '@/envs/auth';
12
+ import { LOBE_CHAT_AUTH_HEADER, LOBE_CHAT_OIDC_AUTH_HEADER, OAUTH_AUTHORIZED } from '@/envs/auth';
17
13
  import { validateOIDCJWT } from '@/libs/oidc-provider/jwt';
18
14
  import { createErrorResponse } from '@/utils/errorResponse';
19
15
 
@@ -58,18 +54,13 @@ export const checkAuth =
58
54
  // get Authorization from header
59
55
  const authorization = req.headers.get(LOBE_CHAT_AUTH_HEADER);
60
56
  const oauthAuthorized = !!req.headers.get(OAUTH_AUTHORIZED);
61
- let betterAuthAuthorized = false;
62
57
 
63
58
  // better auth handler
64
- if (enableBetterAuth) {
65
- const { auth: betterAuth } = await import('@/auth');
66
-
67
- const session = await betterAuth.api.getSession({
68
- headers: req.headers,
69
- });
59
+ const session = await auth.api.getSession({
60
+ headers: req.headers,
61
+ });
70
62
 
71
- betterAuthAuthorized = !!session?.user?.id;
72
- }
63
+ const betterAuthAuthorized = !!session?.user?.id;
73
64
 
74
65
  if (!authorization) throw AgentRuntimeError.createError(ChatErrorType.Unauthorized);
75
66
 
@@ -2,49 +2,17 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
2
 
3
3
  import { checkAuthMethod } from './utils';
4
4
 
5
- let enableNextAuthMock = false;
6
- let enableBetterAuthMock = false;
7
-
8
- vi.mock('@/envs/auth', async (importOriginal) => {
9
- const data = await importOriginal();
10
-
11
- return {
12
- ...(data as any),
13
- get enableBetterAuth() {
14
- return enableBetterAuthMock;
15
- },
16
- get enableNextAuth() {
17
- return enableNextAuthMock;
18
- },
19
- };
20
- });
21
-
22
5
  describe('checkAuthMethod', () => {
23
6
  beforeEach(() => {
24
7
  vi.clearAllMocks();
25
8
  });
26
9
 
27
- it('should pass with valid Next auth', () => {
28
- enableNextAuthMock = true;
29
- expect(() =>
30
- checkAuthMethod({
31
- nextAuthAuthorized: true,
32
- }),
33
- ).not.toThrow();
34
-
35
- enableNextAuthMock = false;
36
- });
37
-
38
10
  it('should pass with valid Better Auth session', () => {
39
- enableBetterAuthMock = true;
40
-
41
11
  expect(() =>
42
12
  checkAuthMethod({
43
13
  betterAuthAuthorized: true,
44
14
  }),
45
15
  ).not.toThrow();
46
-
47
- enableBetterAuthMock = false;
48
16
  });
49
17
 
50
18
  it('should pass with valid API key', () => {
@@ -1,5 +1,3 @@
1
- import { enableBetterAuth, enableNextAuth } from '@/envs/auth';
2
-
3
1
  interface CheckAuthParams {
4
2
  apiKey?: string;
5
3
  betterAuthAuthorized?: boolean;
@@ -11,16 +9,13 @@ interface CheckAuthParams {
11
9
  * @param {CheckAuthParams} params - Authentication parameters extracted from headers.
12
10
  * @param {string} [params.apiKey] - The user API key.
13
11
  * @param {boolean} [params.betterAuthAuthorized] - Whether the Better Auth session exists.
14
- * @param {boolean} [params.nextAuthAuthorized] - Whether the OAuth 2 header is provided.
12
+ * @param {boolean} [params.nextAuthAuthorized] - Whether the OAuth 2 header is provided (legacy, kept for compatibility).
15
13
  */
16
14
  export const checkAuthMethod = (params: CheckAuthParams) => {
17
- const { apiKey, betterAuthAuthorized, nextAuthAuthorized } = params;
15
+ const { apiKey, betterAuthAuthorized } = params;
18
16
 
19
17
  // if better auth session exists
20
- if (enableBetterAuth && betterAuthAuthorized) return;
21
-
22
- // if next auth handler is provided
23
- if (enableNextAuth && nextAuthAuthorized) return;
18
+ if (betterAuthAuthorized) return;
24
19
 
25
20
  // if apiKey exist
26
21
  if (apiKey) return;
@@ -27,10 +27,17 @@ vi.mock('@/envs/auth', async (importOriginal) => {
27
27
  const actual = await importOriginal<typeof import('@/envs/auth')>();
28
28
  return {
29
29
  ...actual,
30
- enableBetterAuth: false,
31
30
  };
32
31
  });
33
32
 
33
+ vi.mock('@/auth', () => ({
34
+ auth: {
35
+ api: {
36
+ getSession: vi.fn().mockResolvedValue(null),
37
+ },
38
+ },
39
+ }));
40
+
34
41
  // 模拟请求和响应
35
42
  let request: Request;
36
43
  beforeEach(() => {
@@ -21,7 +21,6 @@ const handler = async (req: Request, { jwtPayload }: { jwtPayload?: any }) => {
21
21
 
22
22
  const caller = createCaller({
23
23
  jwtPayload,
24
- nextAuth: undefined, // WebAPI routes don't have nextAuth session
25
24
  userId: jwtPayload?.userId, // Required for userAuth middleware
26
25
  });
27
26
 
@@ -21,10 +21,17 @@ vi.mock('@/envs/auth', async (importOriginal) => {
21
21
  const actual = await importOriginal<typeof import('@/envs/auth')>();
22
22
  return {
23
23
  ...actual,
24
- enableBetterAuth: false,
25
24
  };
26
25
  });
27
26
 
27
+ vi.mock('@/auth', () => ({
28
+ auth: {
29
+ api: {
30
+ getSession: vi.fn().mockResolvedValue(null),
31
+ },
32
+ },
33
+ }));
34
+
28
35
  vi.mock('@/server/modules/ModelRuntime', () => ({
29
36
  initModelRuntimeFromDB: vi.fn(),
30
37
  }));
@@ -7,7 +7,7 @@ import { ChevronRight, Mail } from 'lucide-react';
7
7
  import { useEffect, useRef } from 'react';
8
8
  import { Trans, useTranslation } from 'react-i18next';
9
9
 
10
- import AuthIcons from '@/components/NextAuth/AuthIcons';
10
+ import AuthIcons from '@/components/AuthIcons';
11
11
  import { PRIVACY_URL, TERMS_URL } from '@/const/url';
12
12
 
13
13
  import AuthCard from '../../../../features/AuthCard';
@@ -1,5 +1,3 @@
1
- import { enableBetterAuth } from '@/envs/auth';
2
- import { notFound } from '@/libs/next/navigation';
3
1
  import { metadataModule } from '@/server/metadata';
4
2
  import { translation } from '@/server/translation';
5
3
  import { type DynamicLayoutProps } from '@/types/next';
@@ -9,28 +7,17 @@ import BetterAuthSignUpForm from './BetterAuthSignUpForm';
9
7
 
10
8
  export const generateMetadata = async (props: DynamicLayoutProps) => {
11
9
  const locale = await RouteVariants.getLocale(props);
12
-
13
- if (enableBetterAuth) {
14
- const { t } = await translation('auth', locale);
15
- return metadataModule.generate({
16
- description: t('betterAuth.signup.subtitle'),
17
- title: t('betterAuth.signup.title'),
18
- url: '/signup',
19
- });
20
- }
10
+ const { t } = await translation('auth', locale);
21
11
 
22
12
  return metadataModule.generate({
23
- title: 'Sign Up',
13
+ description: t('betterAuth.signup.subtitle'),
14
+ title: t('betterAuth.signup.title'),
24
15
  url: '/signup',
25
16
  });
26
17
  };
27
18
 
28
19
  const Page = () => {
29
- if (enableBetterAuth) {
30
- return <BetterAuthSignUpForm />;
31
- }
32
-
33
- return notFound();
20
+ return <BetterAuthSignUpForm />;
34
21
  };
35
22
 
36
23
  export default Page;
@@ -11,7 +11,7 @@ import { Editor, useEditor } from '@lobehub/editor/react';
11
11
  import { Flexbox, Icon, Text } from '@lobehub/ui';
12
12
  import { Card } from 'antd';
13
13
  import { Clock } from 'lucide-react';
14
- import { memo, useCallback, useEffect, useRef } from 'react';
14
+ import { type RefObject, memo, useCallback, useEffect, useRef } from 'react';
15
15
  import { useTranslation } from 'react-i18next';
16
16
 
17
17
  interface CronJobContentEditorProps {
@@ -20,8 +20,12 @@ interface CronJobContentEditorProps {
20
20
  onChange: (value: string) => void;
21
21
  }
22
22
 
23
- const CronJobContentEditor = memo<CronJobContentEditorProps>(
24
- ({ enableRichRender, initialValue, onChange }) => {
23
+ interface CronJobContentEditorInnerProps extends CronJobContentEditorProps {
24
+ contentRef: RefObject<string>;
25
+ }
26
+
27
+ const CronJobContentEditorInner = memo<CronJobContentEditorInnerProps>(
28
+ ({ enableRichRender, initialValue, onChange, contentRef }) => {
25
29
  const { t } = useTranslation('setting');
26
30
  const editor = useEditor();
27
31
  const currentValueRef = useRef(initialValue);
@@ -31,23 +35,6 @@ const CronJobContentEditor = memo<CronJobContentEditorProps>(
31
35
  currentValueRef.current = initialValue;
32
36
  }, [initialValue]);
33
37
 
34
- // Initialize editor content when editor is ready
35
- useEffect(() => {
36
- if (!editor) return;
37
- try {
38
- setTimeout(() => {
39
- if (initialValue) {
40
- editor.setDocument(enableRichRender ? 'markdown' : 'text', initialValue);
41
- }
42
- }, 100);
43
- } catch (error) {
44
- console.error('[CronJobContentEditor] Failed to initialize editor content:', error);
45
- setTimeout(() => {
46
- editor.setDocument(enableRichRender ? 'markdown' : 'text', initialValue);
47
- }, 100);
48
- }
49
- }, [editor, enableRichRender, initialValue]);
50
-
51
38
  // Handle content changes
52
39
  const handleContentChange = useCallback(
53
40
  (e: any) => {
@@ -57,13 +44,18 @@ const CronJobContentEditor = memo<CronJobContentEditorProps>(
57
44
 
58
45
  const finalContent = nextContent || '';
59
46
 
47
+ // Save to parent ref for restoration
48
+ if (contentRef) {
49
+ (contentRef as { current: string }).current = finalContent;
50
+ }
51
+
60
52
  // Only call onChange if content actually changed
61
53
  if (finalContent !== currentValueRef.current) {
62
54
  currentValueRef.current = finalContent;
63
55
  onChange(finalContent);
64
56
  }
65
57
  },
66
- [enableRichRender, onChange],
58
+ [enableRichRender, onChange, contentRef],
67
59
  );
68
60
 
69
61
  return (
@@ -82,6 +74,14 @@ const CronJobContentEditor = memo<CronJobContentEditorProps>(
82
74
  content={''}
83
75
  editor={editor}
84
76
  lineEmptyPlaceholder={t('agentCronJobs.form.content.placeholder')}
77
+ onInit={(editor) => {
78
+ // Restore content from parent ref when editor re-initializes
79
+ if (contentRef?.current) {
80
+ editor.setDocument(enableRichRender ? 'markdown' : 'text', contentRef.current);
81
+ } else if (initialValue) {
82
+ editor.setDocument(enableRichRender ? 'markdown' : 'text', initialValue);
83
+ }
84
+ }}
85
85
  onTextChange={handleContentChange}
86
86
  placeholder={t('agentCronJobs.form.content.placeholder')}
87
87
  plugins={
@@ -108,4 +108,17 @@ const CronJobContentEditor = memo<CronJobContentEditorProps>(
108
108
  },
109
109
  );
110
110
 
111
+ const CronJobContentEditor = (props: CronJobContentEditorProps) => {
112
+ // Ref to persist content across re-mounts when enableRichRender changes
113
+ const contentRef = useRef<string>(props.initialValue);
114
+
115
+ return (
116
+ <CronJobContentEditorInner
117
+ contentRef={contentRef}
118
+ key={`editor-${props.enableRichRender}`}
119
+ {...props}
120
+ />
121
+ );
122
+ };
123
+
111
124
  export default CronJobContentEditor;
@@ -5,8 +5,7 @@ import { type CSSProperties, memo, useMemo } from 'react';
5
5
  import { useTranslation } from 'react-i18next';
6
6
 
7
7
  import { modal, notification } from '@/components/AntdStaticMethods';
8
- import AuthIcons from '@/components/NextAuth/AuthIcons';
9
- import { userService } from '@/services/user';
8
+ import AuthIcons from '@/components/AuthIcons';
10
9
  import { useServerConfigStore } from '@/store/serverConfig';
11
10
  import { serverConfigSelectors } from '@/store/serverConfig/selectors';
12
11
  import { useUserStore } from '@/store/user';
@@ -17,7 +16,7 @@ const providerNameStyle: CSSProperties = {
17
16
  };
18
17
 
19
18
  export const SSOProvidersList = memo(() => {
20
- const isLoginWithBetterAuth = useUserStore(authSelectors.isLoginWithBetterAuth);
19
+ const isLogin = useUserStore(authSelectors.isLogin);
21
20
  const providers = useUserStore(authSelectors.authProviders);
22
21
  const hasPasswordAccount = useUserStore(authSelectors.hasPasswordAccount);
23
22
  const refreshAuthProviders = useUserStore((s) => s.refreshAuthProviders);
@@ -26,7 +25,7 @@ export const SSOProvidersList = memo(() => {
26
25
 
27
26
  // Allow unlink if user has multiple SSO providers OR has email/password login
28
27
  const allowUnlink = providers.length > 1 || hasPasswordAccount;
29
- const enableBetterAuthActions = !isDesktop && isLoginWithBetterAuth;
28
+ const enableAuthActions = !isDesktop && isLogin;
30
29
 
31
30
  // Get linked provider IDs for filtering
32
31
  const linkedProviderIds = useMemo(() => {
@@ -38,9 +37,9 @@ export const SSOProvidersList = memo(() => {
38
37
  return (oAuthSSOProviders || []).filter((provider) => !linkedProviderIds.has(provider));
39
38
  }, [oAuthSSOProviders, linkedProviderIds]);
40
39
 
41
- const handleUnlinkSSO = async (provider: string, providerAccountId: string) => {
40
+ const handleUnlinkSSO = async (provider: string) => {
42
41
  // Better-auth link/unlink operations are not available on desktop
43
- if (isDesktop && isLoginWithBetterAuth) return;
42
+ if (isDesktop) return;
44
43
 
45
44
  // Prevent unlink if this is the only login method
46
45
  if (!allowUnlink) {
@@ -55,14 +54,8 @@ export const SSOProvidersList = memo(() => {
55
54
  danger: true,
56
55
  },
57
56
  onOk: async () => {
58
- if (isLoginWithBetterAuth) {
59
- // Use better-auth native API
60
- const { unlinkAccount } = await import('@/libs/better-auth/auth-client');
61
- await unlinkAccount({ providerId: provider });
62
- } else {
63
- // Fallback for NextAuth
64
- await userService.unlinkSSOProvider(provider, providerAccountId);
65
- }
57
+ const { unlinkAccount } = await import('@/libs/better-auth/auth-client');
58
+ await unlinkAccount({ providerId: provider });
66
59
  refreshAuthProviders();
67
60
  },
68
61
  title: <span style={providerNameStyle}>{t('profile.sso.unlink.title', { provider })}</span>,
@@ -70,7 +63,7 @@ export const SSOProvidersList = memo(() => {
70
63
  };
71
64
 
72
65
  const handleLinkSSO = async (provider: string) => {
73
- if (enableBetterAuthActions) {
66
+ if (enableAuthActions) {
74
67
  // Use better-auth native linkSocial API
75
68
  const { linkSocial } = await import('@/libs/better-auth/auth-client');
76
69
  await linkSocial({
@@ -107,19 +100,19 @@ export const SSOProvidersList = memo(() => {
107
100
  </Text>
108
101
  )}
109
102
  </Flexbox>
110
- {!(isDesktop && isLoginWithBetterAuth) && (
103
+ {!isDesktop && (
111
104
  <ActionIcon
112
105
  disabled={!allowUnlink}
113
106
  icon={Unlink}
114
- onClick={() => handleUnlinkSSO(item.provider, item.providerAccountId)}
107
+ onClick={() => handleUnlinkSSO(item.provider)}
115
108
  size={'small'}
116
109
  />
117
110
  )}
118
111
  </Flexbox>
119
112
  ))}
120
113
 
121
- {/* Link Account Button - Only show for Better-Auth users with available providers */}
122
- {enableBetterAuthActions && availableProviders.length > 0 && (
114
+ {/* Link Account Button - Only show for logged in users with available providers */}
115
+ {enableAuthActions && availableProviders.length > 0 && (
123
116
  <DropdownMenu items={linkMenuItems} popupProps={{ style: { maxWidth: '200px' } }}>
124
117
  <Flexbox align={'center'} gap={6} horizontal style={{ cursor: 'pointer', fontSize: 12 }}>
125
118
  <Plus size={14} />