Skip to content

Commit

Permalink
added zod to help sanitize and verify comment data
Browse files Browse the repository at this point in the history
  • Loading branch information
kevinreber committed Nov 12, 2024
1 parent c741bfd commit e6ee2d3
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 57 deletions.
55 changes: 26 additions & 29 deletions app/components/CommentForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,7 @@ import { useFetcher } from "@remix-run/react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { toast } from "sonner";

interface CommentResponse {
success: boolean;
comment?: {
id: string;
message: string;
createdAt: Date;
user: {
id: string;
username: string;
image: string | null;
};
};
error?: string;
}
import { CommentSchema, type CommentResponse } from "~/schemas/comment";

export const CommentForm = ({ imageId }: { imageId: string }) => {
const fetcher = useFetcher<CommentResponse>();
Expand All @@ -41,24 +27,34 @@ export const CommentForm = ({ imageId }: { imageId: string }) => {
if (isLoading || now - lastSubmitTime < SUBMIT_DELAY) return;

const formData = new FormData(event.currentTarget);
const comment = formData.get("comment")?.toString().trim();
const comment = formData.get("comment")?.toString() ?? "";

if (!comment) {
toast.error("Please enter a comment");
return;
}
try {
// Validate the data before submitting
const validatedData = CommentSchema.parse({
imageId,
comment,
});

setLastSubmitTime(now);
setLastSubmitTime(now);

fetcher.submit(
{ imageId, comment },
{
method: "POST",
action: `/api/images/${imageId}/comment`,
}
);
fetcher.submit(
{ ...validatedData },
{
method: "POST",
action: `/api/images/${imageId}/comment`,
}
);

formRef.current?.reset();
formRef.current?.reset();
} catch (error) {
if (error instanceof z.ZodError) {
const errors = error.errors.map((e) => e.message);
toast.error(errors.join("\n"));
} else {
toast.error("Failed to validate comment");
}
}
};

return (
Expand All @@ -74,6 +70,7 @@ export const CommentForm = ({ imageId }: { imageId: string }) => {
disabled={isLoading}
minLength={1}
maxLength={500}
required
/>
<Button
type="submit"
Expand Down
55 changes: 27 additions & 28 deletions app/routes/api.images.$imageId.comment.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,56 @@
import { ActionFunctionArgs, json } from "@remix-run/node";
import { requireUserLogin } from "~/services";
import { invariantResponse } from "~/utils";
import { createComment, type CommentResponse } from "~/server/createComment";

interface CommentAPIResponse {
success: boolean;
comment?: CommentResponse;
error?: string;
}
import { createComment } from "~/server/createComment";
import { CommentSchema, CommentResponseSchema } from "~/schemas/comment";
import { z } from "zod";

export const action = async ({
request,
params,
}: ActionFunctionArgs): Promise<Response> => {
const user = await requireUserLogin(request);
const formData = Object.fromEntries(await request.formData());
const imageId = params.imageId;
invariantResponse(imageId, "Image ID is required");

const formData = await request.formData();
const message = formData.get("comment")?.toString().trim();

if (!message) {
return json<CommentAPIResponse>(
{ success: false, error: "Comment message is required" },
{ status: 400 }
);
}

try {
// Validate the incoming data
const validatedData = CommentSchema.parse({
imageId,
comment: formData.comment,
});

if (request.method === "POST") {
const comment = await createComment({
message,
imageId,
message: validatedData.comment,
imageId: validatedData.imageId,
userId: user.id,
});

return json<CommentAPIResponse>({
// Validate the response
const response = CommentResponseSchema.parse({
success: true,
comment,
});

return json(response);
}

return json<CommentAPIResponse>(
return json(
{ success: false, error: "Method not allowed" },
{ status: 405 }
);
} catch (error) {
console.error("Error creating comment:", error);
return json<CommentAPIResponse>(
{
success: false,
error: "Failed to create comment",
},

if (error instanceof z.ZodError) {
return json(
{ success: false, error: error.errors[0].message },
{ status: 400 }
);
}

return json(
{ success: false, error: "Failed to create comment" },
{ status: 500 }
);
}
Expand Down
31 changes: 31 additions & 0 deletions app/schemas/comment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { z } from "zod";

export const CommentSchema = z.object({
imageId: z.string().min(1, "Image ID is required"),
comment: z
.string()
.min(1, "Comment cannot be empty")
.max(500, "Comment must be less than 500 characters")
.transform((str) => str.trim()),
});

export type CommentFormData = z.infer<typeof CommentSchema>;

export const CommentResponseSchema = z.object({
success: z.boolean(),
comment: z
.object({
id: z.string(),
message: z.string(),
createdAt: z.date(),
user: z.object({
id: z.string(),
username: z.string(),
image: z.string().nullable(),
}),
})
.optional(),
error: z.string().optional(),
});

export type CommentResponse = z.infer<typeof CommentResponseSchema>;

0 comments on commit e6ee2d3

Please sign in to comment.