Skip to content

Database setup#2

Merged
lijianhua7 merged 5 commits into
mainfrom
database-setup
Apr 21, 2026
Merged

Database setup#2
lijianhua7 merged 5 commits into
mainfrom
database-setup

Conversation

@lijianhua7

@lijianhua7 lijianhua7 commented Apr 20, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

发布说明

  • 新功能

    • 添加了书籍上传功能,支持PDF文件和封面图片上传
    • 实现了PDF文本自动提取和分段处理
    • 引入全应用范围的消息提示通知系统
    • 支持根据系统主题自动调整通知样式
  • 改进

    • 优化了根页面性能,改用异步数据加载
    • 扩展了远程图片来源支持范围
    • 增强了表单验证和文件大小/类型检查

@vercel

vercel Bot commented Apr 20, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
bookified Error Error Apr 20, 2026 11:24pm

@coderabbitai

coderabbitai Bot commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@lijianhua7 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 15 minutes and 30 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 15 minutes and 30 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d43f0b3f-27ad-4518-af08-668699d4252a

📥 Commits

Reviewing files that changed from the base of the PR and between fdf60ff and d4b998e.

📒 Files selected for processing (3)
  • lib/actions/book.actions.ts
  • lib/utils.ts
  • next.config.ts
📝 Walkthrough

概述

此PR为应用添加了完整的书籍上传和管理功能,包括异步书籍数据获取、服务端PDF上传处理、数据库模型定义、PDF解析和分段、以及全应用的通知系统。新增MongoDB连接、书籍相关的服务端操作,并更新了依赖和配置。

变更清单

内容区分 / 文件 变更摘要
页面与布局更新
app/(root)/page.tsx, app/layout.tsx
将首页从静态样本数据改为异步获取所有书籍;添加全应用通知组件Toaster。
API与表单处理
app/api/upload/route.ts, components/UploadForm.tsx
新增POST上传API端点,支持身份验证和文件类型校验;重构UploadForm组件以支持PDF解析、文件验证、数据库操作和完整的上传流程。
通知系统
components/ui/sonner.tsx
新增客户端Toaster组件,集成next-themes主题支持和lucide-react图标。
数据库模型
database/models/book.model.ts, database/models/book-segment.model.ts, database/models/voice-session.model.ts, database/mongoose.ts
添加三个新Mongoose模型(Book、BookSegment、VoiceSession)及MongoDB连接工具,支持热重载和schema索引。
服务端业务逻辑
lib/actions/book.actions.ts
实现四个服务端操作:getAllBooks、checkBookExists、createBook、saveBookSegments,支持完整的CRUD和数据校验。
工具函数与常数
lib/utils.ts, lib/constants.ts
添加七个新实用工具(PDF解析、文本分段、slug生成、序列化等);移除ASSISTANT_ID常数导出,更新中文注释。
配置与依赖
next.config.ts, package.json
配置Server Actions请求体大小限制,添加远程图像域;添加六个新依赖库。

序列流程图

sequenceDiagram
    actor User
    participant Client as 浏览器客户端
    participant Form as UploadForm组件
    participant API as POST /api/upload
    participant Blob as Vercel Blob存储
    participant DB as MongoDB数据库
    participant Server as 服务端操作

    User->>Form: 选择PDF和封面上传
    Form->>Form: 验证文件类型和大小
    Form->>Client: 解析PDF文件
    Client->>Client: 提取文本和封面
    Form->>API: 上传PDF文件
    API->>Blob: 存储PDF
    Blob->>API: 返回文件URL
    Form->>API: 上传封面图片
    API->>Blob: 存储封面
    Blob->>API: 返回图像URL
    Form->>Server: createBook(书籍元数据)
    Server->>DB: 检查书籍是否存在
    DB->>Server: 返回查询结果
    Server->>DB: 创建书籍记录
    DB->>Server: 返回书籍ID
    Form->>Server: saveBookSegments(分段内容)
    Server->>DB: 批量插入文本分段
    DB->>Server: 确认插入
    Server->>DB: 更新书籍总分段数
    DB->>Server: 更新完成
    Server->>Form: 返回成功响应
    Form->>Client: 显示成功提示
    Client->>User: 重定向到首页
Loading

代码审查工作量

🎯 4 (复杂) | ⏱️ ~45 分钟

相关PR

诗歌

🐰 书籍上传新功能启动,
PDF解析和数据库驻足,
Vercel存储守护每一页,
MongoDB整齐收藏分段美,
通知系统轻声呢喃,
异步操作舞动代码之间,
阅读之旅,从此开启!📚✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive 标题"Database setup"过于宽泛,未能准确反映拉取请求的主要变化。该PR包含数据库模型、服务器操作、PDF处理、文件上传、UI组件等大量功能更新,而标题仅提及数据库设置。 建议使用更具体的标题,如"Add MongoDB models, server actions, and file upload functionality"或"Implement book upload and PDF processing with database integration",以更好地概括PR的核心目标。
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch database-setup

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 and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
components/UploadForm.tsx (1)

30-52: ⚠️ Potential issue | 🟠 Major

不要把展示用语音名保存为 persona

data.voice 当前是 Dave/Rachel 这类展示 ID,但 lib/utils.tsgetVoice() 只按 voiceOptions 的小写 key(如 dave)或 ElevenLabs voice id 匹配;保存后会回退到默认 rachel,用户选择的声音会被忽略。

🐛 建议修复:让表单保存 `voiceOptions` 的 key
-      id: "Dave",
+      id: "dave",
       name: "戴夫 (Dave)",
@@
-      id: "Daniel",
+      id: "daniel",
       name: "丹尼尔 (Daniel)",
@@
-    { id: "Chris", name: "克里斯 (Chris)", desc: "男性,休闲随和" },
+    { id: "chris", name: "克里斯 (Chris)", desc: "男性,休闲随和" },
@@
-      id: "Rachel",
+      id: "rachel",
       name: "瑞秋 (Rachel)",
@@
-    { id: "Sarah", name: "莎拉 (Sarah)", desc: "年轻女性,美式口音,温柔亲切" },
+    { id: "sarah", name: "莎拉 (Sarah)", desc: "年轻女性,美式口音,温柔亲切" },

