diff --git a/.env.example b/.env.example index dd058f5..3e91372 100644 --- a/.env.example +++ b/.env.example @@ -6,7 +6,7 @@ ASSETS_API_KEY=your-assets-api-key # 允许调用 issue-ai API 的 Key(逗号分隔,支持多个),由 issue-ai 管理界面生成 ALLOWED_API_KEYS=your-issue-api-key # NEXT_PUBLIC_ 前缀:构建时内嵌到客户端 JS,云上必须通过 deploy-ai.sh 设置 -# 本地开发:http://localhost:5177 +# 本地开发:http://localhost:6177 # 云上生产:https://assets.tlyq.ai -NEXT_PUBLIC_ASSETS_URL=http://localhost:5177 +NEXT_PUBLIC_ASSETS_URL=http://localhost:6177 NODE_ENV=development diff --git a/.superpowers/brainstorm/65974-1778137360/content/preview-page.html b/.superpowers/brainstorm/65974-1778137360/content/preview-page.html new file mode 100644 index 0000000..31b87c7 --- /dev/null +++ b/.superpowers/brainstorm/65974-1778137360/content/preview-page.html @@ -0,0 +1,192 @@ +

报告预览页

+

从列表点击报告名称或"预览"按钮进入。右上角按钮根据状态动态切换。

+ +
+
预览页 — 状态:数据已就绪(待生成文档)
+
+ + +
+
+ +
+

月报

+ 2026-04-01 ~ 2026-04-30 +
+ + + 数据已就绪 + +
+ +
+ + +
+
+
工单总数
+
12
+
+
+
已解决
+
10
+
83.3%
+
+
+
整体可用性
+
98.52%
+
低于99%
+
+
+
平均处理时长
+
240
+
分钟
+
+
+
进行中
+
2
+
+
+ + +
+
+
+ 1 + 设备概况 +
+
+
+
+
🖥
+
8台 GPU 服务器
+
+
+
🗄
+
3台 存储服务器
+
+
+
+ + +
+
+
+ 2 + 运营数据 +
+
+
+
故障工单 8
+
涉及设备 5
+
无故障天数 24
+
+
+ + +
+
+
+ 3 + 故障分类 +
+
+
+
+
5
+
GPU 故障
+
+
62.5%
+
+
+
1
+
存储故障
+
+
12.5%
+
+
+
2
+
其他工单
+
+
25.0%
+
+
+
+ + +
+
+
+ 4 + 服务可用性 +
+
+
+
+ 整体可用性: + 98.52% +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IP 地址设备类型故障时长可用性状态
10.0.0.35GPU2,160 分钟95.12%进行中
10.0.0.42GPU1,440 分钟96.78%已恢复
10.0.0.18存储720 分钟98.41%已恢复
+
共 3 个 IP 可用性低于 100%,其余设备保持 100% 在线
+
+
+ +
+
+ + +
+

右上角按钮 — 按状态切换

+
+ +
+
+
数据已就绪
+ 生成报告文档 +
+
+
文档生成中
+ 生成中... +
+
+
已完成
+ ⬇ 下载报告 +
+
+
生成失败
+ 重新生成 +
+
diff --git a/.superpowers/brainstorm/65974-1778137360/state/server-info b/.superpowers/brainstorm/65974-1778137360/state/server-info new file mode 100644 index 0000000..00bf740 --- /dev/null +++ b/.superpowers/brainstorm/65974-1778137360/state/server-info @@ -0,0 +1 @@ +{"type":"server-started","port":50153,"host":"127.0.0.1","url_host":"localhost","url":"http://localhost:50153","screen_dir":"/Users/niuniu/programs/docker/issue-ai/.superpowers/brainstorm/65974-1778137360/content","state_dir":"/Users/niuniu/programs/docker/issue-ai/.superpowers/brainstorm/65974-1778137360/state"} diff --git a/.superpowers/brainstorm/9392-1778134394/content/design-directions.html b/.superpowers/brainstorm/9392-1778134394/content/design-directions.html new file mode 100644 index 0000000..d69238f --- /dev/null +++ b/.superpowers/brainstorm/9392-1778134394/content/design-directions.html @@ -0,0 +1,53 @@ +

