refactor: cleanup - schema edition

whole bunch of refactors (especially with schemas)
This commit is contained in:
trafficlunar 2025-04-12 17:01:16 +01:00
parent 626016d689
commit 2e4611520d
7 changed files with 40 additions and 36 deletions

View file

@ -1,4 +1,4 @@
import { NextResponse } from "next/server";
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { auth } from "@/lib/auth";
@ -10,7 +10,7 @@ const usernameSchema = z
.max(20, "Username cannot be more than 20 characters long")
.regex(/^[a-zA-Z0-9_]+$/, "Username can only contain letters, numbers, and underscores");
export async function PATCH(request: Request) {
export async function PATCH(request: NextRequest) {
const session = await auth();
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });

View file

@ -1,16 +1,24 @@
import { NextResponse } from "next/server";
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
export async function PATCH(request: Request) {
const likeSchema = z.object({
miiId: z.coerce.number().int({ message: "Mii ID must be an integer" }).positive({ message: "Mii ID must be valid" }),
});
export async function PATCH(request: NextRequest) {
// todo: rate limit
const session = await auth();
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const { miiId } = await request.json();
if (!miiId) return NextResponse.json({ error: "Mii ID is required" }, { status: 400 });
const body = await request.json();
const parsed = likeSchema.safeParse(body);
if (!parsed.success) return NextResponse.json({ error: parsed.error.errors[0].message }, { status: 400 });
const { miiId } = parsed.data;
const result = await prisma.$transaction(async (tx) => {
const existingLike = await tx.like.findUnique({

View file

@ -1,4 +1,5 @@
import { NextResponse } from "next/server";
import { z } from "zod";
import fs from "fs/promises";
import path from "path";
@ -17,31 +18,33 @@ import TomodachiLifeMii from "@/lib/tomodachi-life-mii";
const uploadsDirectory = path.join(process.cwd(), "public", "mii");
const submitSchema = z.object({
name: nameSchema,
tags: tagsSchema,
qrBytesRaw: z
.array(z.number(), { required_error: "A QR code is required" })
.length(372, { message: "QR code size is not a valid Tomodachi Life QR code" }),
image1: z.instanceof(File).optional(),
image2: z.instanceof(File).optional(),
image3: z.instanceof(File).optional(),
});
export async function POST(request: Request) {
const session = await auth();
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const formData = await request.formData();
const parsed = submitSchema.safeParse({
name: formData.get("name"),
tags: JSON.parse(formData.get("tags") as string),
qrBytesRaw: JSON.parse(formData.get("qrBytesRaw") as string),
image1: formData.get("image1"),
image2: formData.get("image2"),
image3: formData.get("image3"),
});
const name = formData.get("name") as string;
const tags: string[] = JSON.parse(formData.get("tags") as string);
const qrBytesRaw: number[] = JSON.parse(formData.get("qrBytesRaw") as string);
const image1 = formData.get("image1") as File;
const image2 = formData.get("image2") as File;
const image3 = formData.get("image3") as File;
if (!name) return NextResponse.json({ error: "Name is required" }, { status: 400 });
if (!tags || tags.length == 0) return NextResponse.json({ error: "At least one tag is required" }, { status: 400 });
if (!qrBytesRaw || qrBytesRaw.length == 0) return NextResponse.json({ error: "A QR code is required" }, { status: 400 });
const nameValidation = nameSchema.safeParse(name);
if (!nameValidation.success) return NextResponse.json({ error: nameValidation.error.errors[0].message }, { status: 400 });
const tagsValidation = tagsSchema.safeParse(tags);
if (!tagsValidation.success) return NextResponse.json({ error: tagsValidation.error.errors[0].message }, { status: 400 });
if (qrBytesRaw.length !== 372) return NextResponse.json({ error: "QR code size is not a valid Tomodachi Life QR code" }, { status: 400 });
if (!parsed.success) return NextResponse.json({ error: parsed.error.errors[0].message }, { status: 400 });
const { name, tags, qrBytesRaw, image1, image2, image3 } = parsed.data;
// Validate image files
const images: File[] = [];

View file

@ -40,7 +40,6 @@ export default async function MiiList({ searchParams, userId, where }: Props) {
: [];
const whereTags = tagFilter.length > 0 ? { tags: { hasEvery: tagFilter } } : undefined;
// If the mii list is on a user's profile, don't query for the username
const userInclude =
userId == null
? {

View file

@ -3,20 +3,13 @@
import { useState } from "react";
import { Icon } from "@iconify/react";
import { redirect } from "next/navigation";
import { z } from "zod";
const searchSchema = z
.string()
.trim()
.min(2)
.max(64)
.regex(/^[a-zA-Z0-9_]+$/);
import { nameSchema } from "@/lib/schemas";
export default function SearchBar() {
const [query, setQuery] = useState("");
const handleSearch = () => {
const result = searchSchema.safeParse(query);
const result = nameSchema.safeParse(query);
if (!result.success) redirect("/");
redirect(`/search?q=${query}`);

View file

@ -9,7 +9,7 @@ interface Props {
setTags: React.Dispatch<React.SetStateAction<string[]>>;
}
const tagRegex = /^[a-z-]*$/;
const tagRegex = /^[a-z0-9-_]*$/;
const predefinedTags = ["anime", "art", "cartoon", "celebrity", "games", "history", "meme", "movie", "oc", "tv"];
export default function TagSelector({ tags, setTags }: Props) {

View file

@ -2,6 +2,7 @@ import { z } from "zod";
export const nameSchema = z
.string()
.trim()
.min(2, { message: "Name must be at least 2 characters long" })
.max(64, { message: "Name cannot be more than 64 characters long" })
.regex(/^[a-zA-Z0-9-_. ']+$/, {