Skip to content

fix(events): 响应 PR #298 Copilot CR — 路由 / 安全 / 日期 / 链接类型#299

Merged
longsizhuo merged 2 commits intomainfrom
fix/events-cr-local
Apr 17, 2026
Merged

fix(events): 响应 PR #298 Copilot CR — 路由 / 安全 / 日期 / 链接类型#299
longsizhuo merged 2 commits intomainfrom
fix/events-cr-local

Conversation

@longsizhuo
Copy link
Copy Markdown
Member

针对 #298 的 8 条 Copilot CR 全部修完。commit author 是 Claude。

修的 8 条

  1. AdminGuard 去掉误导的 `?next=/admin/events`(登录页当前没实现参数透传)
  2. /admin/events/[id]/edit 用 `Number.isFinite` 严格校验 id,非法不打请求
  3. FloatWindow 从 `next/image` 换成原生 ``(后台允许管理员填外链封面,`next/image` 会因 remotePatterns 白名单命中不了挂掉整个首页浮窗)
  4. lib/events-fetch.ts 砍掉 `data/event.json` 兜底路径(前后端分离架构下后端可用是硬前提),失败直接返回空数组让 ticker / floatWindow 不渲染;同步修掉 Copilot 提到的"fallback 自身会 throw"
  5. ActivityTicker 站内链接改 `next/link` 保留 client-side navigation + prefetch
  6. toYoutubeEmbed 白名单收紧:精确匹配 + 合法子域,不再把 `evilyoutube.com` 判成可嵌入
  7. /events formatDate + 8. /events/[id] formatDateTime 都加 `Number.isNaN(d.getTime())` 守卫,避免渲染 "Invalid Date"

不做的

Copilot #14(补 MockMvc 集成测试)是建议项不是 bug,留给后续专门 test PR。

验证

  • `pnpm exec tsc --noEmit` 绿
  • 逻辑验证:前后端分离失败路径手工过了一遍

依赖

合并前等后端 PR 一起上:https://github.com/InvolutionHell/involutionhell-backend/pull (见 fix/events-cr-local 分支)

修复 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 同上。
Copilot AI review requested due to automatic review settings April 17, 2026 03:36
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 17, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
involutionhell-github-io Ready Ready Preview, Comment Apr 17, 2026 4:00am
website-preview Ready Ready Preview, Comment Apr 17, 2026 4:00am

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/linkAdminGuard 去掉无效 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.

Comment thread app/admin/events/[id]/edit/page.tsx Outdated
Comment on lines +24 to +29
// 路由参数不受控,可能是 "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;
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里用 Number.parseInt(id, 10) 并不能“严格解析”:像 "12ab" 会被解析成 12,仍然会发请求并编辑到错误的活动。建议改为严格校验整串都是正整数(例如正则 ^[1-9]\d*$,或用 const n = Number(id) + Number.isInteger(n)String(n) === id),不满足则置为 null。

Suggested change
// 路由参数不受控,可能是 "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;

Copilot uses AI. Check for mistakes.
Comment thread lib/events-fetch.ts Outdated
return fallbackFromJson();
}
const items = json.data
if (!json.success || !json.data) return [];
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetchHomepageEvents 的注释说明“任何失败…都会把异常打到 server log”,但当后端返回 200 且 json.success === false(或缺少 data)时会直接 return [],不会记录 json.message,排障信息会丢失。建议在 !json.success!json.data 分支也 console.warn(包含 message / 响应体摘要)。

Suggested change
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 [];
}

Copilot uses AI. Check for mistakes.
next/image 需要提前把外链域名加到 next.config.mjs remotePatterns 白名单,
否则运行时会 500,把 FloatWindow 整个挂掉。原生 <img> 只负责渲染,
安全性由后端 EventRequest.coverUrl 做 URL scheme 校验承担。
/ events 列表页也是这个策略,保持一致。
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

注释里写成了“/ events 列表页”,看起来是想表达路由“/events”;建议去掉空格以避免误导。

Suggested change
/ events
/events

Copilot uses AI. Check for mistakes.
## 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)。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants