@lobehub/chat 1.19.11 → 1.19.13
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/CHANGELOG.md +50 -0
- package/package.json +2 -1
- package/src/app/api/webhooks/casdoor/__tests__/route.test.ts +60 -0
- package/src/app/api/webhooks/casdoor/route.ts +40 -0
- package/src/app/api/webhooks/casdoor/validateRequest.ts +38 -0
- package/src/app/webapi/proxy/route.ts +19 -0
- package/src/config/auth.ts +6 -0
- package/src/libs/next-auth/sso-providers/casdoor.ts +49 -0
- package/src/libs/next-auth/sso-providers/index.ts +13 -1
- package/src/server/routers/lambda/user.ts +1 -1
- package/src/services/_url.ts +2 -2
- package/src/services/user/client.ts +1 -1
- package/src/app/api/proxy/route.ts +0 -34
package/CHANGELOG.md
CHANGED
@@ -2,6 +2,56 @@
|
|
2
2
|
|
3
3
|
# Changelog
|
4
4
|
|
5
|
+
### [Version 1.19.13](https://github.com/lobehub/lobe-chat/compare/v1.19.12...v1.19.13)
|
6
|
+
|
7
|
+
<sup>Released on **2024-09-20**</sup>
|
8
|
+
|
9
|
+
#### 🐛 Bug Fixes
|
10
|
+
|
11
|
+
- **misc**: Try to implement better ssrf-protect.
|
12
|
+
|
13
|
+
<br/>
|
14
|
+
|
15
|
+
<details>
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
17
|
+
|
18
|
+
#### What's fixed
|
19
|
+
|
20
|
+
- **misc**: Try to implement better ssrf-protect, closes [#4044](https://github.com/lobehub/lobe-chat/issues/4044) ([e960a23](https://github.com/lobehub/lobe-chat/commit/e960a23))
|
21
|
+
|
22
|
+
</details>
|
23
|
+
|
24
|
+
<div align="right">
|
25
|
+
|
26
|
+
[](#readme-top)
|
27
|
+
|
28
|
+
</div>
|
29
|
+
|
30
|
+
### [Version 1.19.12](https://github.com/lobehub/lobe-chat/compare/v1.19.11...v1.19.12)
|
31
|
+
|
32
|
+
<sup>Released on **2024-09-20**</sup>
|
33
|
+
|
34
|
+
#### 💄 Styles
|
35
|
+
|
36
|
+
- **misc**: Support webhooks for casdoor.
|
37
|
+
|
38
|
+
<br/>
|
39
|
+
|
40
|
+
<details>
|
41
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
42
|
+
|
43
|
+
#### Styles
|
44
|
+
|
45
|
+
- **misc**: Support webhooks for casdoor, closes [#3942](https://github.com/lobehub/lobe-chat/issues/3942) ([1f2f6a5](https://github.com/lobehub/lobe-chat/commit/1f2f6a5))
|
46
|
+
|
47
|
+
</details>
|
48
|
+
|
49
|
+
<div align="right">
|
50
|
+
|
51
|
+
[](#readme-top)
|
52
|
+
|
53
|
+
</div>
|
54
|
+
|
5
55
|
### [Version 1.19.11](https://github.com/lobehub/lobe-chat/compare/v1.19.10...v1.19.11)
|
6
56
|
|
7
57
|
<sup>Released on **2024-09-20**</sup>
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@lobehub/chat",
|
3
|
-
"version": "1.19.
|
3
|
+
"version": "1.19.13",
|
4
4
|
"description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
5
5
|
"keywords": [
|
6
6
|
"framework",
|
@@ -203,6 +203,7 @@
|
|
203
203
|
"remark": "^14.0.3",
|
204
204
|
"remark-gfm": "^3.0.1",
|
205
205
|
"remark-html": "^15.0.2",
|
206
|
+
"request-filtering-agent": "^2.0.1",
|
206
207
|
"resolve-accept-language": "^3.1.5",
|
207
208
|
"rtl-detect": "^1.1.2",
|
208
209
|
"semver": "^7.6.3",
|
@@ -0,0 +1,60 @@
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
2
|
+
|
3
|
+
interface User {
|
4
|
+
name: string;
|
5
|
+
id: string;
|
6
|
+
type: 'normal-user' | 'admin' | 'super-admin';
|
7
|
+
displayName: string;
|
8
|
+
firstName: string;
|
9
|
+
lastName: string;
|
10
|
+
avatar: string;
|
11
|
+
email: string;
|
12
|
+
emailVerified: boolean;
|
13
|
+
}
|
14
|
+
|
15
|
+
interface UserDataUpdatedEvent {
|
16
|
+
user: string; // 用户名
|
17
|
+
action: 'update-user';
|
18
|
+
extendedUser: User; // 扩展用户信息
|
19
|
+
}
|
20
|
+
|
21
|
+
const userDataUpdatedEvent: UserDataUpdatedEvent = {
|
22
|
+
user: 'admin',
|
23
|
+
action: 'update-user',
|
24
|
+
extendedUser: {
|
25
|
+
name: 'admin',
|
26
|
+
id: '35edace3-00c6-41e1-895e-97c519b1d8cc',
|
27
|
+
type: 'normal-user',
|
28
|
+
displayName: 'Admin',
|
29
|
+
firstName: '',
|
30
|
+
lastName: '',
|
31
|
+
avatar: 'https://cdn.casbin.org/img/casbin.svg',
|
32
|
+
email: 'admin@example.cn',
|
33
|
+
emailVerified: false,
|
34
|
+
},
|
35
|
+
};
|
36
|
+
|
37
|
+
const AUTH_CASDOOR_WEBHOOK_SECRET = 'casdoor-secret';
|
38
|
+
|
39
|
+
// Test Casdoor Webhooks in Local dev, here is some tips:
|
40
|
+
// - Replace the var `AUTH_CASDOOR_WETHOOK_SECRET` with the actual value in your `.env` file
|
41
|
+
// - Start web request: If you want to run the test, replace `describe.skip` with `describe` below
|
42
|
+
// - Run this test with command:
|
43
|
+
// pnpm vitest --run --testNamePattern='^ ?Test Casdoor Webhooks in Local dev' src/app/api/webhooks/casdoor/__tests__/route.test.ts
|
44
|
+
|
45
|
+
describe.skip('Test Casdoor Webhooks in Local dev', () => {
|
46
|
+
// describe('Test Casdoor Webhooks in Local dev', () => {
|
47
|
+
it('should send a POST request with casdoor headers', async () => {
|
48
|
+
const url = 'http://localhost:3010/api/webhooks/casdoor'; // 替换为目标URL
|
49
|
+
const data = userDataUpdatedEvent;
|
50
|
+
const response = await fetch(url, {
|
51
|
+
method: 'POST',
|
52
|
+
headers: {
|
53
|
+
'Content-Type': 'application/json',
|
54
|
+
'casdoor-secret': AUTH_CASDOOR_WEBHOOK_SECRET,
|
55
|
+
},
|
56
|
+
body: JSON.stringify(data),
|
57
|
+
});
|
58
|
+
expect(response.status).toBe(200); // 检查响应状态
|
59
|
+
});
|
60
|
+
});
|
@@ -0,0 +1,40 @@
|
|
1
|
+
import { NextResponse } from 'next/server';
|
2
|
+
|
3
|
+
import { authEnv } from '@/config/auth';
|
4
|
+
import { pino } from '@/libs/logger';
|
5
|
+
import { NextAuthUserService } from '@/server/services/nextAuthUser';
|
6
|
+
|
7
|
+
import { validateRequest } from './validateRequest';
|
8
|
+
|
9
|
+
export const POST = async (req: Request): Promise<NextResponse> => {
|
10
|
+
const payload = await validateRequest(req, authEnv.CASDOOR_WEBHOOK_SECRET);
|
11
|
+
|
12
|
+
if (!payload) {
|
13
|
+
return NextResponse.json(
|
14
|
+
{ error: 'webhook verification failed or payload was malformed' },
|
15
|
+
{ status: 400 },
|
16
|
+
);
|
17
|
+
}
|
18
|
+
|
19
|
+
const { action, extendedUser } = payload;
|
20
|
+
|
21
|
+
pino.trace(`casdoor webhook payload: ${{ action, extendedUser }}`);
|
22
|
+
|
23
|
+
const nextAuthUserService = new NextAuthUserService();
|
24
|
+
switch (action) {
|
25
|
+
case 'update-user': {
|
26
|
+
return nextAuthUserService.safeUpdateUser(extendedUser.id, {
|
27
|
+
avatar: extendedUser?.avatar,
|
28
|
+
email: extendedUser?.email,
|
29
|
+
fullName: extendedUser.displayName,
|
30
|
+
});
|
31
|
+
}
|
32
|
+
|
33
|
+
default: {
|
34
|
+
pino.warn(
|
35
|
+
`${req.url} received event type "${action}", but no handler is defined for this type`,
|
36
|
+
);
|
37
|
+
return NextResponse.json({ error: `unrecognised payload type: ${action}` }, { status: 400 });
|
38
|
+
}
|
39
|
+
}
|
40
|
+
};
|
@@ -0,0 +1,38 @@
|
|
1
|
+
import { headers } from 'next/headers';
|
2
|
+
|
3
|
+
import { authEnv } from '@/config/auth';
|
4
|
+
|
5
|
+
export type CasdoorUserEntity = {
|
6
|
+
avatar?: string;
|
7
|
+
displayName: string;
|
8
|
+
email?: string;
|
9
|
+
id: string;
|
10
|
+
};
|
11
|
+
|
12
|
+
interface CasdoorWebhookPayload {
|
13
|
+
action: string;
|
14
|
+
// Only support user event currently
|
15
|
+
extendedUser: CasdoorUserEntity;
|
16
|
+
}
|
17
|
+
|
18
|
+
export const validateRequest = async (request: Request, secret?: string) => {
|
19
|
+
const payloadString = await request.text();
|
20
|
+
const headerPayload = headers();
|
21
|
+
const casdoorSecret = headerPayload.get('casdoor-secret')!;
|
22
|
+
try {
|
23
|
+
if (casdoorSecret === secret) {
|
24
|
+
return JSON.parse(payloadString) as CasdoorWebhookPayload;
|
25
|
+
} else {
|
26
|
+
console.warn(
|
27
|
+
'[Casdoor]: secret verify failed, please check your secret in `CASDOOR_WEBHOOK_SECRET`',
|
28
|
+
);
|
29
|
+
return;
|
30
|
+
}
|
31
|
+
} catch (e) {
|
32
|
+
if (!authEnv.CASDOOR_WEBHOOK_SECRET) {
|
33
|
+
throw new Error('`CASDOOR_WEBHOOK_SECRET` environment variable is missing.');
|
34
|
+
}
|
35
|
+
console.error('[Casdoor]: incoming webhook failed in verification.\n', e);
|
36
|
+
return;
|
37
|
+
}
|
38
|
+
};
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import { NextResponse } from 'next/server';
|
2
|
+
import fetch from 'node-fetch';
|
3
|
+
import { useAgent as ssrfAgent } from 'request-filtering-agent';
|
4
|
+
|
5
|
+
/**
|
6
|
+
* just for a proxy
|
7
|
+
*/
|
8
|
+
export const POST = async (req: Request) => {
|
9
|
+
const url = await req.text();
|
10
|
+
|
11
|
+
try {
|
12
|
+
const res = await fetch(url, { agent: ssrfAgent(url) });
|
13
|
+
|
14
|
+
return new Response(await res.arrayBuffer(), { headers: { ...res.headers } });
|
15
|
+
} catch (err) {
|
16
|
+
console.error(err); // DNS lookup 127.0.0.1(family:4, host:127.0.0.1.nip.io) is not allowed. Because, It is private IP address.
|
17
|
+
return NextResponse.json({ error: 'Not support internal host proxy' }, { status: 400 });
|
18
|
+
}
|
19
|
+
};
|
package/src/config/auth.ts
CHANGED
@@ -201,6 +201,9 @@ export const getAuthConfig = () => {
|
|
201
201
|
LOGTO_CLIENT_SECRET: z.string().optional(),
|
202
202
|
LOGTO_ISSUER: z.string().optional(),
|
203
203
|
LOGTO_WEBHOOK_SIGNING_KEY: z.string().optional(),
|
204
|
+
|
205
|
+
// Casdoor
|
206
|
+
CASDOOR_WEBHOOK_SECRET: z.string().optional(),
|
204
207
|
},
|
205
208
|
|
206
209
|
runtimeEnv: {
|
@@ -259,6 +262,9 @@ export const getAuthConfig = () => {
|
|
259
262
|
LOGTO_CLIENT_SECRET: process.env.LOGTO_CLIENT_SECRET,
|
260
263
|
LOGTO_ISSUER: process.env.LOGTO_ISSUER,
|
261
264
|
LOGTO_WEBHOOK_SIGNING_KEY: process.env.LOGTO_WEBHOOK_SIGNING_KEY,
|
265
|
+
|
266
|
+
// Casdoor
|
267
|
+
CASDOOR_WEBHOOK_SECRET: process.env.CASDOOR_WEBHOOK_SECRET,
|
262
268
|
},
|
263
269
|
});
|
264
270
|
};
|
@@ -0,0 +1,49 @@
|
|
1
|
+
import { OIDCConfig, OIDCUserConfig } from '@auth/core/providers';
|
2
|
+
|
3
|
+
import { CommonProviderConfig } from './sso.config';
|
4
|
+
|
5
|
+
interface CasdoorProfile extends Record<string, any> {
|
6
|
+
avatar: string;
|
7
|
+
displayName: string;
|
8
|
+
email: string;
|
9
|
+
emailVerified: boolean;
|
10
|
+
firstName: string;
|
11
|
+
id: string;
|
12
|
+
lastName: string;
|
13
|
+
name: string;
|
14
|
+
owner: string;
|
15
|
+
permanentAvatar: string;
|
16
|
+
}
|
17
|
+
|
18
|
+
function LobeCasdoorProvider(config: OIDCUserConfig<CasdoorProfile>): OIDCConfig<CasdoorProfile> {
|
19
|
+
return {
|
20
|
+
...CommonProviderConfig,
|
21
|
+
...config,
|
22
|
+
id: 'casdoor',
|
23
|
+
name: 'Casdoor',
|
24
|
+
profile(profile) {
|
25
|
+
return {
|
26
|
+
email: profile.email,
|
27
|
+
emailVerified: profile.emailVerified ? new Date() : null,
|
28
|
+
image: profile.avatar,
|
29
|
+
name: profile.displayName ?? profile.firstName ?? profile.lastName,
|
30
|
+
providerAccountId: profile.id,
|
31
|
+
};
|
32
|
+
},
|
33
|
+
type: 'oidc',
|
34
|
+
};
|
35
|
+
}
|
36
|
+
|
37
|
+
const provider = {
|
38
|
+
id: 'casdoor',
|
39
|
+
provider: LobeCasdoorProvider({
|
40
|
+
authorization: {
|
41
|
+
params: { scope: 'openid profile email' },
|
42
|
+
},
|
43
|
+
clientId: process.env.AUTH_CASDOOR_ID,
|
44
|
+
clientSecret: process.env.AUTH_CASDOOR_SECRET,
|
45
|
+
issuer: process.env.AUTH_CASDOOR_ISSUER,
|
46
|
+
}),
|
47
|
+
};
|
48
|
+
|
49
|
+
export default provider;
|
@@ -2,10 +2,22 @@ import Auth0 from './auth0';
|
|
2
2
|
import Authelia from './authelia';
|
3
3
|
import Authentik from './authentik';
|
4
4
|
import AzureAD from './azure-ad';
|
5
|
+
import Casdoor from './casdoor';
|
5
6
|
import CloudflareZeroTrust from './cloudflare-zero-trust';
|
6
7
|
import GenericOIDC from './generic-oidc';
|
7
8
|
import Github from './github';
|
8
9
|
import Logto from './logto';
|
9
10
|
import Zitadel from './zitadel';
|
10
11
|
|
11
|
-
export const ssoProviders = [
|
12
|
+
export const ssoProviders = [
|
13
|
+
Auth0,
|
14
|
+
Authentik,
|
15
|
+
AzureAD,
|
16
|
+
GenericOIDC,
|
17
|
+
Github,
|
18
|
+
Zitadel,
|
19
|
+
Authelia,
|
20
|
+
Logto,
|
21
|
+
CloudflareZeroTrust,
|
22
|
+
Casdoor,
|
23
|
+
];
|
@@ -63,7 +63,7 @@ export const userRouter = router({
|
|
63
63
|
const sessionCount = await sessionModel.count();
|
64
64
|
|
65
65
|
return {
|
66
|
-
canEnablePWAGuide: messageCount >=
|
66
|
+
canEnablePWAGuide: messageCount >= 4,
|
67
67
|
canEnableTrace: messageCount >= 4,
|
68
68
|
// 有消息,或者创建过助手,则认为有 conversation
|
69
69
|
hasConversation: messageCount > 0 || sessionCount > 1,
|
package/src/services/_url.ts
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
// TODO:
|
1
|
+
// TODO: 未来所有核心路由需要迁移到 trpc,部分不需要迁移的则走 webapi
|
2
2
|
|
3
3
|
/* eslint-disable sort-keys-fix/sort-keys-fix */
|
4
4
|
import { transform } from 'lodash-es';
|
@@ -17,7 +17,7 @@ const mapWithBasePath = <T extends object>(apis: T): T => {
|
|
17
17
|
};
|
18
18
|
|
19
19
|
export const API_ENDPOINTS = mapWithBasePath({
|
20
|
-
proxy: '/
|
20
|
+
proxy: '/webapi/proxy',
|
21
21
|
oauth: '/api/auth',
|
22
22
|
|
23
23
|
// agent markets
|
@@ -23,7 +23,7 @@ export class ClientService implements IUserService {
|
|
23
23
|
|
24
24
|
return {
|
25
25
|
avatar: user.avatar,
|
26
|
-
canEnablePWAGuide: messageCount >=
|
26
|
+
canEnablePWAGuide: messageCount >= 4,
|
27
27
|
canEnableTrace: messageCount >= 4,
|
28
28
|
hasConversation: messageCount > 0 || sessionCount > 0,
|
29
29
|
isOnboard: true,
|
@@ -1,34 +0,0 @@
|
|
1
|
-
import { isPrivate } from 'ip';
|
2
|
-
import { NextResponse } from 'next/server';
|
3
|
-
import dns from 'node:dns';
|
4
|
-
import { promisify } from 'node:util';
|
5
|
-
|
6
|
-
const lookupAsync = promisify(dns.lookup);
|
7
|
-
|
8
|
-
export const runtime = 'nodejs';
|
9
|
-
|
10
|
-
/**
|
11
|
-
* just for a proxy
|
12
|
-
*/
|
13
|
-
export const POST = async (req: Request) => {
|
14
|
-
const url = new URL(await req.text());
|
15
|
-
let address;
|
16
|
-
|
17
|
-
try {
|
18
|
-
const lookupResult = await lookupAsync(url.hostname);
|
19
|
-
address = lookupResult.address;
|
20
|
-
} catch (err) {
|
21
|
-
console.error(`${url.hostname} DNS parser error:`, err);
|
22
|
-
|
23
|
-
return NextResponse.json({ error: 'DNS parser error' }, { status: 504 });
|
24
|
-
}
|
25
|
-
|
26
|
-
const isInternalHost = isPrivate(address);
|
27
|
-
|
28
|
-
if (isInternalHost)
|
29
|
-
return NextResponse.json({ error: 'Not support internal host proxy' }, { status: 400 });
|
30
|
-
|
31
|
-
const res = await fetch(url.toString());
|
32
|
-
|
33
|
-
return new Response(res.body, { headers: res.headers });
|
34
|
-
};
|