报告预览页 — 设计方向

+

当前页面只展示 4 个数字卡片 + 3 个故障分类卡片。以下三个方向兼顾数据丰富度与视觉提升,各有侧重。

+ +
+
+
A
+
+

仪表盘风格

+

将预览页做成迷你仪表盘:顶部 KPI 卡片带环比/同比趋势箭头,中间用横向条状图展示故障分类,底部展示服务可用性分布。数据量大但不杂乱,适合快速扫读。

+
+
+
+
B
+
+

摘要 + 详情分区

+

顶部为摘要区(设备规模 + 核心指标如可用性/SLA/解决率),下方可展开的折叠面板展示故障明细(按分类、按设备、按日期)。信息层级清晰,用户按需深入。

+
+
+
+
C
+
+

单页报告预览

+

接近真实报告的排版风格:带章节标题(一、设备概况 二、运营数据 三、故障分类 四、服务可用性),每个章节内嵌简表或数据卡片。预览即缩略版报告,视觉专业正式。

+
+
+
+ +
+

共同增强的数据指标

+

无论选哪个方向,以下字段都会从 metadata 中新增暴露:

+
+ +
+
+

新增指标

+ +
+
+

改动范围

+ +
+
diff --git a/.superpowers/brainstorm/9392-1778134394/content/final-layout.html b/.superpowers/brainstorm/9392-1778134394/content/final-layout.html new file mode 100644 index 0000000..6d9532a --- /dev/null +++ b/.superpowers/brainstorm/9392-1778134394/content/final-layout.html @@ -0,0 +1,200 @@ +

报告预览页 — 最终方案

+

5 KPI + 4 章节全部展开。第 4 章列出所有可用性 < 100% 的 IP。

+ +
+
完整页面布局 — 月报示例(2026年4月)
+
+ + +
+
+ +
+

月报

+ 2026-04-01 ~ 2026-04-30 +
+ 已完成 +
+ +
+ + +
+
+
工单总数
+
12
+
+
+
已解决
+
10
+
83.3%
+
+
+
整体可用性
+
98.52%
+
低于99%
+
+
+
平均处理时长
+
240
+
分钟
+
+
+
进行中
+
2
+
+
+ + +
+ + +
+
+
+ 1 + 设备概况 +
+ +
+
+
+
🖥
+
8台 GPU 服务器
+
+
+
🗄
+
3台 存储服务器
+
+
+
+ + +
+
+
+ 2 + 运营数据 +
+ +
+
+
故障工单 8
+
涉及设备 5
+
无故障天数 24
+
+
+ + +
+
+
+ 3 + 故障分类 +
+ +
+
+
+
5
+
GPU 故障
+
+
62.5%
+
+
+
1
+
存储故障
+
+
12.5%
+
+
+
2
+
其他工单
+
+
25.0%
+
+
+
+ + +
+
+
+ 4 + 服务可用性 +
+ +
+
+ +
+ 整体可用性: + 98.52% +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IP 地址设备类型故障时长可用性状态
10.0.0.35GPU2,160 分钟95.12%进行中
10.0.0.42GPU1,440 分钟96.78%已恢复
10.0.0.18存储720 分钟98.41%已恢复
+
共 3 个 IP 可用性低于 100%,其余设备保持 100% 在线
+
+
+ +
+
+
+ +
+

方案确认清单

+
+ +
+
+

✓ 已定

+ +
+
+

? 待确认

+ +
+
diff --git a/.superpowers/brainstorm/9392-1778134394/content/full-layout.html b/.superpowers/brainstorm/9392-1778134394/content/full-layout.html new file mode 100644 index 0000000..d4918e7 --- /dev/null +++ b/.superpowers/brainstorm/9392-1778134394/content/full-layout.html @@ -0,0 +1,172 @@ +

报告预览页 — 融合方案全景

+

B(摘要+详情分区)+ C(报告章节结构)。周报/月报共用同一布局,章节内容自适应。