Also applies to: 152-162

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/UploadForm.tsx` around lines 30 - 52, The form is saving display
IDs like "Dave"/"Rachel" into data.voice which don't match getVoice()'s expected
voiceOptions keys (e.g., "dave"), causing fallback to default; update the
UploadForm so it stores the voiceOptions key instead of the display name: when
building the VOICES select (or handling selection), use the voiceOptions key
(lowercased id or the actual key from voiceOptions) as the option value and set
data.voice to that key; ensure getVoice() can find the selection by matching
against voiceOptions keys and ElevenLabs ids (reference: VOICES, data.voice,
getVoice(), voiceOptions).
🧹 Nitpick comments (4)
database/mongoose.ts (2)

3-5: MONGODB_URI 缺失的校验改为懒校验。

在模块顶层直接 throw 会在 import 时 立即失败。Next.js 在 next build 过程(以及某些工具化场景,比如生成路由清单)中可能会在没有完整环境变量的情况下 import 该模块,导致整个构建失败;而实际运行时根本用不到数据库的页面(比如静态的 404/500、纯前端路由)也被拖累。

建议把校验挪到 connectToDatabase() 首次调用时执行:

♻️ 建议修改
-const MONGODB_URI = process.env.MONGODB_URI;
-
-if (!MONGODB_URI) throw new Error("请配置MONGODB_URI 环境变量");
+const MONGODB_URI = process.env.MONGODB_URI;
...
 export const connectToDatabase = async () => {
+  if (!MONGODB_URI) throw new Error("请配置 MONGODB_URI 环境变量");
   if (cached.conn) return cached.conn;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@database/mongoose.ts` around lines 3 - 5, Remove the top-level throw for
MONGODB_URI so importing the module won't fail; instead, move the existence
check into the connectToDatabase() function (or perform a lazy check the first
time it runs) and throw a descriptive Error there if process.env.MONGODB_URI is
missing. Update any places that read the MONGODB_URI constant inside
connectToDatabase to read process.env.MONGODB_URI (or lazily initialize the
constant inside the function) so the module can be imported during build-time
without a DB config but still errors at runtime when a DB connection is actually
attempted.

16-18: let 可改为 const

cached 被绑定到 global.mongooseCache 的引用后不再重新赋值(只改它的字段),用 const 更能表达意图并避免后续误赋值。

-let cached =
-  global.mongooseCache ||
-  (global.mongooseCache = { conn: null, promise: null });
+const cached =
+  global.mongooseCache ||
+  (global.mongooseCache = { conn: null, promise: null });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@database/mongoose.ts` around lines 16 - 18, The variable declaration for
cached should be changed from let to const because cached is assigned once to
global.mongooseCache and only its properties (conn, promise) are mutated; update
the declaration that creates cached (the expression using global.mongooseCache)
to use const cached = ... and ensure no later code reassigns cached (only mutate
cached.conn or cached.promise) so the code remains correct and intent is
explicit.
next.config.ts (1)

16-19: 避免硬编码 Vercel Blob 的 store 主机名。

mgjacvezc1sbmenp.public.blob.vercel-storage.com 是与具体 Blob store 绑定的随机子域;一旦团队切换/重建 store,或不同环境(dev/preview/prod)使用不同 store,此处就会静默失败(图片 403/被 next/image 拒绝优化)。

建议改为通配 Vercel Blob 的公共子域,或从环境变量注入:

♻️ 建议修改
-      {
-        protocol: "https",
-        hostname: "mgjacvezc1sbmenp.public.blob.vercel-storage.com",
-      },
+      {
+        protocol: "https",
+        hostname: "*.public.blob.vercel-storage.com",
+      },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@next.config.ts` around lines 16 - 19, The remotePatterns entry currently
hardcodes the Vercel Blob subdomain (the object with protocol:"https" and
hostname:"mgjacvezc1sbmenp.public.blob.vercel-storage.com"); replace that fixed
hostname with a resilient value: either a wildcard public blob pattern (e.g.,
hostname:"*.public.blob.vercel-storage.com" or
"**.public.blob.vercel-storage.com" depending on Next.js version) or read the
host from an env var (e.g., process.env.VERCEL_BLOB_HOST) and use that when
building the remotePatterns object so different stores/environments won’t break
image loading.
package.json (1)

21-22: mongodb 作为显式依赖是多余的,应从 dependencies 中移除。

代码中无直接使用 mongodb 原生驱动的地方,而 mongoose 9.3.3 已内置并锁定了兼容的 mongodb ~7.1 版本。显式声明 mongodb ^7.1.1 会导致:

  1. 包管理器安装重复的依赖
  2. 不必要的版本管理复杂性
  3. 在涉及 ObjectId/BSONinstanceof 检查时可能出现类型不匹配的隐患

统一通过 mongoose 暴露的 API(如 mongoose.Types.ObjectId)即可满足所有需求。

♻️ 建议修改
-    "mongodb": "^7.1.1",
     "mongoose": "^9.3.3",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` around lines 21 - 22, Remove the explicit "mongodb" dependency
from package.json since the codebase does not use the native driver and mongoose
(version "mongoose": "^9.3.3") already provides and pins a compatible mongodb
implementation; update package.json to drop the "mongodb" entry and ensure all
code uses mongoose's exports (e.g., mongoose.Types.ObjectId) rather than
importing from "mongodb" to avoid duplicate installs and instanceof/BSON
mismatches.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/`(root)/page.tsx:
- Around line 5-7: The Page server component currently treats a failed
getAllBooks() the same as an empty result, losing error visibility; update Page
to detect when bookResult.success is false (check bookResult.error or similar),
record/report the error (e.g., call a logging/reporting helper) and pass an
explicit error flag or error message down to the UI instead of converting to an
empty array (i.e., keep books = bookResult.data ?? [] only when success is true,
otherwise set an error state like booksError and render a retry/error UI),
referencing the Page function and the bookResult variable to locate where to add
logging/reporting and the new error prop for the UI.

