refactor: cleanup - schema edition
whole bunch of refactors (especially with schemas)
This commit is contained in:
parent
626016d689
commit
2e4611520d
7 changed files with 40 additions and 36 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
import { NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
|
|
@ -10,7 +10,7 @@ const usernameSchema = z
|
||||||
.max(20, "Username cannot be more than 20 characters long")
|
.max(20, "Username cannot be more than 20 characters long")
|
||||||
.regex(/^[a-zA-Z0-9_]+$/, "Username can only contain letters, numbers, and underscores");
|
.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();
|
const session = await auth();
|
||||||
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 { auth } from "@/lib/auth";
|
||||||
import { prisma } from "@/lib/prisma";
|
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
|
// todo: rate limit
|
||||||
|
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
|
||||||
const { miiId } = await request.json();
|
const body = await request.json();
|
||||||
if (!miiId) return NextResponse.json({ error: "Mii ID is required" }, { status: 400 });
|
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 result = await prisma.$transaction(async (tx) => {
|
||||||
const existingLike = await tx.like.findUnique({
|
const existingLike = await tx.like.findUnique({
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
@ -17,31 +18,33 @@ import TomodachiLifeMii from "@/lib/tomodachi-life-mii";
|
||||||
|
|
||||||
const uploadsDirectory = path.join(process.cwd(), "public", "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) {
|
export async function POST(request: Request) {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
|
||||||
const formData = await request.formData();
|
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;
|
if (!parsed.success) return NextResponse.json({ error: parsed.error.errors[0].message }, { status: 400 });
|
||||||
const tags: string[] = JSON.parse(formData.get("tags") as string);
|
const { name, tags, qrBytesRaw, image1, image2, image3 } = parsed.data;
|
||||||
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 });
|
|
||||||
|
|
||||||
// Validate image files
|
// Validate image files
|
||||||
const images: File[] = [];
|
const images: File[] = [];
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,6 @@ export default async function MiiList({ searchParams, userId, where }: Props) {
|
||||||
: [];
|
: [];
|
||||||
const whereTags = tagFilter.length > 0 ? { tags: { hasEvery: tagFilter } } : undefined;
|
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 =
|
const userInclude =
|
||||||
userId == null
|
userId == null
|
||||||
? {
|
? {
|
||||||
|
|
|
||||||
|
|
@ -3,20 +3,13 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Icon } from "@iconify/react";
|
import { Icon } from "@iconify/react";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { z } from "zod";
|
import { nameSchema } from "@/lib/schemas";
|
||||||
|
|
||||||
const searchSchema = z
|
|
||||||
.string()
|
|
||||||
.trim()
|
|
||||||
.min(2)
|
|
||||||
.max(64)
|
|
||||||
.regex(/^[a-zA-Z0-9_]+$/);
|
|
||||||
|
|
||||||
export default function SearchBar() {
|
export default function SearchBar() {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
|
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
const result = searchSchema.safeParse(query);
|
const result = nameSchema.safeParse(query);
|
||||||
if (!result.success) redirect("/");
|
if (!result.success) redirect("/");
|
||||||
|
|
||||||
redirect(`/search?q=${query}`);
|
redirect(`/search?q=${query}`);
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ interface Props {
|
||||||
setTags: React.Dispatch<React.SetStateAction<string[]>>;
|
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"];
|
const predefinedTags = ["anime", "art", "cartoon", "celebrity", "games", "history", "meme", "movie", "oc", "tv"];
|
||||||
|
|
||||||
export default function TagSelector({ tags, setTags }: Props) {
|
export default function TagSelector({ tags, setTags }: Props) {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { z } from "zod";
|
||||||
|
|
||||||
export const nameSchema = z
|
export const nameSchema = z
|
||||||
.string()
|
.string()
|
||||||
|
.trim()
|
||||||
.min(2, { message: "Name must be at least 2 characters long" })
|
.min(2, { message: "Name must be at least 2 characters long" })
|
||||||
.max(64, { message: "Name cannot be more than 64 characters long" })
|
.max(64, { message: "Name cannot be more than 64 characters long" })
|
||||||
.regex(/^[a-zA-Z0-9-_. ']+$/, {
|
.regex(/^[a-zA-Z0-9-_. ']+$/, {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue