@lobehub/lobehub 2.0.0-next.124 → 2.0.0-next.126

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) 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 +50 -0
  8. package/Dockerfile +7 -5
  9. package/GEMINI.md +63 -0
  10. package/changelog/v1.json +18 -0
  11. package/docs/development/database-schema.dbml +37 -0
  12. package/docs/self-hosting/advanced/auth.mdx +82 -2
  13. package/docs/self-hosting/advanced/auth.zh-CN.mdx +82 -2
  14. package/docs/self-hosting/environment-variables/auth.mdx +187 -1
  15. package/docs/self-hosting/environment-variables/auth.zh-CN.mdx +187 -1
  16. package/locales/en-US/auth.json +93 -0
  17. package/locales/zh-CN/auth.json +107 -1
  18. package/package.json +5 -2
  19. package/packages/const/src/auth.ts +2 -1
  20. package/packages/database/migrations/0049_better_auth.sql +49 -0
  21. package/packages/database/migrations/meta/0048_snapshot.json +312 -932
  22. package/packages/database/migrations/meta/0049_snapshot.json +8151 -0
  23. package/packages/database/migrations/meta/_journal.json +8 -1
  24. package/packages/database/src/core/migrations.json +13 -0
  25. package/packages/database/src/index.ts +1 -0
  26. package/packages/database/src/models/__tests__/session.test.ts +1 -2
  27. package/packages/database/src/models/user.ts +9 -8
  28. package/packages/database/src/repositories/tableViewer/index.test.ts +2 -2
  29. package/packages/database/src/schemas/betterAuth.ts +63 -0
  30. package/packages/database/src/schemas/index.ts +1 -0
  31. package/packages/database/src/schemas/ragEvals.ts +1 -2
  32. package/packages/database/src/schemas/user.ts +3 -2
  33. package/packages/database/src/server/models/__tests__/user.test.ts +1 -4
  34. package/packages/types/src/user/preference.ts +11 -0
  35. package/packages/utils/src/server/__tests__/auth.test.ts +52 -0
  36. package/packages/utils/src/server/auth.ts +18 -1
  37. package/src/app/(backend)/api/auth/[...all]/route.ts +19 -0
  38. package/src/app/(backend)/api/auth/check-user/route.ts +62 -0
  39. package/src/app/(backend)/middleware/auth/index.ts +14 -0
  40. package/src/app/(backend)/middleware/auth/utils.test.ts +16 -0
  41. package/src/app/(backend)/middleware/auth/utils.ts +13 -10
  42. package/src/app/(backend)/webapi/chat/[provider]/route.test.ts +1 -0
  43. package/src/app/[variants]/(auth)/reset-password/layout.tsx +12 -0
  44. package/src/app/[variants]/(auth)/reset-password/page.tsx +209 -0
  45. package/src/app/[variants]/(auth)/signin/layout.tsx +12 -0
  46. package/src/app/[variants]/(auth)/signin/page.tsx +448 -0
  47. package/src/app/[variants]/(auth)/signup/[[...signup]]/BetterAuthSignUpForm.tsx +192 -0
  48. package/src/app/[variants]/(auth)/signup/[[...signup]]/page.tsx +31 -6
  49. package/src/app/[variants]/(auth)/verify-email/layout.tsx +12 -0
  50. package/src/app/[variants]/(auth)/verify-email/page.tsx +164 -0
  51. package/src/app/[variants]/(main)/(mobile)/me/(home)/__tests__/UserBanner.test.tsx +12 -10
  52. package/src/app/[variants]/(main)/(mobile)/me/(home)/__tests__/useCategory.test.tsx +13 -11
  53. package/src/app/[variants]/(main)/profile/(home)/Client.tsx +306 -52
  54. package/src/app/[variants]/(main)/profile/(home)/features/SSOProvidersList/index.tsx +89 -47
  55. package/src/auth.ts +118 -0
  56. package/src/components/NextAuth/AuthIcons.tsx +3 -1
  57. package/src/envs/auth.ts +260 -13
  58. package/src/envs/email.ts +37 -0
  59. package/src/features/User/UserPanel/PanelContent.tsx +6 -5
  60. package/src/features/User/__tests__/PanelContent.test.tsx +15 -6
  61. package/src/features/User/__tests__/UserAvatar.test.tsx +17 -6
  62. package/src/features/User/__tests__/useMenu.test.tsx +14 -12
  63. package/src/layout/AuthProvider/BetterAuth/UserUpdater.tsx +51 -0
  64. package/src/layout/AuthProvider/BetterAuth/index.tsx +14 -0
  65. package/src/layout/AuthProvider/index.tsx +3 -0
  66. package/src/libs/better-auth/auth-client.ts +34 -0
  67. package/src/libs/better-auth/constants.ts +13 -0
  68. package/src/libs/better-auth/email-templates/index.ts +3 -0
  69. package/src/libs/better-auth/email-templates/magic-link.ts +98 -0
  70. package/src/libs/better-auth/email-templates/reset-password.ts +91 -0
  71. package/src/libs/better-auth/email-templates/verification.ts +108 -0
  72. package/src/libs/better-auth/sso/helpers.ts +61 -0
  73. package/src/libs/better-auth/sso/index.ts +113 -0
  74. package/src/libs/better-auth/sso/providers/auth0.ts +33 -0
  75. package/src/libs/better-auth/sso/providers/authelia.ts +35 -0
  76. package/src/libs/better-auth/sso/providers/authentik.ts +35 -0
  77. package/src/libs/better-auth/sso/providers/casdoor.ts +48 -0
  78. package/src/libs/better-auth/sso/providers/cloudflare-zero-trust.ts +41 -0
  79. package/src/libs/better-auth/sso/providers/cognito.ts +45 -0
  80. package/src/libs/better-auth/sso/providers/feishu.ts +181 -0
  81. package/src/libs/better-auth/sso/providers/generic-oidc.ts +44 -0
  82. package/src/libs/better-auth/sso/providers/github.ts +30 -0
  83. package/src/libs/better-auth/sso/providers/google.ts +30 -0
  84. package/src/libs/better-auth/sso/providers/keycloak.ts +35 -0
  85. package/src/libs/better-auth/sso/providers/logto.ts +38 -0
  86. package/src/libs/better-auth/sso/providers/microsoft.ts +65 -0
  87. package/src/libs/better-auth/sso/providers/okta.ts +37 -0
  88. package/src/libs/better-auth/sso/providers/wechat.ts +140 -0
  89. package/src/libs/better-auth/sso/providers/zitadel.ts +54 -0
  90. package/src/libs/better-auth/sso/types.ts +25 -0
  91. package/src/libs/better-auth/utils/client.ts +1 -0
  92. package/src/libs/better-auth/utils/common.ts +20 -0
  93. package/src/libs/better-auth/utils/server.test.ts +61 -0
  94. package/src/libs/better-auth/utils/server.ts +18 -0
  95. package/src/libs/trpc/lambda/context.test.ts +116 -0
  96. package/src/libs/trpc/lambda/context.ts +27 -0
  97. package/src/libs/trpc/middleware/userAuth.ts +4 -2
  98. package/src/locales/default/auth.ts +114 -1
  99. package/src/proxy.ts +71 -7
  100. package/src/server/globalConfig/index.ts +12 -1
  101. package/src/server/routers/lambda/user.ts +4 -0
  102. package/src/server/services/email/README.md +241 -0
  103. package/src/server/services/email/impls/index.test.ts +39 -0
  104. package/src/server/services/email/impls/index.ts +32 -0
  105. package/src/server/services/email/impls/nodemailer/index.ts +108 -0
  106. package/src/server/services/email/impls/nodemailer/type.ts +31 -0
  107. package/src/server/services/email/impls/type.ts +61 -0
  108. package/src/server/services/email/index.test.ts +144 -0
  109. package/src/server/services/email/index.ts +40 -0
  110. package/src/services/user/index.test.ts +162 -2
  111. package/src/services/user/index.ts +6 -3
  112. package/src/store/user/slices/auth/action.test.ts +213 -16
  113. package/src/store/user/slices/auth/action.ts +86 -1
  114. package/src/store/user/slices/auth/initialState.ts +13 -2
  115. package/src/store/user/slices/auth/selectors.ts +6 -2
  116. package/src/store/user/slices/common/action.ts +5 -1
  117. package/src/app/(backend)/api/auth/[...nextauth]/route.ts +0 -3
