Browse Source

feat(api): public and unlisted comment list and create endpoints

Made-with: Cursor
main
npmrun 5 hours ago
parent
commit
b92a393ceb
  1. 19
      server/api/public/profile/[publicSlug]/posts/[postSlug]/comments.get.ts
  2. 55
      server/api/public/profile/[publicSlug]/posts/[postSlug]/comments.post.ts
  3. 19
      server/api/public/unlisted/[publicSlug]/[shareToken]/comments.get.ts
  4. 55
      server/api/public/unlisted/[publicSlug]/[shareToken]/comments.post.ts
  5. 24
      server/utils/auth-api-routes.ts

19
server/api/public/profile/[publicSlug]/posts/[postSlug]/comments.get.ts

@ -0,0 +1,19 @@
import { getPublicPostCommentContext } from "#server/service/posts";
import { listCommentsPayload } from "#server/service/post-comments";
export default defineEventHandler(async (event) => {
const publicSlug = event.context.params?.publicSlug;
const postSlug = event.context.params?.postSlug;
if (!publicSlug || !postSlug || typeof publicSlug !== "string" || typeof postSlug !== "string") {
throw createError({ statusCode: 400, statusMessage: "无效请求" });
}
const ctx = await getPublicPostCommentContext(publicSlug, postSlug);
if (!ctx) {
throw createError({ statusCode: 404, statusMessage: "未找到" });
}
const viewer = await event.context.auth.getCurrent();
const data = await listCommentsPayload(ctx.id, ctx.userId, viewer?.id ?? null);
return R.success(data);
});

55
server/api/public/profile/[publicSlug]/posts/[postSlug]/comments.post.ts

@ -0,0 +1,55 @@
import { getRequestIP } from "h3";
import { getPublicPostCommentContext } from "#server/service/posts";
import { createComment } from "#server/service/post-comments";
import { assertUnderRateLimit } from "#server/utils/simple-rate-limit";
export default defineEventHandler(async (event) => {
const publicSlug = event.context.params?.publicSlug;
const postSlug = event.context.params?.postSlug;
if (!publicSlug || !postSlug || typeof publicSlug !== "string" || typeof postSlug !== "string") {
throw createError({ statusCode: 400, statusMessage: "无效请求" });
}
const ctx = await getPublicPostCommentContext(publicSlug, postSlug);
if (!ctx) {
throw createError({ statusCode: 404, statusMessage: "未找到" });
}
const viewer = await event.context.auth.getCurrent();
const ip = getRequestIP(event, { xForwardedFor: true }) ?? "unknown";
if (viewer == null) {
assertUnderRateLimit(`comment-post-guest:${ip}`, 20, 15 * 60 * 1000);
} else {
assertUnderRateLimit(`comment-post-user:${viewer.id}`, 60, 15 * 60 * 1000);
}
const body = await readBody<{
parentId?: number | null;
guestDisplayName?: string;
body: unknown;
}>(event);
if (typeof body.body !== "string") {
throw createError({ statusCode: 400, statusMessage: "无效请求" });
}
let parentId: number | null;
if (body.parentId === undefined || body.parentId === null) {
parentId = null;
} else if (typeof body.parentId === "number" && Number.isFinite(body.parentId)) {
parentId = body.parentId;
} else {
throw createError({ statusCode: 400, statusMessage: "无效请求" });
}
const newCommentId = await createComment({
postId: ctx.id,
ownerUserId: ctx.userId,
parentId,
viewer,
guestDisplayName: typeof body.guestDisplayName === "string" ? body.guestDisplayName : undefined,
body: body.body,
});
return R.success({ id: newCommentId });
});

19
server/api/public/unlisted/[publicSlug]/[shareToken]/comments.get.ts

@ -0,0 +1,19 @@
import { getUnlistedPostCommentContext } from "#server/service/posts";
import { listCommentsPayload } from "#server/service/post-comments";
export default defineEventHandler(async (event) => {
const publicSlug = event.context.params?.publicSlug;
const shareToken = event.context.params?.shareToken;
if (!publicSlug || !shareToken || typeof publicSlug !== "string" || typeof shareToken !== "string") {
throw createError({ statusCode: 400, statusMessage: "无效链接" });
}
const ctx = await getUnlistedPostCommentContext(publicSlug, shareToken);
if (!ctx) {
throw createError({ statusCode: 404, statusMessage: "未找到" });
}
const viewer = await event.context.auth.getCurrent();
const data = await listCommentsPayload(ctx.id, ctx.userId, viewer?.id ?? null);
return R.success(data);
});

55
server/api/public/unlisted/[publicSlug]/[shareToken]/comments.post.ts

@ -0,0 +1,55 @@
import { getRequestIP } from "h3";
import { getUnlistedPostCommentContext } from "#server/service/posts";
import { createComment } from "#server/service/post-comments";
import { assertUnderRateLimit } from "#server/utils/simple-rate-limit";
export default defineEventHandler(async (event) => {
const publicSlug = event.context.params?.publicSlug;
const shareToken = event.context.params?.shareToken;
if (!publicSlug || !shareToken || typeof publicSlug !== "string" || typeof shareToken !== "string") {
throw createError({ statusCode: 400, statusMessage: "无效链接" });
}
const ctx = await getUnlistedPostCommentContext(publicSlug, shareToken);
if (!ctx) {
throw createError({ statusCode: 404, statusMessage: "未找到" });
}
const viewer = await event.context.auth.getCurrent();
const ip = getRequestIP(event, { xForwardedFor: true }) ?? "unknown";
if (viewer == null) {
assertUnderRateLimit(`comment-post-guest:${ip}`, 20, 15 * 60 * 1000);
} else {
assertUnderRateLimit(`comment-post-user:${viewer.id}`, 60, 15 * 60 * 1000);
}
const body = await readBody<{
parentId?: number | null;
guestDisplayName?: string;
body: unknown;
}>(event);
if (typeof body.body !== "string") {
throw createError({ statusCode: 400, statusMessage: "无效请求" });
}
let parentId: number | null;
if (body.parentId === undefined || body.parentId === null) {
parentId = null;
} else if (typeof body.parentId === "number" && Number.isFinite(body.parentId)) {
parentId = body.parentId;
} else {
throw createError({ statusCode: 400, statusMessage: "无效请求" });
}
const newCommentId = await createComment({
postId: ctx.id,
ownerUserId: ctx.userId,
parentId,
viewer,
guestDisplayName: typeof body.guestDisplayName === "string" ? body.guestDisplayName : undefined,
body: body.body,
});
return R.success({ id: newCommentId });
});

24
server/utils/auth-api-routes.ts

@ -9,13 +9,33 @@ const API_ALLOWLIST: RouteRule[] = [
{ path: "/api/config/global", methods: ["GET"] }, { path: "/api/config/global", methods: ["GET"] },
]; ];
/** 公开 API 只读,避免未认证写操作 */ /** 允许访客发表评论的公开 POST(其余 /api/public/ 写操作仍拒绝) */
function isPublicCommentPostPath(path: string) {
if (!path.endsWith("/comments")) {
return false;
}
if (/^\/api\/public\/profile\/[^/]+\/posts\/[^/]+\/comments$/.test(path)) {
return true;
}
if (/^\/api\/public\/unlisted\/[^/]+\/[^/]+\/comments$/.test(path)) {
return true;
}
return false;
}
/** 公开 API 以只读为主;评论创建为例外,需配合服务端校验与限流 */
export function isPublicApiPath(path: string, method?: string) { export function isPublicApiPath(path: string, method?: string) {
if (!path.startsWith("/api/public/")) { if (!path.startsWith("/api/public/")) {
return false; return false;
} }
const requestMethod = method?.toUpperCase() ?? "GET"; const requestMethod = method?.toUpperCase() ?? "GET";
return requestMethod === "GET"; if (requestMethod === "GET") {
return true;
}
if (requestMethod === "POST" && isPublicCommentPostPath(path)) {
return true;
}
return false;
} }
export function isAllowlistedApiPath(path: string, method?: string) { export function isAllowlistedApiPath(path: string, method?: string) {

Loading…
Cancel
Save