Skip to content

feat(friends): restore moderated friend links - #118

Merged
one-ea merged 2 commits into
mainfrom
fix/restore-friend-links
Jul 25, 2026
Merged

feat(friends): restore moderated friend links#118
one-ea merged 2 commits into
mainfrom
fix/restore-friend-links

Conversation

@one-ea

@one-ea one-ea commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Summary

  • restore the moderated friend links feature that was previously deployed from dev
  • add the public /friends page and admin /admin/friends moderation UI
  • restore the /api/friends endpoints and D1/Turso/PostgreSQL adapter support
  • include migration 0010_friend_links.sql

Context

The current production main deployment no longer exposes /api/friends because the friend-links commit lived on dev and was never merged into main. This PR applies only the standalone friend-links commit onto the latest main, preserving the merged RSS and sitemap fixes and recent dependency updates.

Validation

  • npm run check
  • npm run lint
  • npm run build
  • cherry-pick onto latest main completed without conflicts

Deployment follow-up

After merge, apply the D1 migration, deploy Worker and Pages, then verify /api/friends returns HTTP 200.

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 99c996b7-f31b-4519-ab9f-6fd3a669ed0e

📥 Commits

Reviewing files that changed from the base of the PR and between 0555a1e and 090089f.

📒 Files selected for processing (3)
  • client/src/lib/api.ts
  • client/src/pages/friends.tsx
  • server/src/index.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • client/src/lib/api.ts
  • server/src/index.ts
  • client/src/pages/friends.tsx

📝 Walkthrough

Summary by CodeRabbit

  • 新功能
    • 新增友链展示页面:展示已通过友链,支持提交友链申请,并提供加载失败后的重试。
    • 新增后台友链管理:支持创建、编辑、按状态筛选查看,并提供通过/拒绝/删除等审核操作。
    • 支持“一键导入”旧数据到友链列表。
    • 前台导航新增“友链”,后台侧导航新增“友链管理”。

Walkthrough

新增完整友链功能,覆盖数据库存储、公开查询与申请、后台审核管理、旧链接导入,以及前后台路由和导航入口。

Changes

友链功能