In `@app/api/upload/route.ts`:
- Around line 23-32: 当前上传路由在生成上传 token 时一律使用 maximumSizeInBytes: MAX_FILE_SIZE
导致服务端对图片没有 10MB 限制;请在 onBeforeGenerateToken 中根据请求的内容类型(或
pathname)判断若为图片类型(image/jpeg, image/png, image/webp)则将 maximumSizeInBytes 设置为 10
* 1024 * 1024,若为 application/pdf 则设置为 50 * 1024 * 1024,确保 tokenPayload/返回的 token
包含该限制;如果当前 `@vercel/blob` 版本不支持为不同类型动态设置 maximumSizeInBytes,则改为实现两个独立端点(例如
upload/pdf 和 upload/cover)各自返回不同的 maximumSizeInBytes,并在 onUploadCompleted
中再次校验实际文件大小
与对应类型的最大值(参考符号:onBeforeGenerateToken,onUploadCompleted,allowedContentTypes,maximumSizeInBytes,MAX_FILE_SIZE,tokenPayload)。

In `@components/UploadForm.tsx`:
- Around line 123-149: The uploaded PDF and cover blobs (uploadedPdfBlob,
uploadedCoverBlob, coverUrl produced by upload()) are not cleaned up when
createBook() fails, returns alreadyExists, or saveBookSegments() throws, leaving
orphaned public blobs; import del from "@vercel/blob" and call
del([uploadedPdfBlob.url, coverUrl]) in each failure path (the catch around
createBook(), the branch handling createBook returning alreadyExists: true, and
the catch around saveBookSegments()) or invoke a cleanup API endpoint from those
failure handlers so both uploadedPdfBlob.url and coverUrl are deleted on any
failure.

In `@database/models/book-segment.model.ts`:
- Around line 7-18: The single-field indexes on bookId, segmentIndex and
pageNumber are redundant because the compound indexes
BookSegmentSchema.index({bookId:1, segmentIndex:1}) and
BookSegmentSchema.index({bookId:1, pageNumber:1}) already cover those prefixes;
remove the per-field index flags (remove index: true from bookId, segmentIndex
and pageNumber in the schema) so only the compound indexes remain, reducing
write overhead and storage while preserving query plans that use the
bookId-prefixed compound indexes.

In `@database/models/book.model.ts`:
- Around line 6-8: The schema currently enforces slug: { unique: true } which
conflicts with per-user books; remove the global unique constraint on the slug
field in the Book schema (retain lowercase/trim) and instead add a compound
unique index on { clerkId, slug } to enforce slug-uniqueness per user; then
update the existence check in the Book-creation flow (the Book.findOne lookup
used in the create flow) to include clerkId (use Book.findOne({ clerkId:
data.clerkId, slug })) so lookups and uniqueness align with the per-user design.

In `@lib/actions/book.actions.ts`:
- Around line 19-24: The catch blocks in lib/actions/book.actions.ts currently
return the raw Error object (error: e), which cannot be JSON-serialized for
Next.js Server Actions; update each catch so you keep the server-side
console.error(e) but return a serializable error string instead (e.g. error:
e?.message ?? String(e)) for all failing returns in this file (the catch blocks
around the book action functions), applying the same change at the four
occurrences noted so every server action returns a plain message string rather
than the Error object.
- Around line 57-77: The createBook and saveBookSegments functions must stop
trusting client-supplied clerkId/bookId: import and call auth() server-side to
derive the authenticated userId, remove clerkId from the functions' parameters
(derive it inside), and update all DB operations (Book.create, Book.findOne,
Book.findOneAndDelete, BookSegment.deleteMany, Book.findOneAndUpdate, etc.) to
include ownership constraints (e.g., { _id: bookId, clerkId: userId } or at
minimum { clerkId: userId }) so deletes and queries only affect the caller's
records; also ensure any rollback deletes in saveBookSegments use the same
ownership-constrained queries rather than deleting by bookId alone.

In `@lib/utils.ts`:
- Around line 45-65: The current word-based splitting (using words =
text.split(/\s+/)) treats long CJK strings as a single "word" and can create an
oversized segment; modify the logic around words/segments to detect
no-whitespace or single-word inputs (e.g., if words.length === 1 &&
!/\s/.test(text) or when the single word length > segmentSize) and fall back to
character-level splitting using a Unicode-safe splitter (Array.from(text)) to
build segmentWords/segmentText and then push segments with the same
segmentIndex/wordCount semantics; ensure overlapSize and segmentSize are applied
to the character array the same way as the word array so functions/variables
like words, segmentSize, overlapSize, segments, segmentIndex, startIndex are
reused/maintained.
- Around line 108-150: Wrap the PDF processing (getting pdfDocument, rendering
firstPage, extracting text, etc.) in a try/finally so pdfDocument.destroy() is
always awaited on the finally path; specifically, ensure pdfDocument is declared
before the try, do all work (getPage, render, getTextContent, splitIntoSegments)
inside try, and in finally check if pdfDocument is truthy and await
pdfDocument.destroy() to guarantee worker/resources are released even when
firstPage.render or page.getTextContent throws.
- Around line 16-23: The generateSlug function currently strips non-ASCII
characters because the replace(/[^\w\s-]/g, '') removes all Unicode letters
(causing empty slugs for Chinese); update generateSlug to preserve Unicode
letters and numbers by using Unicode property escapes (e.g. allow \p{L} and
\p{N} with the u flag) when removing invalid chars and when collapsing
whitespace so Chinese/Japanese/Korean characters remain (keep the existing
extension-strip, toLowercase, trim, and dash-collapse logic). Also ensure
pdfDocument.destroy() is always called by moving it into a finally block (or
adding cleanup in the catch) where pdf extraction happens so resources aren’t
leaked, and improve splitIntoSegments to handle CJK languages (either by using a
language-aware tokenizer or by treating CJK characters/grapheme clusters as
separate tokens or inserting soft breaks for chunking) so Chinese text isn’t
emitted as a single giant segment; refer to generateSlug, pdfDocument.destroy,
and splitIntoSegments to locate the changes.

