@lobehub/lobehub 2.0.0-next.355 → 2.0.0-next.357
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.desktop +0 -1
- package/.env.example +16 -20
- package/.env.example.development +1 -4
- package/.github/workflows/e2e.yml +10 -11
- package/CHANGELOG.md +60 -0
- package/Dockerfile +28 -4
- package/changelog/v1.json +18 -0
- package/docker-compose/local/docker-compose.yml +2 -2
- package/docker-compose/local/grafana/docker-compose.yml +2 -2
- package/docker-compose/local/logto/docker-compose.yml +2 -2
- package/docker-compose/local/zitadel/.env.example +2 -2
- package/docker-compose/local/zitadel/.env.zh-CN.example +2 -2
- package/docker-compose/production/grafana/docker-compose.yml +2 -2
- package/docker-compose/production/logto/.env.example +2 -2
- package/docker-compose/production/logto/.env.zh-CN.example +2 -2
- package/docker-compose/production/zitadel/.env.example +2 -2
- package/docker-compose/production/zitadel/.env.zh-CN.example +2 -2
- package/docs/development/basic/add-new-authentication-providers.mdx +144 -136
- package/docs/development/basic/add-new-authentication-providers.zh-CN.mdx +146 -136
- package/docs/self-hosting/advanced/auth/legacy.mdx +4 -0
- package/docs/self-hosting/advanced/auth/legacy.zh-CN.mdx +4 -0
- package/docs/self-hosting/advanced/auth/nextauth-to-betterauth.mdx +326 -0
- package/docs/self-hosting/advanced/auth/nextauth-to-betterauth.zh-CN.mdx +323 -0
- package/docs/self-hosting/advanced/auth.mdx +43 -16
- package/docs/self-hosting/advanced/auth.zh-CN.mdx +44 -16
- package/docs/self-hosting/advanced/redis/upstash.mdx +69 -0
- package/docs/self-hosting/advanced/redis/upstash.zh-CN.mdx +69 -0
- package/docs/self-hosting/advanced/redis.mdx +128 -0
- package/docs/self-hosting/advanced/redis.zh-CN.mdx +126 -0
- package/docs/self-hosting/environment-variables/auth.mdx +15 -1
- package/docs/self-hosting/environment-variables/auth.zh-CN.mdx +15 -1
- package/docs/self-hosting/environment-variables/basic.mdx +13 -0
- package/docs/self-hosting/environment-variables/basic.zh-CN.mdx +13 -0
- package/docs/self-hosting/environment-variables/redis.mdx +68 -0
- package/docs/self-hosting/environment-variables/redis.zh-CN.mdx +67 -0
- package/docs/self-hosting/migration/v2/breaking-changes.mdx +23 -23
- package/docs/self-hosting/migration/v2/breaking-changes.zh-CN.mdx +23 -23
- package/docs/self-hosting/server-database/docker-compose.mdx +4 -4
- package/docs/self-hosting/server-database/docker-compose.zh-CN.mdx +4 -4
- package/e2e/CLAUDE.md +5 -6
- package/e2e/docs/local-setup.md +9 -12
- package/e2e/scripts/setup.ts +9 -15
- package/e2e/src/support/webServer.ts +6 -5
- package/package.json +4 -6
- package/packages/database/src/schemas/nextauth.ts +7 -2
- package/packages/model-runtime/src/core/contextBuilders/anthropic.test.ts +370 -0
- package/packages/model-runtime/src/core/contextBuilders/anthropic.ts +18 -5
- package/packages/utils/src/server/__tests__/auth.test.ts +1 -63
- package/packages/utils/src/server/auth.ts +8 -24
- package/scripts/_shared/checkDeprecatedAuth.js +99 -0
- package/scripts/clerk-to-betterauth/index.ts +8 -3
- package/scripts/nextauth-to-betterauth/_internal/config.ts +41 -0
- package/scripts/nextauth-to-betterauth/_internal/db.ts +32 -0
- package/scripts/nextauth-to-betterauth/_internal/env.ts +6 -0
- package/scripts/nextauth-to-betterauth/index.ts +226 -0
- package/scripts/nextauth-to-betterauth/verify.ts +188 -0
- package/scripts/prebuild.mts +66 -13
- package/scripts/serverLauncher/startServer.js +5 -5
- package/src/app/(backend)/api/auth/[...all]/route.ts +5 -23
- package/src/app/(backend)/api/webhooks/casdoor/route.ts +5 -5
- package/src/app/(backend)/api/webhooks/logto/route.ts +8 -8
- package/src/app/(backend)/middleware/auth/index.test.ts +8 -1
- package/src/app/(backend)/middleware/auth/index.ts +6 -15
- package/src/app/(backend)/middleware/auth/utils.test.ts +0 -32
- package/src/app/(backend)/middleware/auth/utils.ts +3 -8
- package/src/app/(backend)/webapi/chat/[provider]/route.test.ts +8 -1
- package/src/app/(backend)/webapi/create-image/comfyui/route.ts +0 -1
- package/src/app/(backend)/webapi/models/[provider]/route.test.ts +8 -1
- package/src/app/[variants]/(auth)/signin/SignInEmailStep.tsx +1 -1
- package/src/app/[variants]/(auth)/signup/[[...signup]]/page.tsx +4 -17
- package/src/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobContentEditor.tsx +34 -21
- package/src/app/[variants]/(main)/settings/profile/features/SSOProvidersList/index.tsx +12 -19
- package/src/app/[variants]/(main)/settings/profile/index.tsx +8 -14
- package/src/components/{NextAuth/AuthIcons.tsx → AuthIcons.tsx} +8 -10
- package/src/envs/auth.ts +12 -51
- package/src/envs/email.ts +3 -0
- package/src/envs/redis.ts +12 -54
- package/src/features/ChatInput/ChatInputProvider.tsx +22 -2
- package/src/features/ChatInput/InputEditor/index.tsx +14 -3
- package/src/features/ChatInput/store/initialState.ts +2 -0
- package/src/features/EditorCanvas/DiffAllToolbar.tsx +4 -5
- package/src/features/EditorCanvas/DocumentIdMode.tsx +21 -1
- package/src/features/User/__tests__/PanelContent.test.tsx +0 -11
- package/src/features/User/__tests__/UserAvatar.test.tsx +1 -16
- package/src/layout/AuthProvider/index.tsx +1 -6
- package/src/layout/GlobalProvider/StoreInitialization.tsx +2 -4
- package/src/libs/better-auth/define-config.ts +2 -0
- package/src/libs/better-auth/plugins/email-whitelist.test.ts +120 -0
- package/src/libs/better-auth/plugins/email-whitelist.ts +62 -0
- package/src/libs/next/config/define-config.ts +13 -1
- package/src/libs/next/proxy/define-config.ts +2 -75
- package/src/libs/oidc-provider/provider.test.ts +0 -4
- package/src/libs/redis/index.ts +0 -1
- package/src/libs/redis/manager.test.ts +9 -45
- package/src/libs/redis/manager.ts +2 -16
- package/src/libs/redis/redis.test.ts +2 -4
- package/src/libs/redis/redis.ts +2 -4
- package/src/libs/redis/types.ts +2 -24
- package/src/libs/redis/utils.test.ts +0 -10
- package/src/libs/redis/utils.ts +0 -19
- package/src/libs/trpc/lambda/context.test.ts +0 -13
- package/src/libs/trpc/lambda/context.ts +21 -59
- package/src/libs/trpc/middleware/userAuth.ts +1 -7
- package/src/libs/trusted-client/getSessionUser.ts +15 -35
- package/src/server/globalConfig/index.ts +1 -3
- package/src/server/routers/lambda/__tests__/user.test.ts +0 -48
- package/src/server/routers/lambda/user.ts +1 -12
- package/src/server/services/email/impls/nodemailer/index.ts +2 -2
- package/src/server/services/webhookUser/index.ts +88 -0
- package/src/services/user/index.test.ts +0 -14
- package/src/services/user/index.ts +0 -4
- package/src/store/document/slices/document/action.ts +1 -0
- package/src/store/user/slices/auth/action.test.ts +22 -126
- package/src/store/user/slices/auth/action.ts +32 -65
- package/src/store/user/slices/auth/initialState.ts +0 -3
- package/src/store/user/slices/auth/selectors.ts +0 -3
- package/tests/setup.ts +10 -0
- package/scripts/_shared/checkDeprecatedClerkEnv.js +0 -42
- package/src/app/(backend)/api/auth/adapter/route.ts +0 -137
- package/src/app/[variants]/(auth)/next-auth/error/AuthErrorPage.tsx +0 -40
- package/src/app/[variants]/(auth)/next-auth/error/page.tsx +0 -11
- package/src/app/[variants]/(auth)/next-auth/signin/AuthSignInBox.tsx +0 -167
- package/src/app/[variants]/(auth)/next-auth/signin/page.tsx +0 -11
- package/src/app/[variants]/(auth)/reset-password/layout.tsx +0 -12
- package/src/app/[variants]/(auth)/signin/layout.tsx +0 -12
- package/src/app/[variants]/(auth)/verify-email/layout.tsx +0 -12
- package/src/envs/auth.test.ts +0 -47
- package/src/layout/AuthProvider/NextAuth/UserUpdater.tsx +0 -44
- package/src/layout/AuthProvider/NextAuth/index.tsx +0 -17
- package/src/libs/next-auth/adapter/index.ts +0 -177
- package/src/libs/next-auth/auth.config.ts +0 -64
- package/src/libs/next-auth/index.ts +0 -20
- package/src/libs/next-auth/sso-providers/auth0.ts +0 -24
- package/src/libs/next-auth/sso-providers/authelia.ts +0 -39
- package/src/libs/next-auth/sso-providers/authentik.ts +0 -25
- package/src/libs/next-auth/sso-providers/casdoor.ts +0 -50
- package/src/libs/next-auth/sso-providers/cloudflare-zero-trust.ts +0 -34
- package/src/libs/next-auth/sso-providers/cognito.ts +0 -8
- package/src/libs/next-auth/sso-providers/feishu.ts +0 -83
- package/src/libs/next-auth/sso-providers/generic-oidc.ts +0 -38
- package/src/libs/next-auth/sso-providers/github.ts +0 -23
- package/src/libs/next-auth/sso-providers/google.ts +0 -18
- package/src/libs/next-auth/sso-providers/index.ts +0 -35
- package/src/libs/next-auth/sso-providers/keycloak.ts +0 -22
- package/src/libs/next-auth/sso-providers/logto.ts +0 -48
- package/src/libs/next-auth/sso-providers/microsoft-entra-id-helper.ts +0 -29
- package/src/libs/next-auth/sso-providers/microsoft-entra-id.ts +0 -19
- package/src/libs/next-auth/sso-providers/okta.ts +0 -22
- package/src/libs/next-auth/sso-providers/sso.config.ts +0 -8
- package/src/libs/next-auth/sso-providers/wechat.ts +0 -36
- package/src/libs/next-auth/sso-providers/zitadel.ts +0 -21
- package/src/libs/redis/upstash.test.ts +0 -158
- package/src/libs/redis/upstash.ts +0 -136
- package/src/server/services/nextAuthUser/index.ts +0 -318
- package/src/server/services/nextAuthUser/utils.ts +0 -62
- package/src/types/next-auth.d.ts +0 -26
package/src/envs/auth.ts
CHANGED
|
@@ -6,23 +6,15 @@ declare global {
|
|
|
6
6
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
7
7
|
namespace NodeJS {
|
|
8
8
|
interface ProcessEnv {
|
|
9
|
-
// =====
|
|
9
|
+
// ===== Better Auth ===== //
|
|
10
10
|
AUTH_SECRET?: string;
|
|
11
11
|
AUTH_EMAIL_VERIFICATION?: string;
|
|
12
12
|
ENABLE_MAGIC_LINK?: string;
|
|
13
13
|
AUTH_SSO_PROVIDERS?: string;
|
|
14
14
|
AUTH_TRUSTED_ORIGINS?: string;
|
|
15
|
+
AUTH_ALLOWED_EMAILS?: string;
|
|
15
16
|
|
|
16
|
-
// =====
|
|
17
|
-
NEXT_AUTH_SECRET?: string;
|
|
18
|
-
|
|
19
|
-
NEXT_AUTH_SSO_PROVIDERS?: string;
|
|
20
|
-
|
|
21
|
-
NEXT_AUTH_DEBUG?: string;
|
|
22
|
-
|
|
23
|
-
NEXT_AUTH_SSO_SESSION_STRATEGY?: string;
|
|
24
|
-
|
|
25
|
-
// ===== Next Auth Provider Credentials ===== //
|
|
17
|
+
// ===== Auth Provider Credentials ===== //
|
|
26
18
|
AUTH_GOOGLE_ID?: string;
|
|
27
19
|
AUTH_GOOGLE_SECRET?: string;
|
|
28
20
|
|
|
@@ -130,26 +122,14 @@ declare global {
|
|
|
130
122
|
|
|
131
123
|
export const getAuthConfig = () => {
|
|
132
124
|
return createEnv({
|
|
133
|
-
client: {
|
|
134
|
-
// ---------------------------------- better auth ----------------------------------
|
|
135
|
-
NEXT_PUBLIC_ENABLE_BETTER_AUTH: z.boolean().optional(),
|
|
136
|
-
|
|
137
|
-
// ---------------------------------- next auth ----------------------------------
|
|
138
|
-
NEXT_PUBLIC_ENABLE_NEXT_AUTH: z.boolean().optional(),
|
|
139
|
-
},
|
|
125
|
+
client: {},
|
|
140
126
|
server: {
|
|
141
|
-
// ---------------------------------- better auth ----------------------------------
|
|
142
127
|
AUTH_SECRET: z.string().optional(),
|
|
143
128
|
AUTH_SSO_PROVIDERS: z.string().optional().default(''),
|
|
144
129
|
AUTH_TRUSTED_ORIGINS: z.string().optional(),
|
|
145
130
|
AUTH_EMAIL_VERIFICATION: z.boolean().optional().default(false),
|
|
146
131
|
ENABLE_MAGIC_LINK: z.boolean().optional().default(false),
|
|
147
|
-
|
|
148
|
-
// ---------------------------------- next auth ----------------------------------
|
|
149
|
-
NEXT_AUTH_SECRET: z.string().optional(),
|
|
150
|
-
NEXT_AUTH_SSO_PROVIDERS: z.string().optional().default('auth0'),
|
|
151
|
-
NEXT_AUTH_DEBUG: z.boolean().optional().default(false),
|
|
152
|
-
NEXT_AUTH_SSO_SESSION_STRATEGY: z.enum(['jwt', 'database']).optional().default('jwt'),
|
|
132
|
+
AUTH_ALLOWED_EMAILS: z.string().optional(),
|
|
153
133
|
|
|
154
134
|
AUTH_GOOGLE_ID: z.string().optional(),
|
|
155
135
|
AUTH_GOOGLE_SECRET: z.string().optional(),
|
|
@@ -248,33 +228,19 @@ export const getAuthConfig = () => {
|
|
|
248
228
|
},
|
|
249
229
|
|
|
250
230
|
runtimeEnv: {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
process.env.AUTH_EMAIL_VERIFICATION === '1' ||
|
|
256
|
-
process.env.NEXT_PUBLIC_AUTH_EMAIL_VERIFICATION === '1',
|
|
257
|
-
ENABLE_MAGIC_LINK:
|
|
258
|
-
process.env.ENABLE_MAGIC_LINK === '1' || process.env.NEXT_PUBLIC_ENABLE_MAGIC_LINK === '1',
|
|
259
|
-
// Fallback to NEXT_AUTH_SECRET for seamless migration from next-auth
|
|
260
|
-
AUTH_SECRET: process.env.AUTH_SECRET || process.env.NEXT_AUTH_SECRET,
|
|
261
|
-
// Fallback to NEXT_AUTH_SSO_PROVIDERS for seamless migration from next-auth
|
|
262
|
-
AUTH_SSO_PROVIDERS: process.env.AUTH_SSO_PROVIDERS || process.env.NEXT_AUTH_SSO_PROVIDERS,
|
|
231
|
+
AUTH_EMAIL_VERIFICATION: process.env.AUTH_EMAIL_VERIFICATION === '1',
|
|
232
|
+
ENABLE_MAGIC_LINK: process.env.ENABLE_MAGIC_LINK === '1',
|
|
233
|
+
AUTH_SECRET: process.env.AUTH_SECRET,
|
|
234
|
+
AUTH_SSO_PROVIDERS: process.env.AUTH_SSO_PROVIDERS,
|
|
263
235
|
AUTH_TRUSTED_ORIGINS: process.env.AUTH_TRUSTED_ORIGINS,
|
|
236
|
+
AUTH_ALLOWED_EMAILS: process.env.AUTH_ALLOWED_EMAILS,
|
|
264
237
|
|
|
265
|
-
//
|
|
238
|
+
// Cognito provider specific env vars
|
|
266
239
|
AUTH_COGNITO_DOMAIN: process.env.AUTH_COGNITO_DOMAIN,
|
|
267
240
|
AUTH_COGNITO_REGION: process.env.AUTH_COGNITO_REGION,
|
|
268
241
|
AUTH_COGNITO_USERPOOL_ID: process.env.AUTH_COGNITO_USERPOOL_ID,
|
|
269
242
|
|
|
270
|
-
//
|
|
271
|
-
NEXT_PUBLIC_ENABLE_NEXT_AUTH: process.env.NEXT_PUBLIC_ENABLE_NEXT_AUTH === '1',
|
|
272
|
-
NEXT_AUTH_SSO_PROVIDERS: process.env.NEXT_AUTH_SSO_PROVIDERS,
|
|
273
|
-
NEXT_AUTH_SECRET: process.env.NEXT_AUTH_SECRET,
|
|
274
|
-
NEXT_AUTH_DEBUG: !!process.env.NEXT_AUTH_DEBUG,
|
|
275
|
-
NEXT_AUTH_SSO_SESSION_STRATEGY: process.env.NEXT_AUTH_SSO_SESSION_STRATEGY || 'jwt',
|
|
276
|
-
|
|
277
|
-
// Next Auth Provider Credentials
|
|
243
|
+
// Auth Provider Credentials
|
|
278
244
|
AUTH_GOOGLE_ID: process.env.AUTH_GOOGLE_ID,
|
|
279
245
|
AUTH_GOOGLE_SECRET: process.env.AUTH_GOOGLE_SECRET,
|
|
280
246
|
|
|
@@ -374,11 +340,6 @@ export const getAuthConfig = () => {
|
|
|
374
340
|
|
|
375
341
|
export const authEnv = getAuthConfig();
|
|
376
342
|
|
|
377
|
-
// Auth flags - use process.env directly for build-time dead code elimination
|
|
378
|
-
// Better Auth is the default auth solution when NextAuth is not explicitly enabled
|
|
379
|
-
export const enableNextAuth = process.env.NEXT_PUBLIC_ENABLE_NEXT_AUTH === '1';
|
|
380
|
-
export const enableBetterAuth = !enableNextAuth;
|
|
381
|
-
|
|
382
343
|
// Auth headers and constants
|
|
383
344
|
export const LOBE_CHAT_AUTH_HEADER = 'X-lobe-chat-auth';
|
|
384
345
|
export const LOBE_CHAT_OIDC_AUTH_HEADER = 'Oidc-Auth';
|
package/src/envs/email.ts
CHANGED
|
@@ -9,6 +9,7 @@ declare global {
|
|
|
9
9
|
EMAIL_SERVICE_PROVIDER?: string;
|
|
10
10
|
RESEND_API_KEY?: string;
|
|
11
11
|
RESEND_FROM?: string;
|
|
12
|
+
SMTP_FROM?: string;
|
|
12
13
|
SMTP_HOST?: string;
|
|
13
14
|
SMTP_PASS?: string;
|
|
14
15
|
SMTP_PORT?: string;
|
|
@@ -24,6 +25,7 @@ export const getEmailConfig = () => {
|
|
|
24
25
|
EMAIL_SERVICE_PROVIDER: z.enum(['nodemailer', 'resend']).optional(),
|
|
25
26
|
RESEND_API_KEY: z.string().optional(),
|
|
26
27
|
RESEND_FROM: z.string().optional(),
|
|
28
|
+
SMTP_FROM: z.string().optional(),
|
|
27
29
|
SMTP_HOST: z.string().optional(),
|
|
28
30
|
SMTP_PORT: z.coerce.number().optional(),
|
|
29
31
|
SMTP_SECURE: z.boolean().optional(),
|
|
@@ -31,6 +33,7 @@ export const getEmailConfig = () => {
|
|
|
31
33
|
SMTP_PASS: z.string().optional(),
|
|
32
34
|
},
|
|
33
35
|
runtimeEnv: {
|
|
36
|
+
SMTP_FROM: process.env.SMTP_FROM,
|
|
34
37
|
SMTP_HOST: process.env.SMTP_HOST,
|
|
35
38
|
SMTP_PORT: process.env.SMTP_PORT ? Number(process.env.SMTP_PORT) : undefined,
|
|
36
39
|
SMTP_SECURE: process.env.SMTP_SECURE === 'true',
|
package/src/envs/redis.ts
CHANGED
|
@@ -4,8 +4,6 @@ import { z } from 'zod';
|
|
|
4
4
|
|
|
5
5
|
import type { RedisConfig } from '@/libs/redis';
|
|
6
6
|
|
|
7
|
-
type UpstashRedisConfig = { token: string; url: string };
|
|
8
|
-
|
|
9
7
|
const parseNumber = (value?: string) => {
|
|
10
8
|
const parsed = Number.parseInt(value ?? '', 10);
|
|
11
9
|
|
|
@@ -30,8 +28,6 @@ export const getRedisEnv = () => {
|
|
|
30
28
|
REDIS_TLS: parseRedisTls(process.env.REDIS_TLS),
|
|
31
29
|
REDIS_URL: process.env.REDIS_URL,
|
|
32
30
|
REDIS_USERNAME: process.env.REDIS_USERNAME,
|
|
33
|
-
UPSTASH_REDIS_REST_TOKEN: process.env.UPSTASH_REDIS_REST_TOKEN,
|
|
34
|
-
UPSTASH_REDIS_REST_URL: process.env.UPSTASH_REDIS_REST_URL,
|
|
35
31
|
},
|
|
36
32
|
server: {
|
|
37
33
|
REDIS_DATABASE: z.number().int().optional(),
|
|
@@ -40,67 +36,29 @@ export const getRedisEnv = () => {
|
|
|
40
36
|
REDIS_TLS: z.boolean().default(false),
|
|
41
37
|
REDIS_URL: z.string().url().optional(),
|
|
42
38
|
REDIS_USERNAME: z.string().optional(),
|
|
43
|
-
UPSTASH_REDIS_REST_TOKEN: z.string().optional(),
|
|
44
|
-
UPSTASH_REDIS_REST_URL: z.string().url().optional(),
|
|
45
39
|
},
|
|
46
40
|
});
|
|
47
41
|
};
|
|
48
42
|
|
|
49
43
|
export const redisEnv = getRedisEnv();
|
|
50
44
|
|
|
51
|
-
export const getUpstashRedisConfig = (): UpstashRedisConfig | null => {
|
|
52
|
-
const upstashConfigSchema = z.union([
|
|
53
|
-
z.object({
|
|
54
|
-
token: z.string(),
|
|
55
|
-
url: z.string().url(),
|
|
56
|
-
}),
|
|
57
|
-
z.object({
|
|
58
|
-
token: z.undefined().optional(),
|
|
59
|
-
url: z.undefined().optional(),
|
|
60
|
-
}),
|
|
61
|
-
]);
|
|
62
|
-
|
|
63
|
-
const parsed = upstashConfigSchema.safeParse({
|
|
64
|
-
token: redisEnv.UPSTASH_REDIS_REST_TOKEN,
|
|
65
|
-
url: redisEnv.UPSTASH_REDIS_REST_URL,
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
if (!parsed.success) throw parsed.error;
|
|
69
|
-
if (!parsed.data.token || !parsed.data.url) return null;
|
|
70
|
-
|
|
71
|
-
return parsed.data;
|
|
72
|
-
};
|
|
73
|
-
|
|
74
45
|
export const getRedisConfig = (): RedisConfig => {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (redisEnv.REDIS_URL) {
|
|
78
|
-
return {
|
|
79
|
-
database: redisEnv.REDIS_DATABASE,
|
|
80
|
-
enabled: true,
|
|
81
|
-
password: redisEnv.REDIS_PASSWORD,
|
|
82
|
-
prefix,
|
|
83
|
-
provider: 'redis',
|
|
84
|
-
tls: redisEnv.REDIS_TLS,
|
|
85
|
-
url: redisEnv.REDIS_URL,
|
|
86
|
-
username: redisEnv.REDIS_USERNAME,
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const upstashConfig = getUpstashRedisConfig();
|
|
91
|
-
if (upstashConfig) {
|
|
46
|
+
if (!redisEnv.REDIS_URL) {
|
|
92
47
|
return {
|
|
93
|
-
enabled:
|
|
94
|
-
prefix,
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
url: upstashConfig.url,
|
|
48
|
+
enabled: false,
|
|
49
|
+
prefix: redisEnv.REDIS_PREFIX,
|
|
50
|
+
tls: false,
|
|
51
|
+
url: '',
|
|
98
52
|
};
|
|
99
53
|
}
|
|
100
54
|
|
|
101
55
|
return {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
56
|
+
database: redisEnv.REDIS_DATABASE,
|
|
57
|
+
enabled: true,
|
|
58
|
+
password: redisEnv.REDIS_PASSWORD,
|
|
59
|
+
prefix: redisEnv.REDIS_PREFIX,
|
|
60
|
+
tls: redisEnv.REDIS_TLS,
|
|
61
|
+
url: redisEnv.REDIS_URL,
|
|
62
|
+
username: redisEnv.REDIS_USERNAME,
|
|
105
63
|
};
|
|
106
64
|
};
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { useEditor } from '@lobehub/editor/react';
|
|
2
|
-
import { type ReactNode, memo, useRef } from 'react';
|
|
2
|
+
import { type MutableRefObject, type ReactNode, memo, useRef } from 'react';
|
|
3
|
+
|
|
4
|
+
import { useUserStore } from '@/store/user';
|
|
5
|
+
import { labPreferSelectors } from '@/store/user/selectors';
|
|
3
6
|
|
|
4
7
|
import StoreUpdater, { type StoreUpdaterProps } from './StoreUpdater';
|
|
5
8
|
import { Provider, createStore } from './store';
|
|
@@ -8,10 +11,16 @@ interface ChatInputProviderProps extends StoreUpdaterProps {
|
|
|
8
11
|
children: ReactNode;
|
|
9
12
|
}
|
|
10
13
|
|
|
11
|
-
|
|
14
|
+
interface ChatInputProviderInnerProps extends StoreUpdaterProps {
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
contentRef: MutableRefObject<string>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const ChatInputProviderInner = memo<ChatInputProviderInnerProps>(
|
|
12
20
|
({
|
|
13
21
|
agentId,
|
|
14
22
|
children,
|
|
23
|
+
contentRef,
|
|
15
24
|
leftActions,
|
|
16
25
|
rightActions,
|
|
17
26
|
mobile,
|
|
@@ -31,6 +40,7 @@ export const ChatInputProvider = memo<ChatInputProviderProps>(
|
|
|
31
40
|
createStore={() =>
|
|
32
41
|
createStore({
|
|
33
42
|
allowExpand,
|
|
43
|
+
contentRef,
|
|
34
44
|
editor,
|
|
35
45
|
leftActions,
|
|
36
46
|
mentionItems,
|
|
@@ -60,3 +70,13 @@ export const ChatInputProvider = memo<ChatInputProviderProps>(
|
|
|
60
70
|
);
|
|
61
71
|
},
|
|
62
72
|
);
|
|
73
|
+
|
|
74
|
+
export const ChatInputProvider = (props: ChatInputProviderProps) => {
|
|
75
|
+
const enableRichRender = useUserStore(labPreferSelectors.enableInputMarkdown);
|
|
76
|
+
// Ref to persist content across re-mounts when enableRichRender changes
|
|
77
|
+
const contentRef = useRef<string>('');
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<ChatInputProviderInner contentRef={contentRef} key={`editor-${enableRichRender}`} {...props} />
|
|
81
|
+
);
|
|
82
|
+
};
|
|
@@ -37,7 +37,7 @@ const className = cx(css`
|
|
|
37
37
|
`);
|
|
38
38
|
|
|
39
39
|
const InputEditor = memo<{ defaultRows?: number }>(({ defaultRows = 2 }) => {
|
|
40
|
-
const [editor, slashMenuRef, send, updateMarkdownContent, expand, mentionItems] =
|
|
40
|
+
const [editor, slashMenuRef, send, updateMarkdownContent, expand, mentionItems, contentRef] =
|
|
41
41
|
useChatInputStore((s) => [
|
|
42
42
|
s.editor,
|
|
43
43
|
s.slashMenuRef,
|
|
@@ -45,6 +45,7 @@ const InputEditor = memo<{ defaultRows?: number }>(({ defaultRows = 2 }) => {
|
|
|
45
45
|
s.updateMarkdownContent,
|
|
46
46
|
s.expand,
|
|
47
47
|
s.mentionItems,
|
|
48
|
+
s.contentRef,
|
|
48
49
|
]);
|
|
49
50
|
|
|
50
51
|
const storeApi = useStoreApi();
|
|
@@ -151,7 +152,11 @@ const InputEditor = memo<{ defaultRows?: number }>(({ defaultRows = 2 }) => {
|
|
|
151
152
|
onBlur={() => {
|
|
152
153
|
disableScope(HotkeyEnum.AddUserMessage);
|
|
153
154
|
}}
|
|
154
|
-
onChange={() => {
|
|
155
|
+
onChange={(e) => {
|
|
156
|
+
// Save content to parent ref for restoration when enableRichRender changes
|
|
157
|
+
if (contentRef) {
|
|
158
|
+
contentRef.current = e.getDocument('markdown') as unknown as string;
|
|
159
|
+
}
|
|
155
160
|
updateMarkdownContent();
|
|
156
161
|
}}
|
|
157
162
|
onCompositionEnd={() => {
|
|
@@ -177,7 +182,13 @@ const InputEditor = memo<{ defaultRows?: number }>(({ defaultRows = 2 }) => {
|
|
|
177
182
|
onFocus={() => {
|
|
178
183
|
enableScope(HotkeyEnum.AddUserMessage);
|
|
179
184
|
}}
|
|
180
|
-
onInit={(editor) =>
|
|
185
|
+
onInit={(editor) => {
|
|
186
|
+
storeApi.setState({ editor });
|
|
187
|
+
// Restore content from parent ref when editor re-initializes
|
|
188
|
+
if (contentRef?.current) {
|
|
189
|
+
editor.setDocument('markdown', contentRef.current);
|
|
190
|
+
}
|
|
191
|
+
}}
|
|
181
192
|
onPressEnter={({ event: e }) => {
|
|
182
193
|
if (e.shiftKey || isChineseInput.current) return;
|
|
183
194
|
// when user like alt + enter to add ai message
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type IEditor, type SlashOptions } from '@lobehub/editor';
|
|
2
2
|
import type { ChatInputProps } from '@lobehub/editor/react';
|
|
3
3
|
import type { MenuProps } from '@lobehub/ui';
|
|
4
|
+
import type { MutableRefObject } from 'react';
|
|
4
5
|
|
|
5
6
|
import { type ActionKeys } from '@/features/ChatInput';
|
|
6
7
|
|
|
@@ -39,6 +40,7 @@ export interface PublicState {
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
export interface State extends PublicState {
|
|
43
|
+
contentRef?: MutableRefObject<string>;
|
|
42
44
|
editor?: IEditor;
|
|
43
45
|
isContentEmpty: boolean;
|
|
44
46
|
markdownContent: string;
|
|
@@ -42,7 +42,6 @@ const useIsEditorInit = (editor: IEditor) => {
|
|
|
42
42
|
if (!editor) return;
|
|
43
43
|
|
|
44
44
|
const onInit = () => {
|
|
45
|
-
console.log('init: id', editor.getLexicalEditor()?._key);
|
|
46
45
|
setEditInit(true);
|
|
47
46
|
};
|
|
48
47
|
editor.on('initialized', onInit);
|
|
@@ -103,13 +102,13 @@ interface DiffAllToolbarProps {
|
|
|
103
102
|
const DiffAllToolbar = memo<DiffAllToolbarProps>(({ documentId }) => {
|
|
104
103
|
const { t } = useTranslation('editor');
|
|
105
104
|
const isDarkMode = useIsDark();
|
|
106
|
-
const [
|
|
105
|
+
const [storeEditor, performSave, markDirty] = useDocumentStore((s) => [
|
|
107
106
|
s.editor!,
|
|
108
107
|
s.performSave,
|
|
109
108
|
s.markDirty,
|
|
110
109
|
]);
|
|
111
110
|
|
|
112
|
-
const hasPendingDiffs = useEditorHasPendingDiffs(
|
|
111
|
+
const hasPendingDiffs = useEditorHasPendingDiffs(storeEditor);
|
|
113
112
|
|
|
114
113
|
if (!hasPendingDiffs) return null;
|
|
115
114
|
|
|
@@ -131,7 +130,7 @@ const DiffAllToolbar = memo<DiffAllToolbarProps>(({ documentId }) => {
|
|
|
131
130
|
<Space>
|
|
132
131
|
<Button
|
|
133
132
|
onClick={async () => {
|
|
134
|
-
|
|
133
|
+
storeEditor?.dispatchCommand(LITEXML_DIFFNODE_ALL_COMMAND, {
|
|
135
134
|
action: DiffAction.Reject,
|
|
136
135
|
});
|
|
137
136
|
await handleSave();
|
|
@@ -145,7 +144,7 @@ const DiffAllToolbar = memo<DiffAllToolbarProps>(({ documentId }) => {
|
|
|
145
144
|
<Button
|
|
146
145
|
color={'default'}
|
|
147
146
|
onClick={async () => {
|
|
148
|
-
|
|
147
|
+
storeEditor?.dispatchCommand(LITEXML_DIFFNODE_ALL_COMMAND, {
|
|
149
148
|
action: DiffAction.Accept,
|
|
150
149
|
});
|
|
151
150
|
await handleSave();
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { type IEditor } from '@lobehub/editor';
|
|
4
4
|
import { Alert, Skeleton } from '@lobehub/ui';
|
|
5
|
-
import { memo } from 'react';
|
|
5
|
+
import { memo, useEffect, useRef } from 'react';
|
|
6
6
|
import { useTranslation } from 'react-i18next';
|
|
7
7
|
import { createStoreUpdater } from 'zustand-utils';
|
|
8
8
|
|
|
@@ -82,6 +82,26 @@ const DocumentIdMode = memo<DocumentIdModeProps>(
|
|
|
82
82
|
onContentChange?.();
|
|
83
83
|
};
|
|
84
84
|
|
|
85
|
+
const isEditorInitialized = !!editor?.getLexicalEditor();
|
|
86
|
+
|
|
87
|
+
// 追踪已经为哪个 documentId 调用过 onEditorInit
|
|
88
|
+
const initializedDocIdRef = useRef<string | null>(null);
|
|
89
|
+
|
|
90
|
+
// 关键修复:如果 editor 已经初始化,需要主动调用 onEditorInit
|
|
91
|
+
// 因为 onInit 回调只在 editor 首次初始化时触发
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
// 避免重复调用:只在 documentId 变化且 editor 已初始化时调用
|
|
94
|
+
if (
|
|
95
|
+
editor &&
|
|
96
|
+
isEditorInitialized &&
|
|
97
|
+
!isLoading &&
|
|
98
|
+
initializedDocIdRef.current !== documentId
|
|
99
|
+
) {
|
|
100
|
+
initializedDocIdRef.current = documentId;
|
|
101
|
+
onEditorInit(editor);
|
|
102
|
+
}
|
|
103
|
+
}, [documentId, editor, isEditorInitialized, isLoading, onEditorInit]);
|
|
104
|
+
|
|
85
105
|
// Show loading state
|
|
86
106
|
if (isLoading) {
|
|
87
107
|
return <EditorSkeleton />;
|
|
@@ -67,17 +67,6 @@ vi.mock('@/const/version', () => ({
|
|
|
67
67
|
isDesktop: false,
|
|
68
68
|
}));
|
|
69
69
|
|
|
70
|
-
// Use vi.hoisted to ensure variables exist before vi.mock factory executes
|
|
71
|
-
const { enableNextAuth } = vi.hoisted(() => ({
|
|
72
|
-
enableNextAuth: { value: false },
|
|
73
|
-
}));
|
|
74
|
-
|
|
75
|
-
vi.mock('@/envs/auth', () => ({
|
|
76
|
-
get enableNextAuth() {
|
|
77
|
-
return enableNextAuth.value;
|
|
78
|
-
},
|
|
79
|
-
}));
|
|
80
|
-
|
|
81
70
|
describe('PanelContent', () => {
|
|
82
71
|
const closePopover = vi.fn();
|
|
83
72
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BRANDING_NAME } from '@lobechat/business-const';
|
|
2
2
|
import { act, render, screen } from '@testing-library/react';
|
|
3
|
-
import {
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
4
|
|
|
5
5
|
import { DEFAULT_USER_AVATAR_URL } from '@/const/meta';
|
|
6
6
|
import { useUserStore } from '@/store/user';
|
|
@@ -9,21 +9,6 @@ import UserAvatar from '../UserAvatar';
|
|
|
9
9
|
|
|
10
10
|
vi.mock('zustand/traditional');
|
|
11
11
|
|
|
12
|
-
// Use vi.hoisted to ensure variables exist before vi.mock factory executes
|
|
13
|
-
const { enableNextAuth } = vi.hoisted(() => ({
|
|
14
|
-
enableNextAuth: { value: false },
|
|
15
|
-
}));
|
|
16
|
-
|
|
17
|
-
vi.mock('@/envs/auth', () => ({
|
|
18
|
-
get enableNextAuth() {
|
|
19
|
-
return enableNextAuth.value;
|
|
20
|
-
},
|
|
21
|
-
}));
|
|
22
|
-
|
|
23
|
-
afterEach(() => {
|
|
24
|
-
enableNextAuth.value = false;
|
|
25
|
-
});
|
|
26
|
-
|
|
27
12
|
describe('UserAvatar', () => {
|
|
28
13
|
it('should show the username and avatar are displayed when the user is logged in', async () => {
|
|
29
14
|
const mockAvatar = 'https://example.com/avatar.png';
|
|
@@ -5,7 +5,6 @@ import { authEnv } from '@/envs/auth';
|
|
|
5
5
|
|
|
6
6
|
import BetterAuth from './BetterAuth';
|
|
7
7
|
import Desktop from './Desktop';
|
|
8
|
-
import NextAuth from './NextAuth';
|
|
9
8
|
import NoAuth from './NoAuth';
|
|
10
9
|
|
|
11
10
|
const AuthProvider = ({ children }: PropsWithChildren) => {
|
|
@@ -13,14 +12,10 @@ const AuthProvider = ({ children }: PropsWithChildren) => {
|
|
|
13
12
|
return <Desktop>{children}</Desktop>;
|
|
14
13
|
}
|
|
15
14
|
|
|
16
|
-
if (authEnv.
|
|
15
|
+
if (authEnv.AUTH_SECRET) {
|
|
17
16
|
return <BetterAuth>{children}</BetterAuth>;
|
|
18
17
|
}
|
|
19
18
|
|
|
20
|
-
if (authEnv.NEXT_PUBLIC_ENABLE_NEXT_AUTH) {
|
|
21
|
-
return <NextAuth>{children}</NextAuth>;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
19
|
return <NoAuth>{children}</NoAuth>;
|
|
25
20
|
};
|
|
26
21
|
|
|
@@ -6,7 +6,6 @@ import { useTranslation } from 'react-i18next';
|
|
|
6
6
|
import { createStoreUpdater } from 'zustand-utils';
|
|
7
7
|
|
|
8
8
|
import { isDesktop } from '@/const/version';
|
|
9
|
-
import { enableNextAuth } from '@/envs/auth';
|
|
10
9
|
import { useIsMobile } from '@/hooks/useIsMobile';
|
|
11
10
|
import { useAgentStore } from '@/store/agent';
|
|
12
11
|
import { useAiInfraStore } from '@/store/aiInfra';
|
|
@@ -25,9 +24,8 @@ const StoreInitialization = memo(() => {
|
|
|
25
24
|
// prefetch error ns to avoid don't show error content correctly
|
|
26
25
|
useTranslation('error');
|
|
27
26
|
|
|
28
|
-
const [isLogin,
|
|
27
|
+
const [isLogin, useInitUserState] = useUserStore((s) => [
|
|
29
28
|
authSelectors.isLogin(s),
|
|
30
|
-
s.isSignedIn,
|
|
31
29
|
s.useInitUserState,
|
|
32
30
|
]);
|
|
33
31
|
|
|
@@ -65,7 +63,7 @@ const StoreInitialization = memo(() => {
|
|
|
65
63
|
* IMPORTANT: Explicitly convert to boolean to avoid passing null/undefined downstream,
|
|
66
64
|
* which would cause unnecessary API requests with invalid login state.
|
|
67
65
|
*/
|
|
68
|
-
const isLoginOnInit = Boolean(
|
|
66
|
+
const isLoginOnInit = Boolean(isLogin);
|
|
69
67
|
|
|
70
68
|
// init inbox agent via builtin agent mechanism
|
|
71
69
|
useInitBuiltinAgent(INBOX_SESSION_ID, { isLogin: isLoginOnInit });
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
getVerificationOTPEmailTemplate,
|
|
24
24
|
} from '@/libs/better-auth/email-templates';
|
|
25
25
|
import { initBetterAuthSSOProviders } from '@/libs/better-auth/sso';
|
|
26
|
+
import { emailWhitelist } from '@/libs/better-auth/plugins/email-whitelist';
|
|
26
27
|
import { createSecondaryStorage, getTrustedOrigins } from '@/libs/better-auth/utils/config';
|
|
27
28
|
import { parseSSOProviders } from '@/libs/better-auth/utils/server';
|
|
28
29
|
import { EmailService } from '@/server/services/email';
|
|
@@ -222,6 +223,7 @@ export function defineConfig(customOptions: CustomBetterAuthOptions) {
|
|
|
222
223
|
},
|
|
223
224
|
plugins: [
|
|
224
225
|
...customOptions.plugins,
|
|
226
|
+
emailWhitelist(),
|
|
225
227
|
expo(),
|
|
226
228
|
emailHarmony({ allowNormalizedSignin: false, validator: customEmailValidator }),
|
|
227
229
|
admin(),
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// Get mocked module
|
|
4
|
+
import { authEnv } from '@/envs/auth';
|
|
5
|
+
|
|
6
|
+
import { isEmailAllowed } from './email-whitelist';
|
|
7
|
+
|
|
8
|
+
// Mock authEnv
|
|
9
|
+
vi.mock('@/envs/auth', () => ({
|
|
10
|
+
authEnv: {
|
|
11
|
+
AUTH_ALLOWED_EMAILS: undefined as string | undefined,
|
|
12
|
+
},
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
describe('isEmailAllowed', () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
// Reset to undefined before each test
|
|
18
|
+
(authEnv as { AUTH_ALLOWED_EMAILS: string | undefined }).AUTH_ALLOWED_EMAILS = undefined;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('when whitelist is empty', () => {
|
|
22
|
+
it('should allow all emails when AUTH_ALLOWED_EMAILS is undefined', () => {
|
|
23
|
+
expect(isEmailAllowed('anyone@example.com')).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should allow all emails when AUTH_ALLOWED_EMAILS is empty string', () => {
|
|
27
|
+
(authEnv as { AUTH_ALLOWED_EMAILS: string | undefined }).AUTH_ALLOWED_EMAILS = '';
|
|
28
|
+
expect(isEmailAllowed('anyone@example.com')).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('domain matching', () => {
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
(authEnv as { AUTH_ALLOWED_EMAILS: string | undefined }).AUTH_ALLOWED_EMAILS =
|
|
35
|
+
'example.com,company.org';
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should allow email from whitelisted domain', () => {
|
|
39
|
+
expect(isEmailAllowed('user@example.com')).toBe(true);
|
|
40
|
+
expect(isEmailAllowed('admin@company.org')).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should reject email from non-whitelisted domain', () => {
|
|
44
|
+
expect(isEmailAllowed('user@other.com')).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should be case-sensitive for domain', () => {
|
|
48
|
+
expect(isEmailAllowed('user@Example.com')).toBe(false);
|
|
49
|
+
expect(isEmailAllowed('user@EXAMPLE.COM')).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('exact email matching', () => {
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
(authEnv as { AUTH_ALLOWED_EMAILS: string | undefined }).AUTH_ALLOWED_EMAILS =
|
|
56
|
+
'admin@special.com,vip@other.com';
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should allow exact email match', () => {
|
|
60
|
+
expect(isEmailAllowed('admin@special.com')).toBe(true);
|
|
61
|
+
expect(isEmailAllowed('vip@other.com')).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should reject different email at same domain', () => {
|
|
65
|
+
expect(isEmailAllowed('user@special.com')).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should be case-sensitive for email', () => {
|
|
69
|
+
expect(isEmailAllowed('Admin@special.com')).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('mixed domain and email matching', () => {
|
|
74
|
+
beforeEach(() => {
|
|
75
|
+
(authEnv as { AUTH_ALLOWED_EMAILS: string | undefined }).AUTH_ALLOWED_EMAILS =
|
|
76
|
+
'example.com,admin@other.com';
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should allow any email from whitelisted domain', () => {
|
|
80
|
+
expect(isEmailAllowed('anyone@example.com')).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should allow specific whitelisted email', () => {
|
|
84
|
+
expect(isEmailAllowed('admin@other.com')).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should reject non-whitelisted email from non-whitelisted domain', () => {
|
|
88
|
+
expect(isEmailAllowed('user@other.com')).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('whitespace handling', () => {
|
|
93
|
+
it('should trim whitespace from whitelist entries', () => {
|
|
94
|
+
(authEnv as { AUTH_ALLOWED_EMAILS: string | undefined }).AUTH_ALLOWED_EMAILS =
|
|
95
|
+
' example.com , admin@other.com ';
|
|
96
|
+
expect(isEmailAllowed('user@example.com')).toBe(true);
|
|
97
|
+
expect(isEmailAllowed('admin@other.com')).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should filter empty entries', () => {
|
|
101
|
+
(authEnv as { AUTH_ALLOWED_EMAILS: string | undefined }).AUTH_ALLOWED_EMAILS =
|
|
102
|
+
'example.com,,other.com';
|
|
103
|
+
expect(isEmailAllowed('user@example.com')).toBe(true);
|
|
104
|
+
expect(isEmailAllowed('user@other.com')).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('edge cases', () => {
|
|
109
|
+
it('should reject malformed email without @', () => {
|
|
110
|
+
(authEnv as { AUTH_ALLOWED_EMAILS: string | undefined }).AUTH_ALLOWED_EMAILS = 'example.com';
|
|
111
|
+
expect(isEmailAllowed('invalid-email')).toBe(false);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should handle email with multiple @ symbols', () => {
|
|
115
|
+
(authEnv as { AUTH_ALLOWED_EMAILS: string | undefined }).AUTH_ALLOWED_EMAILS = 'example.com';
|
|
116
|
+
// split('@')[1] returns 'middle@example.com', which won't match 'example.com'
|
|
117
|
+
expect(isEmailAllowed('user@middle@example.com')).toBe(false);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|