+ +
+
完整页面布局 — 月报示例(2026年4月)
+
+ + +
+
+ +
+

月报

+ 2026-04-01 ~ 2026-04-30 +
+ 已完成 +
+ +
+ + +
+
+
工单总数
+
12
+
+
+
已解决
+
10
+
83.3%
+
+
+
可用性均值
+
98.52%
+
+
+
SLA 达标率
+
91.7%
+
+
+
平均处理时长
+
240
+
分钟
+
+
+
进行中
+
2
+
+
+ + +
+ + +
+
+
+ 1 + 设备概况 +
+ +
+
+
+
🖥
+
8台 GPU 服务器
+
+
+
🗄
+
3台 存储服务器
+
+
+
+ + +
+
+
+ 2 + 运营数据 +
+ +
+
+
故障工单 8
+
涉及设备 5
+
无故障天数 24
+
+
+ + +
+
+
+ 3 + 故障分类 +
+ +
+
+
+
5
+
GPU 故障
+
+
62.5%
+
+
+
1
+
存储故障
+
+
12.5%
+
+
+
2
+
其他工单
+
+
25.0%
+
+
+
+ + +
+
+
+ 4 + 服务可用性 +
+ +
+
+
+ 98.52% + 整体可用性 +
+
+ 最低可用 IP: + 10.0.0.35 + (95.12%) +
+
+
+ +
+ +
+
+ + +
+

设计要点

+
+ +
+
+

交互特性

+ +
+
+

待确认

+ +
+
diff --git a/.superpowers/brainstorm/9392-1778134394/content/preview-page.html b/.superpowers/brainstorm/9392-1778134394/content/preview-page.html new file mode 100644 index 0000000..31b87c7 --- /dev/null +++ b/.superpowers/brainstorm/9392-1778134394/content/preview-page.html @@ -0,0 +1,192 @@ +

报告预览页

+

从列表点击报告名称或"预览"按钮进入。右上角按钮根据状态动态切换。

+ +
+
预览页 — 状态:数据已就绪(待生成文档)
+
+ + +
+
+ +
+

月报

+ 2026-04-01 ~ 2026-04-30 +
+ + + 数据已就绪 + +
+ +
+ + +
+
+
工单总数
+
12
+
+
+
已解决
+
10
+
83.3%
+
+
+
整体可用性
+
98.52%
+
低于99%
+
+
+
平均处理时长
+
240
+
分钟
+
+
+
进行中
+
2
+
+
+ + +
+
+
+ 1 + 设备概况 +
+
+
+
+
🖥
+
8台 GPU 服务器
+
+
+
🗄
+
3台 存储服务器
+
+
+
+ + +
+
+
+ 2 + 运营数据 +
+
+
+
故障工单 8
+
涉及设备 5
+
无故障天数 24
+
+
+ + +
+
+
+ 3 + 故障分类 +
+
+
+
+
5
+
GPU 故障
+
+
62.5%
+
+
+
1
+
存储故障
+
+
12.5%
+
+
+
2
+
其他工单
+
+
25.0%
+
+
+
+ + +
+
+
+ 4 + 服务可用性 +
+
+
+
+ 整体可用性: + 98.52% +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IP 地址设备类型故障时长可用性状态
10.0.0.35GPU2,160 分钟95.12%进行中
10.0.0.42GPU1,440 分钟96.78%已恢复
10.0.0.18存储720 分钟98.41%已恢复
+
共 3 个 IP 可用性低于 100%,其余设备保持 100% 在线
+
+
+ +
+
+ + +
+

右上角按钮 — 按状态切换

+
+ +
+
+
数据已就绪
+ 生成报告文档 +
+
+
文档生成中
+ 生成中... +
+
+
已完成
+ ⬇ 下载报告 +
+
+
生成失败
+ 重新生成 +
+
diff --git a/.superpowers/brainstorm/9392-1778134394/content/reports-list-v2.html b/.superpowers/brainstorm/9392-1778134394/content/reports-list-v2.html new file mode 100644 index 0000000..829f507 --- /dev/null +++ b/.superpowers/brainstorm/9392-1778134394/content/reports-list-v2.html @@ -0,0 +1,155 @@ +