In `@next.config.ts`:
- Around line 5-9: The experimental.serverActions.bodySizeLimit entry in
next.config.ts is ineffective because your upload logic lives in the Route
Handler (app/api/upload/route.ts) and uses Vercel Blob's handleUpload, not
Server Actions; remove the experimental.serverActions.bodySizeLimit
configuration from next.config.ts (or replace it with a platform-level request
size limit) and ensure the Route Handler uses the existing MAX_FILE_SIZE by
setting maximumSizeInBytes in the onBeforeGenerateToken hook (refer to
onBeforeGenerateToken, maximumSizeInBytes, MAX_FILE_SIZE and handleUpload to
locate the relevant code to update).

---

Outside diff comments:
In `@components/UploadForm.tsx`:
- Around line 30-52: The form is saving display IDs like "Dave"/"Rachel" into
data.voice which don't match getVoice()'s expected voiceOptions keys (e.g.,
"dave"), causing fallback to default; update the UploadForm so it stores the
voiceOptions key instead of the display name: when building the VOICES select
(or handling selection), use the voiceOptions key (lowercased id or the actual
key from voiceOptions) as the option value and set data.voice to that key;
ensure getVoice() can find the selection by matching against voiceOptions keys
and ElevenLabs ids (reference: VOICES, data.voice, getVoice(), voiceOptions).

---

Nitpick comments:
In `@database/mongoose.ts`:
- Around line 3-5: Remove the top-level throw for MONGODB_URI so importing the
module won't fail; instead, move the existence check into the
connectToDatabase() function (or perform a lazy check the first time it runs)
and throw a descriptive Error there if process.env.MONGODB_URI is missing.
Update any places that read the MONGODB_URI constant inside connectToDatabase to
read process.env.MONGODB_URI (or lazily initialize the constant inside the
function) so the module can be imported during build-time without a DB config
but still errors at runtime when a DB connection is actually attempted.
- Around line 16-18: The variable declaration for cached should be changed from
let to const because cached is assigned once to global.mongooseCache and only
its properties (conn, promise) are mutated; update the declaration that creates
cached (the expression using global.mongooseCache) to use const cached = ... and
ensure no later code reassigns cached (only mutate cached.conn or
cached.promise) so the code remains correct and intent is explicit.

In `@next.config.ts`:
- Around line 16-19: The remotePatterns entry currently hardcodes the Vercel
Blob subdomain (the object with protocol:"https" and
hostname:"mgjacvezc1sbmenp.public.blob.vercel-storage.com"); replace that fixed
hostname with a resilient value: either a wildcard public blob pattern (e.g.,
hostname:"*.public.blob.vercel-storage.com" or
"**.public.blob.vercel-storage.com" depending on Next.js version) or read the
host from an env var (e.g., process.env.VERCEL_BLOB_HOST) and use that when
building the remotePatterns object so different stores/environments won’t break
image loading.

In `@package.json`:
- Around line 21-22: Remove the explicit "mongodb" dependency from package.json
since the codebase does not use the native driver and mongoose (version
"mongoose": "^9.3.3") already provides and pins a compatible mongodb
implementation; update package.json to drop the "mongodb" entry and ensure all
code uses mongoose's exports (e.g., mongoose.Types.ObjectId) rather than
importing from "mongodb" to avoid duplicate installs and instanceof/BSON
mismatches.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 9028c117-32e1-4b5e-8ab1-5c48035cf2a1

📥 Commits

Reviewing files that changed from the base of the PR and between 07d29d3 and fdf60ff.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (14)
  • app/(root)/page.tsx
  • app/api/upload/route.ts
  • app/layout.tsx
  • components/UploadForm.tsx
  • components/ui/sonner.tsx
  • database/models/book-segment.model.ts
  • database/models/book.model.ts
  • database/models/voice-session.model.ts
  • database/mongoose.ts
  • lib/actions/book.actions.ts
  • lib/constants.ts
  • lib/utils.ts
  • next.config.ts
  • package.json

