@lobehub/lobehub 2.0.0-next.355 → 2.0.0-next.356
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +33 -0
- package/Dockerfile +28 -4
- package/changelog/v1.json +9 -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/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/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/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
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { APIError } from 'better-auth/api';
|
|
2
|
+
import { type BetterAuthPlugin } from 'better-auth/types';
|
|
3
|
+
|
|
4
|
+
import { authEnv } from '@/envs/auth';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parse comma-separated email whitelist string into array.
|
|
8
|
+
*/
|
|
9
|
+
function parseAllowedEmails(value: string | undefined): string[] {
|
|
10
|
+
if (!value) return [];
|
|
11
|
+
return value
|
|
12
|
+
.split(',')
|
|
13
|
+
.map((s) => s.trim())
|
|
14
|
+
.filter(Boolean);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Check if email is allowed based on whitelist.
|
|
19
|
+
* Supports full email (user@example.com) or domain (example.com).
|
|
20
|
+
*/
|
|
21
|
+
export function isEmailAllowed(email: string): boolean {
|
|
22
|
+
const allowedList = parseAllowedEmails(authEnv.AUTH_ALLOWED_EMAILS);
|
|
23
|
+
if (allowedList.length === 0) return true;
|
|
24
|
+
|
|
25
|
+
const domain = email.split('@')[1];
|
|
26
|
+
|
|
27
|
+
return allowedList.some((item) => {
|
|
28
|
+
// Full email match
|
|
29
|
+
if (item.includes('@')) return item === email;
|
|
30
|
+
// Domain match
|
|
31
|
+
return item === domain;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Better Auth plugin to restrict registration to whitelisted emails/domains.
|
|
37
|
+
* Intercepts user creation (both email signup and SSO) via databaseHooks.
|
|
38
|
+
*/
|
|
39
|
+
export const emailWhitelist = (): BetterAuthPlugin => ({
|
|
40
|
+
id: 'email-whitelist',
|
|
41
|
+
init() {
|
|
42
|
+
return {
|
|
43
|
+
options: {
|
|
44
|
+
databaseHooks: {
|
|
45
|
+
user: {
|
|
46
|
+
create: {
|
|
47
|
+
before: async (user) => {
|
|
48
|
+
if (!user.email) return { data: user };
|
|
49
|
+
|
|
50
|
+
if (!isEmailAllowed(user.email)) {
|
|
51
|
+
throw new APIError('FORBIDDEN', { message: 'Email not allowed for registration' });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { data: user };
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
});
|
|
@@ -31,10 +31,22 @@ export function defineConfig(config: CustomNextConfig) {
|
|
|
31
31
|
outputFileTracingIncludes: { '*': ['public/**/*', '.next/static/**/*'] },
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
+
// Vercel serverless optimization: exclude musl binaries
|
|
35
|
+
// Vercel uses Amazon Linux (glibc), not Alpine Linux (musl)
|
|
36
|
+
// This saves ~45MB (29MB canvas-musl + 16MB sharp-musl)
|
|
37
|
+
const vercelConfig: NextConfig = {
|
|
38
|
+
outputFileTracingExcludes: {
|
|
39
|
+
'*': [
|
|
40
|
+
'node_modules/.pnpm/@napi-rs+canvas-*-musl*',
|
|
41
|
+
'node_modules/.pnpm/@img+sharp-libvips-*musl*',
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
34
46
|
const assetPrefix = process.env.NEXT_PUBLIC_ASSET_PREFIX;
|
|
35
47
|
|
|
36
48
|
const nextConfig: NextConfig = {
|
|
37
|
-
...(isStandaloneMode ? standaloneConfig :
|
|
49
|
+
...(isStandaloneMode ? standaloneConfig : vercelConfig),
|
|
38
50
|
assetPrefix,
|
|
39
51
|
|
|
40
52
|
compiler: {
|
|
@@ -7,8 +7,7 @@ import { auth } from '@/auth';
|
|
|
7
7
|
import { LOBE_LOCALE_COOKIE } from '@/const/locale';
|
|
8
8
|
import { isDesktop } from '@/const/version';
|
|
9
9
|
import { appEnv } from '@/envs/app';
|
|
10
|
-
import {
|
|
11
|
-
import NextAuth from '@/libs/next-auth';
|
|
10
|
+
import { authEnv } from '@/envs/auth';
|
|
12
11
|
import { type Locales } from '@/locales/resources';
|
|
13
12
|
import { parseBrowserLanguage } from '@/utils/locale';
|
|
14
13
|
import { RouteVariants } from '@/utils/server/routeVariants';
|
|
@@ -17,12 +16,8 @@ import { createRouteMatcher } from './createRouteMatcher';
|
|
|
17
16
|
|
|
18
17
|
// Create debug logger instances
|
|
19
18
|
const logDefault = debug('middleware:default');
|
|
20
|
-
const logNextAuth = debug('middleware:next-auth');
|
|
21
19
|
const logBetterAuth = debug('middleware:better-auth');
|
|
22
20
|
|
|
23
|
-
// OIDC session pre-sync constant
|
|
24
|
-
const OIDC_SESSION_HEADER = 'x-oidc-session-sync';
|
|
25
|
-
|
|
26
21
|
export function defineConfig() {
|
|
27
22
|
const backendApiEndpoints = ['/api', '/trpc', '/webapi', '/oidc'];
|
|
28
23
|
|
|
@@ -169,8 +164,6 @@ export function defineConfig() {
|
|
|
169
164
|
'/api/agent(.*)',
|
|
170
165
|
'/webapi(.*)',
|
|
171
166
|
'/trpc(.*)',
|
|
172
|
-
// next auth
|
|
173
|
-
'/next-auth/(.*)',
|
|
174
167
|
// better auth
|
|
175
168
|
'/signin',
|
|
176
169
|
'/signup',
|
|
@@ -187,70 +180,6 @@ export function defineConfig() {
|
|
|
187
180
|
'/share(.*)',
|
|
188
181
|
]);
|
|
189
182
|
|
|
190
|
-
const isProtectedRoute = createRouteMatcher([
|
|
191
|
-
'/settings(.*)',
|
|
192
|
-
'/knowledge(.*)',
|
|
193
|
-
'/onboard(.*)',
|
|
194
|
-
'/oauth(.*)',
|
|
195
|
-
// ↓ cloud ↓
|
|
196
|
-
]);
|
|
197
|
-
|
|
198
|
-
// Initialize an Edge compatible NextAuth middleware
|
|
199
|
-
const nextAuthMiddleware = NextAuth.auth((req) => {
|
|
200
|
-
logNextAuth('NextAuth middleware processing request: %s %s', req.method, req.url);
|
|
201
|
-
|
|
202
|
-
const response = defaultMiddleware(req);
|
|
203
|
-
|
|
204
|
-
// when enable auth protection, only public route is not protected, others are all protected
|
|
205
|
-
const isProtected = appEnv.ENABLE_AUTH_PROTECTION ? !isPublicRoute(req) : isProtectedRoute(req);
|
|
206
|
-
|
|
207
|
-
logNextAuth('Route protection status: %s, %s', req.url, isProtected ? 'protected' : 'public');
|
|
208
|
-
|
|
209
|
-
// Just check if session exists
|
|
210
|
-
const session = req.auth;
|
|
211
|
-
|
|
212
|
-
// Check if next-auth throws errors
|
|
213
|
-
// refs: https://github.com/lobehub/lobe-chat/pull/1323
|
|
214
|
-
const isLoggedIn = !!session?.expires;
|
|
215
|
-
|
|
216
|
-
logNextAuth('NextAuth session status: %O', {
|
|
217
|
-
expires: session?.expires,
|
|
218
|
-
isLoggedIn,
|
|
219
|
-
userId: session?.user?.id,
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
// Remove & amend OAuth authorized header
|
|
223
|
-
response.headers.delete(OAUTH_AUTHORIZED);
|
|
224
|
-
if (isLoggedIn) {
|
|
225
|
-
logNextAuth('Setting auth header: %s = %s', OAUTH_AUTHORIZED, 'true');
|
|
226
|
-
response.headers.set(OAUTH_AUTHORIZED, 'true');
|
|
227
|
-
|
|
228
|
-
// If OIDC is enabled and user is logged in, add OIDC session pre-sync header
|
|
229
|
-
if (authEnv.ENABLE_OIDC && session?.user?.id) {
|
|
230
|
-
logNextAuth('OIDC session pre-sync: Setting %s = %s', OIDC_SESSION_HEADER, session.user.id);
|
|
231
|
-
response.headers.set(OIDC_SESSION_HEADER, session.user.id);
|
|
232
|
-
}
|
|
233
|
-
} else {
|
|
234
|
-
// If request a protected route, redirect to sign-in page
|
|
235
|
-
// ref: https://authjs.dev/getting-started/session-management/protecting
|
|
236
|
-
if (isProtected) {
|
|
237
|
-
logNextAuth('Request a protected route, redirecting to sign-in page');
|
|
238
|
-
const callbackUrl = `${appEnv.APP_URL}${req.nextUrl.pathname}${req.nextUrl.search}`;
|
|
239
|
-
const nextLoginUrl = new URL('/next-auth/signin', appEnv.APP_URL);
|
|
240
|
-
nextLoginUrl.searchParams.set('callbackUrl', callbackUrl);
|
|
241
|
-
const hl = req.nextUrl.searchParams.get('hl');
|
|
242
|
-
if (hl) {
|
|
243
|
-
nextLoginUrl.searchParams.set('hl', hl);
|
|
244
|
-
logNextAuth('Preserving locale to sign-in: hl=%s', hl);
|
|
245
|
-
}
|
|
246
|
-
return Response.redirect(nextLoginUrl);
|
|
247
|
-
}
|
|
248
|
-
logNextAuth('Request a free route but not login, allow visit without auth header');
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
return response;
|
|
252
|
-
});
|
|
253
|
-
|
|
254
183
|
const betterAuthMiddleware = async (req: NextRequest) => {
|
|
255
184
|
logBetterAuth('BetterAuth middleware processing request: %s %s', req.method, req.url);
|
|
256
185
|
|
|
@@ -298,12 +227,10 @@ export function defineConfig() {
|
|
|
298
227
|
|
|
299
228
|
logDefault('Middleware configuration: %O', {
|
|
300
229
|
enableAuthProtection: appEnv.ENABLE_AUTH_PROTECTION,
|
|
301
|
-
enableBetterAuth: authEnv.NEXT_PUBLIC_ENABLE_BETTER_AUTH,
|
|
302
|
-
enableNextAuth: authEnv.NEXT_PUBLIC_ENABLE_NEXT_AUTH,
|
|
303
230
|
enableOIDC: authEnv.ENABLE_OIDC,
|
|
304
231
|
});
|
|
305
232
|
|
|
306
233
|
return {
|
|
307
|
-
middleware:
|
|
234
|
+
middleware: betterAuthMiddleware,
|
|
308
235
|
};
|
|
309
236
|
}
|
package/src/libs/redis/index.ts
CHANGED
|
@@ -1,23 +1,15 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
|
|
3
3
|
import { RedisManager, initializeRedis, resetRedisClient } from './manager';
|
|
4
|
-
import {
|
|
4
|
+
import { RedisConfig } from './types';
|
|
5
5
|
|
|
6
|
-
const {
|
|
7
|
-
mockIoRedisInitialize,
|
|
8
|
-
mockIoRedisDisconnect,
|
|
9
|
-
mockUpstashInitialize,
|
|
10
|
-
mockUpstashDisconnect,
|
|
11
|
-
} = vi.hoisted(() => ({
|
|
6
|
+
const { mockIoRedisInitialize, mockIoRedisDisconnect } = vi.hoisted(() => ({
|
|
12
7
|
mockIoRedisInitialize: vi.fn().mockResolvedValue(undefined),
|
|
13
8
|
mockIoRedisDisconnect: vi.fn().mockResolvedValue(undefined),
|
|
14
|
-
mockUpstashInitialize: vi.fn().mockResolvedValue(undefined),
|
|
15
|
-
mockUpstashDisconnect: vi.fn().mockResolvedValue(undefined),
|
|
16
9
|
}));
|
|
17
10
|
|
|
18
11
|
vi.mock('./redis', () => {
|
|
19
12
|
const IoRedisRedisProvider = vi.fn().mockImplementation((config) => ({
|
|
20
|
-
provider: 'redis' as const,
|
|
21
13
|
config,
|
|
22
14
|
initialize: mockIoRedisInitialize,
|
|
23
15
|
disconnect: mockIoRedisDisconnect,
|
|
@@ -26,20 +18,9 @@ vi.mock('./redis', () => {
|
|
|
26
18
|
return { IoRedisRedisProvider };
|
|
27
19
|
});
|
|
28
20
|
|
|
29
|
-
vi.mock('./upstash', () => {
|
|
30
|
-
const UpstashRedisProvider = vi.fn().mockImplementation((config) => ({
|
|
31
|
-
provider: 'upstash' as const,
|
|
32
|
-
config,
|
|
33
|
-
initialize: mockUpstashInitialize,
|
|
34
|
-
disconnect: mockUpstashDisconnect,
|
|
35
|
-
}));
|
|
36
|
-
|
|
37
|
-
return { UpstashRedisProvider };
|
|
38
|
-
});
|
|
39
|
-
|
|
40
21
|
afterEach(async () => {
|
|
41
|
-
vi.clearAllMocks();
|
|
42
22
|
await RedisManager.reset();
|
|
23
|
+
vi.clearAllMocks();
|
|
43
24
|
});
|
|
44
25
|
|
|
45
26
|
describe('RedisManager', () => {
|
|
@@ -47,14 +28,14 @@ describe('RedisManager', () => {
|
|
|
47
28
|
const config = {
|
|
48
29
|
enabled: false,
|
|
49
30
|
prefix: 'test',
|
|
50
|
-
|
|
51
|
-
|
|
31
|
+
tls: false,
|
|
32
|
+
url: '',
|
|
33
|
+
} satisfies RedisConfig;
|
|
52
34
|
|
|
53
35
|
const instance = await initializeRedis(config);
|
|
54
36
|
|
|
55
37
|
expect(instance).toBeNull();
|
|
56
38
|
expect(mockIoRedisInitialize).not.toHaveBeenCalled();
|
|
57
|
-
expect(mockUpstashInitialize).not.toHaveBeenCalled();
|
|
58
39
|
});
|
|
59
40
|
|
|
60
41
|
it('initializes ioredis provider once and memoizes the instance', async () => {
|
|
@@ -63,41 +44,24 @@ describe('RedisManager', () => {
|
|
|
63
44
|
enabled: true,
|
|
64
45
|
password: 'pwd',
|
|
65
46
|
prefix: 'test',
|
|
66
|
-
provider: 'redis' as const,
|
|
67
47
|
tls: false,
|
|
68
48
|
url: 'redis://localhost:6379',
|
|
69
49
|
username: 'user',
|
|
70
|
-
};
|
|
50
|
+
} satisfies RedisConfig;
|
|
51
|
+
|
|
71
52
|
const [first, second] = await Promise.all([initializeRedis(config), initializeRedis(config)]);
|
|
72
53
|
|
|
73
54
|
expect(first).toBe(second);
|
|
74
55
|
expect(mockIoRedisInitialize).toHaveBeenCalledTimes(1);
|
|
75
|
-
expect(mockUpstashInitialize).not.toHaveBeenCalled();
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('initializes upstash provider when configured', async () => {
|
|
79
|
-
const config = {
|
|
80
|
-
enabled: true,
|
|
81
|
-
prefix: 'test',
|
|
82
|
-
provider: 'upstash' as const,
|
|
83
|
-
token: 'token',
|
|
84
|
-
url: 'https://example.upstash.io',
|
|
85
|
-
};
|
|
86
|
-
const instance = await initializeRedis(config);
|
|
87
|
-
|
|
88
|
-
expect(instance?.provider).toBe('upstash');
|
|
89
|
-
expect(mockUpstashInitialize).toHaveBeenCalledTimes(1);
|
|
90
|
-
expect(mockIoRedisInitialize).not.toHaveBeenCalled();
|
|
91
56
|
});
|
|
92
57
|
|
|
93
58
|
it('disconnects existing provider on reset', async () => {
|
|
94
59
|
const config = {
|
|
95
60
|
enabled: true,
|
|
96
61
|
prefix: 'test',
|
|
97
|
-
provider: 'redis' as const,
|
|
98
62
|
tls: false,
|
|
99
63
|
url: 'redis://localhost:6379',
|
|
100
|
-
};
|
|
64
|
+
} satisfies RedisConfig;
|
|
101
65
|
|
|
102
66
|
await initializeRedis(config);
|
|
103
67
|
await resetRedisClient();
|
|
@@ -1,32 +1,18 @@
|
|
|
1
1
|
import { IoRedisRedisProvider } from './redis';
|
|
2
2
|
import { type BaseRedisProvider, type RedisConfig } from './types';
|
|
3
|
-
import { UpstashRedisProvider } from './upstash';
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* Create a Redis provider instance based on config
|
|
7
6
|
*
|
|
8
7
|
* @param config - Redis config
|
|
9
8
|
* @param prefix - Optional custom prefix to override config.prefix
|
|
10
|
-
* @returns Provider instance or null if disabled
|
|
9
|
+
* @returns Provider instance or null if disabled
|
|
11
10
|
*/
|
|
12
11
|
const createProvider = (config: RedisConfig, prefix?: string): BaseRedisProvider | null => {
|
|
13
12
|
if (!config.enabled) return null;
|
|
14
13
|
|
|
15
14
|
const actualPrefix = prefix ?? config.prefix;
|
|
16
|
-
|
|
17
|
-
if (config.provider === 'redis') {
|
|
18
|
-
return new IoRedisRedisProvider({ ...config, prefix: actualPrefix });
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
if (config.provider === 'upstash') {
|
|
22
|
-
return new UpstashRedisProvider({
|
|
23
|
-
prefix: actualPrefix,
|
|
24
|
-
token: config.token,
|
|
25
|
-
url: config.url,
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return null;
|
|
15
|
+
return new IoRedisRedisProvider({ ...config, prefix: actualPrefix });
|
|
30
16
|
};
|
|
31
17
|
|
|
32
18
|
class RedisManager {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { RedisConfig } from './types';
|
|
4
4
|
|
|
5
|
-
const buildRedisConfig = ():
|
|
5
|
+
const buildRedisConfig = (): RedisConfig | null => {
|
|
6
6
|
const url = process.env.REDIS_URL;
|
|
7
7
|
|
|
8
8
|
if (!url) return null;
|
|
@@ -14,7 +14,6 @@ const buildRedisConfig = (): IoRedisConfig | null => {
|
|
|
14
14
|
enabled: true,
|
|
15
15
|
password: process.env.REDIS_PASSWORD,
|
|
16
16
|
prefix: process.env.REDIS_PREFIX ?? 'lobe-chat-test',
|
|
17
|
-
provider: 'redis',
|
|
18
17
|
tls: process.env.REDIS_TLS === 'true',
|
|
19
18
|
url,
|
|
20
19
|
username: process.env.REDIS_USERNAME,
|
|
@@ -79,7 +78,6 @@ const createMockedProvider = async () => {
|
|
|
79
78
|
const provider = new IoRedisRedisProvider({
|
|
80
79
|
enabled: true,
|
|
81
80
|
prefix: 'mock',
|
|
82
|
-
provider: 'redis',
|
|
83
81
|
tls: false,
|
|
84
82
|
url: 'redis://localhost:6379',
|
|
85
83
|
});
|
package/src/libs/redis/redis.ts
CHANGED
|
@@ -3,10 +3,9 @@ import type { Redis } from 'ioredis';
|
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
type BaseRedisProvider,
|
|
6
|
-
type
|
|
6
|
+
type RedisConfig,
|
|
7
7
|
type RedisKey,
|
|
8
8
|
type RedisMSetArgument,
|
|
9
|
-
type RedisProviderName,
|
|
10
9
|
type RedisSetResult,
|
|
11
10
|
type RedisValue,
|
|
12
11
|
type SetOptions,
|
|
@@ -16,10 +15,9 @@ import { buildIORedisSetArgs, normalizeMsetValues } from './utils';
|
|
|
16
15
|
const log = debug('lobe:redis');
|
|
17
16
|
|
|
18
17
|
export class IoRedisRedisProvider implements BaseRedisProvider {
|
|
19
|
-
provider: RedisProviderName = 'redis';
|
|
20
18
|
private client: Redis | null = null;
|
|
21
19
|
|
|
22
|
-
constructor(private config:
|
|
20
|
+
constructor(private config: RedisConfig) {}
|
|
23
21
|
|
|
24
22
|
async initialize() {
|
|
25
23
|
const IORedis = await import('ioredis');
|
package/src/libs/redis/types.ts
CHANGED
|
@@ -1,35 +1,16 @@
|
|
|
1
1
|
export type RedisKey = string | Buffer;
|
|
2
2
|
export type RedisValue = string | Buffer | number;
|
|
3
|
-
export type RedisProvider = false | 'redis' | 'upstash';
|
|
4
|
-
export type RedisProviderName = Exclude<RedisProvider, false>;
|
|
5
3
|
|
|
6
|
-
export type
|
|
4
|
+
export type RedisConfig = {
|
|
7
5
|
database?: number;
|
|
8
6
|
enabled: boolean;
|
|
9
7
|
password?: string;
|
|
10
8
|
prefix: string;
|
|
11
|
-
provider: 'redis';
|
|
12
9
|
tls: boolean;
|
|
13
10
|
url: string;
|
|
14
11
|
username?: string;
|
|
15
12
|
};
|
|
16
13
|
|
|
17
|
-
export type UpstashConfig = {
|
|
18
|
-
enabled: boolean;
|
|
19
|
-
prefix: string;
|
|
20
|
-
provider: 'upstash';
|
|
21
|
-
token: string;
|
|
22
|
-
url: string;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
export type DisabledRedisConfig = {
|
|
26
|
-
enabled: false;
|
|
27
|
-
prefix: string;
|
|
28
|
-
provider: false;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
export type RedisConfig = IoRedisConfig | UpstashConfig | DisabledRedisConfig;
|
|
32
|
-
|
|
33
14
|
export interface SetOptions {
|
|
34
15
|
ex?: number;
|
|
35
16
|
exat?: number;
|
|
@@ -41,8 +22,7 @@ export interface SetOptions {
|
|
|
41
22
|
xx?: boolean;
|
|
42
23
|
}
|
|
43
24
|
|
|
44
|
-
|
|
45
|
-
export type RedisSetResult = 'OK' | null | string | number;
|
|
25
|
+
export type RedisSetResult = 'OK' | null | string;
|
|
46
26
|
export type RedisMSetArgument = Record<string, RedisValue> | Map<RedisKey, RedisValue>;
|
|
47
27
|
|
|
48
28
|
export interface RedisClient {
|
|
@@ -65,7 +45,5 @@ export interface RedisClient {
|
|
|
65
45
|
|
|
66
46
|
export interface BaseRedisProvider extends RedisClient {
|
|
67
47
|
disconnect(): Promise<void>;
|
|
68
|
-
|
|
69
48
|
initialize(): Promise<void>;
|
|
70
|
-
provider: RedisProviderName;
|
|
71
49
|
}
|
|
@@ -2,7 +2,6 @@ import { describe, expect, it } from 'vitest';
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
buildIORedisSetArgs,
|
|
5
|
-
buildUpstashSetOptions,
|
|
6
5
|
normalizeMsetValues,
|
|
7
6
|
normalizeRedisKey,
|
|
8
7
|
normalizeRedisKeys,
|
|
@@ -34,13 +33,4 @@ describe('redis utils', () => {
|
|
|
34
33
|
|
|
35
34
|
expect(args).toEqual(['EX', 1, 'NX', 'GET']);
|
|
36
35
|
});
|
|
37
|
-
|
|
38
|
-
it('builds upstash set options', () => {
|
|
39
|
-
expect(buildUpstashSetOptions()).toBeUndefined();
|
|
40
|
-
expect(buildUpstashSetOptions({ ex: 10, nx: true, get: true })).toEqual({
|
|
41
|
-
ex: 10,
|
|
42
|
-
nx: true,
|
|
43
|
-
get: true,
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
36
|
});
|
package/src/libs/redis/utils.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { SetCommandOptions } from '@upstash/redis';
|
|
2
|
-
|
|
3
1
|
import { type RedisKey, type RedisMSetArgument, type RedisValue, type SetOptions } from './types';
|
|
4
2
|
|
|
5
3
|
export const normalizeRedisKey = (key: RedisKey) =>
|
|
@@ -34,20 +32,3 @@ export const buildIORedisSetArgs = (options?: SetOptions): Array<string | number
|
|
|
34
32
|
|
|
35
33
|
return args;
|
|
36
34
|
};
|
|
37
|
-
|
|
38
|
-
export const buildUpstashSetOptions = (options?: SetOptions): SetCommandOptions | undefined => {
|
|
39
|
-
if (!options) return undefined;
|
|
40
|
-
|
|
41
|
-
const mapped: Partial<SetCommandOptions> = {};
|
|
42
|
-
|
|
43
|
-
if (options.ex !== undefined) mapped.ex = options.ex;
|
|
44
|
-
if (options.px !== undefined) mapped.px = options.px;
|
|
45
|
-
if (options.exat !== undefined) mapped.exat = options.exat;
|
|
46
|
-
if (options.pxat !== undefined) mapped.pxat = options.pxat;
|
|
47
|
-
if (options.keepTtl) mapped.keepTtl = true;
|
|
48
|
-
if (options.nx) mapped.nx = true;
|
|
49
|
-
if (options.xx) mapped.xx = true;
|
|
50
|
-
if (options.get) mapped.get = true;
|
|
51
|
-
|
|
52
|
-
return Object.keys(mapped).length ? (mapped as SetCommandOptions) : undefined;
|
|
53
|
-
};
|
|
@@ -9,7 +9,6 @@ describe('createContextInner', () => {
|
|
|
9
9
|
expect(context).toMatchObject({
|
|
10
10
|
authorizationHeader: undefined,
|
|
11
11
|
marketAccessToken: undefined,
|
|
12
|
-
nextAuth: undefined,
|
|
13
12
|
oidcAuth: undefined,
|
|
14
13
|
userAgent: undefined,
|
|
15
14
|
userId: undefined,
|
|
@@ -58,18 +57,6 @@ describe('createContextInner', () => {
|
|
|
58
57
|
expect(context.oidcAuth).toEqual(oidcAuth);
|
|
59
58
|
});
|
|
60
59
|
|
|
61
|
-
it('should create context with NextAuth user data', async () => {
|
|
62
|
-
const nextAuth = {
|
|
63
|
-
id: 'next-auth-user-id',
|
|
64
|
-
name: 'Test User',
|
|
65
|
-
email: 'test@example.com',
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const context = await createContextInner({ nextAuth });
|
|
69
|
-
|
|
70
|
-
expect(context.nextAuth).toEqual(nextAuth);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
60
|
it('should create context with all parameters combined', async () => {
|
|
74
61
|
const params = {
|
|
75
62
|
authorizationHeader: 'Bearer token',
|