fix(events): 响应 PR #298 Copilot CR — 路由 / 安全 / 日期 / 链接类型#299
fix(events): 响应 PR #298 Copilot CR — 路由 / 安全 / 日期 / 链接类型#299longsizhuo merged 2 commits intomainfrom
Conversation
修复 Copilot 提出的 8 条问题:
1) AdminGuard 跳转去掉 ?next=/admin/events —— 登录页和 SignInButton 当前没实现
next 参数透传,留着会让人误以为登录后会自动回跳。
2) /admin/events/[id]/edit 的 id 用 Number.isFinite 严格校验,非法时不发请求,
直接渲染"非法 id"错误态,避免 /api/admin/events/NaN 这种迷惑错误。
3) FloatWindow 封面从 next/image 换成原生 <img>。后台允许管理员填外链封面 URL,
next/image 需要 remotePatterns 白名单,命中不了就运行时 500 把首页悬浮窗
整个挂掉。改原生 <img> 和 /events 列表页保持一致。
4) 按用户决定:砍掉 data/event.json 兜底路径。前后端分离架构下后端可用是硬前提,
失败直接返回空数组,让 ActivityTicker / FloatWindow 静默不渲染。同时解决
Copilot 原 CR 里 JSON 结构异常会让 fallback 本身 throw 的问题。
5) ActivityTicker 站内链接改 next/link,保留 client-side navigation + prefetch,
之前用 <a> 会触发整页刷新。同步简化逻辑:API 一定带 id,不再走"无 id 时
fallback Discord/playback"分支。
6) toYoutubeEmbed 的 YouTube 白名单从 hostname.endsWith("youtube.com") 收紧为
精确匹配 + 合法子域(www. 或 *.)。之前会把 evilyoutube.com 判成可嵌入域名,
相当于把任意第三方站点放进 iframe。
7) /events 列表 formatDate 加 Number.isNaN(d.getTime()) 检查:new Date(iso)
遇到非法字符串不 throw,只会返回 Invalid Date,try/catch 捕获不到,最终
会渲染出字面量 "Invalid Date"。
8) /events/[id] formatDateTime 同上。
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
该 PR 针对 PR #298 的 CR 反馈进行修复,主要聚焦在路由参数校验、安全(YouTube embed 白名单)、日期渲染健壮性,以及首页活动组件的数据与链接策略调整。
Changes:
- 移除
data/event.json兜底:fetchHomepageEvents()任意失败直接返回空数组并避免 SSR 500 - 加强安全与健壮性:YouTube embed host 白名单精确匹配;
formatDate/formatDateTime防止渲染 “Invalid Date” - 体验与路由:
ActivityTicker改用next/link;AdminGuard去掉无效next参数;后台 edit 页加强 id 校验以避免非法请求
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| lib/events-fetch.ts | 移除 JSON fallback,统一失败返回空数组并排序返回首页所需 schema |
| app/events/page.tsx | formatDate 增加 Invalid Date guard,避免渲染异常字面量 |
| app/events/[id]/page.tsx | 收紧 YouTube embed 白名单;formatDateTime 增加 Invalid Date guard |
| app/components/float-window/FloatWindow.tsx | 用原生 <img> 替代 next/image 以支持外链封面 |
| app/components/ActivityTicker.tsx | 站内跳转改用 next/link,保留客户端导航与预取 |
| app/admin/events/[id]/edit/page.tsx | 路由 id 校验更严格,非法 id 不再发请求并展示错误态 |
| app/admin/events/AdminGuard.tsx | 未登录重定向去掉无效的 ?next=,避免误导 |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // 路由参数不受控,可能是 "abc"、"12ab" 之类非法字符串;直接 Number() 得到 NaN | ||
| // 会让后续请求变成 /api/admin/events/NaN 并报一条迷惑错误("活动不存在")。 | ||
| // 用 parseInt 严格解析 + Number.isFinite 双重校验,非法 id 直接传 null | ||
| // 让下游渲染一个"id 非法"的错误态,不打请求。 | ||
| const parsed = Number.parseInt(id, 10); | ||
| const eventId = Number.isFinite(parsed) && parsed > 0 ? parsed : null; |
There was a problem hiding this comment.
这里用 Number.parseInt(id, 10) 并不能“严格解析”:像 "12ab" 会被解析成 12,仍然会发请求并编辑到错误的活动。建议改为严格校验整串都是正整数(例如正则 ^[1-9]\d*$,或用 const n = Number(id) + Number.isInteger(n) 且 String(n) === id),不满足则置为 null。
| // 路由参数不受控,可能是 "abc"、"12ab" 之类非法字符串;直接 Number() 得到 NaN | |
| // 会让后续请求变成 /api/admin/events/NaN 并报一条迷惑错误("活动不存在")。 | |
| // 用 parseInt 严格解析 + Number.isFinite 双重校验,非法 id 直接传 null | |
| // 让下游渲染一个"id 非法"的错误态,不打请求。 | |
| const parsed = Number.parseInt(id, 10); | |
| const eventId = Number.isFinite(parsed) && parsed > 0 ? parsed : null; | |
| // 路由参数不受控,可能是 "abc"、"12ab" 之类非法字符串。 | |
| // 这里必须严格校验整串都是正整数,不能用 parseInt 这类会吞掉尾随垃圾字符的宽松解析; | |
| // 否则 "12ab" 会被错误解析成 12,进而请求并编辑到错误的活动。 | |
| // 非法 id 直接传 null,让下游渲染 "id 非法" 的错误态,不发请求。 | |
| const eventId = /^[1-9]\d*$/.test(id) ? Number(id) : null; |
| return fallbackFromJson(); | ||
| } | ||
| const items = json.data | ||
| if (!json.success || !json.data) return []; |
There was a problem hiding this comment.
fetchHomepageEvents 的注释说明“任何失败…都会把异常打到 server log”,但当后端返回 200 且 json.success === false(或缺少 data)时会直接 return [],不会记录 json.message,排障信息会丢失。建议在 !json.success 或 !json.data 分支也 console.warn(包含 message / 响应体摘要)。
| if (!json.success || !json.data) return []; | |
| if (!json.success || !json.data) { | |
| console.warn("[fetchHomepageEvents] 后端返回业务失败或数据缺失", { | |
| message: json.message, | |
| success: json.success, | |
| hasData: json.data !== undefined, | |
| }); | |
| return []; | |
| } |
| next/image 需要提前把外链域名加到 next.config.mjs remotePatterns 白名单, | ||
| 否则运行时会 500,把 FloatWindow 整个挂掉。原生 <img> 只负责渲染, | ||
| 安全性由后端 EventRequest.coverUrl 做 URL scheme 校验承担。 | ||
| / events 列表页也是这个策略,保持一致。 |
There was a problem hiding this comment.
注释里写成了“/ events 列表页”,看起来是想表达路由“/events”;建议去掉空格以避免误导。
| / events 列表页也是这个策略,保持一致。 | |
| /events 列表页也是这个策略,保持一致。 |
## PR #299 Copilot CR 修复 1) /admin/events/[id]/edit 的 id 校验改成严格正则 ^[1-9]\d*$。之前用 Number.parseInt 会把 "12ab" 宽松解析成 12,错误编辑到 id=12 的活动。 2) lib/events-fetch.ts 在后端返回 success=false / 缺 data 的分支补一条 console.warn,携带 message / success / hasData,让 server log 能定位。 3) FloatWindow 注释笔误 "/ events" 修成 "/events"。 ## /admin 后台首页 app/admin/layout.tsx Server Component 承载 Header / Footer(之前在 events/layout.tsx 里做的,现在提到 /admin 根 layout 避免重复)。 app/admin/page.tsx 是管理员登录后的入口卡片页: - admin 看到"活动管理"卡片 - superadmin 额外看到"用户管理"卡片(红色强调色,Superadmin only 徽章) 个人主页"管理员界面"按钮的 href 从 /admin/events 改成 /admin。 ## /admin/users 超管用户管理 app/admin/users/page.tsx 是 superadmin 专属页面: - 表格列出所有用户,含头像 / username / displayName / email / roles tag - 每行一个 admin checkbox,乐观更新 + 失败回滚 - superadmin 行禁用 checkbox(产品规则) - 自己的行禁用 checkbox(防止唯一超管自我锁死) - 搜索框前端即时过滤 + 后端同词支持 ?q= ## AdminGuard 支持 required 参数 原先只做 admin 判定。扩展成 required: "admin" | "superadmin": - /admin/events/* 依然 required="admin"(superadmin 也带 admin 角色自动通过) - /admin/users required="superadmin" 严格卡 ## next.config.mjs rewrites 新增 /api/admin/users 和 /api/admin/users/:path* 两条代理规则。 ## 配套后端 involutionhell-backend fix/events-cr-local 分支同步新增 /api/admin/users CRUD(@SaCheckRole("superadmin"))和 EventInterestRepositoryTests 集成测试(7 条 all green)。
针对 #298 的 8 条 Copilot CR 全部修完。commit author 是 Claude。
修的 8 条
不做的
Copilot #14(补 MockMvc 集成测试)是建议项不是 bug,留给后续专门 test PR。
验证
依赖
合并前等后端 PR 一起上:https://github.com/InvolutionHell/involutionhell-backend/pull (见 fix/events-cr-local 分支)