-
Notifications
You must be signed in to change notification settings - Fork 38
feat: 新增仿notion设计的markdown编辑器 #230
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
6e7536d
065bbf3
9060f48
875dd76
4d682bb
037f9c0
bfa8af3
ef0f53f
99f07f9
01f4468
e093559
1509e2d
871d832
2a52b07
b56effe
d13d9ea
15c63ee
c032a48
5f07377
eafc34b
efb7d77
503b03b
eb0adbd
6b21a3c
d33dbbf
f44f5df
15e1988
66bfa55
e00ceaf
83776ee
75cdadf
f67bc88
eea8b85
608d26b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 文件大小 & 类型上限没在后端做限制, 可能直接打爆 |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,191 @@ | ||||||||||||||||||||
| import { auth } from "@/auth"; | ||||||||||||||||||||
| import { NextRequest, NextResponse } from "next/server"; | ||||||||||||||||||||
| import { sanitizeSlug, validateSlug } from "@/lib/submission"; | ||||||||||||||||||||
| import { | ||||||||||||||||||||
| MAX_IMAGE_UPLOAD_BYTES, | ||||||||||||||||||||
| isAllowedImageContentType, | ||||||||||||||||||||
| isExtensionAllowedForContentType, | ||||||||||||||||||||
| isValidFileSize, | ||||||||||||||||||||
| sanitizeFilename, | ||||||||||||||||||||
| } from "@/lib/uploads"; | ||||||||||||||||||||
| import { createImageUploadPost, createR2Client } from "@/lib/r2"; | ||||||||||||||||||||
| import type { AllowedImageContentType } from "@/lib/uploads"; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const MAX_OBJECT_KEY_BYTES = 1024; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| interface UploadRequest { | ||||||||||||||||||||
| filename: string; | ||||||||||||||||||||
| contentType: string; | ||||||||||||||||||||
| articleSlug: string; | ||||||||||||||||||||
| fileSize: number; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /** | ||||||||||||||||||||
| * @description POST /api/upload - 生成 R2 预签名 URL,用于客户端直接上传图片 | ||||||||||||||||||||
| * @param request - NextRequest 对象,请求体包含以下字段: | ||||||||||||||||||||
| * - filename: 文件名 | ||||||||||||||||||||
| * - contentType: 文件 MIME 类型 | ||||||||||||||||||||
| * - articleSlug: 文章 slug(用于组织文件路径) | ||||||||||||||||||||
| * - fileSize: 文件大小(字节,用于限制超大上传) | ||||||||||||||||||||
| * @returns NextResponse - 返回 JSON 对象: | ||||||||||||||||||||
| * - uploadUrl: 预签名上传 URL(用于表单 POST) | ||||||||||||||||||||
| * - fields: 需随表单一同提交的字段 | ||||||||||||||||||||
| * - publicUrl: 图片的公开访问 URL | ||||||||||||||||||||
| * - key: R2 对象键 | ||||||||||||||||||||
| */ | ||||||||||||||||||||
Crokily marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||
| export async function POST(request: NextRequest) { | ||||||||||||||||||||
| try { | ||||||||||||||||||||
| const session = await auth(); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if (!session?.user?.id) { | ||||||||||||||||||||
| return NextResponse.json({ error: "未授权访问" }, { status: 401 }); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const { | ||||||||||||||||||||
| R2_ACCOUNT_ID, | ||||||||||||||||||||
| R2_ACCESS_KEY_ID, | ||||||||||||||||||||
| R2_SECRET_ACCESS_KEY, | ||||||||||||||||||||
| R2_BUCKET_NAME, | ||||||||||||||||||||
| R2_PUBLIC_URL, | ||||||||||||||||||||
| } = process.env; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if ( | ||||||||||||||||||||
| !R2_ACCOUNT_ID || | ||||||||||||||||||||
| !R2_ACCESS_KEY_ID || | ||||||||||||||||||||
| !R2_SECRET_ACCESS_KEY || | ||||||||||||||||||||
| !R2_BUCKET_NAME || | ||||||||||||||||||||
| !R2_PUBLIC_URL | ||||||||||||||||||||
| ) { | ||||||||||||||||||||
| console.error("R2 环境变量未配置"); | ||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||
| { error: "服务器配置错误:R2 未配置" }, | ||||||||||||||||||||
| { status: 500 }, | ||||||||||||||||||||
| ); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| let body: UploadRequest; | ||||||||||||||||||||
| try { | ||||||||||||||||||||
| body = (await request.json()) as UploadRequest; | ||||||||||||||||||||
| } catch { | ||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||
| { error: "请求体格式错误:应为 JSON" }, | ||||||||||||||||||||
| { status: 400 }, | ||||||||||||||||||||
| ); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const { filename, contentType, articleSlug, fileSize } = body; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if ( | ||||||||||||||||||||
| typeof filename !== "string" || | ||||||||||||||||||||
| typeof contentType !== "string" || | ||||||||||||||||||||
| typeof articleSlug !== "string" || | ||||||||||||||||||||
| typeof fileSize === "undefined" | ||||||||||||||||||||
| ) { | ||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||
| { | ||||||||||||||||||||
| error: "缺少必要参数:filename, contentType, articleSlug, fileSize", | ||||||||||||||||||||
| }, | ||||||||||||||||||||
| { status: 400 }, | ||||||||||||||||||||
| ); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const normalizedContentType = contentType.toLowerCase(); | ||||||||||||||||||||
| const sanitizedSlug = sanitizeSlug(articleSlug); | ||||||||||||||||||||
| if (!validateSlug(sanitizedSlug) || sanitizedSlug !== articleSlug) { | ||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||
| { | ||||||||||||||||||||
| error: | ||||||||||||||||||||
| "articleSlug 不符合规范(需为 1-100 位小写字母、数字、连字符或下划线)", | ||||||||||||||||||||
| }, | ||||||||||||||||||||
| { status: 400 }, | ||||||||||||||||||||
| ); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if (!isValidFileSize(fileSize)) { | ||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||
| { | ||||||||||||||||||||
| error: `文件大小无效或超过限制(最大 ${ | ||||||||||||||||||||
| MAX_IMAGE_UPLOAD_BYTES / (1024 * 1024) | ||||||||||||||||||||
| }MB)`, | ||||||||||||||||||||
| }, | ||||||||||||||||||||
| { status: 400 }, | ||||||||||||||||||||
| ); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
||||||||||||||||||||
| // Validate articleSlug | |
| if (!/^[a-zA-Z0-9_-]+$/.test(articleSlug)) { | |
| return NextResponse.json( | |
| { error: "articleSlug 包含非法字符" }, | |
| { status: 400 } | |
| ); | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The pre-commit hook script uses shell variable comparison which may not work reliably across different systems. The comparison
[ "$BEFORE_STATUS" != "$AFTER_STATUS" ]could fail if the status contains special characters or multiline output.Consider a more robust approach:
This uses
git diff --quietwhich properly handles all edge cases and is the recommended way to check for changes.