@lobehub/lobehub 2.0.0-next.221 → 2.0.0-next.223
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/.github/workflows/claude-auto-testing.yml +6 -3
- package/.github/workflows/claude-dedupe-issues.yml +8 -1
- package/.github/workflows/claude-issue-triage.yml +8 -14
- package/.github/workflows/claude-translate-comments.yml +6 -3
- package/.github/workflows/claude-translator.yml +12 -14
- package/.github/workflows/claude.yml +10 -20
- package/.github/workflows/test.yml +26 -0
- package/CHANGELOG.md +58 -0
- package/changelog/v1.json +18 -0
- package/e2e/package.json +1 -1
- package/e2e/src/mocks/index.ts +2 -2
- package/e2e/src/steps/{discover → community}/detail-pages.steps.ts +8 -8
- package/e2e/src/steps/{discover → community}/interactions.steps.ts +4 -4
- package/locales/zh-CN/components.json +1 -0
- package/package.json +3 -3
- package/packages/const/src/index.ts +0 -1
- package/packages/memory-user-memory/package.json +1 -0
- package/packages/memory-user-memory/src/extractors/context.test.ts +3 -2
- package/packages/memory-user-memory/src/extractors/experience.test.ts +3 -2
- package/packages/memory-user-memory/src/extractors/identity.test.ts +23 -6
- package/packages/memory-user-memory/src/extractors/preference.test.ts +3 -2
- package/packages/memory-user-memory/vitest.config.ts +4 -0
- package/packages/model-runtime/src/providers/replicate/index.ts +1 -1
- package/packages/ssrf-safe-fetch/index.test.ts +2 -2
- package/packages/ssrf-safe-fetch/package.json +3 -2
- package/packages/types/src/serverConfig.ts +2 -0
- package/packages/utils/package.json +1 -1
- package/packages/utils/src/client/xor-obfuscation.test.ts +32 -32
- package/packages/utils/src/client/xor-obfuscation.ts +3 -4
- package/packages/utils/src/imageToBase64.ts +1 -1
- package/packages/utils/src/server/__tests__/auth.test.ts +1 -1
- package/packages/utils/src/server/auth.ts +1 -1
- package/packages/utils/src/server/correctOIDCUrl.test.ts +80 -19
- package/packages/utils/src/server/correctOIDCUrl.ts +89 -24
- package/packages/utils/src/server/index.ts +1 -0
- package/packages/utils/src/server/xor.test.ts +9 -7
- package/packages/utils/src/server/xor.ts +1 -1
- package/packages/web-crawler/package.json +1 -1
- package/packages/web-crawler/src/crawImpl/__tests__/naive.test.ts +1 -1
- package/packages/web-crawler/src/crawImpl/naive.ts +1 -1
- package/scripts/prebuild.mts +58 -1
- package/src/app/(backend)/api/auth/[...all]/route.ts +2 -1
- package/src/app/(backend)/middleware/auth/index.ts +3 -3
- package/src/app/(backend)/middleware/auth/utils.test.ts +1 -1
- package/src/app/(backend)/middleware/auth/utils.ts +1 -1
- package/src/app/(backend)/oidc/callback/desktop/route.ts +7 -36
- package/src/app/(backend)/webapi/chat/[provider]/route.test.ts +2 -2
- package/src/app/(backend)/webapi/models/[provider]/route.test.ts +1 -1
- package/src/app/(backend)/webapi/plugin/gateway/route.ts +1 -1
- package/src/app/(backend)/webapi/proxy/route.ts +1 -1
- package/src/app/[variants]/(auth)/login/[[...login]]/page.tsx +1 -1
- package/src/app/[variants]/(auth)/reset-password/layout.tsx +1 -1
- package/src/app/[variants]/(auth)/signin/layout.tsx +1 -1
- package/src/app/[variants]/(auth)/signin/useSignIn.ts +2 -2
- package/src/app/[variants]/(auth)/signup/[[...signup]]/page.tsx +1 -1
- package/src/app/[variants]/(auth)/signup/[[...signup]]/useSignUp.tsx +12 -6
- package/src/app/[variants]/(auth)/verify-email/layout.tsx +1 -1
- package/src/app/[variants]/(main)/settings/profile/features/AvatarRow.tsx +1 -1
- package/src/app/[variants]/(main)/settings/security/index.tsx +1 -1
- package/src/app/[variants]/(mobile)/me/(home)/__tests__/UserBanner.test.tsx +1 -1
- package/src/app/[variants]/(mobile)/me/(home)/__tests__/useCategory.test.tsx +1 -1
- package/src/app/[variants]/(mobile)/me/(home)/features/UserBanner.tsx +1 -1
- package/src/app/[variants]/(mobile)/settings/_layout/Header.tsx +1 -1
- package/src/components/ModelSelect/index.tsx +103 -72
- package/src/envs/auth.ts +30 -9
- package/src/features/Conversation/Messages/AssistantGroup/components/EditState.tsx +15 -32
- package/src/features/Conversation/Messages/AssistantGroup/index.tsx +9 -7
- package/src/features/EditorModal/EditorCanvas.tsx +31 -50
- package/src/features/EditorModal/TextareCanvas.tsx +3 -1
- package/src/features/EditorModal/index.tsx +14 -4
- package/src/features/ModelSwitchPanel/components/Footer.tsx +42 -0
- package/src/features/ModelSwitchPanel/components/List/MultipleProvidersModelItem.tsx +103 -0
- package/src/features/ModelSwitchPanel/components/List/SingleProviderModelItem.tsx +24 -0
- package/src/features/ModelSwitchPanel/components/List/VirtualItemRenderer.tsx +180 -0
- package/src/features/ModelSwitchPanel/components/List/index.tsx +99 -0
- package/src/features/ModelSwitchPanel/components/PanelContent.tsx +77 -0
- package/src/features/ModelSwitchPanel/components/Toolbar.tsx +54 -0
- package/src/features/ModelSwitchPanel/const.ts +29 -0
- package/src/features/ModelSwitchPanel/hooks/useBuildVirtualItems.ts +122 -0
- package/src/features/ModelSwitchPanel/hooks/useCurrentModelName.ts +18 -0
- package/src/features/ModelSwitchPanel/hooks/useDelayedRender.ts +18 -0
- package/src/features/ModelSwitchPanel/hooks/useModelAndProvider.ts +14 -0
- package/src/features/ModelSwitchPanel/hooks/usePanelHandlers.ts +33 -0
- package/src/features/ModelSwitchPanel/hooks/usePanelSize.ts +33 -0
- package/src/features/ModelSwitchPanel/hooks/usePanelState.ts +20 -0
- package/src/features/ModelSwitchPanel/index.tsx +25 -706
- package/src/features/ModelSwitchPanel/styles.ts +58 -0
- package/src/features/ModelSwitchPanel/types.ts +73 -0
- package/src/features/ModelSwitchPanel/utils.ts +24 -0
- package/src/features/User/UserPanel/PanelContent.tsx +1 -1
- package/src/features/User/__tests__/PanelContent.test.tsx +1 -1
- package/src/features/User/__tests__/UserAvatar.test.tsx +1 -1
- package/src/features/User/__tests__/useMenu.test.tsx +1 -1
- package/src/layout/GlobalProvider/StoreInitialization.tsx +2 -1
- package/src/libs/better-auth/auth-client.ts +7 -3
- package/src/libs/better-auth/define-config.ts +2 -2
- package/src/libs/next/proxy/define-config.ts +9 -6
- package/src/libs/oidc-provider/provider.test.ts +1 -1
- package/src/libs/trpc/async/context.ts +1 -1
- package/src/libs/trpc/lambda/context.ts +7 -8
- package/src/libs/trpc/middleware/userAuth.ts +1 -1
- package/src/libs/trusted-client/getSessionUser.ts +1 -1
- package/src/locales/default/components.ts +1 -0
- package/src/server/globalConfig/index.ts +2 -0
- package/src/server/routers/async/caller.ts +1 -1
- package/src/server/routers/lambda/__tests__/user.test.ts +2 -2
- package/src/server/routers/lambda/user.ts +2 -1
- package/src/services/_auth.ts +3 -3
- package/src/services/chat/index.ts +1 -1
- package/src/services/chat/mecha/contextEngineering.ts +1 -1
- package/src/store/global/initialState.ts +10 -0
- package/src/store/global/selectors/systemStatus.ts +5 -0
- package/src/store/serverConfig/selectors.ts +5 -1
- package/src/store/tool/slices/mcpStore/action.ts +74 -75
- package/src/store/user/slices/auth/action.test.ts +1 -1
- package/src/store/user/slices/auth/action.ts +1 -1
- package/src/store/user/slices/auth/initialState.ts +1 -1
- package/src/store/user/slices/auth/selectors.test.ts +1 -1
- package/src/store/user/slices/auth/selectors.ts +1 -1
- package/src/store/user/slices/common/action.ts +1 -1
- package/src/store/userMemory/slices/context/action.ts +6 -6
- package/packages/const/src/auth.ts +0 -14
- /package/e2e/src/features/{discover → community}/detail-pages.feature +0 -0
- /package/e2e/src/features/{discover → community}/interactions.feature +0 -0
- /package/e2e/src/features/{discover → community}/smoke.feature +0 -0
- /package/e2e/src/mocks/{discover → community}/data.ts +0 -0
- /package/e2e/src/mocks/{discover → community}/handlers.ts +0 -0
- /package/e2e/src/mocks/{discover → community}/index.ts +0 -0
- /package/e2e/src/mocks/{discover → community}/types.ts +0 -0
- /package/e2e/src/steps/{discover → community}/smoke.steps.ts +0 -0
|
@@ -5,29 +5,69 @@ import { validateRedirectHost } from './validateRedirectHost';
|
|
|
5
5
|
|
|
6
6
|
const log = debug('lobe-oidc:correctOIDCUrl');
|
|
7
7
|
|
|
8
|
+
// Allowed protocols for security
|
|
9
|
+
const ALLOWED_PROTOCOLS = ['http', 'https'] as const;
|
|
10
|
+
|
|
8
11
|
/**
|
|
9
12
|
* Fix OIDC redirect URL issues in proxy environments
|
|
13
|
+
*
|
|
14
|
+
* This function:
|
|
15
|
+
* 1. Validates protocol against whitelist (http, https only)
|
|
16
|
+
* 2. Handles X-Forwarded-Host with multiple values (RFC 7239)
|
|
17
|
+
* 3. Validates X-Forwarded-Host against APP_URL to prevent open redirect attacks
|
|
18
|
+
* 4. Provides fallback logic for invalid forwarded values
|
|
19
|
+
*
|
|
20
|
+
* Note: Only X-Forwarded-Host is validated, not the Host header. This is because:
|
|
21
|
+
* - X-Forwarded-Host can be injected by attackers
|
|
22
|
+
* - Host header comes from the reverse proxy or direct access, which is trusted
|
|
23
|
+
*
|
|
10
24
|
* @param req - Next.js request object
|
|
11
25
|
* @param url - URL object to fix
|
|
12
26
|
* @returns Fixed URL object
|
|
13
27
|
*/
|
|
14
28
|
export const correctOIDCUrl = (req: NextRequest, url: URL): URL => {
|
|
29
|
+
log('Input URL: %s', url.toString());
|
|
30
|
+
|
|
31
|
+
// Get request headers for origin determination
|
|
15
32
|
const requestHost = req.headers.get('host');
|
|
16
33
|
const forwardedHost = req.headers.get('x-forwarded-host');
|
|
17
34
|
const forwardedProto =
|
|
18
35
|
req.headers.get('x-forwarded-proto') || req.headers.get('x-forwarded-protocol');
|
|
19
36
|
|
|
20
|
-
log('Input URL: %s', url.toString());
|
|
21
37
|
log(
|
|
22
|
-
'
|
|
38
|
+
'Getting safe origin - requestHost: %s, forwardedHost: %s, forwardedProto: %s',
|
|
23
39
|
requestHost,
|
|
24
40
|
forwardedHost,
|
|
25
41
|
forwardedProto,
|
|
26
42
|
);
|
|
27
43
|
|
|
28
|
-
// Determine actual hostname
|
|
29
|
-
|
|
30
|
-
|
|
44
|
+
// Determine actual hostname with fallback values
|
|
45
|
+
// Handle multiple hosts in X-Forwarded-Host (RFC 7239: comma-separated)
|
|
46
|
+
let actualHost = forwardedHost || requestHost;
|
|
47
|
+
if (forwardedHost && forwardedHost.includes(',')) {
|
|
48
|
+
// Take the first (leftmost) host as the original client's request
|
|
49
|
+
actualHost = forwardedHost.split(',')[0]!.trim();
|
|
50
|
+
log('Multiple hosts in X-Forwarded-Host, using first: %s', actualHost);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Determine actual protocol with validation
|
|
54
|
+
// Use URL's protocol as fallback to preserve original behavior
|
|
55
|
+
let actualProto: string | null | undefined = forwardedProto;
|
|
56
|
+
if (actualProto) {
|
|
57
|
+
// Validate protocol is http or https
|
|
58
|
+
const protoLower = actualProto.toLowerCase();
|
|
59
|
+
if (!ALLOWED_PROTOCOLS.includes(protoLower as any)) {
|
|
60
|
+
log('Warning: Invalid protocol %s, ignoring', actualProto);
|
|
61
|
+
actualProto = null;
|
|
62
|
+
} else {
|
|
63
|
+
actualProto = protoLower;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Fallback protocol priority: URL protocol > request.nextUrl.protocol > 'https'
|
|
68
|
+
if (!actualProto) {
|
|
69
|
+
actualProto = url.protocol === 'https:' ? 'https' : 'http';
|
|
70
|
+
}
|
|
31
71
|
|
|
32
72
|
// If unable to determine valid hostname, return original URL
|
|
33
73
|
if (!actualHost || actualHost === 'null') {
|
|
@@ -35,9 +75,30 @@ export const correctOIDCUrl = (req: NextRequest, url: URL): URL => {
|
|
|
35
75
|
return url;
|
|
36
76
|
}
|
|
37
77
|
|
|
38
|
-
// Validate
|
|
39
|
-
|
|
40
|
-
|
|
78
|
+
// Validate only X-Forwarded-Host for security, prevent Open Redirect attacks
|
|
79
|
+
// Host header is trusted (comes from reverse proxy or direct access)
|
|
80
|
+
if (forwardedHost && !validateRedirectHost(actualHost)) {
|
|
81
|
+
log('Warning: X-Forwarded-Host %s failed validation, falling back to request host', actualHost);
|
|
82
|
+
// Try to fall back to request host if forwarded host is invalid
|
|
83
|
+
if (requestHost) {
|
|
84
|
+
actualHost = requestHost;
|
|
85
|
+
} else {
|
|
86
|
+
// No valid host available
|
|
87
|
+
log('Error: No valid host available after validation, returning original URL');
|
|
88
|
+
return url;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Build safe origin
|
|
93
|
+
const safeOrigin = `${actualProto}://${actualHost}`;
|
|
94
|
+
log('Safe origin: %s', safeOrigin);
|
|
95
|
+
|
|
96
|
+
// Parse safe origin to get hostname and protocol
|
|
97
|
+
let safeOriginUrl: URL;
|
|
98
|
+
try {
|
|
99
|
+
safeOriginUrl = new URL(safeOrigin);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
log('Error parsing safe origin: %O', error);
|
|
41
102
|
return url;
|
|
42
103
|
}
|
|
43
104
|
|
|
@@ -46,24 +107,28 @@ export const correctOIDCUrl = (req: NextRequest, url: URL): URL => {
|
|
|
46
107
|
url.hostname === 'localhost' ||
|
|
47
108
|
url.hostname === '127.0.0.1' ||
|
|
48
109
|
url.hostname === '0.0.0.0' ||
|
|
49
|
-
url.hostname !==
|
|
110
|
+
url.hostname !== safeOriginUrl.hostname;
|
|
111
|
+
|
|
112
|
+
if (!needsCorrection) {
|
|
113
|
+
log('URL does not need correction, returning original: %s', url.toString());
|
|
114
|
+
return url;
|
|
115
|
+
}
|
|
50
116
|
|
|
51
|
-
|
|
52
|
-
|
|
117
|
+
log(
|
|
118
|
+
'URL needs correction. Original hostname: %s, correcting to: %s',
|
|
119
|
+
url.hostname,
|
|
120
|
+
safeOriginUrl.hostname,
|
|
121
|
+
);
|
|
53
122
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
123
|
+
try {
|
|
124
|
+
const correctedUrl = new URL(url.toString());
|
|
125
|
+
correctedUrl.protocol = safeOriginUrl.protocol;
|
|
126
|
+
correctedUrl.host = safeOriginUrl.host;
|
|
58
127
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
128
|
+
log('Corrected URL: %s', correctedUrl.toString());
|
|
129
|
+
return correctedUrl;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
log('Error creating corrected URL, returning original: %O', error);
|
|
132
|
+
return url;
|
|
65
133
|
}
|
|
66
|
-
|
|
67
|
-
log('URL does not need correction, returning original: %s', url.toString());
|
|
68
|
-
return url;
|
|
69
134
|
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
|
|
3
|
+
import { SECRET_XOR_KEY } from '@/envs/auth';
|
|
4
|
+
|
|
3
5
|
import { obfuscatePayloadWithXOR } from '../client/xor-obfuscation';
|
|
4
6
|
import { getXorPayload } from './xor';
|
|
5
7
|
|
|
@@ -12,7 +14,7 @@ describe('getXorPayload', () => {
|
|
|
12
14
|
};
|
|
13
15
|
|
|
14
16
|
// 使用客户端的混淆函数生成token
|
|
15
|
-
const obfuscatedToken = obfuscatePayloadWithXOR(originalPayload);
|
|
17
|
+
const obfuscatedToken = obfuscatePayloadWithXOR(originalPayload, SECRET_XOR_KEY);
|
|
16
18
|
|
|
17
19
|
// 使用服务端的解码函数解码
|
|
18
20
|
const decodedPayload = getXorPayload(obfuscatedToken);
|
|
@@ -25,7 +27,7 @@ describe('getXorPayload', () => {
|
|
|
25
27
|
userId: '12345',
|
|
26
28
|
};
|
|
27
29
|
|
|
28
|
-
const obfuscatedToken = obfuscatePayloadWithXOR(originalPayload);
|
|
30
|
+
const obfuscatedToken = obfuscatePayloadWithXOR(originalPayload, SECRET_XOR_KEY);
|
|
29
31
|
const decodedPayload = getXorPayload(obfuscatedToken);
|
|
30
32
|
|
|
31
33
|
expect(decodedPayload).toEqual(originalPayload);
|
|
@@ -40,7 +42,7 @@ describe('getXorPayload', () => {
|
|
|
40
42
|
awsSessionToken: 'session-token-example',
|
|
41
43
|
};
|
|
42
44
|
|
|
43
|
-
const obfuscatedToken = obfuscatePayloadWithXOR(originalPayload);
|
|
45
|
+
const obfuscatedToken = obfuscatePayloadWithXOR(originalPayload, SECRET_XOR_KEY);
|
|
44
46
|
const decodedPayload = getXorPayload(obfuscatedToken);
|
|
45
47
|
|
|
46
48
|
expect(decodedPayload).toEqual(originalPayload);
|
|
@@ -54,7 +56,7 @@ describe('getXorPayload', () => {
|
|
|
54
56
|
azureApiVersion: '2024-02-15-preview',
|
|
55
57
|
};
|
|
56
58
|
|
|
57
|
-
const obfuscatedToken = obfuscatePayloadWithXOR(originalPayload);
|
|
59
|
+
const obfuscatedToken = obfuscatePayloadWithXOR(originalPayload, SECRET_XOR_KEY);
|
|
58
60
|
const decodedPayload = getXorPayload(obfuscatedToken);
|
|
59
61
|
|
|
60
62
|
expect(decodedPayload).toEqual(originalPayload);
|
|
@@ -67,7 +69,7 @@ describe('getXorPayload', () => {
|
|
|
67
69
|
cloudflareBaseURLOrAccountID: 'account-id-example',
|
|
68
70
|
};
|
|
69
71
|
|
|
70
|
-
const obfuscatedToken = obfuscatePayloadWithXOR(originalPayload);
|
|
72
|
+
const obfuscatedToken = obfuscatePayloadWithXOR(originalPayload, SECRET_XOR_KEY);
|
|
71
73
|
const decodedPayload = getXorPayload(obfuscatedToken);
|
|
72
74
|
|
|
73
75
|
expect(decodedPayload).toEqual(originalPayload);
|
|
@@ -76,7 +78,7 @@ describe('getXorPayload', () => {
|
|
|
76
78
|
it('should handle empty payload correctly', () => {
|
|
77
79
|
const originalPayload = {};
|
|
78
80
|
|
|
79
|
-
const obfuscatedToken = obfuscatePayloadWithXOR(originalPayload);
|
|
81
|
+
const obfuscatedToken = obfuscatePayloadWithXOR(originalPayload, SECRET_XOR_KEY);
|
|
80
82
|
const decodedPayload = getXorPayload(obfuscatedToken);
|
|
81
83
|
|
|
82
84
|
expect(decodedPayload).toEqual(originalPayload);
|
|
@@ -89,7 +91,7 @@ describe('getXorPayload', () => {
|
|
|
89
91
|
apiKey: 'test-key',
|
|
90
92
|
};
|
|
91
93
|
|
|
92
|
-
const obfuscatedToken = obfuscatePayloadWithXOR(originalPayload);
|
|
94
|
+
const obfuscatedToken = obfuscatePayloadWithXOR(originalPayload, SECRET_XOR_KEY);
|
|
93
95
|
const decodedPayload = getXorPayload(obfuscatedToken);
|
|
94
96
|
|
|
95
97
|
expect(decodedPayload).toEqual(originalPayload);
|
package/scripts/prebuild.mts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
1
2
|
import * as dotenv from 'dotenv';
|
|
2
3
|
import dotenvExpand from 'dotenv-expand';
|
|
3
4
|
import { existsSync } from 'node:fs';
|
|
@@ -14,6 +15,61 @@ if (isDesktop) {
|
|
|
14
15
|
} else {
|
|
15
16
|
dotenvExpand.expand(dotenv.config());
|
|
16
17
|
}
|
|
18
|
+
|
|
19
|
+
// Auth flags - use process.env directly for build-time dead code elimination
|
|
20
|
+
const enableClerk =
|
|
21
|
+
process.env.NEXT_PUBLIC_ENABLE_CLERK_AUTH === '1'
|
|
22
|
+
? true
|
|
23
|
+
: !!process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY;
|
|
24
|
+
const enableBetterAuth = process.env.NEXT_PUBLIC_ENABLE_BETTER_AUTH === '1';
|
|
25
|
+
const enableNextAuth = process.env.NEXT_PUBLIC_ENABLE_NEXT_AUTH === '1';
|
|
26
|
+
const enableAuth = enableClerk || enableBetterAuth || enableNextAuth || false;
|
|
27
|
+
|
|
28
|
+
const getCommandVersion = (command: string): string | null => {
|
|
29
|
+
try {
|
|
30
|
+
return execSync(`${command} --version`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] })
|
|
31
|
+
.trim()
|
|
32
|
+
.split('\n')[0];
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const printEnvInfo = () => {
|
|
39
|
+
console.log('\n📋 Build Environment Info:');
|
|
40
|
+
console.log('─'.repeat(50));
|
|
41
|
+
|
|
42
|
+
// Runtime versions
|
|
43
|
+
console.log(` Node.js: ${process.version}`);
|
|
44
|
+
console.log(` npm: ${getCommandVersion('npm') ?? 'not installed'}`);
|
|
45
|
+
|
|
46
|
+
const bunVersion = getCommandVersion('bun');
|
|
47
|
+
if (bunVersion) console.log(` bun: ${bunVersion}`);
|
|
48
|
+
|
|
49
|
+
const pnpmVersion = getCommandVersion('pnpm');
|
|
50
|
+
if (pnpmVersion) console.log(` pnpm: ${pnpmVersion}`);
|
|
51
|
+
|
|
52
|
+
// Auth-related env vars
|
|
53
|
+
console.log('\n Auth Environment Variables:');
|
|
54
|
+
console.log(` NEXT_PUBLIC_AUTH_URL: ${process.env.NEXT_PUBLIC_AUTH_URL ?? '(not set)'}`);
|
|
55
|
+
console.log(` NEXTAUTH_URL: ${process.env.NEXTAUTH_URL ?? '(not set)'}`);
|
|
56
|
+
console.log(` APP_URL: ${process.env.APP_URL ?? '(not set)'}`);
|
|
57
|
+
console.log(` VERCEL_URL: ${process.env.VERCEL_URL ?? '(not set)'}`);
|
|
58
|
+
console.log(` AUTH_EMAIL_VERIFICATION: ${process.env.AUTH_EMAIL_VERIFICATION ?? '(not set)'}`);
|
|
59
|
+
console.log(` ENABLE_MAGIC_LINK: ${process.env.ENABLE_MAGIC_LINK ?? '(not set)'}`);
|
|
60
|
+
console.log(` AUTH_SECRET: ${process.env.AUTH_SECRET ? '✓ set' : '✗ not set'}`);
|
|
61
|
+
console.log(` KEY_VAULTS_SECRET: ${process.env.KEY_VAULTS_SECRET ? '✓ set' : '✗ not set'}`);
|
|
62
|
+
|
|
63
|
+
// Auth flags
|
|
64
|
+
console.log('\n Auth Flags:');
|
|
65
|
+
console.log(` enableClerk: ${enableClerk}`);
|
|
66
|
+
console.log(` enableBetterAuth: ${enableBetterAuth}`);
|
|
67
|
+
console.log(` enableNextAuth: ${enableNextAuth}`);
|
|
68
|
+
console.log(` enableAuth: ${enableAuth}`);
|
|
69
|
+
|
|
70
|
+
console.log('─'.repeat(50));
|
|
71
|
+
};
|
|
72
|
+
|
|
17
73
|
// 创建需要排除的特性映射
|
|
18
74
|
/* eslint-disable sort-keys-fix/sort-keys-fix */
|
|
19
75
|
const partialBuildPages = [
|
|
@@ -105,8 +161,9 @@ export const runPrebuild = async (targetDir: string = 'src') => {
|
|
|
105
161
|
const isMainModule = process.argv[1] === fileURLToPath(import.meta.url);
|
|
106
162
|
|
|
107
163
|
if (isMainModule) {
|
|
164
|
+
printEnvInfo();
|
|
108
165
|
// 执行删除操作
|
|
109
|
-
console.log('
|
|
166
|
+
console.log('\nStarting prebuild cleanup...');
|
|
110
167
|
await runPrebuild();
|
|
111
168
|
console.log('Prebuild cleanup completed.');
|
|
112
169
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { enableBetterAuth, enableNextAuth } from '@lobechat/const';
|
|
2
1
|
import type { NextRequest } from 'next/server';
|
|
3
2
|
|
|
3
|
+
import { enableBetterAuth, enableNextAuth } from '@/envs/auth';
|
|
4
|
+
|
|
4
5
|
const createHandler = async () => {
|
|
5
6
|
if (enableBetterAuth) {
|
|
6
7
|
const [{ toNextJsHandler }, { auth }] = await Promise.all([
|
|
@@ -8,15 +8,15 @@ import { ChatErrorType, type ClientSecretPayload } from '@lobechat/types';
|
|
|
8
8
|
import { getXorPayload } from '@lobechat/utils/server';
|
|
9
9
|
import { type NextRequest } from 'next/server';
|
|
10
10
|
|
|
11
|
+
import { getServerDB } from '@/database/core/db-adaptor';
|
|
12
|
+
import { type LobeChatDatabase } from '@/database/type';
|
|
11
13
|
import {
|
|
12
14
|
LOBE_CHAT_AUTH_HEADER,
|
|
13
15
|
LOBE_CHAT_OIDC_AUTH_HEADER,
|
|
14
16
|
OAUTH_AUTHORIZED,
|
|
15
17
|
enableBetterAuth,
|
|
16
18
|
enableClerk,
|
|
17
|
-
} from '@/
|
|
18
|
-
import { getServerDB } from '@/database/core/db-adaptor';
|
|
19
|
-
import { type LobeChatDatabase } from '@/database/type';
|
|
19
|
+
} from '@/envs/auth';
|
|
20
20
|
import { ClerkAuth } from '@/libs/clerk-auth';
|
|
21
21
|
import { validateOIDCJWT } from '@/libs/oidc-provider/jwt';
|
|
22
22
|
import { createErrorResponse } from '@/utils/errorResponse';
|
|
@@ -2,7 +2,7 @@ import { type AuthObject } from '@clerk/backend';
|
|
|
2
2
|
import { AgentRuntimeError } from '@lobechat/model-runtime';
|
|
3
3
|
import { ChatErrorType } from '@lobechat/types';
|
|
4
4
|
|
|
5
|
-
import { enableBetterAuth, enableClerk, enableNextAuth } from '@/
|
|
5
|
+
import { enableBetterAuth, enableClerk, enableNextAuth } from '@/envs/auth';
|
|
6
6
|
|
|
7
7
|
interface CheckAuthParams {
|
|
8
8
|
apiKey?: string;
|
|
@@ -10,41 +10,15 @@ const log = debug('lobe-oidc:callback:desktop');
|
|
|
10
10
|
const errorPathname = '/oauth/callback/error';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* 安全地构建重定向URL
|
|
13
|
+
* 安全地构建重定向URL,使用经过验证的 correctOIDCUrl 防止开放重定向攻击
|
|
14
14
|
*/
|
|
15
15
|
const buildRedirectUrl = (req: NextRequest, pathname: string): URL => {
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
req.headers.get('x-forwarded-proto') || req.headers.get('x-forwarded-protocol');
|
|
20
|
-
|
|
21
|
-
// 确定实际的主机名,提供后备值
|
|
22
|
-
const actualHost = forwardedHost || requestHost;
|
|
23
|
-
const actualProto = forwardedProto || 'https';
|
|
24
|
-
|
|
25
|
-
log(
|
|
26
|
-
'Building redirect URL - host: %s, proto: %s, pathname: %s',
|
|
27
|
-
actualHost,
|
|
28
|
-
actualProto,
|
|
29
|
-
pathname,
|
|
30
|
-
);
|
|
31
|
-
|
|
32
|
-
// 如果主机名仍然无效,使用req.nextUrl作为后备
|
|
33
|
-
if (!actualHost) {
|
|
34
|
-
log('Warning: Invalid host detected, using req.nextUrl as fallback');
|
|
35
|
-
const fallbackUrl = req.nextUrl.clone();
|
|
36
|
-
fallbackUrl.pathname = pathname;
|
|
37
|
-
return fallbackUrl;
|
|
38
|
-
}
|
|
16
|
+
// 使用 req.nextUrl 作为基础URL,然后通过 correctOIDCUrl 进行验证和修正
|
|
17
|
+
const baseUrl = req.nextUrl.clone();
|
|
18
|
+
baseUrl.pathname = pathname;
|
|
39
19
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
} catch (error) {
|
|
43
|
-
log('Error constructing URL, using req.nextUrl as fallback: %O', error);
|
|
44
|
-
const fallbackUrl = req.nextUrl.clone();
|
|
45
|
-
fallbackUrl.pathname = pathname;
|
|
46
|
-
return fallbackUrl;
|
|
47
|
-
}
|
|
20
|
+
// correctOIDCUrl 会验证 X-Forwarded-* 头部并防止开放重定向攻击
|
|
21
|
+
return correctOIDCUrl(req, baseUrl);
|
|
48
22
|
};
|
|
49
23
|
|
|
50
24
|
export const GET = async (req: NextRequest) => {
|
|
@@ -82,9 +56,6 @@ export const GET = async (req: NextRequest) => {
|
|
|
82
56
|
log('Request x-forwarded-proto: %s', req.headers.get('x-forwarded-proto'));
|
|
83
57
|
log('Constructed success URL: %s', successUrl.toString());
|
|
84
58
|
|
|
85
|
-
const correctedUrl = correctOIDCUrl(req, successUrl);
|
|
86
|
-
log('Final redirect URL: %s', correctedUrl.toString());
|
|
87
|
-
|
|
88
59
|
// cleanup expired
|
|
89
60
|
after(async () => {
|
|
90
61
|
const cleanedCount = await authHandoffModel.cleanupExpired();
|
|
@@ -92,7 +63,7 @@ export const GET = async (req: NextRequest) => {
|
|
|
92
63
|
log('Cleaned up %d expired handoff records', cleanedCount);
|
|
93
64
|
});
|
|
94
65
|
|
|
95
|
-
return NextResponse.redirect(
|
|
66
|
+
return NextResponse.redirect(successUrl);
|
|
96
67
|
} catch (error) {
|
|
97
68
|
log('Error in OIDC callback: %O', error);
|
|
98
69
|
|
|
@@ -6,7 +6,7 @@ import { getXorPayload } from '@lobechat/utils/server';
|
|
|
6
6
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
7
7
|
|
|
8
8
|
import { checkAuthMethod } from '@/app/(backend)/middleware/auth/utils';
|
|
9
|
-
import { LOBE_CHAT_AUTH_HEADER, OAUTH_AUTHORIZED } from '@/
|
|
9
|
+
import { LOBE_CHAT_AUTH_HEADER, OAUTH_AUTHORIZED } from '@/envs/auth';
|
|
10
10
|
import { initModelRuntimeFromDB } from '@/server/modules/ModelRuntime';
|
|
11
11
|
|
|
12
12
|
import { POST } from './route';
|
|
@@ -32,7 +32,7 @@ vi.mock('@/server/modules/ModelRuntime', () => ({
|
|
|
32
32
|
const mockState = vi.hoisted(() => ({ enableClerk: false }));
|
|
33
33
|
|
|
34
34
|
// 模拟 @/const/auth 模块
|
|
35
|
-
vi.mock('@/
|
|
35
|
+
vi.mock('@/envs/auth', async (importOriginal) => {
|
|
36
36
|
const modules = await importOriginal();
|
|
37
37
|
return {
|
|
38
38
|
...(modules as any),
|
|
@@ -4,7 +4,7 @@ import { ChatErrorType } from '@lobechat/types';
|
|
|
4
4
|
import { getXorPayload } from '@lobechat/utils/server';
|
|
5
5
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
6
|
|
|
7
|
-
import { LOBE_CHAT_AUTH_HEADER } from '@/
|
|
7
|
+
import { LOBE_CHAT_AUTH_HEADER } from '@/envs/auth';
|
|
8
8
|
import { initModelRuntimeFromDB } from '@/server/modules/ModelRuntime';
|
|
9
9
|
|
|
10
10
|
import { GET } from './route';
|
|
@@ -3,9 +3,9 @@ import { ChatErrorType, TraceNameMap } from '@lobechat/types';
|
|
|
3
3
|
import type { PluginRequestPayload } from '@lobehub/chat-plugin-sdk';
|
|
4
4
|
import { createGatewayOnEdgeRuntime } from '@lobehub/chat-plugins-gateway';
|
|
5
5
|
|
|
6
|
-
import { LOBE_CHAT_AUTH_HEADER } from '@/const/auth';
|
|
7
6
|
import { LOBE_CHAT_TRACE_ID } from '@/const/trace';
|
|
8
7
|
import { getAppConfig } from '@/envs/app';
|
|
8
|
+
import { LOBE_CHAT_AUTH_HEADER } from '@/envs/auth';
|
|
9
9
|
import { TraceClient } from '@/libs/traces';
|
|
10
10
|
import { parserPluginSettings } from '@/server/services/pluginGateway/settings';
|
|
11
11
|
import { getTracePayload } from '@/utils/trace';
|
|
@@ -2,7 +2,7 @@ import { SignIn } from '@clerk/nextjs';
|
|
|
2
2
|
import { BRANDING_NAME } from '@lobechat/business-const';
|
|
3
3
|
import { notFound } from 'next/navigation';
|
|
4
4
|
|
|
5
|
-
import { enableClerk } from '@/
|
|
5
|
+
import { enableClerk } from '@/envs/auth';
|
|
6
6
|
import { metadataModule } from '@/server/metadata';
|
|
7
7
|
import { translation } from '@/server/translation';
|
|
8
8
|
import { type DynamicLayoutProps } from '@/types/next';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { notFound } from 'next/navigation';
|
|
2
2
|
import { type PropsWithChildren } from 'react';
|
|
3
3
|
|
|
4
|
-
import { enableBetterAuth } from '@/
|
|
4
|
+
import { enableBetterAuth } from '@/envs/auth';
|
|
5
5
|
|
|
6
6
|
const Layout = ({ children }: PropsWithChildren) => {
|
|
7
7
|
if (!enableBetterAuth) return notFound();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { notFound } from 'next/navigation';
|
|
2
2
|
import { type PropsWithChildren } from 'react';
|
|
3
3
|
|
|
4
|
-
import { enableBetterAuth } from '@/
|
|
4
|
+
import { enableBetterAuth } from '@/envs/auth';
|
|
5
5
|
|
|
6
6
|
const Layout = ({ children }: PropsWithChildren) => {
|
|
7
7
|
if (!enableBetterAuth) return notFound();
|
|
@@ -8,10 +8,10 @@ import type { CheckUserResponseData } from '@/app/(backend)/api/auth/check-user/
|
|
|
8
8
|
import type { ResolveUsernameResponseData } from '@/app/(backend)/api/auth/resolve-username/route';
|
|
9
9
|
import { useBusinessSignin } from '@/business/client/hooks/useBusinessSignin';
|
|
10
10
|
import { message } from '@/components/AntdStaticMethods';
|
|
11
|
-
import { getAuthConfig } from '@/envs/auth';
|
|
12
11
|
import { requestPasswordReset, signIn } from '@/libs/better-auth/auth-client';
|
|
13
12
|
import { isBuiltinProvider, normalizeProviderId } from '@/libs/better-auth/utils/client';
|
|
14
13
|
import { useServerConfigStore } from '@/store/serverConfig';
|
|
14
|
+
import { serverConfigSelectors } from '@/store/serverConfig/selectors';
|
|
15
15
|
|
|
16
16
|
import { EMAIL_REGEX, USERNAME_REGEX } from './SignInEmailStep';
|
|
17
17
|
|
|
@@ -31,7 +31,7 @@ export const useSignIn = () => {
|
|
|
31
31
|
const { t } = useTranslation('auth');
|
|
32
32
|
const router = useRouter();
|
|
33
33
|
const searchParams = useSearchParams();
|
|
34
|
-
const
|
|
34
|
+
const enableMagicLink = useServerConfigStore(serverConfigSelectors.enableMagicLink);
|
|
35
35
|
const [form] = Form.useForm<SignInFormValues>();
|
|
36
36
|
const [loading, setLoading] = useState(false);
|
|
37
37
|
const [socialLoading, setSocialLoading] = useState<string | null>(null);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { SignUp } from '@clerk/nextjs';
|
|
2
2
|
import { notFound } from 'next/navigation';
|
|
3
3
|
|
|
4
|
-
import { enableBetterAuth, enableClerk } from '@/
|
|
4
|
+
import { enableBetterAuth, enableClerk } from '@/envs/auth';
|
|
5
5
|
import { metadataModule } from '@/server/metadata';
|
|
6
6
|
import { translation } from '@/server/translation';
|
|
7
7
|
import { type DynamicLayoutProps } from '@/types/next';
|
|
@@ -2,24 +2,30 @@ import { ENABLE_BUSINESS_FEATURES } from '@lobechat/business-const';
|
|
|
2
2
|
import { form } from 'motion/react-m';
|
|
3
3
|
import { useRouter, useSearchParams } from 'next/navigation';
|
|
4
4
|
import { useState } from 'react';
|
|
5
|
+
import { useTranslation } from 'react-i18next';
|
|
5
6
|
|
|
6
7
|
import {
|
|
7
8
|
BusinessSignupFomData,
|
|
8
9
|
useBusinessSignup,
|
|
9
10
|
} from '@/business/client/hooks/useBusinessSignup';
|
|
10
11
|
import { message } from '@/components/AntdStaticMethods';
|
|
11
|
-
import { authEnv } from '@/envs/auth';
|
|
12
12
|
import { signUp } from '@/libs/better-auth/auth-client';
|
|
13
|
+
import { useServerConfigStore } from '@/store/serverConfig';
|
|
14
|
+
import { serverConfigSelectors } from '@/store/serverConfig/selectors';
|
|
13
15
|
|
|
14
16
|
import { BaseSignUpFormValues } from './types';
|
|
15
17
|
|
|
16
18
|
export type SignUpFormValues = BaseSignUpFormValues & BusinessSignupFomData;
|
|
17
19
|
|
|
18
20
|
export const useSignUp = () => {
|
|
21
|
+
const { t } = useTranslation('auth');
|
|
19
22
|
const router = useRouter();
|
|
20
23
|
const searchParams = useSearchParams();
|
|
21
24
|
const [loading, setLoading] = useState(false);
|
|
22
25
|
const { getFetchOptions, preSocialSignupCheck, businessElement } = useBusinessSignup(form);
|
|
26
|
+
const enableEmailVerification = useServerConfigStore(
|
|
27
|
+
serverConfigSelectors.enableEmailVerification,
|
|
28
|
+
);
|
|
23
29
|
|
|
24
30
|
const handleSignUp = async (values: SignUpFormValues) => {
|
|
25
31
|
setLoading(true);
|
|
@@ -46,20 +52,20 @@ export const useSignUp = () => {
|
|
|
46
52
|
(error as any)?.details?.cause?.code === '23505';
|
|
47
53
|
|
|
48
54
|
if (isEmailDuplicate) {
|
|
49
|
-
message.error('betterAuth.errors.emailExists');
|
|
55
|
+
message.error(t('betterAuth.errors.emailExists'));
|
|
50
56
|
return;
|
|
51
57
|
}
|
|
52
58
|
|
|
53
59
|
if (error.code === 'INVALID_EMAIL') {
|
|
54
|
-
message.error('betterAuth.errors.emailInvalid');
|
|
60
|
+
message.error(t('betterAuth.errors.emailInvalid'));
|
|
55
61
|
return;
|
|
56
62
|
}
|
|
57
63
|
|
|
58
|
-
message.error(error.message || 'betterAuth.signup.error');
|
|
64
|
+
message.error(error.message || t('betterAuth.signup.error'));
|
|
59
65
|
return;
|
|
60
66
|
}
|
|
61
67
|
|
|
62
|
-
if (
|
|
68
|
+
if (enableEmailVerification) {
|
|
63
69
|
router.push(
|
|
64
70
|
`/verify-email?email=${encodeURIComponent(values.email)}&callbackUrl=${encodeURIComponent(callbackUrl)}`,
|
|
65
71
|
);
|
|
@@ -67,7 +73,7 @@ export const useSignUp = () => {
|
|
|
67
73
|
router.push(callbackUrl);
|
|
68
74
|
}
|
|
69
75
|
} catch {
|
|
70
|
-
message.error('betterAuth.signup.error');
|
|
76
|
+
message.error(t('betterAuth.signup.error'));
|
|
71
77
|
} finally {
|
|
72
78
|
setLoading(false);
|
|
73
79
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { notFound } from 'next/navigation';
|
|
2
2
|
import { type PropsWithChildren } from 'react';
|
|
3
3
|
|
|
4
|
-
import { enableBetterAuth } from '@/
|
|
4
|
+
import { enableBetterAuth } from '@/envs/auth';
|
|
5
5
|
|
|
6
6
|
const Layout = ({ children }: PropsWithChildren) => {
|
|
7
7
|
if (!enableBetterAuth) return notFound();
|