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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/.cursor/rules/db-migrations.mdc +16 -1
  2. package/.cursor/rules/project-introduce.mdc +1 -1
  3. package/.cursor/rules/project-structure.mdc +20 -2
  4. package/.env.example +148 -65
  5. package/.env.example.development +6 -8
  6. package/AGENTS.md +1 -3
  7. package/CHANGELOG.md +51 -0
  8. package/Dockerfile +6 -6
  9. package/GEMINI.md +63 -0
  10. package/README.md +8 -8
  11. package/README.zh-CN.md +8 -8
  12. package/changelog/v1.json +18 -0
  13. package/docs/development/database-schema.dbml +38 -0
  14. package/docs/self-hosting/advanced/auth.mdx +75 -2
  15. package/docs/self-hosting/advanced/auth.zh-CN.mdx +75 -2
  16. package/docs/self-hosting/environment-variables/auth.mdx +187 -1
  17. package/docs/self-hosting/environment-variables/auth.zh-CN.mdx +187 -1
  18. package/locales/en-US/auth.json +93 -0
  19. package/locales/zh-CN/auth.json +107 -1
  20. package/package.json +5 -2
  21. package/packages/const/src/auth.ts +2 -1
  22. package/packages/database/migrations/0048_add_editor_data.sql +1 -0
  23. package/packages/database/migrations/0049_better_auth.sql +49 -0
  24. package/packages/database/migrations/meta/0048_snapshot.json +7913 -0
  25. package/packages/database/migrations/meta/0049_snapshot.json +8151 -0
  26. package/packages/database/migrations/meta/_journal.json +14 -0
  27. package/packages/database/src/core/migrations.json +19 -0
  28. package/packages/database/src/index.ts +1 -0
  29. package/packages/database/src/models/__tests__/session.test.ts +1 -2
  30. package/packages/database/src/models/user.ts +9 -8
  31. package/packages/database/src/repositories/tableViewer/index.test.ts +2 -2
  32. package/packages/database/src/schemas/agent.ts +1 -0
  33. package/packages/database/src/schemas/betterAuth.ts +63 -0
  34. package/packages/database/src/schemas/index.ts +1 -0
  35. package/packages/database/src/schemas/ragEvals.ts +1 -2
  36. package/packages/database/src/schemas/user.ts +3 -2
  37. package/packages/database/src/server/models/__tests__/user.test.ts +1 -4
  38. package/packages/types/src/user/preference.ts +11 -0
  39. package/packages/utils/src/server/__tests__/auth.test.ts +52 -0
  40. package/packages/utils/src/server/auth.ts +18 -1
  41. package/src/app/(backend)/api/auth/[...all]/route.ts +19 -0
  42. package/src/app/(backend)/api/auth/check-user/route.ts +62 -0
  43. package/src/app/(backend)/middleware/auth/index.ts +14 -0
  44. package/src/app/(backend)/middleware/auth/utils.test.ts +16 -0
  45. package/src/app/(backend)/middleware/auth/utils.ts +13 -10
  46. package/src/app/(backend)/webapi/chat/[provider]/route.test.ts +1 -0
  47. package/src/app/[variants]/(auth)/reset-password/layout.tsx +12 -0
  48. package/src/app/[variants]/(auth)/reset-password/page.tsx +209 -0
  49. package/src/app/[variants]/(auth)/signin/layout.tsx +12 -0
  50. package/src/app/[variants]/(auth)/signin/page.tsx +448 -0
  51. package/src/app/[variants]/(auth)/signup/[[...signup]]/BetterAuthSignUpForm.tsx +192 -0
  52. package/src/app/[variants]/(auth)/signup/[[...signup]]/page.tsx +31 -6
  53. package/src/app/[variants]/(auth)/verify-email/layout.tsx +12 -0
  54. package/src/app/[variants]/(auth)/verify-email/page.tsx +164 -0
  55. package/src/app/[variants]/(main)/(mobile)/me/(home)/__tests__/UserBanner.test.tsx +12 -10
  56. package/src/app/[variants]/(main)/(mobile)/me/(home)/__tests__/useCategory.test.tsx +13 -11
  57. package/src/app/[variants]/(main)/chat/components/topic/features/Topic/TopicListContent/TopicItem/TopicContent.tsx +15 -8
  58. package/src/app/[variants]/(main)/chat/components/topic/features/Topic/TopicListContent/TopicItem/index.tsx +27 -30
  59. package/src/app/[variants]/(main)/profile/(home)/Client.tsx +306 -52
  60. package/src/app/[variants]/(main)/profile/(home)/features/SSOProvidersList/index.tsx +89 -47
  61. package/src/auth.ts +118 -0
  62. package/src/components/NextAuth/AuthIcons.tsx +3 -1
  63. package/src/envs/auth.ts +260 -13
  64. package/src/envs/email.ts +37 -0
  65. package/src/features/AgentSetting/AgentPlugin/index.tsx +6 -2
  66. package/src/features/User/UserPanel/PanelContent.tsx +6 -5
  67. package/src/features/User/__tests__/PanelContent.test.tsx +15 -6
  68. package/src/features/User/__tests__/UserAvatar.test.tsx +17 -6
  69. package/src/features/User/__tests__/useMenu.test.tsx +14 -12
  70. package/src/layout/AuthProvider/BetterAuth/UserUpdater.tsx +51 -0
  71. package/src/layout/AuthProvider/BetterAuth/index.tsx +14 -0
  72. package/src/layout/AuthProvider/index.tsx +3 -0
  73. package/src/layout/GlobalProvider/StoreInitialization.tsx +3 -3
  74. package/src/libs/better-auth/auth-client.ts +34 -0
  75. package/src/libs/better-auth/constants.ts +13 -0
  76. package/src/libs/better-auth/email-templates/index.ts +3 -0
  77. package/src/libs/better-auth/email-templates/magic-link.ts +98 -0
  78. package/src/libs/better-auth/email-templates/reset-password.ts +91 -0
  79. package/src/libs/better-auth/email-templates/verification.ts +108 -0
  80. package/src/libs/better-auth/sso/helpers.ts +61 -0
  81. package/src/libs/better-auth/sso/index.ts +113 -0
  82. package/src/libs/better-auth/sso/providers/auth0.ts +33 -0
  83. package/src/libs/better-auth/sso/providers/authelia.ts +35 -0
  84. package/src/libs/better-auth/sso/providers/authentik.ts +35 -0
  85. package/src/libs/better-auth/sso/providers/casdoor.ts +48 -0
  86. package/src/libs/better-auth/sso/providers/cloudflare-zero-trust.ts +41 -0
  87. package/src/libs/better-auth/sso/providers/cognito.ts +45 -0
  88. package/src/libs/better-auth/sso/providers/feishu.ts +181 -0
  89. package/src/libs/better-auth/sso/providers/generic-oidc.ts +44 -0
  90. package/src/libs/better-auth/sso/providers/github.ts +30 -0
  91. package/src/libs/better-auth/sso/providers/google.ts +30 -0
  92. package/src/libs/better-auth/sso/providers/keycloak.ts +35 -0
  93. package/src/libs/better-auth/sso/providers/logto.ts +38 -0
  94. package/src/libs/better-auth/sso/providers/microsoft.ts +65 -0
  95. package/src/libs/better-auth/sso/providers/okta.ts +37 -0
  96. package/src/libs/better-auth/sso/providers/wechat.ts +140 -0
  97. package/src/libs/better-auth/sso/providers/zitadel.ts +54 -0
  98. package/src/libs/better-auth/sso/types.ts +25 -0
  99. package/src/libs/better-auth/utils/client.ts +1 -0
  100. package/src/libs/better-auth/utils/common.ts +20 -0
  101. package/src/libs/better-auth/utils/server.test.ts +61 -0
  102. package/src/libs/better-auth/utils/server.ts +18 -0
  103. package/src/libs/trpc/lambda/context.test.ts +116 -0
  104. package/src/libs/trpc/lambda/context.ts +27 -0
  105. package/src/libs/trpc/middleware/userAuth.ts +4 -2
  106. package/src/locales/default/auth.ts +114 -1
  107. package/src/proxy.ts +71 -7
  108. package/src/server/globalConfig/index.ts +12 -1
  109. package/src/server/routers/lambda/user.ts +4 -0
  110. package/src/server/services/email/README.md +241 -0
  111. package/src/server/services/email/impls/index.test.ts +39 -0
  112. package/src/server/services/email/impls/index.ts +32 -0
  113. package/src/server/services/email/impls/nodemailer/index.ts +108 -0
  114. package/src/server/services/email/impls/nodemailer/type.ts +31 -0
  115. package/src/server/services/email/impls/type.ts +61 -0
  116. package/src/server/services/email/index.test.ts +144 -0
  117. package/src/server/services/email/index.ts +40 -0
  118. package/src/services/user/index.test.ts +162 -2
  119. package/src/services/user/index.ts +6 -3
  120. package/src/store/aiInfra/slices/aiProvider/action.ts +4 -4
  121. package/src/store/user/slices/auth/action.test.ts +213 -16
  122. package/src/store/user/slices/auth/action.ts +86 -1
  123. package/src/store/user/slices/auth/initialState.ts +13 -2
  124. package/src/store/user/slices/auth/selectors.ts +6 -2
  125. package/src/store/user/slices/common/action.ts +5 -1
  126. package/src/app/(backend)/api/auth/[...nextauth]/route.ts +0 -3
