@lobehub/chat 1.79.6 → 1.79.8
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/changelog/v1.json +18 -0
- package/docs/development/database-schema.dbml +119 -0
- package/docs/self-hosting/advanced/online-search.mdx +63 -0
- package/locales/ar/models.json +12 -0
- package/locales/ar/oauth.json +39 -0
- package/locales/bg-BG/models.json +12 -0
- package/locales/bg-BG/oauth.json +39 -0
- package/locales/de-DE/models.json +12 -0
- package/locales/de-DE/oauth.json +39 -0
- package/locales/en-US/models.json +12 -0
- package/locales/en-US/oauth.json +39 -0
- package/locales/es-ES/models.json +12 -0
- package/locales/es-ES/oauth.json +39 -0
- package/locales/fa-IR/models.json +12 -0
- package/locales/fa-IR/oauth.json +39 -0
- package/locales/fr-FR/models.json +12 -0
- package/locales/fr-FR/oauth.json +39 -0
- package/locales/it-IT/models.json +12 -0
- package/locales/it-IT/oauth.json +39 -0
- package/locales/ja-JP/models.json +12 -0
- package/locales/ja-JP/oauth.json +39 -0
- package/locales/ko-KR/models.json +12 -0
- package/locales/ko-KR/oauth.json +39 -0
- package/locales/nl-NL/models.json +12 -0
- package/locales/nl-NL/oauth.json +39 -0
- package/locales/pl-PL/models.json +12 -0
- package/locales/pl-PL/oauth.json +39 -0
- package/locales/pt-BR/models.json +12 -0
- package/locales/pt-BR/oauth.json +39 -0
- package/locales/ru-RU/models.json +12 -0
- package/locales/ru-RU/oauth.json +39 -0
- package/locales/tr-TR/models.json +12 -0
- package/locales/tr-TR/oauth.json +39 -0
- package/locales/vi-VN/models.json +12 -0
- package/locales/vi-VN/oauth.json +39 -0
- package/locales/zh-CN/models.json +12 -0
- package/locales/zh-CN/oauth.json +39 -0
- package/locales/zh-TW/models.json +12 -0
- package/locales/zh-TW/oauth.json +39 -0
- package/package.json +5 -2
- package/scripts/generate-oidc-jwk.mjs +59 -0
- package/scripts/migrateServerDB/index.ts +3 -1
- package/src/app/(backend)/oidc/[...oidc]/route.ts +270 -0
- package/src/app/(backend)/oidc/consent/route.ts +97 -0
- package/src/app/[variants]/oauth/consent/[uid]/Client.tsx +97 -0
- package/src/app/[variants]/oauth/consent/[uid]/failed/page.tsx +36 -0
- package/src/app/[variants]/oauth/consent/[uid]/page.tsx +71 -0
- package/src/app/[variants]/oauth/consent/[uid]/success/page.tsx +30 -0
- package/src/const/hotkeys.ts +2 -2
- package/src/const/trace.ts +1 -0
- package/src/database/client/migrations.json +27 -8
- package/src/database/migrations/0020_add_oidc.sql +124 -0
- package/src/database/migrations/meta/0020_snapshot.json +4975 -0
- package/src/database/migrations/meta/_journal.json +7 -0
- package/src/database/repositories/tableViewer/index.test.ts +1 -1
- package/src/database/schemas/index.ts +1 -0
- package/src/database/schemas/oidc.ts +158 -0
- package/src/database/server/models/__tests__/adapter.test.ts +503 -0
- package/src/envs/oidc.ts +18 -0
- package/src/libs/agent-runtime/azureOpenai/index.ts +4 -1
- package/src/libs/agent-runtime/utils/streams/protocol.ts +2 -4
- package/src/libs/oidc-provider/adapter.ts +494 -0
- package/src/libs/oidc-provider/config.ts +53 -0
- package/src/libs/oidc-provider/http-adapter.ts +279 -0
- package/src/libs/oidc-provider/interaction-policy.ts +37 -0
- package/src/libs/oidc-provider/provider.ts +260 -0
- package/src/locales/default/index.ts +2 -0
- package/src/locales/default/oauth.ts +41 -0
- package/src/middleware.ts +94 -6
- package/src/server/services/oidc/index.ts +29 -0
- package/src/server/services/oidc/oidcProvider.ts +27 -0
- package/src/store/chat/slices/aiChat/actions/memory.ts +6 -1
- package/src/types/hotkey.ts +54 -3
@@ -0,0 +1,39 @@
|
|
1
|
+
{
|
2
|
+
"consent": {
|
3
|
+
"buttons": {
|
4
|
+
"accept": "授权",
|
5
|
+
"deny": "拒绝"
|
6
|
+
},
|
7
|
+
"description": "应用 {clientId} 请求访问您的 LobeChat 账户",
|
8
|
+
"error": {
|
9
|
+
"sessionInvalid": {
|
10
|
+
"message": "授权会话已过期或无效,请重新发起授权流程。",
|
11
|
+
"title": "授权会话无效"
|
12
|
+
},
|
13
|
+
"title": "发生错误",
|
14
|
+
"unsupportedInteraction": {
|
15
|
+
"message": "不支持的交互类型: {promptName}",
|
16
|
+
"title": "不支持的交互类型"
|
17
|
+
}
|
18
|
+
},
|
19
|
+
"permissionsTitle": "应用请求以下权限:",
|
20
|
+
"scope": {
|
21
|
+
"email": "访问您的电子邮件地址",
|
22
|
+
"offline_access": "在您离线时继续访问您的数据",
|
23
|
+
"openid": "使用您的 LobeChat 账户进行身份验证",
|
24
|
+
"profile": "访问您的基本资料信息(名称、头像等)",
|
25
|
+
"sync:read": "读取您的同步数据",
|
26
|
+
"sync:write": "写入并更新您的同步数据"
|
27
|
+
},
|
28
|
+
"title": "授权请求"
|
29
|
+
},
|
30
|
+
"failed": {
|
31
|
+
"backToHome": "返回首页",
|
32
|
+
"subTitle": "您已拒绝授权应用访问您的 LobeChat 账户",
|
33
|
+
"title": "授权被拒绝"
|
34
|
+
},
|
35
|
+
"success": {
|
36
|
+
"subTitle": "您已成功授权应用访问您的 LobeChat 账户,可以关闭该页面了",
|
37
|
+
"title": "授权成功"
|
38
|
+
}
|
39
|
+
}
|
@@ -1109,6 +1109,18 @@
|
|
1109
1109
|
"grok-2-vision-1212": {
|
1110
1110
|
"description": "該模型在準確性、指令遵循和多語言能力方面有所改進。"
|
1111
1111
|
},
|
1112
|
+
"grok-3-beta": {
|
1113
|
+
"description": "旗艦級模型,擅長數據提取、程式設計和文本摘要等企業級應用,擁有金融、醫療、法律和科學等領域的深厚知識。"
|
1114
|
+
},
|
1115
|
+
"grok-3-fast-beta": {
|
1116
|
+
"description": "旗艦級模型,擅長數據提取、程式設計和文本摘要等企業級應用,擁有金融、醫療、法律和科學等領域的深厚知識。"
|
1117
|
+
},
|
1118
|
+
"grok-3-mini-beta": {
|
1119
|
+
"description": "輕量級模型,會在對話前先思考。運行快速、智能,適用於不需要深層領域知識的邏輯任務,並能獲取原始的思維軌跡。"
|
1120
|
+
},
|
1121
|
+
"grok-3-mini-fast-beta": {
|
1122
|
+
"description": "輕量級模型,會在對話前先思考。運行快速、智能,適用於不需要深層領域知識的邏輯任務,並能獲取原始的思維軌跡。"
|
1123
|
+
},
|
1112
1124
|
"grok-beta": {
|
1113
1125
|
"description": "擁有與 Grok 2 相當的性能,但具備更高的效率、速度和功能。"
|
1114
1126
|
},
|
@@ -0,0 +1,39 @@
|
|
1
|
+
{
|
2
|
+
"consent": {
|
3
|
+
"buttons": {
|
4
|
+
"accept": "授權",
|
5
|
+
"deny": "拒絕"
|
6
|
+
},
|
7
|
+
"description": "應用 {clientId} 請求訪問您的 LobeChat 帳戶",
|
8
|
+
"error": {
|
9
|
+
"sessionInvalid": {
|
10
|
+
"message": "授權會話已過期或無效,請重新發起授權流程。",
|
11
|
+
"title": "授權會話無效"
|
12
|
+
},
|
13
|
+
"title": "發生錯誤",
|
14
|
+
"unsupportedInteraction": {
|
15
|
+
"message": "不支援的互動類型: {promptName}",
|
16
|
+
"title": "不支援的互動類型"
|
17
|
+
}
|
18
|
+
},
|
19
|
+
"permissionsTitle": "應用請求以下權限:",
|
20
|
+
"scope": {
|
21
|
+
"email": "訪問您的電子郵件地址",
|
22
|
+
"offline_access": "在您離線時繼續訪問您的數據",
|
23
|
+
"openid": "使用您的 LobeChat 帳戶進行身份驗證",
|
24
|
+
"profile": "訪問您的基本資料信息(名稱、頭像等)",
|
25
|
+
"sync:read": "讀取您的同步數據",
|
26
|
+
"sync:write": "寫入並更新您的同步數據"
|
27
|
+
},
|
28
|
+
"title": "授權請求"
|
29
|
+
},
|
30
|
+
"failed": {
|
31
|
+
"backToHome": "返回首頁",
|
32
|
+
"subTitle": "您已拒絕授權應用訪問您的 LobeChat 帳戶",
|
33
|
+
"title": "授權被拒絕"
|
34
|
+
},
|
35
|
+
"success": {
|
36
|
+
"subTitle": "您已成功授權應用訪問您的 LobeChat 帳戶,可以關閉該頁面了",
|
37
|
+
"title": "授權成功"
|
38
|
+
}
|
39
|
+
}
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@lobehub/chat",
|
3
|
-
"version": "1.79.
|
3
|
+
"version": "1.79.8",
|
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",
|
@@ -33,7 +33,7 @@
|
|
33
33
|
"postbuild": "npm run build-sitemap && npm run build-migrate-db",
|
34
34
|
"build-migrate-db": "bun run db:migrate",
|
35
35
|
"build-sitemap": "tsx ./scripts/buildSitemapIndex/index.ts",
|
36
|
-
"build:analyze": "ANALYZE=true next build",
|
36
|
+
"build:analyze": "NODE_OPTIONS=--max-old-space-size=6144 ANALYZE=true next build",
|
37
37
|
"build:docker": "DOCKER=true next build && npm run build-sitemap",
|
38
38
|
"prebuild:electron": "cross-env NEXT_PUBLIC_IS_DESKTOP_APP=1 tsx scripts/prebuild.mts",
|
39
39
|
"build:electron": "cross-env NODE_OPTIONS=--max-old-space-size=6144 NEXT_PUBLIC_IS_DESKTOP_APP=1 NEXT_PUBLIC_SERVICE_MODE=server next build",
|
@@ -188,6 +188,7 @@
|
|
188
188
|
"jose": "^5.10.0",
|
189
189
|
"js-sha256": "^0.11.0",
|
190
190
|
"jsonl-parse-stringify": "^1.0.3",
|
191
|
+
"keyv": "^4.5.4",
|
191
192
|
"langchain": "^0.3.19",
|
192
193
|
"langfuse": "^3.37.1",
|
193
194
|
"langfuse-core": "^3.37.1",
|
@@ -204,6 +205,7 @@
|
|
204
205
|
"numeral": "^2.0.6",
|
205
206
|
"nuqs": "^2.4.1",
|
206
207
|
"officeparser": "^5.1.1",
|
208
|
+
"oidc-provider": "^8.4.0",
|
207
209
|
"ollama": "^0.5.14",
|
208
210
|
"openai": "^4.91.1",
|
209
211
|
"openapi-fetch": "^0.9.8",
|
@@ -286,6 +288,7 @@
|
|
286
288
|
"@types/lodash-es": "^4.17.12",
|
287
289
|
"@types/node": "^22.13.17",
|
288
290
|
"@types/numeral": "^2.0.5",
|
291
|
+
"@types/oidc-provider": "^8.8.1",
|
289
292
|
"@types/pg": "^8.11.11",
|
290
293
|
"@types/react": "^19.1.0",
|
291
294
|
"@types/react-dom": "^19.1.1",
|
@@ -0,0 +1,59 @@
|
|
1
|
+
#!/usr/bin/env node
|
2
|
+
/**
|
3
|
+
* OIDC JWKS 密钥生成脚本
|
4
|
+
* 用于生成 OIDC Provider 使用的 RSA 密钥对并转换为 JWKS 格式
|
5
|
+
*
|
6
|
+
* 使用方法:
|
7
|
+
* node scripts/generate-oidc-jwk.mjs
|
8
|
+
*
|
9
|
+
* 将输出的单行 JSON 字符串设置为环境变量 OIDC_JWKS_KEY
|
10
|
+
*/
|
11
|
+
import crypto from 'node:crypto';
|
12
|
+
import { exportJWK, generateKeyPair } from 'jose';
|
13
|
+
|
14
|
+
// 生成密钥 ID
|
15
|
+
function generateKeyId() {
|
16
|
+
return crypto.randomBytes(8).toString('hex');
|
17
|
+
}
|
18
|
+
|
19
|
+
async function generateJwks() {
|
20
|
+
try {
|
21
|
+
console.error('正在生成 RSA 密钥对...');
|
22
|
+
|
23
|
+
// 生成 RS256 密钥对
|
24
|
+
const { privateKey } = await generateKeyPair('RS256');
|
25
|
+
|
26
|
+
// 导出为 JWK 格式
|
27
|
+
const jwk = await exportJWK(privateKey);
|
28
|
+
|
29
|
+
// 添加必要的字段
|
30
|
+
jwk.use = 'sig'; // 用途: 签名
|
31
|
+
jwk.kid = generateKeyId(); // 密钥 ID
|
32
|
+
jwk.alg = 'RS256'; // 算法
|
33
|
+
|
34
|
+
// 创建 JWKS (JSON Web Key Set)
|
35
|
+
const jwks = { keys: [jwk] };
|
36
|
+
|
37
|
+
// 转换为JSON字符串
|
38
|
+
const jwksString = JSON.stringify(jwks);
|
39
|
+
|
40
|
+
// 输出 JWKS JSON 单行字符串
|
41
|
+
console.log(jwksString);
|
42
|
+
|
43
|
+
// 控制台提示
|
44
|
+
console.error('\n✅ JWKS 已生成');
|
45
|
+
console.error('请将上面输出的 JSON 字符串直接设置为环境变量 OIDC_JWKS_KEY');
|
46
|
+
console.error('例如在 .env 文件中添加:');
|
47
|
+
console.error('\n> 环境变量配置行 (可直接复制):');
|
48
|
+
console.error(`OIDC_JWKS_KEY='${jwksString}'`);
|
49
|
+
console.error('\n⚠️ 重要: 请妥善保管此密钥,它用于签署所有 OIDC 令牌');
|
50
|
+
|
51
|
+
return jwks;
|
52
|
+
} catch (error) {
|
53
|
+
console.error('❌ 生成 JWKS 时出错:', error);
|
54
|
+
process.exit(1);
|
55
|
+
}
|
56
|
+
}
|
57
|
+
|
58
|
+
// 执行主函数
|
59
|
+
generateJwks();
|
@@ -3,7 +3,6 @@ import { migrate as neonMigrate } from 'drizzle-orm/neon-serverless/migrator';
|
|
3
3
|
import { migrate as nodeMigrate } from 'drizzle-orm/node-postgres/migrator';
|
4
4
|
import { join } from 'node:path';
|
5
5
|
|
6
|
-
import { serverDB } from '../../src/database/server';
|
7
6
|
import { DB_FAIL_INIT_HINT, PGVECTOR_HINT } from './errorHint';
|
8
7
|
|
9
8
|
// Read the `.env` file if it exists, or a file specified by the
|
@@ -13,7 +12,10 @@ dotenv.config();
|
|
13
12
|
const migrationsFolder = join(__dirname, '../../src/database/migrations');
|
14
13
|
|
15
14
|
const isDesktop = process.env.NEXT_PUBLIC_IS_DESKTOP_APP === '1';
|
15
|
+
|
16
16
|
const runMigrations = async () => {
|
17
|
+
const { serverDB } = await import('../../src/database/server');
|
18
|
+
|
17
19
|
if (process.env.DATABASE_DRIVER === 'node') {
|
18
20
|
await nodeMigrate(serverDB, { migrationsFolder });
|
19
21
|
} else {
|
@@ -0,0 +1,270 @@
|
|
1
|
+
import debug from 'debug';
|
2
|
+
import { NextRequest, NextResponse } from 'next/server';
|
3
|
+
import { randomUUID } from 'node:crypto';
|
4
|
+
import { URL } from 'node:url';
|
5
|
+
|
6
|
+
import { getDBInstance } from '@/database/core/web-server';
|
7
|
+
import { oidcEnv } from '@/envs/oidc';
|
8
|
+
import { DrizzleAdapter } from '@/libs/oidc-provider/adapter';
|
9
|
+
import { createNodeRequest, createNodeResponse } from '@/libs/oidc-provider/http-adapter';
|
10
|
+
import { getOIDCProvider } from '@/server/services/oidc/oidcProvider';
|
11
|
+
|
12
|
+
const log = debug('lobe-oidc:route'); // Create a debug instance with a namespace
|
13
|
+
|
14
|
+
// 会话同步标头
|
15
|
+
const OIDC_SESSION_HEADER = 'x-oidc-session-sync';
|
16
|
+
|
17
|
+
/**
|
18
|
+
* 处理会话同步
|
19
|
+
* 如果请求中包含 x-oidc-session-sync 头,预先创建一个 OIDC 会话
|
20
|
+
*/
|
21
|
+
const syncOIDCSession = async (req: NextRequest): Promise<void> => {
|
22
|
+
const externalUserId = req.headers.get(OIDC_SESSION_HEADER);
|
23
|
+
|
24
|
+
if (!externalUserId) {
|
25
|
+
log('没有找到 x-oidc-session-sync 头,跳过会话同步');
|
26
|
+
return;
|
27
|
+
}
|
28
|
+
|
29
|
+
log('找到会话同步请求,外部用户 ID: %s', externalUserId);
|
30
|
+
|
31
|
+
try {
|
32
|
+
const db = getDBInstance();
|
33
|
+
const sessionAdapter = new DrizzleAdapter('Session', db);
|
34
|
+
|
35
|
+
// 查找是否已有对应的 OIDC 会话
|
36
|
+
// 使用新方法按用户 ID 查找会话
|
37
|
+
const existingSession = await sessionAdapter.findSessionByUserId(externalUserId);
|
38
|
+
|
39
|
+
if (existingSession) {
|
40
|
+
log(
|
41
|
+
'已找到与用户 %s 关联的现有 OIDC 会话,ID: %s',
|
42
|
+
externalUserId,
|
43
|
+
existingSession.uid || existingSession.jti,
|
44
|
+
);
|
45
|
+
return;
|
46
|
+
}
|
47
|
+
|
48
|
+
// 创建新的会话
|
49
|
+
const sessionId = randomUUID();
|
50
|
+
const now = Math.floor(Date.now() / 1000);
|
51
|
+
const expiresIn = 30 * 24 * 60 * 60; // 30 天,与 provider.ts 中的 TTL 配置一致
|
52
|
+
|
53
|
+
// 创建基本会话数据
|
54
|
+
const sessionData = {
|
55
|
+
accountId: externalUserId,
|
56
|
+
|
57
|
+
// 添加额外会话信息
|
58
|
+
authTime: now,
|
59
|
+
|
60
|
+
// 添加所有必要的 OIDC 会话字段
|
61
|
+
cookie: `oidc.session.${sessionId}`,
|
62
|
+
|
63
|
+
exp: now + expiresIn,
|
64
|
+
|
65
|
+
iat: now,
|
66
|
+
|
67
|
+
jti: sessionId,
|
68
|
+
|
69
|
+
// 为 findAccount 添加必要的字段
|
70
|
+
login: {
|
71
|
+
accountId: externalUserId,
|
72
|
+
remember: true,
|
73
|
+
},
|
74
|
+
|
75
|
+
uid: sessionId,
|
76
|
+
username: externalUserId,
|
77
|
+
};
|
78
|
+
|
79
|
+
// 使用适配器创建会话
|
80
|
+
await sessionAdapter.upsert(sessionId, sessionData, expiresIn);
|
81
|
+
log('成功为外部用户 %s 创建 OIDC 会话 %s', externalUserId, sessionId);
|
82
|
+
} catch (error) {
|
83
|
+
log('同步 OIDC 会话时出错: %O', error);
|
84
|
+
console.error('同步 OIDC 会话错误:', error);
|
85
|
+
// 不抛出错误,让请求继续处理
|
86
|
+
}
|
87
|
+
};
|
88
|
+
|
89
|
+
/**
|
90
|
+
* 处理 catch-all 路由下的所有 OIDC 请求
|
91
|
+
* 这个处理器会捕获所有 /oauth/[...oidc] 的请求
|
92
|
+
* 例如: /oauth/auth, /oauth/token, /oauth/userinfo 等
|
93
|
+
*/
|
94
|
+
export async function GET(req: NextRequest) {
|
95
|
+
const requestUrl = new URL(req.url);
|
96
|
+
log('Received GET request: %s %s', req.method, req.url);
|
97
|
+
log('Path: %s, Pathname: %s', requestUrl.pathname, requestUrl.pathname);
|
98
|
+
log('Headers: %O', Object.fromEntries(req.headers.entries())); // Log headers object
|
99
|
+
|
100
|
+
// 声明响应收集器
|
101
|
+
let responseCollector;
|
102
|
+
|
103
|
+
try {
|
104
|
+
if (!oidcEnv.ENABLE_OIDC) {
|
105
|
+
log('OIDC is not enabled');
|
106
|
+
return new NextResponse('OIDC is not enabled', { status: 404 });
|
107
|
+
}
|
108
|
+
|
109
|
+
// 在获取 OIDC 提供者实例前同步会话
|
110
|
+
await syncOIDCSession(req);
|
111
|
+
|
112
|
+
// 获取 OIDC Provider 实例
|
113
|
+
const provider = await getOIDCProvider();
|
114
|
+
|
115
|
+
log('Calling provider.callback() for GET');
|
116
|
+
await new Promise<void>((resolve, reject) => {
|
117
|
+
let middleware: any;
|
118
|
+
try {
|
119
|
+
log('Attempting to get middleware from provider.callback()');
|
120
|
+
middleware = provider.callback();
|
121
|
+
log('Successfully obtained middleware function.');
|
122
|
+
} catch (syncError) {
|
123
|
+
log('SYNC ERROR during provider.callback() call itself: %O', syncError);
|
124
|
+
reject(syncError);
|
125
|
+
return;
|
126
|
+
}
|
127
|
+
|
128
|
+
// 使用辅助方法创建响应收集器
|
129
|
+
responseCollector = createNodeResponse(resolve);
|
130
|
+
const nodeResponse = responseCollector.nodeResponse;
|
131
|
+
|
132
|
+
// 使用辅助方法创建 Node.js 请求对象
|
133
|
+
const nodeRequest = createNodeRequest(req);
|
134
|
+
|
135
|
+
log('Calling the obtained middleware...');
|
136
|
+
middleware(nodeRequest, nodeResponse, (error?: Error) => {
|
137
|
+
log('Middleware callback function HAS BEEN EXECUTED.');
|
138
|
+
if (error) {
|
139
|
+
log('Middleware error reported via callback: %O', error);
|
140
|
+
reject(error); // Reject if callback reports error
|
141
|
+
} else {
|
142
|
+
log(
|
143
|
+
'Middleware completed successfully via callback (may be redundant if .end() was called).',
|
144
|
+
);
|
145
|
+
// Ensure promise resolves even if end() wasn't called but callback was
|
146
|
+
resolve();
|
147
|
+
}
|
148
|
+
});
|
149
|
+
log('Middleware call initiated, waiting for its callback OR nodeResponse.end()...');
|
150
|
+
});
|
151
|
+
|
152
|
+
log('Promise surrounding middleware call resolved.');
|
153
|
+
|
154
|
+
if (!responseCollector) {
|
155
|
+
throw new Error('ResponseCollector was not initialized.');
|
156
|
+
}
|
157
|
+
|
158
|
+
const {
|
159
|
+
responseStatus: finalStatus,
|
160
|
+
responseBody: finalBody,
|
161
|
+
responseHeaders: finalHeaders,
|
162
|
+
} = responseCollector;
|
163
|
+
|
164
|
+
log('Final Response Status: %d', finalStatus);
|
165
|
+
log('Final Response Headers: %O', finalHeaders);
|
166
|
+
|
167
|
+
return new NextResponse(finalBody, {
|
168
|
+
// eslint-disable-next-line no-undef
|
169
|
+
headers: finalHeaders as HeadersInit,
|
170
|
+
status: finalStatus,
|
171
|
+
});
|
172
|
+
} catch (error) {
|
173
|
+
log('Error handling OIDC GET request: %O', error); // Log the full error object
|
174
|
+
// Ensure responseCollector is checked even in catch block if needed, though error likely occurred before/during promise
|
175
|
+
return new NextResponse(`Internal Server Error: ${(error as Error).message}`, { status: 500 });
|
176
|
+
}
|
177
|
+
}
|
178
|
+
|
179
|
+
/**
|
180
|
+
* 处理 POST 请求 (用于令牌端点等)
|
181
|
+
*/
|
182
|
+
export async function POST(req: NextRequest) {
|
183
|
+
log('Received POST request: %s %s', req.method, req.url);
|
184
|
+
const bodyText = await req.text(); // Read body first
|
185
|
+
log('Body: %s', bodyText); // Log body as string
|
186
|
+
|
187
|
+
// 声明响应收集器
|
188
|
+
let responseCollector;
|
189
|
+
|
190
|
+
try {
|
191
|
+
if (!oidcEnv.ENABLE_OIDC) {
|
192
|
+
log('OIDC is not enabled');
|
193
|
+
return new NextResponse('OIDC is not enabled', { status: 404 });
|
194
|
+
}
|
195
|
+
|
196
|
+
// 在获取 OIDC 提供者实例前同步会话
|
197
|
+
await syncOIDCSession(req);
|
198
|
+
|
199
|
+
// 获取 OIDC Provider 实例
|
200
|
+
const provider = await getOIDCProvider();
|
201
|
+
|
202
|
+
log('Calling provider.callback() for POST');
|
203
|
+
await new Promise<void>((resolve, reject) => {
|
204
|
+
let middleware: any;
|
205
|
+
try {
|
206
|
+
log('Attempting to get middleware from provider.callback()');
|
207
|
+
middleware = provider.callback();
|
208
|
+
log('Successfully obtained middleware function.');
|
209
|
+
} catch (syncError) {
|
210
|
+
log('SYNC ERROR during provider.callback() call itself: %O', syncError);
|
211
|
+
reject(syncError);
|
212
|
+
return;
|
213
|
+
}
|
214
|
+
|
215
|
+
// 使用辅助方法创建响应收集器
|
216
|
+
responseCollector = createNodeResponse(resolve);
|
217
|
+
const nodeResponse = responseCollector.nodeResponse;
|
218
|
+
|
219
|
+
// 使用辅助方法创建 Node.js 请求对象,包含 POST 请求体
|
220
|
+
const nodeRequest = createNodeRequest(req, '/oauth', bodyText);
|
221
|
+
|
222
|
+
log('Calling the obtained middleware...');
|
223
|
+
middleware(nodeRequest, nodeResponse, (error?: Error) => {
|
224
|
+
log('Middleware callback function HAS BEEN EXECUTED.');
|
225
|
+
if (error) {
|
226
|
+
log('Middleware error reported via callback: %O', error);
|
227
|
+
reject(error);
|
228
|
+
} else {
|
229
|
+
log(
|
230
|
+
'Middleware completed successfully via callback (may be redundant if .end() was called).',
|
231
|
+
);
|
232
|
+
resolve();
|
233
|
+
}
|
234
|
+
});
|
235
|
+
log('Middleware call initiated, waiting for its callback OR nodeResponse.end()...');
|
236
|
+
});
|
237
|
+
|
238
|
+
log('Promise surrounding middleware call resolved.');
|
239
|
+
|
240
|
+
// 访问最终的响应状态
|
241
|
+
if (!responseCollector) {
|
242
|
+
throw new Error('ResponseCollector was not initialized.');
|
243
|
+
}
|
244
|
+
|
245
|
+
const {
|
246
|
+
responseStatus: finalStatus,
|
247
|
+
responseBody: finalBody,
|
248
|
+
responseHeaders: finalHeaders,
|
249
|
+
} = responseCollector;
|
250
|
+
|
251
|
+
log('Final Response Status: %d', finalStatus);
|
252
|
+
log('Final Response Headers: %O', finalHeaders);
|
253
|
+
|
254
|
+
return new NextResponse(finalBody, {
|
255
|
+
// eslint-disable-next-line no-undef
|
256
|
+
headers: finalHeaders as HeadersInit,
|
257
|
+
status: finalStatus,
|
258
|
+
});
|
259
|
+
} catch (error) {
|
260
|
+
log('Error handling OIDC POST request: %O', error); // Log the full error object
|
261
|
+
return new NextResponse(`Internal Server Error: ${(error as Error).message}`, { status: 500 });
|
262
|
+
}
|
263
|
+
}
|
264
|
+
|
265
|
+
/**
|
266
|
+
* 同样处理其他 HTTP 方法
|
267
|
+
*/
|
268
|
+
export const PUT = POST;
|
269
|
+
export const DELETE = POST;
|
270
|
+
export const PATCH = POST;
|
@@ -0,0 +1,97 @@
|
|
1
|
+
import debug from 'debug';
|
2
|
+
import { NextRequest, NextResponse } from 'next/server';
|
3
|
+
import urlJoin from 'url-join';
|
4
|
+
|
5
|
+
import { appEnv } from '@/config/app';
|
6
|
+
import { OIDCService } from '@/server/services/oidc';
|
7
|
+
|
8
|
+
const log = debug('lobe-oidc:consent');
|
9
|
+
|
10
|
+
export async function POST(request: NextRequest) {
|
11
|
+
try {
|
12
|
+
const formData = await request.formData();
|
13
|
+
const consent = formData.get('consent') as string;
|
14
|
+
const uid = formData.get('uid') as string;
|
15
|
+
|
16
|
+
log('POST /oauth/consent - uid=%s, choice=%s', uid, consent);
|
17
|
+
|
18
|
+
const oidcService = await OIDCService.initialize();
|
19
|
+
|
20
|
+
let details;
|
21
|
+
try {
|
22
|
+
details = await oidcService.getInteractionDetails(uid);
|
23
|
+
log(
|
24
|
+
'Interaction details found - prompt=%s, client=%s',
|
25
|
+
details.prompt.name,
|
26
|
+
details.params.client_id,
|
27
|
+
);
|
28
|
+
} catch (error) {
|
29
|
+
log(
|
30
|
+
'Error: Interaction details not found - %s',
|
31
|
+
error instanceof Error ? error.message : 'unknown error',
|
32
|
+
);
|
33
|
+
if (error instanceof Error && error.message.includes('interaction session not found')) {
|
34
|
+
return NextResponse.json(
|
35
|
+
{
|
36
|
+
error: 'invalid_request',
|
37
|
+
error_description:
|
38
|
+
'Authorization session expired or invalid, please restart the authorization flow',
|
39
|
+
},
|
40
|
+
{ status: 400 },
|
41
|
+
);
|
42
|
+
}
|
43
|
+
throw error;
|
44
|
+
}
|
45
|
+
|
46
|
+
let result;
|
47
|
+
if (consent === 'accept') {
|
48
|
+
if (details.prompt.name === 'login') {
|
49
|
+
result = {
|
50
|
+
login: { accountId: details.session?.accountId, remember: true },
|
51
|
+
};
|
52
|
+
} else {
|
53
|
+
result = {
|
54
|
+
consent: {
|
55
|
+
rejectedClaims: [],
|
56
|
+
rejectedScopes: [],
|
57
|
+
},
|
58
|
+
};
|
59
|
+
}
|
60
|
+
log('User %s the authorization', consent);
|
61
|
+
} else {
|
62
|
+
result = {
|
63
|
+
error: 'access_denied',
|
64
|
+
error_description: 'User denied the authorization request',
|
65
|
+
};
|
66
|
+
log('User %s the authorization', consent);
|
67
|
+
}
|
68
|
+
|
69
|
+
// 获取OIDC提供商的默认重定向URL,但不会直接使用它
|
70
|
+
const redirectUrl = await oidcService.getInteractionResult(uid, result);
|
71
|
+
log('Default redirectUrl: %s', redirectUrl);
|
72
|
+
|
73
|
+
// 根据用户选择定制重定向地址
|
74
|
+
|
75
|
+
if (consent === 'accept') {
|
76
|
+
// 用户同意授权,跳转到success页面
|
77
|
+
const successUrl = urlJoin(appEnv.APP_URL!, `/oauth/consent/${uid}/success`);
|
78
|
+
log('Redirecting to success page: %s', successUrl);
|
79
|
+
return NextResponse.redirect(successUrl);
|
80
|
+
} else {
|
81
|
+
// 用户拒绝授权,跳转到failed页面
|
82
|
+
const failedUrl = urlJoin(appEnv.APP_URL!, `/oauth/consent/${uid}/failed`);
|
83
|
+
log('Redirecting to failed page: %s', failedUrl);
|
84
|
+
return NextResponse.redirect(failedUrl);
|
85
|
+
}
|
86
|
+
} catch (error) {
|
87
|
+
log('Error processing consent: %s', error instanceof Error ? error.message : 'unknown error');
|
88
|
+
console.error('Error processing consent:', error);
|
89
|
+
return NextResponse.json(
|
90
|
+
{
|
91
|
+
error: 'server_error',
|
92
|
+
error_description: 'Error processing consent',
|
93
|
+
},
|
94
|
+
{ status: 500 },
|
95
|
+
);
|
96
|
+
}
|
97
|
+
}
|
@@ -0,0 +1,97 @@
|
|
1
|
+
'use client';
|
2
|
+
|
3
|
+
import { Button, Card, Typography } from 'antd';
|
4
|
+
import { createStyles } from 'antd-style';
|
5
|
+
import React, { memo } from 'react';
|
6
|
+
import { useTranslation } from 'react-i18next';
|
7
|
+
import { Center, Flexbox } from 'react-layout-kit';
|
8
|
+
|
9
|
+
type ClientProps = {
|
10
|
+
clientId: string;
|
11
|
+
error?: {
|
12
|
+
message: string;
|
13
|
+
title: string;
|
14
|
+
};
|
15
|
+
scopes: string[];
|
16
|
+
uid: string;
|
17
|
+
};
|
18
|
+
|
19
|
+
const { Title, Text, Paragraph } = Typography;
|
20
|
+
|
21
|
+
const useStyles = createStyles(({ css, token }) => ({
|
22
|
+
error: css`
|
23
|
+
text-align: center;
|
24
|
+
`,
|
25
|
+
scope: css`
|
26
|
+
margin-block: 8px;
|
27
|
+
padding: 12px;
|
28
|
+
border-radius: 4px;
|
29
|
+
background: ${token.colorFillTertiary};
|
30
|
+
`,
|
31
|
+
scopes: css`
|
32
|
+
width: 100%;
|
33
|
+
margin-block: 16px;
|
34
|
+
`,
|
35
|
+
}));
|
36
|
+
|
37
|
+
/**
|
38
|
+
* 获取 Scope 的描述
|
39
|
+
*/
|
40
|
+
function getScopeDescription(scope: string, t: any): string {
|
41
|
+
return t(`consent.scope.${scope}`, scope);
|
42
|
+
}
|
43
|
+
|
44
|
+
const ConsentClient = memo(({ uid, clientId, scopes, error }: ClientProps) => {
|
45
|
+
const { styles } = useStyles();
|
46
|
+
const { t } = useTranslation('oauth');
|
47
|
+
|
48
|
+
// 如果有错误,显示错误信息
|
49
|
+
if (error) {
|
50
|
+
return (
|
51
|
+
<Center height="100vh">
|
52
|
+
<div className={styles.error}>
|
53
|
+
<Title level={2}>{error.title}</Title>
|
54
|
+
<Paragraph>{error.message}</Paragraph>
|
55
|
+
</div>
|
56
|
+
</Center>
|
57
|
+
);
|
58
|
+
}
|
59
|
+
|
60
|
+
return (
|
61
|
+
<Center height="100vh">
|
62
|
+
<Card style={{ maxWidth: 500, width: '100%' }}>
|
63
|
+
<Flexbox gap={24}>
|
64
|
+
<Title level={3} style={{ margin: 0 }}>
|
65
|
+
{t('consent.title')}
|
66
|
+
</Title>
|
67
|
+
<Paragraph>{t('consent.description', { clientId })}</Paragraph>
|
68
|
+
|
69
|
+
<div className={styles.scopes}>
|
70
|
+
<Paragraph>{t('consent.permissionsTitle')}</Paragraph>
|
71
|
+
{scopes.map((scope) => (
|
72
|
+
<div className={styles.scope} key={scope}>
|
73
|
+
<Text>{getScopeDescription(scope, t)}</Text>
|
74
|
+
</div>
|
75
|
+
))}
|
76
|
+
</div>
|
77
|
+
|
78
|
+
<form action="/oidc/consent" method="post">
|
79
|
+
<input name="uid" type="hidden" value={uid} />
|
80
|
+
<Flexbox gap={12} horizontal justify="flex-end">
|
81
|
+
<Button htmlType="submit" name="consent" value="deny">
|
82
|
+
{t('consent.buttons.deny')}
|
83
|
+
</Button>
|
84
|
+
<Button htmlType="submit" name="consent" type="primary" value="accept">
|
85
|
+
{t('consent.buttons.accept')}
|
86
|
+
</Button>
|
87
|
+
</Flexbox>
|
88
|
+
</form>
|
89
|
+
</Flexbox>
|
90
|
+
</Card>
|
91
|
+
</Center>
|
92
|
+
);
|
93
|
+
});
|
94
|
+
|
95
|
+
ConsentClient.displayName = 'ConsentClient';
|
96
|
+
|
97
|
+
export { ConsentClient };
|