@@ -1,4 +1,4 @@
1
- import { Google } from '@lobehub/icons';
1
+ import { Aws, Google, Microsoft } from '@lobehub/icons';
2
2
  import {
3
3
  Auth0,
4
4
  Authelia,
@@ -19,10 +19,12 @@ const iconComponents: { [key: string]: React.ElementType } = {
19
19
  'authentik': Authentik.Color,
20
20
  'casdoor': Casdoor.Color,
21
21
  'cloudflare': Cloudflare.Color,
22
+ 'cognito': Aws.Color,
22
23
  'default': NextAuth.Color,
23
24
  'github': Github,
24
25
  'google': Google.Color,
25
26
  'logto': Logto.Color,
27
+ 'microsoft': Microsoft.Color,
26
28
  'microsoft-entra-id': MicrosoftEntra.Color,
27
29
  'zitadel': Zitadel.Color,
28
30
  };
package/src/envs/auth.ts CHANGED
@@ -11,6 +11,12 @@ declare global {
11
11
  CLERK_SECRET_KEY?: string;
12
12
  CLERK_WEBHOOK_SECRET?: string;
13
13
 
14
+ // ===== Auth (shared by Better Auth / Next Auth) ===== //
15
+ AUTH_SECRET?: string;
16
+ NEXT_PUBLIC_AUTH_URL?: string;
17
+ NEXT_PUBLIC_AUTH_EMAIL_VERIFICATION?: string;
18
+ AUTH_SSO_PROVIDERS?: string;
19
+
14
20
  // ===== Next Auth ===== //
15
21
  NEXT_AUTH_SECRET?: string;
16
22
 
@@ -20,11 +26,78 @@ declare global {
20
26
 
21
27
  NEXT_AUTH_SSO_SESSION_STRATEGY?: string;
22
28
 
23
- // Github
24
- GITHUB_CLIENT_ID?: string;
25
- GITHUB_CLIENT_SECRET?: string;
29
+ // ===== Next Auth Provider Credentials ===== //
30
+ AUTH_GOOGLE_ID?: string;
31
+ AUTH_GOOGLE_SECRET?: string;
32
+
33
+ AUTH_GITHUB_ID?: string;
34
+ AUTH_GITHUB_SECRET?: string;
35
+
36
+ AUTH_COGNITO_ID?: string;
37
+ AUTH_COGNITO_SECRET?: string;
38
+ AUTH_COGNITO_ISSUER?: string;
39
+ AUTH_COGNITO_DOMAIN?: string;
40
+ AUTH_COGNITO_REGION?: string;
41
+ AUTH_COGNITO_USERPOOL_ID?: string;
42
+
43
+ AUTH_MICROSOFT_ID?: string;
44
+ AUTH_MICROSOFT_SECRET?: string;
45
+
46
+ AUTH_AUTH0_ID?: string;
47
+ AUTH_AUTH0_SECRET?: string;
48
+ AUTH_AUTH0_ISSUER?: string;
49
+
50
+ AUTH_AUTHELIA_ID?: string;
51
+ AUTH_AUTHELIA_SECRET?: string;
52
+ AUTH_AUTHELIA_ISSUER?: string;
53
+
54
+ AUTH_AUTHENTIK_ID?: string;
55
+ AUTH_AUTHENTIK_SECRET?: string;
56
+ AUTH_AUTHENTIK_ISSUER?: string;
57
+
58
+ AUTH_CASDOOR_ID?: string;
59
+ AUTH_CASDOOR_SECRET?: string;
60
+ AUTH_CASDOOR_ISSUER?: string;
61
+
62
+ AUTH_CLOUDFLARE_ZERO_TRUST_ID?: string;
63
+ AUTH_CLOUDFLARE_ZERO_TRUST_SECRET?: string;
64
+ AUTH_CLOUDFLARE_ZERO_TRUST_ISSUER?: string;
65
+
66
+ AUTH_FEISHU_APP_ID?: string;
67
+ AUTH_FEISHU_APP_SECRET?: string;
68
+
69
+ AUTH_GENERIC_OIDC_ID?: string;
70
+ AUTH_GENERIC_OIDC_SECRET?: string;
71
+ AUTH_GENERIC_OIDC_ISSUER?: string;
72
+
73
+ AUTH_KEYCLOAK_ID?: string;
74
+ AUTH_KEYCLOAK_SECRET?: string;
75
+ AUTH_KEYCLOAK_ISSUER?: string;
76
+
77
+ AUTH_LOGTO_ID?: string;
78
+ AUTH_LOGTO_SECRET?: string;
79
+ AUTH_LOGTO_ISSUER?: string;
80
+
81
+ AUTH_MICROSOFT_ENTRA_ID_ID?: string;
82
+ AUTH_MICROSOFT_ENTRA_ID_SECRET?: string;
83
+ AUTH_MICROSOFT_ENTRA_ID_TENANT_ID?: string;
84
+ AUTH_MICROSOFT_ENTRA_ID_BASE_URL?: string;
85
+
86
+ AUTH_OKTA_ID?: string;
87
+ AUTH_OKTA_SECRET?: string;
88
+ AUTH_OKTA_ISSUER?: string;
89
+
90
+ AUTH_WECHAT_ID?: string;
91
+ AUTH_WECHAT_SECRET?: string;
92
+
93
+ AUTH_ZITADEL_ID?: string;
94
+ AUTH_ZITADEL_SECRET?: string;
95
+ AUTH_ZITADEL_ISSUER?: string;
96
+
97
+ AUTH_AZURE_AD_ID?: string;
98
+ AUTH_AZURE_AD_SECRET?: string;
99
+ AUTH_AZURE_AD_TENANT_ID?: string;
26
100
 
27
- // Azure AD
28
101
  AZURE_AD_CLIENT_ID?: string;
29
102
  AZURE_AD_CLIENT_SECRET?: string;
30
103
  AZURE_AD_TENANT_ID?: string;
@@ -40,30 +113,113 @@ declare global {
40
113
  export const getAuthConfig = () => {
41
114
  return createEnv({
42
115
  client: {
116
+ // ---------------------------------- clerk ----------------------------------
117
+ NEXT_PUBLIC_ENABLE_CLERK_AUTH: z.boolean().optional().default(false),
43
118
  NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string().optional(),
44
- /**
45
- * whether to enabled clerk
46
- */
47
- NEXT_PUBLIC_ENABLE_CLERK_AUTH: z.boolean().optional(),
48
119
 
120
+ // ---------------------------------- better auth ----------------------------------
121
+ NEXT_PUBLIC_ENABLE_BETTER_AUTH: z.boolean().optional(),
122
+ NEXT_PUBLIC_AUTH_URL: z.string().optional(),
123
+ NEXT_PUBLIC_AUTH_EMAIL_VERIFICATION: z.boolean().optional().default(false),
124
+ NEXT_PUBLIC_ENABLE_MAGIC_LINK: z.boolean().optional().default(false),
125
+
126
+ // ---------------------------------- next auth ----------------------------------
49
127
  NEXT_PUBLIC_ENABLE_NEXT_AUTH: z.boolean().optional(),
50
128
  },
51
129
  server: {
52
- // Clerk
130
+ // ---------------------------------- clerk ----------------------------------
53
131
  CLERK_SECRET_KEY: z.string().optional(),
54
132
  CLERK_WEBHOOK_SECRET: z.string().optional(),
55
133
 
56
- // NEXT-AUTH
134
+ // ---------------------------------- better auth ----------------------------------
135
+ AUTH_SECRET: z.string().optional(),
136
+ AUTH_SSO_PROVIDERS: z.string().optional().default(''),
137
+
138
+ // ---------------------------------- next auth ----------------------------------
57
139
  NEXT_AUTH_SECRET: z.string().optional(),
58
140
  NEXT_AUTH_SSO_PROVIDERS: z.string().optional().default('auth0'),
59
141
  NEXT_AUTH_DEBUG: z.boolean().optional().default(false),
60
142
  NEXT_AUTH_SSO_SESSION_STRATEGY: z.enum(['jwt', 'database']).optional().default('jwt'),
61
143
 
62
- // Azure AD
144
+ AUTH_GOOGLE_ID: z.string().optional(),
145
+ AUTH_GOOGLE_SECRET: z.string().optional(),
146
+
147
+ AUTH_GITHUB_ID: z.string().optional(),
148
+ AUTH_GITHUB_SECRET: z.string().optional(),
149
+
150
+ AUTH_COGNITO_ID: z.string().optional(),
151
+ AUTH_COGNITO_SECRET: z.string().optional(),
152
+ AUTH_COGNITO_ISSUER: z.string().optional(),
153
+ AUTH_COGNITO_DOMAIN: z.string().optional(),
154
+ AUTH_COGNITO_REGION: z.string().optional(),
155
+ AUTH_COGNITO_USERPOOL_ID: z.string().optional(),
156
+
157
+ AUTH_MICROSOFT_ID: z.string().optional(),
158
+ AUTH_MICROSOFT_SECRET: z.string().optional(),
159
+
160
+ AUTH_AUTH0_ID: z.string().optional(),
161
+ AUTH_AUTH0_SECRET: z.string().optional(),
162
+ AUTH_AUTH0_ISSUER: z.string().optional(),
163
+
164
+ AUTH_AUTHELIA_ID: z.string().optional(),
165
+ AUTH_AUTHELIA_SECRET: z.string().optional(),
166
+ AUTH_AUTHELIA_ISSUER: z.string().optional(),
167
+
168
+ AUTH_AUTHENTIK_ID: z.string().optional(),
169
+ AUTH_AUTHENTIK_SECRET: z.string().optional(),
170
+ AUTH_AUTHENTIK_ISSUER: z.string().optional(),
171
+
172
+ AUTH_CASDOOR_ID: z.string().optional(),
173
+ AUTH_CASDOOR_SECRET: z.string().optional(),
174
+ AUTH_CASDOOR_ISSUER: z.string().optional(),
175
+
176
+ AUTH_CLOUDFLARE_ZERO_TRUST_ID: z.string().optional(),
177
+ AUTH_CLOUDFLARE_ZERO_TRUST_SECRET: z.string().optional(),
178
+ AUTH_CLOUDFLARE_ZERO_TRUST_ISSUER: z.string().optional(),
179
+
180
+ AUTH_FEISHU_APP_ID: z.string().optional(),
181
+ AUTH_FEISHU_APP_SECRET: z.string().optional(),
182
+
183
+ AUTH_GENERIC_OIDC_ID: z.string().optional(),
184
+ AUTH_GENERIC_OIDC_SECRET: z.string().optional(),
185
+ AUTH_GENERIC_OIDC_ISSUER: z.string().optional(),
186
+
187
+ AUTH_KEYCLOAK_ID: z.string().optional(),
188
+ AUTH_KEYCLOAK_SECRET: z.string().optional(),
189
+ AUTH_KEYCLOAK_ISSUER: z.string().optional(),
190
+
191
+ AUTH_LOGTO_ID: z.string().optional(),
192
+ AUTH_LOGTO_SECRET: z.string().optional(),
193
+ AUTH_LOGTO_ISSUER: z.string().optional(),
194
+
195
+ AUTH_MICROSOFT_ENTRA_ID_ID: z.string().optional(),
196
+ AUTH_MICROSOFT_ENTRA_ID_SECRET: z.string().optional(),
197
+ AUTH_MICROSOFT_ENTRA_ID_TENANT_ID: z.string().optional(),
198
+ AUTH_MICROSOFT_ENTRA_ID_BASE_URL: z.string().optional(),
199
+
200
+ AUTH_OKTA_ID: z.string().optional(),
201
+ AUTH_OKTA_SECRET: z.string().optional(),
202
+ AUTH_OKTA_ISSUER: z.string().optional(),
203
+
204
+ AUTH_WECHAT_ID: z.string().optional(),
205
+ AUTH_WECHAT_SECRET: z.string().optional(),
206
+
207
+ AUTH_ZITADEL_ID: z.string().optional(),
208
+ AUTH_ZITADEL_SECRET: z.string().optional(),
209
+ AUTH_ZITADEL_ISSUER: z.string().optional(),
210
+
211
+ AUTH_AZURE_AD_ID: z.string().optional(),
212
+ AUTH_AZURE_AD_SECRET: z.string().optional(),
213
+ AUTH_AZURE_AD_TENANT_ID: z.string().optional(),
214
+
63
215
  AZURE_AD_CLIENT_ID: z.string().optional(),
64
216
  AZURE_AD_CLIENT_SECRET: z.string().optional(),
65
217
  AZURE_AD_TENANT_ID: z.string().optional(),
66
218
 
219
+ ZITADEL_CLIENT_ID: z.string().optional(),
220
+ ZITADEL_CLIENT_SECRET: z.string().optional(),
221
+ ZITADEL_ISSUER: z.string().optional(),
222
+
67
223
  LOGTO_WEBHOOK_SIGNING_KEY: z.string().optional(),
68
224
 
69
225
  // Casdoor
@@ -77,18 +233,109 @@ export const getAuthConfig = () => {
77
233
  CLERK_SECRET_KEY: process.env.CLERK_SECRET_KEY,
78
234
  CLERK_WEBHOOK_SECRET: process.env.CLERK_WEBHOOK_SECRET,
79
235
 
80
- // Next Auth
236
+ // ---------------------------------- better auth ----------------------------------
237
+ NEXT_PUBLIC_ENABLE_BETTER_AUTH: process.env.NEXT_PUBLIC_ENABLE_BETTER_AUTH === '1',
238
+ // Fallback to NEXTAUTH_URL origin for seamless migration from next-auth
239
+ NEXT_PUBLIC_AUTH_URL:
240
+ process.env.NEXT_PUBLIC_AUTH_URL ??
241
+ (process.env.NEXTAUTH_URL ? new URL(process.env.NEXTAUTH_URL).origin : undefined),
242
+ NEXT_PUBLIC_AUTH_EMAIL_VERIFICATION: process.env.NEXT_PUBLIC_AUTH_EMAIL_VERIFICATION === '1',
243
+ NEXT_PUBLIC_ENABLE_MAGIC_LINK: process.env.NEXT_PUBLIC_ENABLE_MAGIC_LINK === '1',
244
+ // Fallback to NEXT_AUTH_SECRET for seamless migration from next-auth
245
+ AUTH_SECRET: process.env.AUTH_SECRET ?? process.env.NEXT_AUTH_SECRET,
246
+ // Fallback to NEXT_AUTH_SSO_PROVIDERS for seamless migration from next-auth
247
+ AUTH_SSO_PROVIDERS: process.env.AUTH_SSO_PROVIDERS ?? process.env.NEXT_AUTH_SSO_PROVIDERS,
248
+
249
+ // better-auth env for Cognito provider is different from next-auth's one
250
+ AUTH_COGNITO_DOMAIN: process.env.AUTH_COGNITO_DOMAIN,
251
+ AUTH_COGNITO_REGION: process.env.AUTH_COGNITO_REGION,
252
+ AUTH_COGNITO_USERPOOL_ID: process.env.AUTH_COGNITO_USERPOOL_ID,
253
+
254
+ // ---------------------------------- next auth ----------------------------------
81
255
  NEXT_PUBLIC_ENABLE_NEXT_AUTH: process.env.NEXT_PUBLIC_ENABLE_NEXT_AUTH === '1',
82
256
  NEXT_AUTH_SSO_PROVIDERS: process.env.NEXT_AUTH_SSO_PROVIDERS,
83
257
  NEXT_AUTH_SECRET: process.env.NEXT_AUTH_SECRET,
84
258
  NEXT_AUTH_DEBUG: !!process.env.NEXT_AUTH_DEBUG,
85
259
  NEXT_AUTH_SSO_SESSION_STRATEGY: process.env.NEXT_AUTH_SSO_SESSION_STRATEGY || 'jwt',
86
260
 
87
- // Azure AD
261
+ // Next Auth Provider Credentials
262
+ AUTH_GOOGLE_ID: process.env.AUTH_GOOGLE_ID,
263
+ AUTH_GOOGLE_SECRET: process.env.AUTH_GOOGLE_SECRET,
264
+
265
+ AUTH_GITHUB_ID: process.env.AUTH_GITHUB_ID,
266
+ AUTH_GITHUB_SECRET: process.env.AUTH_GITHUB_SECRET,
267
+
268
+ AUTH_MICROSOFT_ID: process.env.AUTH_MICROSOFT_ID,
269
+ AUTH_MICROSOFT_SECRET: process.env.AUTH_MICROSOFT_SECRET,
270
+
271
+ AUTH_COGNITO_ID: process.env.AUTH_COGNITO_ID,
272
+ AUTH_COGNITO_SECRET: process.env.AUTH_COGNITO_SECRET,
273
+ AUTH_COGNITO_ISSUER: process.env.AUTH_COGNITO_ISSUER,
274
+
275
+ AUTH_AUTH0_ID: process.env.AUTH_AUTH0_ID,
276
+ AUTH_AUTH0_SECRET: process.env.AUTH_AUTH0_SECRET,
277
+ AUTH_AUTH0_ISSUER: process.env.AUTH_AUTH0_ISSUER,
278
+
279
+ AUTH_AUTHELIA_ID: process.env.AUTH_AUTHELIA_ID,
280
+ AUTH_AUTHELIA_SECRET: process.env.AUTH_AUTHELIA_SECRET,
281
+ AUTH_AUTHELIA_ISSUER: process.env.AUTH_AUTHELIA_ISSUER,
282
+
283
+ AUTH_AUTHENTIK_ID: process.env.AUTH_AUTHENTIK_ID,
284
+ AUTH_AUTHENTIK_SECRET: process.env.AUTH_AUTHENTIK_SECRET,
285
+ AUTH_AUTHENTIK_ISSUER: process.env.AUTH_AUTHENTIK_ISSUER,
286
+
287
+ AUTH_CASDOOR_ID: process.env.AUTH_CASDOOR_ID,
288
+ AUTH_CASDOOR_SECRET: process.env.AUTH_CASDOOR_SECRET,
289
+ AUTH_CASDOOR_ISSUER: process.env.AUTH_CASDOOR_ISSUER,
290
+
291
+ AUTH_CLOUDFLARE_ZERO_TRUST_ID: process.env.AUTH_CLOUDFLARE_ZERO_TRUST_ID,
292
+ AUTH_CLOUDFLARE_ZERO_TRUST_SECRET: process.env.AUTH_CLOUDFLARE_ZERO_TRUST_SECRET,
293
+ AUTH_CLOUDFLARE_ZERO_TRUST_ISSUER: process.env.AUTH_CLOUDFLARE_ZERO_TRUST_ISSUER,
294
+
295
+ AUTH_FEISHU_APP_ID: process.env.AUTH_FEISHU_APP_ID,
296
+ AUTH_FEISHU_APP_SECRET: process.env.AUTH_FEISHU_APP_SECRET,
297
+
298
+ AUTH_GENERIC_OIDC_ID: process.env.AUTH_GENERIC_OIDC_ID,
299
+ AUTH_GENERIC_OIDC_SECRET: process.env.AUTH_GENERIC_OIDC_SECRET,
300
+ AUTH_GENERIC_OIDC_ISSUER: process.env.AUTH_GENERIC_OIDC_ISSUER,
301
+
302
+ AUTH_KEYCLOAK_ID: process.env.AUTH_KEYCLOAK_ID,
303
+ AUTH_KEYCLOAK_SECRET: process.env.AUTH_KEYCLOAK_SECRET,
304
+ AUTH_KEYCLOAK_ISSUER: process.env.AUTH_KEYCLOAK_ISSUER,
305
+
306
+ AUTH_LOGTO_ID: process.env.AUTH_LOGTO_ID,
307
+ AUTH_LOGTO_SECRET: process.env.AUTH_LOGTO_SECRET,
308
+ AUTH_LOGTO_ISSUER: process.env.AUTH_LOGTO_ISSUER,
309
+
310
+ AUTH_MICROSOFT_ENTRA_ID_ID: process.env.AUTH_MICROSOFT_ENTRA_ID_ID,
311
+ AUTH_MICROSOFT_ENTRA_ID_SECRET: process.env.AUTH_MICROSOFT_ENTRA_ID_SECRET,
312
+ AUTH_MICROSOFT_ENTRA_ID_TENANT_ID: process.env.AUTH_MICROSOFT_ENTRA_ID_TENANT_ID,
313
+ AUTH_MICROSOFT_ENTRA_ID_BASE_URL: process.env.AUTH_MICROSOFT_ENTRA_ID_BASE_URL,
314
+
315
+ AUTH_OKTA_ID: process.env.AUTH_OKTA_ID,
316
+ AUTH_OKTA_SECRET: process.env.AUTH_OKTA_SECRET,
317
+ AUTH_OKTA_ISSUER: process.env.AUTH_OKTA_ISSUER,
318
+
319
+ AUTH_WECHAT_ID: process.env.AUTH_WECHAT_ID,
320
+ AUTH_WECHAT_SECRET: process.env.AUTH_WECHAT_SECRET,
321
+
322
+ AUTH_ZITADEL_ID: process.env.AUTH_ZITADEL_ID,
323
+ AUTH_ZITADEL_SECRET: process.env.AUTH_ZITADEL_SECRET,
324
+ AUTH_ZITADEL_ISSUER: process.env.AUTH_ZITADEL_ISSUER,
325
+
326
+ AUTH_AZURE_AD_ID: process.env.AUTH_AZURE_AD_ID,
327
+ AUTH_AZURE_AD_SECRET: process.env.AUTH_AZURE_AD_SECRET,
328
+ AUTH_AZURE_AD_TENANT_ID: process.env.AUTH_AZURE_AD_TENANT_ID,
329
+
330
+ // legacy Azure AD envs for backward compatibility
88
331
  AZURE_AD_CLIENT_ID: process.env.AZURE_AD_CLIENT_ID,
89
332
  AZURE_AD_CLIENT_SECRET: process.env.AZURE_AD_CLIENT_SECRET,
90
333
  AZURE_AD_TENANT_ID: process.env.AZURE_AD_TENANT_ID,
91
334
 
335
+ ZITADEL_CLIENT_ID: process.env.ZITADEL_CLIENT_ID,
336
+ ZITADEL_CLIENT_SECRET: process.env.ZITADEL_CLIENT_SECRET,
337
+ ZITADEL_ISSUER: process.env.ZITADEL_ISSUER,
338
+
92
339
  // LOGTO
93
340
  LOGTO_WEBHOOK_SIGNING_KEY: process.env.LOGTO_WEBHOOK_SIGNING_KEY,
94
341
 
@@ -0,0 +1,37 @@
1
+ /* eslint-disable sort-keys-fix/sort-keys-fix */
2
+ import { createEnv } from '@t3-oss/env-nextjs';
3
+ import { z } from 'zod';
4
+
5
+ declare global {
6
+ // eslint-disable-next-line @typescript-eslint/no-namespace
7
+ namespace NodeJS {
8
+ interface ProcessEnv {
9
+ SMTP_HOST?: string;
10
+ SMTP_PASS?: string;
11
+ SMTP_PORT?: string;
12
+ SMTP_SECURE?: string;
13
+ SMTP_USER?: string;
14
+ }
15
+ }
16
+ }
17
+
18
+ export const getEmailConfig = () => {
19
+ return createEnv({
20
+ server: {
21
+ SMTP_HOST: z.string().optional(),
22
+ SMTP_PORT: z.coerce.number().optional(),
23
+ SMTP_SECURE: z.boolean().optional(),
24
+ SMTP_USER: z.string().optional(),
25
+ SMTP_PASS: z.string().optional(),
26
+ },
27
+ runtimeEnv: {
28
+ SMTP_HOST: process.env.SMTP_HOST,
29
+ SMTP_PORT: process.env.SMTP_PORT ? Number(process.env.SMTP_PORT) : undefined,
30
+ SMTP_SECURE: process.env.SMTP_SECURE === 'true',
31
+ SMTP_USER: process.env.SMTP_USER,
32
+ SMTP_PASS: process.env.SMTP_PASS,
33
+ },
34
+ });
35
+ };
36
+
37
+ export const emailEnv = getEmailConfig();
@@ -1,10 +1,12 @@
1
- import { Link } from 'react-router-dom';
1
+ import { enableBetterAuth, enableNextAuth } from '@lobechat/const';
2
2
  import { useRouter } from 'next/navigation';
3
3
  import { memo } from 'react';
4
4
  import { Flexbox } from 'react-layout-kit';
5
+ import { Link } from 'react-router-dom';
5
6
 
6
7
  import BrandWatermark from '@/components/BrandWatermark';
7
8
  import Menu from '@/components/Menu';
9
+ import { isDesktop } from '@/const/version';
8
10
  import { useUserStore } from '@/store/user';
9
11
  import { authSelectors } from '@/store/user/selectors';
10
12
 
@@ -14,8 +16,6 @@ import UserLoginOrSignup from '../UserLoginOrSignup';
14
16
  import LangButton from './LangButton';
15
17
  import ThemeButton from './ThemeButton';
16
18
  import { useMenu } from './useMenu';
17
- import { enableNextAuth } from '@/const/auth';
18
- import { isDesktop } from '@/const/version';
19
19
 
20
20
  const PanelContent = memo<{ closePopover: () => void }>(({ closePopover }) => {
21
21
  const router = useRouter();
@@ -31,8 +31,9 @@ const PanelContent = memo<{ closePopover: () => void }>(({ closePopover }) => {
31
31
  const handleSignOut = () => {
32
32
  signOut();
33
33
  closePopover();
34
- // NextAuth doesn't need to redirect to login page
35
- if (enableNextAuth) return;
34
+ // NextAuth and Better Auth handle redirect in their own signOut methods
35
+ if (enableNextAuth || enableBetterAuth) return;
36
+ // Clerk uses /login page
36
37
  router.push('/login');
37
38
  };
38
39
 
@@ -1,6 +1,6 @@
1
1
  import { act, render, screen } from '@testing-library/react';
2
2
  import { MemoryRouter } from 'react-router-dom';
3
- import { afterEach, describe, expect, it, vi } from 'vitest';
3
+ import { describe, expect, it, vi } from 'vitest';
4
4
 
5
5
  import { useUserStore } from '@/store/user';
6
6
 
@@ -67,13 +67,22 @@ vi.mock('@/const/version', () => ({
67
67
  isDesktop: false,
68
68
  }));
69
69
 
70
- // 定义一个变量来存储 enableAuth 的值
71
- let enableAuth = true;
70
+ // Use vi.hoisted to ensure variables exist before vi.mock factory executes
71
+ const { enableAuth, enableClerk, enableNextAuth } = vi.hoisted(() => ({
72
+ enableAuth: { value: true },
73
+ enableClerk: { value: false },
74
+ enableNextAuth: { value: false },
75
+ }));
72
76
 
73
- // 模拟 @/const/auth 模块
74
77
  vi.mock('@/const/auth', () => ({
75
78
  get enableAuth() {
76
- return enableAuth;
79
+ return enableAuth.value;
80
+ },
81
+ get enableClerk() {
82
+ return enableClerk.value;
83
+ },
84
+ get enableNextAuth() {
85
+ return enableNextAuth.value;
77
86
  },
78
87
  }));
79
88
 
@@ -145,7 +154,7 @@ describe('PanelContent', () => {
145
154
  });
146
155
 
147
156
  it('should render BrandWatermark when disable auth', () => {
148
- enableAuth = false;
157
+ enableAuth.value = false;
149
158
 
150
159
  act(() => {
151
160
  useUserStore.setState({ isSignedIn: false });
@@ -9,18 +9,29 @@ import UserAvatar from '../UserAvatar';
9
9
 
10
10
  vi.mock('zustand/traditional');
11
11
 
12
- // 定义一个变量来存储 enableAuth 的值
13
- let enableAuth = true;
12
+ // Use vi.hoisted to ensure variables exist before vi.mock factory executes
13
+ const { enableAuth, enableClerk, enableNextAuth } = vi.hoisted(() => ({
14
+ enableAuth: { value: true },
15
+ enableClerk: { value: false },
16
+ enableNextAuth: { value: false },
17
+ }));
14
18
 
15
- // 模拟 @/const/auth 模块
16
19
  vi.mock('@/const/auth', () => ({
17
20
  get enableAuth() {
18
- return enableAuth;
21
+ return enableAuth.value;
22
+ },
23
+ get enableClerk() {
24
+ return enableClerk.value;
25
+ },
26
+ get enableNextAuth() {
27
+ return enableNextAuth.value;
19
28
  },
20
29
  }));
21
30
 
22
31
  afterEach(() => {
23
- enableAuth = true;
32
+ enableAuth.value = true;
33
+ enableClerk.value = false;
34
+ enableNextAuth.value = false;
24
35
  });
25
36
 
26
37
  describe('UserAvatar', () => {
@@ -71,7 +82,7 @@ describe('UserAvatar', () => {
71
82
 
72
83
  describe('disable Auth', () => {
73
84
  it('should show LobeChat and default avatar when the user is not logged in and disabled auth', () => {
74
- enableAuth = false;
85
+ enableAuth.value = false;
75
86
  act(() => {
76
87
  useUserStore.setState({ enableAuth: () => false, isSignedIn: false, user: undefined });
77
88
  });
@@ -48,22 +48,24 @@ vi.mock('./useNewVersion', () => ({
48
48
  useNewVersion: vi.fn(() => false),
49
49
  }));
50
50
 
51
- // 定义一个变量来存储 enableAuth 的值
52
- let enableAuth = true;
53
- let enableClerk = true;
54
- // 模拟 @/const/auth 模块
51
+ // Use vi.hoisted to ensure variables exist before vi.mock factory executes
52
+ const { enableAuth, enableClerk } = vi.hoisted(() => ({
53
+ enableAuth: { value: true },
54
+ enableClerk: { value: true },
55
+ }));
56
+
55
57
  vi.mock('@/const/auth', () => ({
56
58
  get enableAuth() {
57
- return enableAuth;
59
+ return enableAuth.value;
58
60
  },
59
61
  get enableClerk() {
60
- return enableClerk;
62
+ return enableClerk.value;
61
63
  },
62
64
  }));
63
65
 
64
66
  afterEach(() => {
65
- enableAuth = true;
66
- enableClerk = true;
67
+ enableAuth.value = true;
68
+ enableClerk.value = true;
67
69
  });
68
70
 
69
71
  describe('useMenu', () => {
@@ -71,8 +73,8 @@ describe('useMenu', () => {
71
73
  act(() => {
72
74
  useUserStore.setState({ isSignedIn: true, enableAuth: () => true });
73
75
  });
74
- enableAuth = true;
75
- enableClerk = false;
76
+ enableAuth.value = true;
77
+ enableClerk.value = false;
76
78
 
77
79
  const { result } = renderHook(() => useMenu(), { wrapper });
78
80
 
@@ -90,7 +92,7 @@ describe('useMenu', () => {
90
92
  act(() => {
91
93
  useUserStore.setState({ isSignedIn: false, enableAuth: () => false });
92
94
  });
93
- enableAuth = false;
95
+ enableAuth.value = false;
94
96
 
95
97
  const { result } = renderHook(() => useMenu(), { wrapper });
96
98
 
@@ -108,7 +110,7 @@ describe('useMenu', () => {
108
110
  act(() => {
109
111
  useUserStore.setState({ isSignedIn: false, enableAuth: () => true });
110
112
  });
111
- enableAuth = true;
113
+ enableAuth.value = true;
112
114
 
113
115
  const { result } = renderHook(() => useMenu(), { wrapper });
114
116
 
@@ -0,0 +1,51 @@
1
+ 'use client';
2
+
3
+ import { memo, useEffect } from 'react';
4
+ import { createStoreUpdater } from 'zustand-utils';
5
+
6
+ import { useSession } from '@/libs/better-auth/auth-client';
7
+ import { useUserStore } from '@/store/user';
8
+ import { LobeUser } from '@/types/user';
9
+
10
+ /**
11
+ * Sync Better-Auth session state to Zustand store
12
+ */
13
+ const UserUpdater = memo(() => {
14
+ const { data: session, isPending, error } = useSession();
15
+
16
+ const isLoaded = !isPending;
17
+ const isSignedIn = !!session?.user && !error;
18
+
19
+ const betterAuthUser = session?.user;
20
+ const useStoreUpdater = createStoreUpdater(useUserStore);
21
+
22
+ useStoreUpdater('isLoaded', isLoaded);
23
+ useStoreUpdater('isSignedIn', isSignedIn);
24
+
25
+ // Sync user data from Better-Auth session to Zustand store
26
+ useEffect(() => {
27
+ if (betterAuthUser) {
28
+ const userAvatar = useUserStore.getState().user?.avatar;
29
+
30
+ const lobeUser = {
31
+ // Preserve avatar from settings, don't override with auth provider value
32
+ avatar: userAvatar || '',
33
+ email: betterAuthUser.email,
34
+ fullName: betterAuthUser.fullName,
35
+ id: betterAuthUser.id,
36
+ username: betterAuthUser.name,
37
+ } as LobeUser;
38
+
39
+ // Update user data in store
40
+ useUserStore.setState({ user: lobeUser });
41
+ return;
42
+ }
43
+
44
+ // Clear user data when session becomes unavailable
45
+ useUserStore.setState({ user: undefined });
46
+ }, [betterAuthUser]);
47
+
48
+ return null;
49
+ });
50
+
51
+ export default UserUpdater;
@@ -0,0 +1,14 @@
1
+ import { PropsWithChildren } from 'react';
2
+
3
+ import UserUpdater from './UserUpdater';
4
+
5
+ const BetterAuth = ({ children }: PropsWithChildren) => {
6
+ return (
7
+ <>
8
+ {children}
9
+ <UserUpdater />
10
+ </>
11
+ );
12
+ };
13
+
14
+ export default BetterAuth;
@@ -3,6 +3,7 @@ import { PropsWithChildren } from 'react';
3
3
  import { isDesktop } from '@/const/version';
4
4
  import { authEnv } from '@/envs/auth';
5
5
 
6
+ import BetterAuth from './BetterAuth';
6
7
  import Clerk from './Clerk';
7
8
  import { MarketAuthProvider } from './MarketAuth';
8
9
  import NextAuth from './NextAuth';
@@ -13,6 +14,8 @@ const AuthProvider = ({ children }: PropsWithChildren) => {
13
14
  let InnerAuthProvider;
14
15
  if (authEnv.NEXT_PUBLIC_ENABLE_CLERK_AUTH) {
15
16
  InnerAuthProvider = ({ children }: PropsWithChildren) => <Clerk>{children}</Clerk>;
17
+ } else if (authEnv.NEXT_PUBLIC_ENABLE_BETTER_AUTH) {
18
+ InnerAuthProvider = ({ children }: PropsWithChildren) => <BetterAuth>{children}</BetterAuth>;
16
19
  } else if (authEnv.NEXT_PUBLIC_ENABLE_NEXT_AUTH) {
17
20
  InnerAuthProvider = ({ children }: PropsWithChildren) => <NextAuth>{children}</NextAuth>;
18
21
  } else {
@@ -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
+ });