@@ -9,14 +9,14 @@ import { createStoreUpdater } from 'zustand-utils';
9
9
  import { useIsMobile } from '@/hooks/useIsMobile';
10
10
  import { useAgentStore } from '@/store/agent';
11
11
  import { useAiInfraStore } from '@/store/aiInfra';
12
+ import { useElectronStore } from '@/store/electron';
13
+ import { electronSyncSelectors } from '@/store/electron/selectors';
12
14
  import { useGlobalStore } from '@/store/global';
13
15
  import { useServerConfigStore } from '@/store/serverConfig';
14
16
  import { serverConfigSelectors } from '@/store/serverConfig/selectors';
15
17
  import { useUrlHydrationStore } from '@/store/urlHydration';
16
18
  import { useUserStore } from '@/store/user';
17
19
  import { authSelectors } from '@/store/user/selectors';
18
- import { electronSyncSelectors } from '@/store/electron/selectors';
19
- import { useElectronStore } from '@/store/electron';
20
20
 
21
21
  const StoreInitialization = memo(() => {
22
22
  // prefetch error ns to avoid don't show error content correctly
@@ -68,7 +68,7 @@ const StoreInitialization = memo(() => {
68
68
  const isSyncActive = useElectronStore((s) => electronSyncSelectors.isSyncActive(s));
69
69
 
70
70
  // init user provider key vaults
71
- useInitAiProviderKeyVaults(isLoginOnInit,isSyncActive);
71
+ useInitAiProviderKeyVaults(isLoginOnInit, isSyncActive);
72
72
 
73
73
  // init user state
74
74
  useInitUserState(isLoginOnInit, serverConfig, {
@@ -0,0 +1,34 @@
1
+ import {
2
+ genericOAuthClient,
3
+ inferAdditionalFields,
4
+ magicLinkClient,
5
+ } from 'better-auth/client/plugins';
6
+ import { createAuthClient } from 'better-auth/react';
7
+
8
+ import type { auth } from '@/auth';
9
+ import { getAuthConfig } from '@/envs/auth';
10
+
11
+ const { NEXT_PUBLIC_AUTH_URL } = getAuthConfig();
12
+ const enableMagicLink = getAuthConfig().NEXT_PUBLIC_ENABLE_MAGIC_LINK;
13
+
14
+ export const {
15
+ linkSocial,
16
+ accountInfo,
17
+ listAccounts,
18
+ requestPasswordReset,
19
+ resetPassword,
20
+ sendVerificationEmail,
21
+ signIn,
22
+ signOut,
23
+ signUp,
24
+ unlinkAccount,
25
+ useSession,
26
+ } = createAuthClient({
27
+ /** The base URL of the server (optional if you're using the same domain) */
28
+ baseURL: NEXT_PUBLIC_AUTH_URL,
29
+ plugins: [
30
+ inferAdditionalFields<typeof auth>(),
31
+ genericOAuthClient(),
32
+ ...(enableMagicLink ? [magicLinkClient()] : []),
33
+ ],
34
+ });
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Canonical IDs of Better-Auth built-in social providers.
3
+ * Keep this list in sync with provider definitions in `src/libs/better-auth/sso/providers`.
4
+ */
5
+ export const BUILTIN_BETTER_AUTH_PROVIDERS = ['google', 'github', 'cognito', 'microsoft'] as const;
6
+
7
+ /**
8
+ * Provider alias → canonical ID mapping.
9
+ * This is used on the client to normalize configured provider keys.
10
+ */
11
+ export const PROVIDER_ALIAS_MAP: Record<string, string> = {
12
+ 'microsoft-entra-id': 'microsoft',
13
+ };
@@ -0,0 +1,3 @@
1
+ export { getMagicLinkEmailTemplate } from './magic-link';
2
+ export { getResetPasswordEmailTemplate } from './reset-password';
3
+ export { getVerificationEmailTemplate } from './verification';
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Magic link sign-in email template
3
+ * Sent when user requests passwordless login
4
+ */
5
+ export const getMagicLinkEmailTemplate = (params: { expiresInSeconds: number; url: string }) => {
6
+ const { url, expiresInSeconds } = params;
7
+
8
+ const expiresInMinutes = Math.round(expiresInSeconds / 60);
9
+ const expirationText =
10
+ expiresInMinutes >= 1
11
+ ? `${expiresInMinutes} minute${expiresInMinutes > 1 ? 's' : ''}`
12
+ : `${expiresInSeconds} seconds`;
13
+
14
+ return {
15
+ html: `
16
+ <!DOCTYPE html>
17
+ <html lang="en">
18
+ <head>
19
+ <meta charset="utf-8">
20
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
21
+ <title>Sign in to LobeChat</title>
22
+ </head>
23
+ <body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f4f4f5; color: #1a1a1a;">
24
+ <!-- Container -->
25
+ <div style="max-width: 600px; margin: 0 auto; padding: 40px 20px;">
26
+
27
+ <!-- Logo -->
28
+ <div style="text-align: center; margin-bottom: 32px;">
29
+ <div style="display: inline-flex; align-items: center; justify-content: center; background-color: #ffffff; border-radius: 12px; padding: 8px 16px; box-shadow: 0 2px 8px rgba(0,0,0,0.04);">
30
+ <span style="font-size: 24px; line-height: 1; margin-right: 10px;">🤯</span>
31
+ <span style="font-size: 18px; font-weight: 700; color: #000000; letter-spacing: -0.5px;">LobeChat</span>
32
+ </div>
33
+ </div>
34
+
35
+ <!-- Card -->
36
+ <div style="background: #ffffff; border-radius: 20px; padding: 40px; box-shadow: 0 8px 30px rgba(0,0,0,0.04); border: 1px solid rgba(0,0,0,0.02);">
37
+
38
+ <!-- Header -->
39
+ <div style="text-align: center; margin-bottom: 32px;">
40
+ <h1 style="color: #111827; font-size: 24px; font-weight: 700; margin: 0 0 12px 0; letter-spacing: -0.5px;">
41
+ Sign in to LobeChat
42
+ </h1>
43
+ <p style="color: #6b7280; font-size: 16px; margin: 0; line-height: 1.5;">
44
+ Click the link below to sign in to your account.
45
+ </p>
46
+ </div>
47
+
48
+ <!-- Content -->
49
+ <div style="color: #374151; font-size: 16px; line-height: 1.6;">
50
+
51
+ <!-- Button -->
52
+ <div style="text-align: center; margin: 32px 0;">
53
+ <a href="${url}" target="_blank"
54
+ style="display: inline-block; background-color: #000000; color: #ffffff; text-decoration: none; padding: 16px 36px; border-radius: 14px; font-weight: 600; font-size: 16px; transition: all 0.2s ease; box-shadow: 0 4px 12px rgba(0,0,0,0.15);">
55
+ Sign In
56
+ </a>
57
+ </div>
58
+
59
+ <!-- Expiration Note -->
60
+ <div style="background-color: #f9fafb; border-radius: 12px; padding: 16px; margin-bottom: 24px; border: 1px solid #f3f4f6;">
61
+ <p style="color: #6b7280; font-size: 13px; margin: 0; text-align: center; line-height: 1.5;">
62
+ ⏰ This link will expire in <strong>${expirationText}</strong>.
63
+ </p>
64
+ </div>
65
+
66
+ <p style="color: #6b7280; font-size: 14px; margin: 0 0 8px 0; text-align: center;">
67
+ If you didn't request this email, you can safely ignore it.
68
+ </p>
69
+ </div>
70
+
71
+ <!-- Divider -->
72
+ <div style="border-top: 1px solid #e5e7eb; margin: 32px 0;"></div>
73
+
74
+ <!-- Fallback Link -->
75
+ <div style="text-align: center;">
76
+ <p style="color: #9ca3af; font-size: 13px; margin: 0 0 8px 0;">
77
+ Button not working? Copy and paste this link into your browser:
78
+ </p>
79
+ <a href="${url}" style="color: #2563eb; font-size: 13px; text-decoration: none; word-break: break-all; display: block; line-height: 1.4;">
80
+ ${url}
81
+ </a>
82
+ </div>
83
+ </div>
84
+
85
+ <!-- Footer -->
86
+ <div style="text-align: center; margin-top: 32px;">
87
+ <p style="color: #a1a1aa; font-size: 13px; margin: 0;">
88
+ © ${new Date().getFullYear()} LobeChat. All rights reserved.
89
+ </p>
90
+ </div>
91
+ </div>
92
+ </body>
93
+ </html>
94
+ `,
95
+ subject: 'Your LobeChat sign-in link',
96
+ text: `Use this link to sign in: ${url}\n\nThis link expires in ${expirationText}.`,
97
+ };
98
+ };
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Password reset email template
3
+ * Sent to users when they request a password reset
4
+ */
5
+ export const getResetPasswordEmailTemplate = (params: { url: string }) => {
6
+ const { url } = params;
7
+
8
+ return {
9
+ html: `
10
+ <!DOCTYPE html>
11
+ <html lang="en">
12
+ <head>
13
+ <meta charset="utf-8">
14
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
15
+ <title>Reset your password</title>
16
+ </head>
17
+ <body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f4f4f5; color: #1a1a1a;">
18
+ <!-- Container -->
19
+ <div style="max-width: 600px; margin: 0 auto; padding: 40px 20px;">
20
+
21
+ <!-- Logo -->
22
+ <div style="text-align: center; margin-bottom: 32px;">
23
+ <div style="display: inline-flex; align-items: center; justify-content: center; background-color: #ffffff; border-radius: 12px; padding: 8px 16px; box-shadow: 0 2px 8px rgba(0,0,0,0.04);">
24
+ <span style="font-size: 24px; line-height: 1; margin-right: 10px;">🤯</span>
25
+ <span style="font-size: 18px; font-weight: 700; color: #000000; letter-spacing: -0.5px;">LobeChat</span>
26
+ </div>
27
+ </div>
28
+
29
+ <!-- Card -->
30
+ <div style="background: #ffffff; border-radius: 20px; padding: 40px; box-shadow: 0 8px 30px rgba(0,0,0,0.04); border: 1px solid rgba(0,0,0,0.02);">
31
+
32
+ <!-- Header -->
33
+ <div style="text-align: center; margin-bottom: 32px;">
34
+ <h1 style="color: #111827; font-size: 24px; font-weight: 700; margin: 0 0 12px 0; letter-spacing: -0.5px;">
35
+ Reset Your Password
36
+ </h1>
37
+ <p style="color: #6b7280; font-size: 16px; margin: 0; line-height: 1.5;">
38
+ No worries, we'll help you get back on track.
39
+ </p>
40
+ </div>
41
+
42
+ <!-- Content -->
43
+ <div style="color: #374151; font-size: 16px; line-height: 1.6;">
44
+ <p style="margin: 0 0 24px 0; text-align: center;">
45
+ You recently requested to reset your password for your LobeChat account. Click the button below to proceed.
46
+ </p>
47
+
48
+ <!-- Button -->
49
+ <div style="text-align: center; margin: 32px 0;">
50
+ <a href="${url}" target="_blank"
51
+ style="display: inline-block; background-color: #000000; color: #ffffff; text-decoration: none; padding: 16px 36px; border-radius: 14px; font-weight: 600; font-size: 16px; transition: all 0.2s ease; box-shadow: 0 4px 12px rgba(0,0,0,0.15);">
52
+ Reset Password
53
+ </a>
54
+ </div>
55
+
56
+ <!-- Security Note -->
57
+ <div style="background-color: #f9fafb; border-radius: 12px; padding: 16px; margin-bottom: 24px; border: 1px solid #f3f4f6;">
58
+ <p style="color: #6b7280; font-size: 13px; margin: 0; text-align: center; line-height: 1.5;">
59
+ 🔒 If you did not request a password reset, please ignore this email or contact support if you have concerns.
60
+ </p>
61
+ </div>
62
+ </div>
63
+
64
+ <!-- Divider -->
65
+ <div style="border-top: 1px solid #e5e7eb; margin: 32px 0;"></div>
66
+
67
+ <!-- Fallback Link -->
68
+ <div style="text-align: center;">
69
+ <p style="color: #9ca3af; font-size: 13px; margin: 0 0 8px 0;">
70
+ Trouble clicking the button? Copy and paste the URL below:
71
+ </p>
72
+ <a href="${url}" style="color: #2563eb; font-size: 13px; text-decoration: none; word-break: break-all; display: block; line-height: 1.4;">
73
+ ${url}
74
+ </a>
75
+ </div>
76
+ </div>
77
+
78
+ <!-- Footer -->
79
+ <div style="text-align: center; margin-top: 32px;">
80
+ <p style="color: #a1a1aa; font-size: 13px; margin: 0;">
81
+ © ${new Date().getFullYear()} LobeChat. All rights reserved.
82
+ </p>
83
+ </div>
84
+ </div>
85
+ </body>
86
+ </html>
87
+ `,
88
+ subject: 'Reset Your Password - LobeChat',
89
+ text: `Reset your password by clicking this link: ${url}`,
90
+ };
91
+ };
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Email verification template
3
+ * Sent to users when they sign up to verify their email address
4
+ */
5
+ export const getVerificationEmailTemplate = (params: {
6
+ expiresInSeconds: number;
7
+ url: string;
8
+ userName?: string | null;
9
+ }) => {
10
+ const { url, userName, expiresInSeconds } = params;
11
+
12
+ // Format expiration time in a human-readable way
13
+ const expiresInHours = expiresInSeconds / 3600;
14
+ const expirationText =
15
+ expiresInHours >= 1
16
+ ? `${expiresInHours} hour${expiresInHours > 1 ? 's' : ''}`
17
+ : `${expiresInSeconds / 60} minutes`;
18
+
19
+ return {
20
+ html: `
21
+ <!DOCTYPE html>
22
+ <html lang="en">
23
+ <head>
24
+ <meta charset="utf-8">
25
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
26
+ <title>Verify your email</title>
27
+ </head>
28
+ <body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f4f4f5; color: #1a1a1a;">
29
+ <!-- Container -->
30
+ <div style="max-width: 600px; margin: 0 auto; padding: 40px 20px;">
31
+
32
+ <!-- Logo -->
33
+ <div style="text-align: center; margin-bottom: 32px;">
34
+ <div style="display: inline-flex; align-items: center; justify-content: center; background-color: #ffffff; border-radius: 12px; padding: 8px 16px; box-shadow: 0 2px 8px rgba(0,0,0,0.04);">
35
+ <span style="font-size: 24px; line-height: 1; margin-right: 10px;">🤯</span>
36
+ <span style="font-size: 18px; font-weight: 700; color: #000000; letter-spacing: -0.5px;">LobeChat</span>
37
+ </div>
38
+ </div>
39
+
40
+ <!-- Card -->
41
+ <div style="background: #ffffff; border-radius: 20px; padding: 40px; box-shadow: 0 8px 30px rgba(0,0,0,0.04); border: 1px solid rgba(0,0,0,0.02);">
42
+
43
+ <!-- Header -->
44
+ <div style="text-align: center; margin-bottom: 32px;">
45
+ <h1 style="color: #111827; font-size: 24px; font-weight: 700; margin: 0 0 12px 0; letter-spacing: -0.5px;">
46
+ Verify your email address
47
+ </h1>
48
+ <p style="color: #6b7280; font-size: 16px; margin: 0;">
49
+ Let's get you signed in.
50
+ </p>
51
+ </div>
52
+
53
+ <!-- Content -->
54
+ <div style="color: #374151; font-size: 16px; line-height: 1.6;">
55
+ ${userName ? `<p style="margin: 0 0 16px 0;">Hi <strong>${userName}</strong>,</p>` : ''}
56
+
57
+ <p style="margin: 0 0 24px 0;">
58
+ Thanks for creating an account with LobeChat. To access your account, please verify your email address by clicking the button below.
59
+ </p>
60
+
61
+ <!-- Button -->
62
+ <div style="text-align: center; margin: 36px 0;">
63
+ <a href="${url}" target="_blank"
64
+ style="display: inline-block; background-color: #000000; color: #ffffff; text-decoration: none; padding: 16px 36px; border-radius: 14px; font-weight: 600; font-size: 16px; transition: transform 0.1s ease; box-shadow: 0 4px 12px rgba(0,0,0,0.15);">
65
+ Verify Email Address
66
+ </a>
67
+ </div>
68
+
69
+ <!-- Expiration Note -->
70
+ <div style="background-color: #f9fafb; border-radius: 12px; padding: 16px; margin-bottom: 24px; border: 1px solid #f3f4f6;">
71
+ <p style="color: #6b7280; font-size: 14px; margin: 0; text-align: center;">
72
+ ⏰ This link will expire in <strong>${expirationText}</strong>.
73
+ </p>
74
+ </div>
75
+
76
+ <p style="color: #6b7280; font-size: 15px; margin: 0 0 8px 0;">
77
+ If you didn't create an account, you can safely ignore this email.
78
+ </p>
79
+ </div>
80
+
81
+ <!-- Divider -->
82
+ <div style="border-top: 1px solid #e5e7eb; margin: 32px 0;"></div>
83
+
84
+ <!-- Fallback Link -->
85
+ <div style="text-align: center;">
86
+ <p style="color: #9ca3af; font-size: 13px; margin: 0 0 8px 0;">
87
+ Button not working? Copy and paste this link into your browser:
88
+ </p>
89
+ <a href="${url}" style="color: #2563eb; font-size: 13px; text-decoration: none; word-break: break-all; display: block; line-height: 1.4;">
90
+ ${url}
91
+ </a>
92
+ </div>
93
+ </div>
94
+
95
+ <!-- Footer -->
96
+ <div style="text-align: center; margin-top: 32px;">
97
+ <p style="color: #a1a1aa; font-size: 13px; margin: 0;">
98
+ © 2025 LobeChat. All rights reserved.
99
+ </p>
100
+ </div>
101
+ </div>
102
+ </body>
103
+ </html>
104
+ `,
105
+ subject: 'Verify Your Email - LobeChat',
106
+ text: `Please verify your email by clicking this link: ${url}\n\nThis link will expire in ${expirationText}.`,
107
+ };
108
+ };
@@ -0,0 +1,61 @@
1
+ import type { GenericOAuthConfig } from 'better-auth/plugins';
2
+
3
+ export const DEFAULT_OIDC_SCOPES = ['openid', 'email', 'profile'];
4
+
5
+ export const pickEnv = (...values: (string | undefined | null)[]) => {
6
+ for (const value of values) {
7
+ const trimmed = value?.trim();
8
+ if (trimmed) {
9
+ return trimmed;
10
+ }
11
+ }
12
+
13
+ return undefined;
14
+ };
15
+
16
+ const createDiscoveryUrl = (issuer: string) => {
17
+ const normalized = issuer.replace(/\/$/, '');
18
+ return normalized.includes('/.well-known/')
19
+ ? normalized
20
+ : `${normalized}/.well-known/openid-configuration`;
21
+ };
22
+
23
+ type OIDCProviderInput = {
24
+ clientId?: string;
25
+ clientSecret?: string;
26
+ issuer?: string;
27
+ overrides?: Partial<GenericOAuthConfig>;
28
+ pkce?: boolean;
29
+ providerId: string;
30
+ scopes?: string[];
31
+ };
32
+
33
+ export const buildOidcConfig = ({
34
+ providerId,
35
+ clientId,
36
+ clientSecret,
37
+ issuer,
38
+ scopes = DEFAULT_OIDC_SCOPES,
39
+ pkce = true,
40
+ overrides,
41
+ }: OIDCProviderInput): GenericOAuthConfig => {
42
+ const sanitizedIssuer = issuer?.trim();
43
+
44
+ if (!clientId || !clientSecret || !sanitizedIssuer) {
45
+ throw new Error(`[Better-Auth] ${providerId} OAuth enabled but missing credentials`);
46
+ }
47
+
48
+ const normalizedIssuer = sanitizedIssuer.replace(/\/$/, '');
49
+ const discoveryUrl = createDiscoveryUrl(normalizedIssuer);
50
+
51
+ return {
52
+ clientId,
53
+ clientSecret,
54
+ discoveryUrl,
55
+ pkce,
56
+ providerId,
57
+ scopes,
58
+ // ...fallbackEndpoints,
59
+ ...overrides,
60
+ } satisfies GenericOAuthConfig;
61
+ };
@@ -0,0 +1,113 @@
1
+ import type { GenericOAuthConfig } from 'better-auth/plugins';
2
+ import type { SocialProviders } from 'better-auth/social-providers';
3
+
4
+ import { authEnv } from '@/envs/auth';
5
+ import { BUILTIN_BETTER_AUTH_PROVIDERS } from '@/libs/better-auth/constants';
6
+ import { parseSSOProviders } from '@/libs/better-auth/utils/server';
7
+
8
+ import Auth0 from './providers/auth0';
9
+ import Authelia from './providers/authelia';
10
+ import Authentik from './providers/authentik';
11
+ import Casdoor from './providers/casdoor';
12
+ import CloudflareZeroTrust from './providers/cloudflare-zero-trust';
13
+ import Cognito from './providers/cognito';
14
+ import Feishu from './providers/feishu';
15
+ import GenericOIDC from './providers/generic-oidc';
16
+ import Github from './providers/github';
17
+ import Google from './providers/google';
18
+ import Keycloak from './providers/keycloak';
19
+ import Logto from './providers/logto';
20
+ import Microsoft from './providers/microsoft';
21
+ import Okta from './providers/okta';
22
+ import Wechat from './providers/wechat';
23
+ import Zitadel from './providers/zitadel';
24
+
25
+ const providerDefinitions = [
26
+ Google,
27
+ Github,
28
+ Cognito,
29
+ Microsoft,
30
+ Auth0,
31
+ Authelia,
32
+ Authentik,
33
+ Casdoor,
34
+ CloudflareZeroTrust,
35
+ GenericOIDC,
36
+ Keycloak,
37
+ Logto,
38
+ Okta,
39
+ Zitadel,
40
+ Feishu,
41
+ Wechat,
42
+ ] as const;
43
+
44
+ const builtInProviderIds = new Set(BUILTIN_BETTER_AUTH_PROVIDERS);
45
+
46
+ for (const definition of providerDefinitions) {
47
+ if (definition.type === 'builtin' && !builtInProviderIds.has(definition.id)) {
48
+ throw new Error(
49
+ `[Better-Auth] Built-in provider "${definition.id}" is not registered in BUILTIN_BETTER_AUTH_PROVIDERS (src/libs/better-auth/constants.ts). Please update the constant to keep them in sync.`,
50
+ );
51
+ }
52
+ }
53
+
54
+ const providerRegistry = new Map<string, (typeof providerDefinitions)[number]>();
55
+
56
+ for (const definition of providerDefinitions) {
57
+ providerRegistry.set(definition.id, definition);
58
+ definition.aliases?.forEach((alias) => providerRegistry.set(alias, definition));
59
+ }
60
+
61
+ export const initBetterAuthSSOProviders = () => {
62
+ const enabledProviders = parseSSOProviders(authEnv.AUTH_SSO_PROVIDERS);
63
+
64
+ const socialProviders: SocialProviders = {};
65
+ const genericOAuthProviders: GenericOAuthConfig[] = [];
66
+
67
+ for (const rawProvider of enabledProviders) {
68
+ const definition = providerRegistry.get(rawProvider);
69
+
70
+ if (!definition) {
71
+ throw new Error(`[Better-Auth] Unknown SSO provider: ${rawProvider}`);
72
+ }
73
+
74
+ /**
75
+ * Providers expose checkEnvs predicates so we can fail fast when credentials are missing instead
76
+ * of encountering harder-to-trace errors later in the Better-Auth pipeline.
77
+ */
78
+ const env = definition.checkEnvs();
79
+ if (!env) {
80
+ throw new Error(
81
+ `[Better-Auth] ${rawProvider} SSO provider environment variables are not set correctly!`,
82
+ );
83
+ }
84
+
85
+ if (definition.type === 'builtin') {
86
+ const providerId = definition.id;
87
+ if (socialProviders[providerId]) {
88
+ throw new Error(`[Better-Auth] Duplicate SSO provider: ${providerId}`);
89
+ }
90
+
91
+ // @ts-expect-error - build expects specific env type, but we use union definition type
92
+ const config = definition.build(env);
93
+ if (config) {
94
+ // @ts-expect-error hard to type
95
+ socialProviders[providerId] = config;
96
+ }
97
+
98
+ continue;
99
+ }
100
+
101
+ // @ts-expect-error - build expects specific env type, but we use union definition type
102
+ const config = definition.build(env);
103
+
104
+ if (config) {
105
+ genericOAuthProviders.push(config);
106
+ }
107
+ }
108
+
109
+ return {
110
+ genericOAuthProviders,
111
+ socialProviders: socialProviders,
112
+ };
113
+ };
@@ -0,0 +1,33 @@
1
+ import { authEnv } from '@/envs/auth';
2
+
3
+ import { buildOidcConfig } from '../helpers';
4
+ import type { GenericProviderDefinition } from '../types';
5
+
6
+ const provider: GenericProviderDefinition<{
7
+ AUTH_AUTH0_ID: string;
8
+ AUTH_AUTH0_ISSUER: string;
9
+ AUTH_AUTH0_SECRET: string;
10
+ }> = {
11
+ build: (env) => {
12
+ const config = buildOidcConfig({
13
+ clientId: env.AUTH_AUTH0_ID,
14
+ clientSecret: env.AUTH_AUTH0_SECRET,
15
+ issuer: env.AUTH_AUTH0_ISSUER,
16
+ providerId: 'auth0',
17
+ });
18
+ return config;
19
+ },
20
+ checkEnvs: () => {
21
+ return !!(authEnv.AUTH_AUTH0_ID && authEnv.AUTH_AUTH0_SECRET && authEnv.AUTH_AUTH0_ISSUER)
22
+ ? {
23
+ AUTH_AUTH0_ID: authEnv.AUTH_AUTH0_ID,
24
+ AUTH_AUTH0_ISSUER: authEnv.AUTH_AUTH0_ISSUER,
25
+ AUTH_AUTH0_SECRET: authEnv.AUTH_AUTH0_SECRET,
26
+ }
27
+ : false;
28
+ },
29
+ id: 'auth0',
30
+ type: 'generic',
31
+ };
32
+
33
+ export default provider;
@@ -0,0 +1,35 @@
1
+ import { authEnv } from '@/envs/auth';
2
+
3
+ import { buildOidcConfig } from '../helpers';
4
+ import type { GenericProviderDefinition } from '../types';
5
+
6
+ const provider: GenericProviderDefinition<{
7
+ AUTH_AUTHELIA_ID: string;
8
+ AUTH_AUTHELIA_ISSUER: string;
9
+ AUTH_AUTHELIA_SECRET: string;
10
+ }> = {
11
+ build: (env) =>
12
+ buildOidcConfig({
13
+ clientId: env.AUTH_AUTHELIA_ID,
14
+ clientSecret: env.AUTH_AUTHELIA_SECRET,
15
+ issuer: env.AUTH_AUTHELIA_ISSUER,
16
+ providerId: 'authelia',
17
+ }),
18
+ checkEnvs: () => {
19
+ return !!(
20
+ authEnv.AUTH_AUTHELIA_ID &&
21
+ authEnv.AUTH_AUTHELIA_SECRET &&
22
+ authEnv.AUTH_AUTHELIA_ISSUER
23
+ )
24
+ ? {
25
+ AUTH_AUTHELIA_ID: authEnv.AUTH_AUTHELIA_ID,
26
+ AUTH_AUTHELIA_ISSUER: authEnv.AUTH_AUTHELIA_ISSUER,
27
+ AUTH_AUTHELIA_SECRET: authEnv.AUTH_AUTHELIA_SECRET,
28
+ }
29
+ : false;
30
+ },
31
+ id: 'authelia',
32
+ type: 'generic',
33
+ };
34
+
35
+ export default provider;
@@ -0,0 +1,35 @@
1
+ import { authEnv } from '@/envs/auth';
2
+
3
+ import { buildOidcConfig } from '../helpers';
4
+ import type { GenericProviderDefinition } from '../types';
5
+
6
+ const provider: GenericProviderDefinition<{
7
+ AUTH_AUTHENTIK_ID: string;
8
+ AUTH_AUTHENTIK_ISSUER: string;
9
+ AUTH_AUTHENTIK_SECRET: string;
10
+ }> = {
11
+ build: (env) =>
12
+ buildOidcConfig({
13
+ clientId: env.AUTH_AUTHENTIK_ID,
14
+ clientSecret: env.AUTH_AUTHENTIK_SECRET,
15
+ issuer: env.AUTH_AUTHENTIK_ISSUER,
16
+ providerId: 'authentik',
17
+ }),
18
+ checkEnvs: () => {
19
+ return !!(
20
+ authEnv.AUTH_AUTHENTIK_ID &&
21
+ authEnv.AUTH_AUTHENTIK_SECRET &&
22
+ authEnv.AUTH_AUTHENTIK_ISSUER
23
+ )
24
+ ? {
25
+ AUTH_AUTHENTIK_ID: authEnv.AUTH_AUTHENTIK_ID,
26
+ AUTH_AUTHENTIK_ISSUER: authEnv.AUTH_AUTHENTIK_ISSUER,
27
+ AUTH_AUTHENTIK_SECRET: authEnv.AUTH_AUTHENTIK_SECRET,
28
+ }
29
+ : false;
30
+ },
31
+ id: 'authentik',
32
+ type: 'generic',
33
+ };
34
+
35
+ export default provider;