Comment thread app/(root)/page.tsx
Comment on lines +5 to +7
export default async function Page() {
const bookResult = await getAllBooks();
const books = bookResult.success ? bookResult.data ?? [] : [];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

数据获取失败时用户无感知。

getAllBooks() 返回 success: false(数据库连接异常等)时,页面会静默渲染成"无书"状态,看起来和"空书架"完全一致,既误导用户也让前端难以排障。建议至少:

  • 在服务端记录/上报错误;
  • 或向下游 UI 传递一个错误状态以渲染出明确的错误/重试区块,而不是伪装成空集合。
-  const bookResult = await getAllBooks();
-  const books = bookResult.success ? bookResult.data ?? [] : [];
+  const bookResult = await getAllBooks();
+  if (!bookResult.success) {
+    // TODO: 渲染带重试按钮的错误态
+    console.error("获取书籍失败", bookResult.error);
+  }
+  const books = bookResult.success ? bookResult.data ?? [] : [];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export default async function Page() {
const bookResult = await getAllBooks();
const books = bookResult.success ? bookResult.data ?? [] : [];
export default async function Page() {
const bookResult = await getAllBooks();
if (!bookResult.success) {
// TODO: 渲染带重试按钮的错误态
console.error("获取书籍失败", bookResult.error);
}
const books = bookResult.success ? bookResult.data ?? [] : [];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(root)/page.tsx around lines 5 - 7, The Page server component currently
treats a failed getAllBooks() the same as an empty result, losing error
visibility; update Page to detect when bookResult.success is false (check
bookResult.error or similar), record/report the error (e.g., call a
logging/reporting helper) and pass an explicit error flag or error message down
to the UI instead of converting to an empty array (i.e., keep books =
bookResult.data ?? [] only when success is true, otherwise set an error state
like booksError and render a retry/error UI), referencing the Page function and
the bookResult variable to locate where to add logging/reporting and the new
error prop for the UI.

Comment thread app/api/upload/route.ts
Comment on lines +23 to +32
return {
allowedContentTypes: [
"application/pdf",
"image/jpeg",
"image/png",
"image/webp",
],
addRandomSuffix: true,
maximumSizeInBytes: MAX_FILE_SIZE,
tokenPayload: JSON.stringify({ userId }),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 预期:能看到前端/常量存在 10MB 图片限制,但上传路由只使用 50MB MAX_FILE_SIZE。
rg -n -C3 'MAX_IMAGE_SIZE|MAX_FILE_SIZE|maximumSizeInBytes|allowedContentTypes|handleUpload' app/api/upload/route.ts lib/constants.ts components/UploadForm.tsx

Repository: lijianhua7/bookified

Length of output: 4923


🌐 Web query:

@vercel/blob handleUpload onBeforeGenerateToken maximumSizeInBytes content type pathname documentation

💡 Result:

The handleUpload function from @vercel/blob/client is used in server routes for client-side uploads to Vercel Blob. The onBeforeGenerateToken callback is required and receives pathname (the proposed blob path), clientPayload (optional string from client), and possibly multipart (from examples). It authenticates/authorizes the upload and returns an object configuring the client token. Supported return options from official docs and examples include: - allowedContentTypes: array of MIME types (e.g., ['image/jpeg', 'image/png']) - restricts file types. - tokenPayload: string payload sent to onUploadCompleted. - validUntil: timestamp (number) for token expiration (e.g., Math.floor(Date.now / 1000) + 3600). - addRandomSuffix: boolean to append random suffix to pathname. - pathname: string to override the pathname. - access: 'public' or 'private'. - callbackUrl: string for custom upload completion webhook. maximumSizeInBytes (number, e.g., 100_000_000 for 100MB) appears in GitHub issue examples (vercel/storage #664) for limiting upload size, but is not documented in official Vercel docs (as of 2026-02 sources). contentType can be set explicitly (from StackOverflow examples), otherwise inferred from pathname extension or file. Example usage: onBeforeGenerateToken: async (pathname, clientPayload) => { // Auth check here return { allowedContentTypes: ['image/*'], maximumSizeInBytes: 100_000_000, // From community examples tokenPayload: JSON.stringify({ userId: '123' }), addRandomSuffix: true }; } Primary docs: https://vercel.com/docs/vercel-blob/client-upload and https://vercel.com/docs/vercel-blob/using-blob-sdk. For full options, check handleUpload SDK reference linked in client upload docs (not fully surfaced in search snippets).

Citations:


🏁 Script executed:

# Check if onUploadCompleted performs any server-side size validation
rg -n -A10 'onUploadCompleted|uploadedPdfBlob|uploadedCoverBlob' app/api/upload/route.ts

Repository: lijianhua7/bookified

Length of output: 458


在服务端按内容类型分别限制文件大小。

当前上传路由对所有文件类型使用统一的 maximumSizeInBytes: MAX_FILE_SIZE(50MB),但前端对图片强制实施 10MB 限制。通过绕过客户端表单验证,可上传 50MB 的图片文件,违反设计限制。onUploadCompleted 回调中也无服务端大小验证,该限制完全得不到保障。

建议方案:
onBeforeGenerateToken 中根据 allowedContentTypespathname 检查内容类型,为图片返回 maximumSizeInBytes: 10 * 1024 * 1024,为 PDF 返回 50MB。若 @vercel/blob 当前版本不支持此参数或行为不符预期,建议拆分为两个独立上传端点(/api/upload/pdf/api/upload/cover)分别限制大小。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/upload/route.ts` around lines 23 - 32, 当前上传路由在生成上传 token 时一律使用
maximumSizeInBytes: MAX_FILE_SIZE 导致服务端对图片没有 10MB 限制;请在 onBeforeGenerateToken
中根据请求的内容类型(或 pathname)判断若为图片类型(image/jpeg, image/png, image/webp)则将
maximumSizeInBytes 设置为 10 * 1024 * 1024,若为 application/pdf 则设置为 50 * 1024 *
1024,确保 tokenPayload/返回的 token 包含该限制;如果当前 `@vercel/blob` 版本不支持为不同类型动态设置
maximumSizeInBytes,则改为实现两个独立端点(例如 upload/pdf 和 upload/cover)各自返回不同的
maximumSizeInBytes,并在 onUploadCompleted 中再次校验实际文件大小
与对应类型的最大值(参考符号:onBeforeGenerateToken,onUploadCompleted,allowedContentTypes,maximumSizeInBytes,MAX_FILE_SIZE,tokenPayload)。

Comment thread components/UploadForm.tsx
Comment on lines +123 to +149
const uploadedPdfBlob = await upload(fileTitle, pdfFile, {
access: "public",
handleUploadUrl: "/api/upload",
contentType: "application/pdf",
})

// 上传封面
let coverUrl: string;

if (data.coverImage) {
const coverFile = data.coverImage;
const uploadedCoverBlob = await upload(`${fileTitle}_cover.png`, coverFile, {
access: "public",
handleUploadUrl: "/api/upload",
contentType: coverFile.type,
})
coverUrl = uploadedCoverBlob.url;
} else {
const response = await fetch(parsedPdf.cover);
const blob = await response.blob();

const uploadedCoverBlob = await upload(`${fileTitle}_cover.png`, blob, {
access: "public",
handleUploadUrl: "/api/upload",
contentType: "image/png",
})
coverUrl = uploadedCoverBlob.url;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 检查项目里是否已有 Vercel Blob 删除或上传回滚逻辑。
rg -n -C3 --type=ts --type=tsx '\b(upload|del|deleteBlob|createBook|saveBookSegments)\b'

Repository: lijianhua7/bookified

Length of output: 91


🌐 Web query:

@vercel/blob delete blob del API documentation

💡 Result:

To delete a blob using Vercel Blob (@vercel/blob), use the del function from the SDK. It supports deleting a single blob or multiple blobs by URL or pathname. ## SDK Usage (del method) Install: npm install @vercel/blob Basic example (single blob): import { del } from '@vercel/blob'; await del('https://example.blob.vercel-storage.com/blob-id'); // or by pathname await del('/path/to/blob'); Multiple blobs: await del([ 'https://blob1.vercel-storage.com/...', 'https://blob2.vercel-storage.com/...', '/path/to/blob' ]); Options: - token: Read-write token (defaults to BLOB_READ_WRITE_TOKEN env var) - ifMatch: ETag for conditional delete (throws BlobPreconditionFailedError if mismatch; single URL only) - abortSignal: To cancel the operation Returns void. Deletes are free (no billing, but count toward rate limits). Due to caching, blobs may take up to 60 seconds to disappear from CDN. Example API route: import { del } from '@vercel/blob'; export async function DELETE(request: Request) { const { searchParams } = new URL(request.url); const urlToDelete = searchParams.get('url')!; await del(urlToDelete); return new Response(null, { status: 200 }); } For bulk delete all blobs, use list + del in batches with rate limit handling. ## CLI Usage vercel blob del images/old-logo.png or by URL/pathname. ## Dashboard Delete files via the file browser in Vercel Dashboard > Storage > Blob store. Note: Cannot delete stores directly via SDK/CLI del; use vercel blob delete-store or dashboard. Official docs: vercel.com/docs/vercel-blob (overview), vercel.com/docs/[storage/]vercel-blob/using-blob-sdk (SDK reference with del details), vercel.com/docs/cli/blob (CLI).

Citations:


🏁 Script executed:

#!/bin/bash
# 搜索上传、删除、createBook、saveBookSegments 相关逻辑
# 使用正确的语法(--type ts 而不是 tsx)
rg -n '\b(upload|del|deleteBlob|createBook|saveBookSegments)\b' --type ts

Repository: lijianhua7/bookified

Length of output: 2993


🏁 Script executed:

#!/bin/bash
# 也在 tsx 文件中搜索(不用 --type)
rg -n '\b(upload|del|deleteBlob|createBook|saveBookSegments)\b' '.*\.(tsx|ts)$'

Repository: lijianhua7/bookified

Length of output: 117


🏁 Script executed:

#!/bin/bash
# 读取 UploadForm.tsx 文件内容,查看完整的上传和错误处理逻辑
cat -n components/UploadForm.tsx | head -200

Repository: lijianhua7/bookified

Length of output: 6788


🏁 Script executed:

#!/bin/bash
# 检查是否存在任何 cleanup、rollback、error handler 等相关逻辑
rg -n 'catch|error|cleanup|rollback|unlink|delete|remove' components/ --type ts

Repository: lijianhua7/bookified

Length of output: 1202


在上传失败时清理已上传的 Blob。

PDF 和封面上传后(第 123-149 行),若 createBook() 失败(第 166 行)、返回 alreadyExists: true(第 170 行)或 saveBookSegments() 失败(第 189 行),这些公开 Blob 会变成孤儿对象。当前代码缺少清理逻辑,这些文件会持续产生存储成本和隐私风险。

建议:

  1. 在导入中添加 import { del } from "@vercel/blob"
  2. 在失败分支中调用 del([uploadedPdfBlob.url, coverUrl]) 进行清理
  3. 或在 API 路由中实现清理端点,从 try-catch 块调用
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/UploadForm.tsx` around lines 123 - 149, The uploaded PDF and cover
blobs (uploadedPdfBlob, uploadedCoverBlob, coverUrl produced by upload()) are
not cleaned up when createBook() fails, returns alreadyExists, or
saveBookSegments() throws, leaving orphaned public blobs; import del from
"@vercel/blob" and call del([uploadedPdfBlob.url, coverUrl]) in each failure
path (the catch around createBook(), the branch handling createBook returning
alreadyExists: true, and the catch around saveBookSegments()) or invoke a
cleanup API endpoint from those failure handlers so both uploadedPdfBlob.url and
coverUrl are deleted on any failure.

Comment on lines +7 to +18
bookId: {type: Schema.Types.ObjectId, ref: "Book", required: true, index: true},
content: {type: String, required: true},
segmentIndex: {type: Number, required: true, index: true},
pageNumber: {type: Number, index: true},
wordCount: {type: Number, required: true},
}, {timestamps: true})

// 为常见查询场景创建复合字段索引,上面定义的"index: true"是单字段索引
// 当 Vapi 朗读一本书时,它会按顺序获取段落。而这个复合索引可以让查找即时完成,而不是扫描数据集中的每个数据段。
BookSegmentSchema.index({bookId: 1, segmentIndex: 1}, {unique: true});
BookSegmentSchema.index({bookId: 1, pageNumber: 1});
BookSegmentSchema.index({bookId: 1, content: 'text'});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

字段上的单字段索引与下面的复合索引重复,建议删除。

  • bookId 的单字段索引已被复合索引 {bookId:1, segmentIndex:1}(以及其他 bookId 前缀的复合索引)完全覆盖,MongoDB 可以直接用复合索引的前缀匹配。
  • segmentIndex 单独作为查询键的场景极少(几乎总会带 bookId 作为条件),同样被 {bookId:1, segmentIndex:1} 覆盖。
  • pageNumber 同理被 {bookId:1, pageNumber:1} 覆盖。

保留这些重复索引会放大写入代价(每次 insertMany 都要维护更多 B-tree)并占用额外存储/内存,却不带来查询收益。

♻️ 建议修改
-    bookId: {type: Schema.Types.ObjectId, ref: "Book", required: true, index: true},
+    bookId: {type: Schema.Types.ObjectId, ref: "Book", required: true},
     content: {type: String, required: true},
-    segmentIndex: {type: Number, required: true, index: true},
-    pageNumber: {type: Number, index: true},
+    segmentIndex: {type: Number, required: true},
+    pageNumber: {type: Number},
     wordCount: {type: Number, required: true},
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
bookId: {type: Schema.Types.ObjectId, ref: "Book", required: true, index: true},
content: {type: String, required: true},
segmentIndex: {type: Number, required: true, index: true},
pageNumber: {type: Number, index: true},
wordCount: {type: Number, required: true},
}, {timestamps: true})
// 为常见查询场景创建复合字段索引,上面定义的"index: true"是单字段索引
// 当 Vapi 朗读一本书时,它会按顺序获取段落。而这个复合索引可以让查找即时完成,而不是扫描数据集中的每个数据段。
BookSegmentSchema.index({bookId: 1, segmentIndex: 1}, {unique: true});
BookSegmentSchema.index({bookId: 1, pageNumber: 1});
BookSegmentSchema.index({bookId: 1, content: 'text'});
bookId: {type: Schema.Types.ObjectId, ref: "Book", required: true},
content: {type: String, required: true},
segmentIndex: {type: Number, required: true},
pageNumber: {type: Number},
wordCount: {type: Number, required: true},
}, {timestamps: true})
// 为常见查询场景创建复合字段索引,上面定义的"index: true"是单字段索引
// 当 Vapi 朗读一本书时,它会按顺序获取段落。而这个复合索引可以让查找即时完成,而不是扫描数据集中的每个数据段。
BookSegmentSchema.index({bookId: 1, segmentIndex: 1}, {unique: true});
BookSegmentSchema.index({bookId: 1, pageNumber: 1});
BookSegmentSchema.index({bookId: 1, content: 'text'});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@database/models/book-segment.model.ts` around lines 7 - 18, The single-field
indexes on bookId, segmentIndex and pageNumber are redundant because the
compound indexes BookSegmentSchema.index({bookId:1, segmentIndex:1}) and
BookSegmentSchema.index({bookId:1, pageNumber:1}) already cover those prefixes;
remove the per-field index flags (remove index: true from bookId, segmentIndex
and pageNumber in the schema) so only the compound indexes remain, reducing
write overhead and storage while preserving query plans that use the
bookId-prefixed compound indexes.

Comment on lines +6 to +8
clerkId: {type: String, required: true},
title: {type: String, required: true},
slug: {type: String, required: true, unique: true, lowercase: true, trim: true},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 查看 createBook / checkBookExists 里 slug 的使用语义
rg -nP 'slug' --type=ts -C3 lib/actions

Repository: lijianhua7/bookified

Length of output: 1432


🏁 Script executed:

# View the complete book.model.ts schema definition
cat -n database/models/book.model.ts

Repository: lijianhua7/bookified

Length of output: 984


🏁 Script executed:

# Check the createBook function signature and how clerkId is handled
rg -nA 20 'export.*createBook|function createBook' lib/actions/book.actions.ts

Repository: lijianhua7/bookified

Length of output: 734


🏁 Script executed:

# Search for all Book.findOne/find queries to understand slug usage patterns
rg -nP 'Book\.(findOne|find|findById).*slug' lib/actions --type=ts -A 2 -B 2

Repository: lijianhua7/bookified

Length of output: 636


🏁 Script executed:

# Find CreateBook type definition
rg -nP 'type CreateBook|interface CreateBook' --type=ts -A 10

Repository: lijianhua7/bookified

Length of output: 438


🏁 Script executed:

# Check how createBook is called - is clerkId being passed?
rg -nP 'createBook\(' --type=ts -B 2 -A 2

Repository: lijianhua7/bookified

Length of output: 318


🏁 Script executed:

# Search for update/patch operations on books
rg -nP 'Book\.update|Book\.findByIdAndUpdate|Book\.findOneAndUpdate' lib/actions --type=ts -B 2 -A 5

Repository: lijianhua7/bookified

Length of output: 480


🏁 Script executed:

# Check all Book operations to ensure comprehensive view
rg -nP 'Book\.' lib/actions/book.actions.ts --type=ts | head -20

Repository: lijianhua7/bookified

Length of output: 466


slug 的全局唯一约束与"每用户一份书架"的设计不符。

当前 slug: { unique: true } 强制 slug 在整个数据库中全局唯一。然而,CreateBook 类型定义表明 clerkId 是必填项,代码逻辑实际上在为每个用户存储独立的书籍记录。这导致矛盾:

  • 用户 A 上传标题 "moby-dick" → 存储为 slug="moby-dick",clerkId=user_A
  • 用户 B 尝试上传相同标题 → 因为 slug 全局唯一约束,创建失败

代码在 lib/actions/book.actions.ts 第 64 行进行存在性检查时,仅查询 Book.findOne({ slug }),未包含 clerkId,这进一步确认了问题。

建议按用户隔离 slug 唯一性:

♻️ 建议修改
-    slug: {type: String, required: true, unique: true, lowercase: true, trim: true},
+    slug: {type: String, required: true, lowercase: true, trim: true},
...
 }, {timestamps: true})
+
+// 每个用户下 slug 唯一
+BookSchema.index({ clerkId: 1, slug: 1 }, { unique: true });

同时建议更新查询逻辑为 Book.findOne({ clerkId: data.clerkId, slug }) 以确保用户隔离。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@database/models/book.model.ts` around lines 6 - 8, The schema currently
enforces slug: { unique: true } which conflicts with per-user books; remove the
global unique constraint on the slug field in the Book schema (retain
lowercase/trim) and instead add a compound unique index on { clerkId, slug } to
enforce slug-uniqueness per user; then update the existence check in the
Book-creation flow (the Book.findOne lookup used in the create flow) to include
clerkId (use Book.findOne({ clerkId: data.clerkId, slug })) so lookups and
uniqueness align with the per-user design.

