fix: validate server action inputs for views and comments

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
jakeandClaude Sonnet 5.5 committed 2026-10-03 08:33:57 -04:00
1 parent a9091eb4b3
commit bd18c6d553
3 files changed
+67 -5

No files matched your search

+10
View File
@@ -27,6 +27,16 @@ export const getPost = (slug: string): Post | undefined => {
return allPosts.find((post) => post.slug === slug);
};
/**
* Returns the post for a page slug in the "notes/<slug>" format used by the `page` and `comment` tables
* (e.g. "notes/dark-mode"), or undefined if it isn't an existing post.
*/
export const getPostByPageSlug = (pageSlug: string): Post | undefined => {
const prefix = `${POSTS_DIR}/`;
if (!pageSlug.startsWith(prefix)) return undefined;
return getPost(pageSlug.slice(prefix.length));
};
/**
* Returns the front matter of ALL posts, sorted reverse chronologically.
*/
+51 -5
View File
@@ -3,10 +3,21 @@
import { desc, eq, sql } from "drizzle-orm";
import { revalidatePath } from "next/cache";
import { headers } from "next/headers";
import { z } from "zod";
import { auth } from "@/lib/auth";
import { db } from "@/lib/db";
import * as schema from "@/lib/db/schema";
import { getPostByPageSlug } from "@/lib/posts";
const commentContentSchema = z.string().trim().min(1).max(10_000);
const commentIdSchema = z.uuid();
const createCommentSchema = z.object({
content: commentContentSchema,
pageSlug: z.string(),
parentId: commentIdSchema.optional(),
});
export type CommentWithUser = typeof schema.comment.$inferSelect & {
user: Pick<typeof schema.user.$inferSelect, "id" | "name" | "image">;
@@ -95,17 +106,42 @@ export const createComment = async (data: {
throw new Error("You must be logged in to comment");
}
const parsed = createCommentSchema.safeParse(data);
if (!parsed.success) {
throw new Error("Invalid comment");
}
const { content, pageSlug, parentId } = parsed.data;
// only allow comments on existing posts that haven't disabled them
const post = getPostByPageSlug(pageSlug);
if (!post || post.noComments) {
throw new Error("Comments are closed for this page");
}
try {
// Replies must point at an existing comment on the same page
if (parentId) {
const parent = await db
.select({ pageSlug: schema.comment.pageSlug })
.from(schema.comment)
.where(eq(schema.comment.id, parentId))
.then((results) => results[0]);
if (!parent || parent.pageSlug !== pageSlug) {
throw new Error("Parent comment not found");
}
}
// Insert the comment
await db.insert(schema.comment).values({
content: data.content,
pageSlug: data.pageSlug,
parentId: data.parentId || null,
content,
pageSlug,
parentId: parentId ?? null,
userId: session.user.id,
});
// Revalidate page
revalidatePath(`/${data.pageSlug}`);
revalidatePath(`/${pageSlug}`);
} catch (error) {
console.error("[server/comments] error creating comment:", error);
throw new Error("Failed to create comment", { cause: error });
@@ -121,6 +157,12 @@ export const updateComment = async (commentId: string, content: string) => {
throw new Error("You must be logged in to update a comment");
}
const parsedId = commentIdSchema.safeParse(commentId);
const parsedContent = commentContentSchema.safeParse(content);
if (!parsedId.success || !parsedContent.success) {
throw new Error("Invalid comment");
}
try {
// Get the comment to verify ownership
const comment = await db
@@ -145,7 +187,7 @@ export const updateComment = async (commentId: string, content: string) => {
await db
.update(schema.comment)
.set({
content,
content: parsedContent.data,
updatedAt: new Date(),
})
.where(eq(schema.comment.id, commentId));
@@ -167,6 +209,10 @@ export const deleteComment = async (commentId: string) => {
throw new Error("You must be logged in to delete a comment");
}
if (!commentIdSchema.safeParse(commentId).success) {
throw new Error("Invalid comment ID");
}
try {
// Get the comment to verify ownership and get the page_slug for revalidation
const comment = await db
+6
View File
@@ -5,6 +5,7 @@ import { revalidateTag } from "next/cache";
import { db } from "@/lib/db";
import { page } from "@/lib/db/schema";
import { getPostByPageSlug } from "@/lib/posts";
/**
* Retrieves the number of views for a given slug, or 0 if the slug does not exist or on error
@@ -42,6 +43,11 @@ export const getAllViewCounts = async (): Promise<Record<string, number>> => {
* Increments the view count for a given slug (upserts if doesn't exist)
*/
export const incrementViews = async (slug: string): Promise<number> => {
// this is a public endpoint: only count views for posts that actually exist
if (typeof slug !== "string" || !getPostByPageSlug(slug)) {
throw new Error("Invalid slug");
}
try {
// Atomic upsert: insert new row with views=1, or increment existing row
const [result] = await db