报告管理列表

+

类型列用颜色区分月报/周报。批量操作按钮仅在选中后出现。

+ +
+
报告管理页面
+
+ + +
+
+

报告管理

+ 数据预览与文档生成 +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + 报告名称类型时间段状态创建时间操作
+ 2026年4月图灵IT基础设施运营月报 + + 月报 + 2026-04-01 ~ 2026-04-30 + + + 数据已就绪 + + 2026-05-07 14:30 +
+ 预览 + 生成文档 + 🗑 +
+
+ 图灵IT基础设施运营周报(4月28日-5月4日) + + 周报 + 2026-04-28 ~ 2026-05-04 + + + 文档生成中 + + 2026-05-07 14:35 + 预览 +
+ 2026年3月图灵IT基础设施运营月报 + + 月报 + 2026-03-01 ~ 2026-03-31 + + + 已完成 + + 2026-05-06 10:15 +
+ 预览 + ⬇ 下载 + 🗑 +
+
+ 图灵IT基础设施运营周报(4月21日-4月27日) + + 周报 + 2026-04-21 ~ 2026-04-27 + + + 生成失败 + + 2026-05-06 11:00 +
+ 预览 + 重试 + 🗑 +
+
+ +
+
+ +
+
+

✓ 类型颜色区分

+
+ 月报 + 蓝色背景,与报告预览章节编号色呼应 +
+
+ 周报 + 紫色背景,与月报明显区分 +
+
+
+

✓ 批量操作栏

+ +
+
diff --git a/.superpowers/brainstorm/9392-1778134394/content/reports-list.html b/.superpowers/brainstorm/9392-1778134394/content/reports-list.html new file mode 100644 index 0000000..9d2ddb6 --- /dev/null +++ b/.superpowers/brainstorm/9392-1778134394/content/reports-list.html @@ -0,0 +1,226 @@ +

报告管理页 — 新流程重构

+

核心理念:预览数据先行(1-2s),确认后再生成 DOCX(10-30s)。

+ +
+

流程对比

+
+ +
+
+
旧流程
+
+ 选择月份 → 点"生成报告" →
+ 等待 10-30s(数据+图表+DOCX+OLE)
+ 报告完成 → 点"查看"预览 →
+ 发现数据有误 → 删除重来 +
+
+
+
新流程
+
+ 选择月份 → 点"预览报告" →
+ 等待 1-2s(仅数据采集)
+ 自动跳转预览页 → 确认数据无误 →
+ 点"生成报告文档" → 下载 DOCX +
+
+
+ +
+

报告列表页 — 新版布局

+
+ +
+
报告管理页面(列表)
+
+ + +
+
+

报告管理

+ 数据预览与文档生成 +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + 报告名称类型时间段状态创建时间操作
2026年4月图灵IT基础设施运营月报月报2026-04-01 ~ 2026-04-30 + + + 数据已就绪 + + 2026-05-07 14:30 +
+ 预览 + 生成文档 + 🗑 +
+
图灵IT基础设施运营周报(4月28日-5月4日)周报2026-04-28 ~ 2026-05-04 + + + 文档生成中 + + 2026-05-07 14:35 +
+ 预览 +
+
2026年3月图灵IT基础设施运营月报月报2026-03-01 ~ 2026-03-31 + + + 已完成 + + 2026-05-06 10:15 +
+ 预览 + ⬇ 下载 + 🗑 +
+
图灵IT基础设施运营周报(4月21日-4月27日)周报2026-04-21 ~ 2026-04-27 + + + 生成失败 + + 2026-05-06 11:00 +
+ 预览 + 重试 + 🗑 +
+
+ + +
+ 已选 2 项 +
+ 📦 批量下载 + 🗑 批量删除 +
+
+ +
+
+ +
+

状态体系总览

+
+ +
+
+
数据已就绪
+
数据采集完成
文档未生成
+
+ 预览 + 生成文档 +
+
+
+
文档生成中
+
DOCX 构建中
图表+OLE嵌入
+
+ 预览 +
+
+
+
已完成
+
文档就绪
可下载
+
+ 预览 + 下载 +
+
+
+
生成失败
+
文档构建出错
可查看预览数据
+
+ 预览 + 重试 +
+
+
+ +
+

与预览页的衔接

+
+ +
+
用户操作流程
+
+
+
+ 列表页
点"新建报告" +
+ +
+ 弹窗选择
月份/日期 +
+ +
+ 1-2s 数据采集
自动跳转预览页 +
+ +
+ 预览页
确认数据 +
+ +
+ 点"生成文档"
下载 DOCX +
+
+
+
diff --git a/.superpowers/brainstorm/9392-1778134394/state/server-info b/.superpowers/brainstorm/9392-1778134394/state/server-info new file mode 100644 index 0000000..3402949 --- /dev/null +++ b/.superpowers/brainstorm/9392-1778134394/state/server-info @@ -0,0 +1 @@ +{"type":"server-started","port":60240,"host":"127.0.0.1","url_host":"localhost","url":"http://localhost:60240","screen_dir":"/Users/niuniu/programs/docker/issue-ai/.superpowers/brainstorm/9392-1778134394/content","state_dir":"/Users/niuniu/programs/docker/issue-ai/.superpowers/brainstorm/9392-1778134394/state"} diff --git a/CLAUDE.md b/CLAUDE.md index 54fdd63..18156bd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,7 +13,7 @@ issue-ai 是基于 Next.js + SQLite 的工单跟踪管理系统,部署在腾 | 站点域名 | `issue.tlyq.ai` | | 服务器 | txjp(IP: 43.133.38.210) | | 代码路径 | `/root/docker/issue-ai/` | -| 本地端口 | 5176 | +| 本地端口 | 6176 | | 容器名 | `issue-ai` | | 数据库 | SQLite:`data/issue.db` | | 报告存储 | `reports/` 目录(环境变量 `REPORTS_DIR` 可覆盖) | @@ -139,9 +139,9 @@ npm run import # 导入工单 | 环境变量 | 本地开发 | 云服务器(txjp) | 说明 | |---------|---------|----------------|------| -| `ASSETS_API_URL` | `http://localhost:5177/api` | `http://assets-ai:3000/api` | 调用 assets API 地址 | +| `ASSETS_API_URL` | `http://localhost:6177/api` | `http://assets-ai:3000/api` | 调用 assets API 地址 | | `ASSETS_API_KEY` | 本地 assets-ai 生成 | 云上 assets-ai 生成 | **每个环境独立,不可跨环境使用** | -| `NEXT_PUBLIC_ASSETS_URL` | `http://localhost:5177` | `https://assets.tlyq.ai` | 前端跳转链接(构建时内嵌) | +| `NEXT_PUBLIC_ASSETS_URL` | `http://localhost:6177` | `https://assets.tlyq.ai` | 前端跳转链接(构建时内嵌) | | `ALLOWED_API_KEYS` | 本地 issue-ai 生成的 Key | 云上 issue-ai 生成的 Key | 允许外部系统调本系统的 Key,逗号分隔 | | `JWT_SECRET` | `dev-secret-key-local` | `${ISSUE_JWT_SECRET}` | 本地两系统需一致(同 localhost 域) | | `DATABASE_PATH` | `./data/issue.db` | `/app/data/issue.db` | Docker volume 挂载 | @@ -154,10 +154,10 @@ DATABASE_PATH=./data/issue.db JWT_SECRET=dev-secret-key-local ADMIN_PASSWORD=admin123 NODE_ENV=development -ASSETS_API_URL=http://localhost:5177/api +ASSETS_API_URL=http://localhost:6177/api ASSETS_API_KEY=ak_<32字节十六进制> ALLOWED_API_KEYS=ak_<32字节十六进制> -NEXT_PUBLIC_ASSETS_URL=http://localhost:5177 +NEXT_PUBLIC_ASSETS_URL=http://localhost:6177 ``` --- diff --git a/docker-compose.yml b/docker-compose.yml index 9886a8a..0ea31df 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,7 @@ services: build: . container_name: issue-ai ports: - - "5176:3000" + - "6176:3000" volumes: - issue-data:/app/data - issue-uploads:/app/uploads diff --git a/package.json b/package.json index b4e40d9..72b2643 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "private": true, "scripts": { - "dev": "NODE_OPTIONS='--max-old-space-size=2048' next dev --port 5176", + "dev": "NODE_OPTIONS='--max-old-space-size=2048' next dev --port 6176", "build": "next build", "start": "next start", "lint": "next lint", diff --git a/report-54-light.png b/report-54-light.png new file mode 100644 index 0000000..7deeb63 Binary files /dev/null and b/report-54-light.png differ diff --git a/report-preview-53.png b/report-preview-53.png new file mode 100644 index 0000000..dee50e1 Binary files /dev/null and b/report-preview-53.png differ diff --git a/src/app/api/auth/logout/route.ts b/src/app/api/auth/logout/route.ts index 1e45add..b684ae7 100644 --- a/src/app/api/auth/logout/route.ts +++ b/src/app/api/auth/logout/route.ts @@ -1,6 +1,10 @@ import { NextResponse } from 'next/server' + export async function POST() { const r = NextResponse.json({ success: true }) r.cookies.set('session_issue', '', { maxAge: 0, path: '/' }) + r.cookies.set('session', '', { maxAge: 0, path: '/' }) + // 清除 Authelia SSO cookie + r.cookies.set('authelia_session', '', { maxAge: 0, path: '/', domain: '127.0.0.1' }) return r } diff --git a/src/app/api/auth/me/route.ts b/src/app/api/auth/me/route.ts index 450f983..74b7e37 100644 --- a/src/app/api/auth/me/route.ts +++ b/src/app/api/auth/me/route.ts @@ -1,7 +1,49 @@ import { NextResponse } from 'next/server' -import { getCurrentUser } from '@/lib/auth' +import { cookies, headers } from 'next/headers' +import { getDb } from '@/lib/db' +import { getCurrentUser, createToken } from '@/lib/auth' + export async function GET() { - const user = await getCurrentUser() - if (!user) return NextResponse.json({ error: '未登录' }, { status: 401 }) - return NextResponse.json({ user }) + try { + const cookieStore = await cookies() + + // 路径 1:SSO(来自 nginx auth_request 代理) + let ssoUsername = '' + try { + const ssoSession = cookieStore.get('session')?.value + if (ssoSession) ssoUsername = JSON.parse(ssoSession).username || '' + } catch { } + if (!ssoUsername) { + const headersList = await headers() + ssoUsername = headersList.get('x-remote-user') || '' + } + + if (ssoUsername) { + const db = getDb() + db.prepare( + "INSERT OR IGNORE INTO users (username, password_hash, display_name, role, is_active) VALUES (?, '__SSO__', ?, ?, 1)" + ).run(ssoUsername, ssoUsername, 'viewer') + const user = db.prepare( + 'SELECT id, username, display_name, role FROM users WHERE username = ? AND is_active = 1' + ).get(ssoUsername) as Record | undefined + if (user) { + const jwt = await createToken({ + id: user.id as number, + username: user.username as string, + display_name: (user.display_name as string) || '', + role: user.role as string, + }) + const response = NextResponse.json({ user }) + response.cookies.set('session_issue', jwt, { httpOnly: true, sameSite: 'lax', path: '/', maxAge: 7 * 24 * 60 * 60 }) + return response + } + } + + // 路径 2:JWT cookie(本地开发 / @fallback 紧急绕过) + const user = await getCurrentUser() + if (!user) return NextResponse.json({ error: '未登录' }, { status: 401 }) + return NextResponse.json({ user }) + } catch { + return NextResponse.json({ error: '获取用户信息失败' }, { status: 500 }) + } } diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 8fc63bc..7e70e45 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -48,6 +48,7 @@ export default function Sidebar() { 全部工单 )} + {isAdmin && (
系统设置 @@ -58,6 +59,7 @@ export default function Sidebar() { return ({item.label}) })}
+ )} ) diff --git a/src/components/layout/TopBar.tsx b/src/components/layout/TopBar.tsx index aed847f..6855c33 100644 --- a/src/components/layout/TopBar.tsx +++ b/src/components/layout/TopBar.tsx @@ -8,7 +8,10 @@ interface TopBarProps { user: { username: string; display_name: string; role: st export default function TopBar({ user }: TopBarProps) { const { theme, toggleTheme } = useTheme() const router = useRouter() - const handleLogout = async () => { await fetch('/api/auth/logout', { method: 'POST' }); router.push('/login'); router.refresh() } + const handleLogout = async () => { + await fetch('/api/auth/logout', { method: 'POST' }) + router.push('/login'); router.refresh() + } return (
diff --git a/src/lib/assets-client.ts b/src/lib/assets-client.ts index 87912e3..0e10e5f 100644 --- a/src/lib/assets-client.ts +++ b/src/lib/assets-client.ts @@ -1,4 +1,4 @@ -const ASSETS_API_URL = process.env.ASSETS_API_URL || 'http://localhost:5177/api' +const ASSETS_API_URL = process.env.ASSETS_API_URL || 'http://localhost:6177/api' const ASSETS_API_KEY = process.env.ASSETS_API_KEY || '' export interface Asset { diff --git a/src/middleware.ts b/src/middleware.ts index 5f60390..024f36e 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -2,7 +2,6 @@ import { NextRequest, NextResponse } from 'next/server' import { verifyToken } from '@/lib/jwt' function verifyApiKey(key: string): boolean { - // API Key 以环境变量形式存储,支持多个 Key(逗号分隔) const allowedKeys = process.env.ALLOWED_API_KEYS || '' if (!allowedKeys) return false const keys = allowedKeys.split(',').map(k => k.trim()) @@ -11,23 +10,50 @@ function verifyApiKey(key: string): boolean { export async function middleware(request: NextRequest) { const { pathname } = request.nextUrl + + // 清除外部注入的 trust proxy headers(防伪造) + const requestHeaders = new Headers(request.headers) + requestHeaders.delete('x-remote-user') + requestHeaders.delete('x-remote-groups') + + // SSO 代理认证路径:检测 X-Remote-User header + 代理密钥验证 + const remoteUser = request.headers.get('x-remote-user') + const proxyKey = request.headers.get('x-auth-proxy-key') + const isFromNginx = proxyKey === 'internal-auth-key-tlyq-2026' + + if (remoteUser && isFromNginx) { + // logout 路径不设置 SSO session(避免清除后又重新设置) + if (pathname === '/api/auth/logout') return NextResponse.next() + + const response = pathname === '/login' || pathname === '/' + ? NextResponse.redirect(new URL('/tickets', request.url)) + : NextResponse.next() + + response.cookies.set('session', JSON.stringify({ username: remoteUser }), { + httpOnly: true, + sameSite: 'lax', + path: '/', + }) + if (pathname.startsWith('/api/')) { + response.headers.set('x-original-pathname', pathname + (request.nextUrl.search || '')) + } + return response + } + + // 回退:现有 JWT 认证路径 if (pathname.startsWith('/login') || pathname === '/') return NextResponse.next() if (pathname === '/api/auth/login') return NextResponse.next() const authHeader = request.headers.get('authorization') - // API Key 认证:Bearer ak_xxx 格式 if (authHeader?.startsWith('Bearer ak_')) { const key = authHeader.slice(7) if (verifyApiKey(key)) return NextResponse.next() - // 环境变量中未匹配,API 路由仍放行(route handler 可查询数据库二次验证) if (pathname.startsWith('/api/')) return NextResponse.next() } - // Cookie 认证 const token = request.cookies.get('session_issue')?.value - // 构建带 redirect 参数的登录 URL function buildLoginRedirect() { const loginUrl = new URL('/login', request.url) const dest = pathname + (request.nextUrl.search || '')