Comment on lines +57 to +77
export const createBook = async (data: CreateBook) => {
try {
await connectToDatabase();

const slug = generateSlug(data.title);

// 创建图书之前,先检查是否有相同标题的图书已经存在
const existingBook = await Book.findOne({ slug }).lean();
if (existingBook) {
return {
success: true,
// 从服务器操作中传递大型或复杂对象时,必须将数据序列化为纯 JSON 对象
// data: JSON.parse(JSON.stringify(existingBook)),
data: serializeData(existingBook), // 序列化为纯 JSON 对象
alreadyExists: true,
};
}

// Todo: 创建图书前先检查订阅限制

const book = await Book.create({ ...data, slug, totalSegments: 0 });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 预期:所有可由客户端触发的变更型 server action 都应在服务端调用 auth() 并做所有权校验。
rg -n -C3 --type=ts --type=tsx '"use server"|from "@clerk/nextjs/server"|auth\(|createBook\(|saveBookSegments\('

Repository: lijianhua7/bookified

Length of output: 91


🏁 Script executed:

#!/bin/bash
# Search for "use server", auth imports, and function definitions
rg -n -C3 'use server|from.*@clerk/nextjs/server|auth\(' --type ts

Repository: lijianhua7/bookified

Length of output: 1260


🏁 Script executed:

#!/bin/bash
# Read the full book.actions.ts file to examine implementation
wc -l lib/actions/book.actions.ts

Repository: lijianhua7/bookified

Length of output: 95


🏁 Script executed:

#!/bin/bash
# Examine the full file
cat -n lib/actions/book.actions.ts

Repository: lijianhua7/bookified

Length of output: 4019


🏁 Script executed:

#!/bin/bash
# Check how these server actions are called from client components
rg -n 'createBook|saveBookSegments' --type ts --type jsx

Repository: lijianhua7/bookified

Length of output: 91


🏁 Script executed:

#!/bin/bash
# Search for client-side calls to these server actions (use .ts for typescript)
rg -n 'createBook|saveBookSegments' --type ts | head -30

Repository: lijianhua7/bookified

Length of output: 487


🏁 Script executed:

#!/bin/bash
# Check the CreateBook type definition to see what properties are included
rg -n 'type CreateBook|interface CreateBook' --type ts

Repository: lijianhua7/bookified

Length of output: 106


🏁 Script executed:

#!/bin/bash
# Look at components that call these server actions
fd -e ts -e jsx -e js 'UploadForm\|UploadBook'

Repository: lijianhua7/bookified

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Examine the UploadForm component to see how server actions are called
cat -n components/UploadForm.tsx | sed -n '150,185p'

Repository: lijianhua7/bookified

Length of output: 1099


🏁 Script executed:

#!/bin/bash
# Check the CreateBook interface definition
cat -n type.d.ts | sed -n '59,80p'

Repository: lijianhua7/bookified

Length of output: 648


🏁 Script executed:

#!/bin/bash
# Check how clerkId is obtained in UploadForm
rg -n 'clerkId|userId|useUser|useAuth' components/UploadForm.tsx

Repository: lijianhua7/bookified

Length of output: 213


🏁 Script executed:

#!/bin/bash
# Check if Book model has any owner/clerkId constraints or validation
cat -n lib/database/models/book.model.ts | head -50

Repository: lijianhua7/bookified

Length of output: 129


🏁 Script executed:

#!/bin/bash
# Check the Book schema definition to understand its structure
rg -n 'clerkId|schema|owner' lib/database/models/book.model.ts -A 3

Repository: lijianhua7/bookified

Length of output: 137


🏁 Script executed:

#!/bin/bash
# Verify if there's any middleware protecting these routes
fd -e ts -e js 'middleware'

Repository: lijianhua7/bookified

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Find the book model file
fd -i 'book.*model' --type f

Repository: lijianhua7/bookified

Length of output: 131


🏁 Script executed:

#!/bin/bash
# Check what's in the database/models directory
ls -la lib/database/models/ 2>/dev/null || ls -la database/models/ 2>/dev/null || fd -type d 'models'

Repository: lijianhua7/bookified

Length of output: 381


🏁 Script executed:

#!/bin/bash
# Search for Book schema definition
rg -n 'class Book|export.*Book|new Schema' --type ts

Repository: lijianhua7/bookified

Length of output: 1229


🏁 Script executed:

#!/bin/bash
# Examine the Book model schema
cat -n database/models/book.model.ts

Repository: lijianhua7/bookified

Length of output: 984


🏁 Script executed:

#!/bin/bash
# Examine the BookSegment model schema
cat -n database/models/book-segment.model.ts

Repository: lijianhua7/bookified

Length of output: 1172


🏁 Script executed:

#!/bin/bash
# Check the IBook interface
cat -n type.d.ts | sed -n '13,30p'

Repository: lijianhua7/bookified

Length of output: 586


🏁 Script executed:

#!/bin/bash
# Check if there are any route handlers or API endpoints that might validate ownership
fd -type f -e ts 'route\|api' app/ | head -10

Repository: lijianhua7/bookified

Length of output: 234


🏁 Script executed:

#!/bin/bash
# Check the API routes that exist
find app -name 'route.ts' -type f

Repository: lijianhua7/bookified

Length of output: 87


🏁 Script executed:

#!/bin/bash
# Check if any other server actions already use auth() for validation
rg -n 'from.*@clerk.*auth|auth\(\)' lib/actions/ --type ts -A 2

Repository: lijianhua7/bookified

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Check the UploadForm more carefully - how is userId obtained and used
cat -n components/UploadForm.tsx | sed -n '20,100p'

Repository: lijianhua7/bookified

Length of output: 2874


必须在服务端使用 Clerk auth() 验证 userId,并在所有数据库操作中约束所有权。

createBooksaveBookSegments 直接信任客户端传入的 clerkIdbookId,存在严重的身份验证和授权漏洞。特别是 saveBookSegments 在异常时执行的删除操作(第 132、134 行)仅按 bookId 删除,不验证所有权,任何攻人可以通过传入任意 bookId 来删除他人的图书和段落。

修复要点:

  • 在 server action 中导入并调用 auth() 获取经验证的 userId
  • 删除 createBook 参数中的 clerkId,由服务端派生
  • saveBookSegments 中移除 clerkId 参数,改用 server-side auth() 获取
  • 所有数据库查询/删除操作都必须同时约束 clerkIdbookId(或仅 clerkId),例如:
    const userId = await auth();
    await Book.findOneAndDelete({ _id: bookId, clerkId: userId });
    await BookSegment.deleteMany({ bookId, clerkId: userId });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/actions/book.actions.ts` around lines 57 - 77, The createBook and
saveBookSegments functions must stop trusting client-supplied clerkId/bookId:
import and call auth() server-side to derive the authenticated userId, remove
clerkId from the functions' parameters (derive it inside), and update all DB
operations (Book.create, Book.findOne, Book.findOneAndDelete,
BookSegment.deleteMany, Book.findOneAndUpdate, etc.) to include ownership
constraints (e.g., { _id: bookId, clerkId: userId } or at minimum { clerkId:
userId }) so deletes and queries only affect the caller's records; also ensure
any rollback deletes in saveBookSegments use the same ownership-constrained
queries rather than deleting by bookId alone.

Comment thread lib/utils.ts
Comment thread lib/utils.ts Outdated
Comment thread lib/utils.ts Outdated
Comment thread next.config.ts Outdated
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@lijianhua7 lijianhua7 merged commit d7b2844 into main Apr 21, 2026
2 of 3 checks passed
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.

1 participant