Layer / File(s) Summary
友链数据契约与持久化
server/src/storage/interfaces.ts, server/src/db/*, server/src/storage/db/*, server/src/migrations/*
新增友链类型、表结构、索引、迁移脚本及多种数据库适配器的 CRUD、审核和批量导入实现。
友链服务端路由
server/src/index.ts
新增公开查询与申请 API、输入规范化、旧配置解析、申请限流,以及管理端 CRUD、审核和导入接口。
客户端友链 API 封装
client/src/lib/api.ts
新增友链类型、公开 API 和带鉴权的后台管理 API,并统一处理响应错误。
公开友链页面与入口
client/src/pages/friends.tsx, client/src/app.tsx, client/src/components/navbar.tsx
新增友链展示与申请表单,支持加载、排序、提交状态,并接入公开路由和导航。
后台友链管理页面与入口
client/src/pages/admin/friends.tsx, client/src/app.tsx, client/src/components/admin-layout.tsx
新增筛选、编辑、新建、审核、删除和旧链接导入功能,并接入后台路由和侧边导航。

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Visitor
  participant FriendsPage
  participant PublicAPI
  participant Database
  Visitor->>FriendsPage: 打开 /friends
  FriendsPage->>PublicAPI: GET /api/friends
  PublicAPI->>Database: getApprovedFriendLinks
  Database-->>PublicAPI: 返回已审核友链
  PublicAPI-->>FriendsPage: 返回友链列表
  Visitor->>FriendsPage: 提交申请
  FriendsPage->>PublicAPI: POST /api/friends/apply
  PublicAPI->>Database: createFriendLink
  Database-->>PublicAPI: 创建待审核记录
  PublicAPI-->>FriendsPage: 返回申请结果
Loading

Possibly related PRs

  • one-ea/Monolith#65:提供 settings.social_links 处理逻辑,本 PR 用于旧友链解析、公开回退展示和批量导入。

Suggested labels: frontend, backend, feature

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed 标题符合 Conventional Commits 格式,且准确概括了友链恢复与审核功能改动。
Description check ✅ Passed 描述与变更内容一致,覆盖了前台页面、后台管理、API 和数据库适配器恢复。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/restore-friend-links
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch fix/restore-friend-links

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot added backend 后端 (Hono Workers) 相关变更 feature 新功能 frontend 前端 (React/Vite) 相关变更 labels Jul 25, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (6)
server/src/storage/db/turso.ts (1)

186-203: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

ensureCoreTables() 未包含 friend_links,与 D1 适配器的初始化行为不一致。

D1Adapter.ensureSchema() 已把 ensureFriendLinksTable() 纳入统一初始化,而这里 ensureCoreTables()(Line 107-162)没有加入,friend_links 只能靠首个友链请求惰性创建。功能上不受影响(每个友链方法都会自行 ensure),但建议补上以保持多后端初始化行为一致。

As per path instructions:「存储适配器层……3. 多后端(D1/Turso/PostgreSQL)行为一致性」。

♻️ 建议改动
     await this.ensureCommentsTable();
+    await this.ensureFriendLinksTable();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/storage/db/turso.ts` around lines 186 - 203, Update
ensureCoreTables() to invoke ensureFriendLinksTable() during the unified
core-table initialization, matching D1Adapter.ensureSchema() while preserving
the existing lazy ensures in individual friend-link methods.

Source: Path instructions

server/src/storage/db/d1.ts (1)

234-251: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

建议复用 ensureSchema() 的缓存,避免每次友链读写都多发 2 条 DDL。

ensureFriendLinksTable() 已被纳入 ensureSchema()(Line 37)并由 schemaReady 缓存,但 Line 1049/1059/1068… 每个方法又单独调用它,导致每个友链请求在 D1 上额外产生 CREATE TABLE IF NOT EXISTS + CREATE INDEX IF NOT EXISTS 两次往返。虽然与文件内 ensureCommentsTable() 既有风格一致,但读路径(/api/friends 公开接口)会被明显放大。可考虑在方法内改为 await this.ensureSchema()

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/storage/db/d1.ts` around lines 234 - 251, Update
ensureFriendLinksTable to reuse the cached ensureSchema flow by calling await
this.ensureSchema() instead of issuing its own CREATE TABLE and CREATE INDEX
statements. Preserve the existing friend_links schema initialization through
ensureSchema and avoid adding separate DDL round trips for friend-link reads and
writes.
server/src/index.ts (1)

866-889: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

内存限流在 Workers 上只对单个 isolate 生效。

friendLinkAttempts 随 isolate 创建/回收,多实例间不共享,实际配额远高于 3 次/小时,且实例重启即清零。淘汰逻辑本身正确(Map.keys() 按插入顺序,近似 FIFO),对小站点足够。若后续需要可靠配额,建议迁移到 KV 或 Durable Object 计数器。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/index.ts` around lines 866 - 889, Replace the isolate-local
friendLinkAttempts rate limiter with shared persistence suitable for Workers,
such as a KV or Durable Object counter, so the 3-per-hour quota is enforced
across instances and survives isolate recycling. Preserve the existing window
and cleanup semantics from pruneFriendLinkAttempts and isFriendLinkRateLimited,
removing the in-memory Map dependency.
server/src/storage/db/postgres.ts (1)

1015-1032: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

公开读路径上每次都跑一遍完整 ensureCoreTables(),代价偏高。

ensureCoreTables() 会顺序执行约 20 条 CREATE TABLE IF NOT EXISTS / ALTER TABLE ... ADD COLUMN IF NOT EXISTS(Line 36-166),且没有像 D1Adapter.schemaReady 那样的一次性缓存。getApprovedFriendLinks() 服务的是公开的 /api/friends,每个请求都会带上这笔固定开销。建议给 PG 适配器也加一个 schemaReady: Promise<void> | null 缓存,读路径复用即可。

♻️ 建议的缓存写法
+  private schemaReady: Promise<void> | null = null;
+
+  private async ensureSchemaOnce(): Promise<void> {
+    if (!this.schemaReady) this.schemaReady = this.ensureCoreTables();
+    await this.schemaReady;
+  }
+
   async getApprovedFriendLinks(): Promise<FriendLink[]> {
-    await this.ensureCoreTables();
+    await this.ensureSchemaOnce();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/storage/db/postgres.ts` around lines 1015 - 1032, 为 Postgres
适配器增加与 D1Adapter.schemaReady 等效的 Promise<void> | null 一次性缓存,并让
ensureCoreTables() 复用该缓存,避免重复执行完整建表与迁移检查;确保 getApprovedFriendLinks() 和
getAllFriendLinks() 等读路径继续通过 ensureCoreTables() 获取初始化保障,但不会在每次请求中重复运行 schema
初始化。
server/src/db/schema.ts (1)

111-114: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Drizzle schema 声明的 url 具名唯一索引在任何实际 DDL 中都不存在。 迁移脚本与三个适配器的建表语句统一使用列级 url TEXT NOT NULL UNIQUE(产生匿名唯一索引),而两份 schema 声明的是具名 uniqueIndex。唯一性保证在运行时等价,但 drizzle-kit diff 会认为索引缺失并生成多余迁移。建议统一为其中一种写法。

  • server/src/db/schema.ts#L111-L114:将 uniqueIndex("friend_links_url_idx") 改为在列上使用 .unique(),或在 server/src/migrations/0010_friend_links.sqld1.ts/turso.tsensureFriendLinksTable() 中补 CREATE UNIQUE INDEX IF NOT EXISTS friend_links_url_idx ON friend_links(url)
  • server/src/db/schema-pg.ts#L108-L111:同样把 uniqueIndex("pg_friend_links_url_idx") 改为列级 .unique(),或在 postgres.tsensureCoreTables() 中补建同名唯一索引。
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/db/schema.ts` around lines 111 - 114, 统一 friend_links
的唯一约束声明与实际建表方式,避免 Drizzle 反复生成索引迁移:在 server/src/db/schema.ts 的 friend_links 定义中将
urlIdx 改为列级 unique 配置,并在 server/src/db/schema-pg.ts 的对应定义中将
pg_friend_links_url_idx 同样改为列级 unique;不要同时新增具名唯一索引 DDL,保留 statusIdx 及现有唯一性行为。
client/src/pages/friends.tsx (1)

37-43: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

移除 friends.tsx 中多余的 document.title 赋值。

<SeoHead title="友链" /> 已在 effect 中设置 document.title = "友链 | Monolith",父组件 own effect 先于子组件执行,可能导致标题先被父组件赋值、随后又被覆盖或产生不必要的副作用。保留 SeoHead 的 title 管理即可。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/pages/friends.tsx` around lines 37 - 43, Remove the redundant
document.title assignment from the useEffect in friends.tsx, leaving title
management to SeoHead. Keep the fetchFriends loading, success, error, and
completion behavior unchanged.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@client/src/lib/api.ts`:
- Around line 582-588: Update fetchAdminFriends to reuse the existing readError
helper when the response is unsuccessful, replacing the hardcoded “获取友链失败” error
while preserving the current successful res.json() behavior.

In `@client/src/pages/friends.tsx`:
- Around line 39-42: Update the friends page’s fetchFriends promise chain to
track and display a loading error instead of silently replacing failures with an
empty links list. Preserve setLinks([]) only as appropriate for failed state
handling, ensure the rendered UI distinguishes loading failure from “no
displayed links,” and keep setLoading(false) in the existing finally path.

In `@server/src/index.ts`:
- Around line 688-703: Update the catch block around createFriendLink in the
friend-link submission handler to return 409 only for a recognized
unique-constraint conflict. For all other errors, log the exception with the
existing request context and return a generic 500 response instead of the
URL-conflict message.

---

Nitpick comments:
In `@client/src/pages/friends.tsx`:
- Around line 37-43: Remove the redundant document.title assignment from the
useEffect in friends.tsx, leaving title management to SeoHead. Keep the
fetchFriends loading, success, error, and completion behavior unchanged.

In `@server/src/db/schema.ts`:
- Around line 111-114: 统一 friend_links 的唯一约束声明与实际建表方式,避免 Drizzle 反复生成索引迁移:在
server/src/db/schema.ts 的 friend_links 定义中将 urlIdx 改为列级 unique 配置,并在
server/src/db/schema-pg.ts 的对应定义中将 pg_friend_links_url_idx 同样改为列级
unique;不要同时新增具名唯一索引 DDL,保留 statusIdx 及现有唯一性行为。

In `@server/src/index.ts`:
- Around line 866-889: Replace the isolate-local friendLinkAttempts rate limiter
with shared persistence suitable for Workers, such as a KV or Durable Object
counter, so the 3-per-hour quota is enforced across instances and survives
isolate recycling. Preserve the existing window and cleanup semantics from
pruneFriendLinkAttempts and isFriendLinkRateLimited, removing the in-memory Map
dependency.

In `@server/src/storage/db/d1.ts`:
- Around line 234-251: Update ensureFriendLinksTable to reuse the cached
ensureSchema flow by calling await this.ensureSchema() instead of issuing its
own CREATE TABLE and CREATE INDEX statements. Preserve the existing friend_links
schema initialization through ensureSchema and avoid adding separate DDL round
trips for friend-link reads and writes.

In `@server/src/storage/db/postgres.ts`:
- Around line 1015-1032: 为 Postgres 适配器增加与 D1Adapter.schemaReady 等效的
Promise<void> | null 一次性缓存,并让 ensureCoreTables() 复用该缓存,避免重复执行完整建表与迁移检查;确保
getApprovedFriendLinks() 和 getAllFriendLinks() 等读路径继续通过 ensureCoreTables()
获取初始化保障,但不会在每次请求中重复运行 schema 初始化。

In `@server/src/storage/db/turso.ts`:
- Around line 186-203: Update ensureCoreTables() to invoke
ensureFriendLinksTable() during the unified core-table initialization, matching
D1Adapter.ensureSchema() while preserving the existing lazy ensures in
individual friend-link methods.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0a8cfb57-6e8e-4ad1-a539-bf1ffabd3ba3

📥 Commits

Reviewing files that changed from the base of the PR and between b06639d and 0555a1e.

📒 Files selected for processing (14)
  • client/src/app.tsx
  • client/src/components/admin-layout.tsx
  • client/src/components/navbar.tsx
  • client/src/lib/api.ts
  • client/src/pages/admin/friends.tsx
  • client/src/pages/friends.tsx
  • server/src/db/schema-pg.ts
  • server/src/db/schema.ts
  • server/src/index.ts
  • server/src/migrations/0010_friend_links.sql
  • server/src/storage/db/d1.ts
  • server/src/storage/db/postgres.ts
  • server/src/storage/db/turso.ts
  • server/src/storage/interfaces.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (4)
client/src/components/**

⚙️ CodeRabbit configuration file

client/src/components/**: 这是 React 前端组件目录。审查时请关注: 1. 是否同时兼容暗色和亮色主题(检查 CSS 变量和 data-theme) 2. 响应式布局是否完整(移动端/平板/桌面端) 3. 无障碍访问(aria 标签、键盘导航) 4. 组件是否保持单一职责

Files:

  • client/src/components/navbar.tsx
  • client/src/components/admin-layout.tsx
client/src/pages/**

⚙️ CodeRabbit configuration file

client/src/pages/**: 页面级组件。审查时请关注: 1. 数据加载和错误处理是否完善 2. SEO 相关(页面标题、meta 标签) 3. 导航和路由是否正确

Files:

  • client/src/pages/friends.tsx
  • client/src/pages/admin/friends.tsx
server/src/storage/**

⚙️ CodeRabbit configuration file

server/src/storage/**: 存储适配器层(数据库 + 对象存储)。审查时请关注: 1. 接口实现是否完整(IDatabase / IObjectStorage) 2. SQL 注入防护(参数化查询) 3. 多后端(D1/Turso/PostgreSQL)行为一致性 4. 错误处理和边界情况

Files:

  • server/src/storage/db/d1.ts
  • server/src/storage/db/turso.ts
  • server/src/storage/db/postgres.ts
  • server/src/storage/interfaces.ts
server/src/index.ts

⚙️ CodeRabbit configuration file

server/src/index.ts: Hono Workers API 路由总入口。审查时请关注: 1. JWT 认证中间件是否正确保护管理接口 2. CORS 配置是否安全 3. 请求参数验证

Files:

  • server/src/index.ts
🔇 Additional comments (22)
server/src/storage/interfaces.ts (2)

152-184: LGTM!


260-269: LGTM!

server/src/migrations/0010_friend_links.sql (1)

1-17: LGTM!

server/src/storage/db/d1.ts (4)

8-12: LGTM!

Also applies to: 37-37, 72-72


1030-1065: LGTM!


1067-1118: LGTM!


1120-1145: LGTM!

server/src/storage/db/postgres.ts (2)

118-135: LGTM!

Also applies to: 223-225


1034-1112: LGTM!

server/src/storage/db/turso.ts (1)

968-1085: LGTM!

server/src/index.ts (5)

12-12: LGTM!

Also applies to: 136-159, 161-198


646-663: LGTM!


1427-1498: LGTM!

Also applies to: 1500-1505


844-847: 📐 Maintainability & Code Quality

无需修改:friendLinkAttempts 已只声明一次。


1422-1425: 🔒 Security & Privacy

无需修改:/api/admin/friends* 已在 /api/admin/* JWT 前缀下受保护。

client/src/lib/api.ts (2)

68-106: LGTM!

Also applies to: 122-135


590-642: LGTM!

client/src/pages/friends.tsx (1)

45-181: LGTM!

client/src/app.tsx (1)

17-25: LGTM!

Also applies to: 171-171, 221-221

client/src/components/navbar.tsx (1)

15-15: LGTM!

client/src/components/admin-layout.tsx (1)

8-8: LGTM!

Also applies to: 33-33

client/src/pages/admin/friends.tsx (1)

85-345: LGTM!

Comment thread client/src/lib/api.ts
Comment thread client/src/pages/friends.tsx
Comment thread server/src/index.ts
@one-ea
one-ea merged commit 82c28f1 into main Jul 25, 2026
10 checks passed
@one-ea
one-ea deleted the fix/restore-friend-links branch July 25, 2026 14:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend 后端 (Hono Workers) 相关变更 feature 新功能 frontend 前端 (React/Vite) 相关变更

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant