From b7c87d2d95550f3300b28bacdee1c763934c1078 Mon Sep 17 00:00:00 2001 From: npmrun <1549469775@qq.com> Date: Mon, 20 Apr 2026 20:36:56 +0800 Subject: [PATCH] feat(comments): enforce guest email anonymity rules Add guest email validation with anonymous-mode exceptions and persist guestEmail/guestIsAnonymous in comment creation. Update comment form to collect anonymous and email fields for guests and reset them after successful submit. Made-with: Cursor --- app/components/PostComments.vue | 24 +++++++++++++++++++++- .../[publicSlug]/posts/[postSlug]/comments.post.ts | 4 ++++ .../[publicSlug]/[shareToken]/comments.post.ts | 4 ++++ server/service/post-comments/index.ts | 11 ++++++++++ server/utils/post-comment-guest.test.ts | 19 +++++++++++++++++ server/utils/post-comment-guest.ts | 14 +++++++++++++ 6 files changed, 75 insertions(+), 1 deletion(-) diff --git a/app/components/PostComments.vue b/app/components/PostComments.vue index 621f40d..c9225c2 100644 --- a/app/components/PostComments.vue +++ b/app/components/PostComments.vue @@ -81,6 +81,8 @@ function flattenTree(nodes: CommentNode[], depth = 0): Array<{ node: CommentNode const replyToId = ref(null) const draftBody = ref('') const guestName = ref('') +const guestEmail = ref('') +const guestIsAnonymous = ref(false) const submitting = ref(false) function authorLine(node: CommentNode): string { @@ -129,6 +131,10 @@ async function submitComment() { toast.add({ title: '请填写昵称', color: 'error' }) return } + if (!guestIsAnonymous.value && !guestEmail.value.trim()) { + toast.add({ title: '请填写邮箱', color: 'error' }) + return + } } submitting.value = true @@ -136,7 +142,13 @@ async function submitComment() { const payload = { parentId: replyToId.value, body: draftBody.value, - ...(loggedIn.value ? {} : { guestDisplayName: guestName.value.trim() }), + ...(loggedIn.value + ? {} + : { + guestDisplayName: guestName.value.trim(), + guestEmail: guestEmail.value.trim(), + guestIsAnonymous: guestIsAnonymous.value, + }), } await fetchData<{ id: number }>(`${base}/comments`, { @@ -146,6 +158,8 @@ async function submitComment() { draftBody.value = '' guestName.value = '' + guestEmail.value = '' + guestIsAnonymous.value = false replyToId.value = null await refreshComments() toast.add({ title: '评论已发送', color: 'success' }) @@ -223,6 +237,14 @@ async function submitComment() { diff --git a/server/api/public/profile/[publicSlug]/posts/[postSlug]/comments.post.ts b/server/api/public/profile/[publicSlug]/posts/[postSlug]/comments.post.ts index 6390b6c..7e28e23 100644 --- a/server/api/public/profile/[publicSlug]/posts/[postSlug]/comments.post.ts +++ b/server/api/public/profile/[publicSlug]/posts/[postSlug]/comments.post.ts @@ -26,6 +26,8 @@ export default defineEventHandler(async (event) => { const body = await readBody<{ parentId?: number | null; guestDisplayName?: string; + guestEmail?: string; + guestIsAnonymous?: boolean; body: unknown; }>(event); @@ -48,6 +50,8 @@ export default defineEventHandler(async (event) => { parentId, viewer, guestDisplayName: typeof body.guestDisplayName === "string" ? body.guestDisplayName : undefined, + guestEmail: typeof body.guestEmail === "string" ? body.guestEmail : undefined, + guestIsAnonymous: body.guestIsAnonymous === true, body: body.body, }); diff --git a/server/api/public/unlisted/[publicSlug]/[shareToken]/comments.post.ts b/server/api/public/unlisted/[publicSlug]/[shareToken]/comments.post.ts index 00452f6..a1742cd 100644 --- a/server/api/public/unlisted/[publicSlug]/[shareToken]/comments.post.ts +++ b/server/api/public/unlisted/[publicSlug]/[shareToken]/comments.post.ts @@ -26,6 +26,8 @@ export default defineEventHandler(async (event) => { const body = await readBody<{ parentId?: number | null; guestDisplayName?: string; + guestEmail?: string; + guestIsAnonymous?: boolean; body: unknown; }>(event); @@ -48,6 +50,8 @@ export default defineEventHandler(async (event) => { parentId, viewer, guestDisplayName: typeof body.guestDisplayName === "string" ? body.guestDisplayName : undefined, + guestEmail: typeof body.guestEmail === "string" ? body.guestEmail : undefined, + guestIsAnonymous: body.guestIsAnonymous === true, body: body.body, }); diff --git a/server/service/post-comments/index.ts b/server/service/post-comments/index.ts index 4017e6e..32fdad6 100644 --- a/server/service/post-comments/index.ts +++ b/server/service/post-comments/index.ts @@ -7,6 +7,7 @@ import { GuestCommentValidationError, normalizeGuestDisplayName, validateGuestCommentBody, + validateGuestCommentEmail, } from "#server/utils/post-comment-guest"; import type { MinimalUser } from "#server/service/auth"; @@ -128,6 +129,8 @@ export async function createComment(input: { parentId: number | null; viewer: MinimalUser | null; guestDisplayName?: string; + guestEmail?: string; + guestIsAnonymous?: boolean; body: string; }): Promise { if (input.parentId != null) { @@ -144,6 +147,8 @@ export async function createComment(input: { let kind: string; let authorUserId: number | null; let guestDisplayName: string | null; + let guestEmail: string | null; + let guestIsAnonymous: boolean; let body: string; if (input.viewer != null) { @@ -157,10 +162,14 @@ export async function createComment(input: { kind = "user"; authorUserId = input.viewer.id; guestDisplayName = null; + guestEmail = null; + guestIsAnonymous = false; } else { try { + guestIsAnonymous = input.guestIsAnonymous === true; guestDisplayName = normalizeGuestDisplayName(input.guestDisplayName ?? ""); body = validateGuestCommentBody(input.body); + guestEmail = validateGuestCommentEmail(input.guestEmail, guestIsAnonymous); } catch (e) { if (e instanceof GuestCommentValidationError) { throw createError({ statusCode: 400, statusMessage: e.message }); @@ -178,6 +187,8 @@ export async function createComment(input: { parentId: input.parentId, authorUserId, guestDisplayName, + guestEmail, + guestIsAnonymous, body, kind, }); diff --git a/server/utils/post-comment-guest.test.ts b/server/utils/post-comment-guest.test.ts index df45853..3b26458 100644 --- a/server/utils/post-comment-guest.test.ts +++ b/server/utils/post-comment-guest.test.ts @@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"; import { GuestCommentValidationError, normalizeGuestDisplayName, + validateGuestCommentEmail, validateGuestCommentBody, } from "./post-comment-guest"; @@ -36,3 +37,21 @@ describe("validateGuestCommentBody", () => { expect(() => validateGuestCommentBody("z".repeat(501))).toThrow(GuestCommentValidationError); }); }); + +describe("validateGuestCommentEmail", () => { + test("accepts empty email when anonymous", () => { + expect(validateGuestCommentEmail("", true)).toBeNull(); + }); + + test("requires email when not anonymous", () => { + expect(() => validateGuestCommentEmail("", false)).toThrow(GuestCommentValidationError); + }); + + test("rejects invalid email when not anonymous", () => { + expect(() => validateGuestCommentEmail("invalid-email", false)).toThrow(GuestCommentValidationError); + }); + + test("accepts valid email when not anonymous", () => { + expect(validateGuestCommentEmail(" guest@example.com ", false)).toBe("guest@example.com"); + }); +}); diff --git a/server/utils/post-comment-guest.ts b/server/utils/post-comment-guest.ts index aaebd1f..cb3cebc 100644 --- a/server/utils/post-comment-guest.ts +++ b/server/utils/post-comment-guest.ts @@ -36,3 +36,17 @@ export function validateGuestCommentBody(raw: string): string { } return t; } + +export function validateGuestCommentEmail(raw: string | undefined, guestIsAnonymous: boolean): string | null { + const t = (raw ?? "").trim(); + if (guestIsAnonymous && !t) { + return null; + } + if (!t) { + throw new GuestCommentValidationError("请填写邮箱"); + } + if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(t)) { + throw new GuestCommentValidationError("邮箱格式不合法"); + } + return t; +}