chore: update packages

also migrate zod to v4
This commit is contained in:
trafficlunar 2025-07-14 13:03:19 +01:00
parent afb73ec3a6
commit 8b4842b584
20 changed files with 918 additions and 922 deletions

View file

@ -13,10 +13,10 @@
}, },
"dependencies": { "dependencies": {
"@2toad/profanity": "^3.1.1", "@2toad/profanity": "^3.1.1",
"@auth/prisma-adapter": "2.9.1", "@auth/prisma-adapter": "2.10.0",
"@bprogress/next": "^3.2.12", "@bprogress/next": "^3.2.12",
"@hello-pangea/dnd": "^18.0.1", "@hello-pangea/dnd": "^18.0.1",
"@prisma/client": "^6.9.0", "@prisma/client": "^6.11.1",
"@types/sjcl": "^1.0.34", "@types/sjcl": "^1.0.34",
"bit-buffer": "^0.2.5", "bit-buffer": "^0.2.5",
"canvas-confetti": "^1.9.3", "canvas-confetti": "^1.9.3",
@ -26,32 +26,32 @@
"file-type": "^21.0.0", "file-type": "^21.0.0",
"ioredis": "^5.6.1", "ioredis": "^5.6.1",
"jsqr": "^1.4.0", "jsqr": "^1.4.0",
"next": "15.3.3", "next": "15.3.5",
"next-auth": "5.0.0-beta.25", "next-auth": "5.0.0-beta.25",
"qrcode-generator": "^1.5.0", "qrcode-generator": "^2.0.2",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-dropzone": "^14.3.8", "react-dropzone": "^14.3.8",
"react-webcam": "^7.2.0", "react-webcam": "^7.2.0",
"satori": "^0.15.2", "satori": "^0.15.2",
"sharp": "^0.34.2", "sharp": "^0.34.3",
"sjcl-with-all": "1.0.8", "sjcl-with-all": "1.0.8",
"swr": "^2.3.3", "swr": "^2.3.4",
"zod": "^3.25.63" "zod": "^4.0.5"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.1",
"@iconify/react": "^6.0.0", "@iconify/react": "^6.0.0",
"@tailwindcss/postcss": "^4.1.10", "@tailwindcss/postcss": "^4.1.11",
"@types/canvas-confetti": "^1.9.0", "@types/canvas-confetti": "^1.9.0",
"@types/node": "^24.0.1", "@types/node": "^24.0.13",
"@types/react": "^19.1.8", "@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6", "@types/react-dom": "^19.1.6",
"eslint": "^9.28.0", "eslint": "^9.31.0",
"eslint-config-next": "15.3.3", "eslint-config-next": "15.3.5",
"prisma": "^6.9.0", "prisma": "^6.11.1",
"tailwindcss": "^4.1.10", "tailwindcss": "^4.1.11",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vitest": "^3.2.3" "vitest": "^3.2.4"
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -13,7 +13,7 @@ export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams; const searchParams = request.nextUrl.searchParams;
const parsed = idSchema.safeParse(searchParams.get("id")); const parsed = idSchema.safeParse(searchParams.get("id"));
if (!parsed.success) return NextResponse.json({ error: parsed.error.errors[0].message }, { status: 400 }); if (!parsed.success) return NextResponse.json({ error: parsed.error.issues[0].message }, { status: 400 });
const userId = parsed.data; const userId = parsed.data;
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({

View file

@ -11,18 +11,15 @@ import { PunishmentType } from "@prisma/client";
const punishSchema = z.object({ const punishSchema = z.object({
type: z.enum([PunishmentType.WARNING, PunishmentType.TEMP_EXILE, PunishmentType.PERM_EXILE]), type: z.enum([PunishmentType.WARNING, PunishmentType.TEMP_EXILE, PunishmentType.PERM_EXILE]),
duration: z duration: z
.number({ message: "Duration (days) must be a number" }) .number({ error: "Duration (days) must be a number" })
.int({ message: "Duration (days) must be an integer" }) .int({ error: "Duration (days) must be an integer" })
.positive({ message: "Duration (days) must be valid" }), .positive({ error: "Duration (days) must be valid" }),
notes: z.string(), notes: z.string(),
reasons: z.array(z.string()).optional(), reasons: z.array(z.string()).optional(),
miiReasons: z miiReasons: z
.array( .array(
z.object({ z.object({
id: z id: z.number({ error: "Mii ID must be a number" }).int({ error: "Mii ID must be an integer" }).positive({ error: "Mii ID must be valid" }),
.number({ message: "Mii ID must be a number" })
.int({ message: "Mii ID must be an integer" })
.positive({ message: "Mii ID must be valid" }),
reason: z.string(), reason: z.string(),
}) })
) )
@ -38,13 +35,13 @@ export async function POST(request: NextRequest) {
const searchParams = request.nextUrl.searchParams; const searchParams = request.nextUrl.searchParams;
const parsedUserId = idSchema.safeParse(searchParams.get("id")); const parsedUserId = idSchema.safeParse(searchParams.get("id"));
if (!parsedUserId.success) return NextResponse.json({ error: parsedUserId.error.errors[0].message }, { status: 400 }); if (!parsedUserId.success) return NextResponse.json({ error: parsedUserId.error.issues[0].message }, { status: 400 });
const userId = parsedUserId.data; const userId = parsedUserId.data;
const body = await request.json(); const body = await request.json();
const parsed = punishSchema.safeParse(body); const parsed = punishSchema.safeParse(body);
if (!parsed.success) return NextResponse.json({ error: parsed.error.errors[0].message }, { status: 400 }); if (!parsed.success) return NextResponse.json({ error: parsed.error.issues[0].message }, { status: 400 });
const { type, duration, notes, reasons, miiReasons } = parsed.data; const { type, duration, notes, reasons, miiReasons } = parsed.data;
const expiresAt = type === "TEMP_EXILE" ? dayjs().add(duration, "days").toDate() : null; const expiresAt = type === "TEMP_EXILE" ? dayjs().add(duration, "days").toDate() : null;
@ -77,7 +74,7 @@ export async function DELETE(request: NextRequest) {
const searchParams = request.nextUrl.searchParams; const searchParams = request.nextUrl.searchParams;
const parsedPunishmentId = idSchema.safeParse(searchParams.get("id")); const parsedPunishmentId = idSchema.safeParse(searchParams.get("id"));
if (!parsedPunishmentId.success) return NextResponse.json({ error: parsedPunishmentId.error.errors[0].message }, { status: 400 }); if (!parsedPunishmentId.success) return NextResponse.json({ error: parsedPunishmentId.error.issues[0].message }, { status: 400 });
const punishmentId = parsedPunishmentId.data; const punishmentId = parsedPunishmentId.data;
await prisma.punishment.delete({ await prisma.punishment.delete({

View file

@ -18,7 +18,7 @@ export async function PATCH(request: NextRequest) {
if (!displayName) return rateLimit.sendResponse({ error: "New display name is required" }, 400); if (!displayName) return rateLimit.sendResponse({ error: "New display name is required" }, 400);
const validation = displayNameSchema.safeParse(displayName); const validation = displayNameSchema.safeParse(displayName);
if (!validation.success) return rateLimit.sendResponse({ error: validation.error.errors[0].message }, 400); if (!validation.success) return rateLimit.sendResponse({ error: validation.error.issues[0].message }, 400);
// Check for inappropriate words // Check for inappropriate words
if (profanity.exists(displayName)) return rateLimit.sendResponse({ error: "Display name contains inappropriate words" }, 400); if (profanity.exists(displayName)) return rateLimit.sendResponse({ error: "Display name contains inappropriate words" }, 400);

View file

@ -40,7 +40,7 @@ export async function PATCH(request: NextRequest) {
image: formData.get("image"), image: formData.get("image"),
}); });
if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.errors[0].message }, 400); if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.issues[0].message }, 400);
const { image } = parsed.data; const { image } = parsed.data;
// If there is no image, set the profile picture to the guest image // If there is no image, set the profile picture to the guest image

View file

@ -29,7 +29,7 @@ export async function PATCH(request: NextRequest) {
} }
const validation = usernameSchema.safeParse(username); const validation = usernameSchema.safeParse(username);
if (!validation.success) return rateLimit.sendResponse({ error: validation.error.errors[0].message }, 400); if (!validation.success) return rateLimit.sendResponse({ error: validation.error.issues[0].message }, 400);
// Check for inappropriate words // Check for inappropriate words
if (profanity.exists(username)) return rateLimit.sendResponse({ error: "Username contains inappropriate words" }, 400); if (profanity.exists(username)) return rateLimit.sendResponse({ error: "Username contains inappropriate words" }, 400);

View file

@ -20,7 +20,7 @@ export async function DELETE(request: NextRequest, { params }: { params: Promise
const { id: slugId } = await params; const { id: slugId } = await params;
const parsed = idSchema.safeParse(slugId); const parsed = idSchema.safeParse(slugId);
if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.errors[0].message }, 400); if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.issues[0].message }, 400);
const miiId = parsed.data; const miiId = parsed.data;
// Check ownership of Mii // Check ownership of Mii

View file

@ -37,7 +37,7 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
// Get Mii ID // Get Mii ID
const { id: slugId } = await params; const { id: slugId } = await params;
const parsedId = idSchema.safeParse(slugId); const parsedId = idSchema.safeParse(slugId);
if (!parsedId.success) return rateLimit.sendResponse({ error: parsedId.error.errors[0].message }, 400); if (!parsedId.success) return rateLimit.sendResponse({ error: parsedId.error.issues[0].message }, 400);
const miiId = parsedId.data; const miiId = parsedId.data;
// Check ownership of Mii // Check ownership of Mii
@ -78,7 +78,7 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
image3: formData.get("image3"), image3: formData.get("image3"),
}); });
if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.errors[0].message }, 400); if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.issues[0].message }, 400);
const { name, tags, description, image1, image2, image3 } = parsed.data; const { name, tags, description, image1, image2, image3 } = parsed.data;
// Validate image files // Validate image files

View file

@ -15,7 +15,7 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
const { id: slugId } = await params; const { id: slugId } = await params;
const parsed = idSchema.safeParse(slugId); const parsed = idSchema.safeParse(slugId);
if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.errors[0].message }, 400); if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.issues[0].message }, 400);
const miiId = parsed.data; const miiId = parsed.data;
const result = await prisma.$transaction(async (tx) => { const result = await prisma.$transaction(async (tx) => {

View file

@ -8,8 +8,8 @@ import { RateLimit } from "@/lib/rate-limit";
import { MiiWithUsername } from "@/types"; import { MiiWithUsername } from "@/types";
const reportSchema = z.object({ const reportSchema = z.object({
id: z.coerce.number({ message: "ID must be a number" }).int({ message: "ID must be an integer" }).positive({ message: "ID must be valid" }), id: z.coerce.number({ error: "ID must be a number" }).int({ error: "ID must be an integer" }).positive({ error: "ID must be valid" }),
type: z.enum(["mii", "user"], { message: "Type must be either 'mii' or 'user'" }), type: z.enum(["mii", "user"], { error: "Type must be either 'mii' or 'user'" }),
reason: z.enum(["inappropriate", "spam", "copyright", "other"], { reason: z.enum(["inappropriate", "spam", "copyright", "other"], {
message: "Reason must be either 'inappropriate', 'spam', 'copyright', or 'other'", message: "Reason must be either 'inappropriate', 'spam', 'copyright', or 'other'",
}), }),
@ -27,7 +27,7 @@ export async function POST(request: NextRequest) {
const body = await request.json(); const body = await request.json();
const parsed = reportSchema.safeParse(body); const parsed = reportSchema.safeParse(body);
if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.errors[0].message }, 400); if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.issues[0].message }, 400);
const { id, type, reason, notes } = parsed.data; const { id, type, reason, notes } = parsed.data;
let mii: MiiWithUsername | null = null; let mii: MiiWithUsername | null = null;

View file

@ -25,9 +25,7 @@ const submitSchema = z.object({
name: nameSchema, name: nameSchema,
tags: tagsSchema, tags: tagsSchema,
description: z.string().trim().max(256).optional(), description: z.string().trim().max(256).optional(),
qrBytesRaw: z qrBytesRaw: z.array(z.number(), { error: "A QR code is required" }).length(372, { error: "QR code size is not a valid Tomodachi Life QR code" }),
.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.union([z.instanceof(File), z.any()]).optional(), image1: z.union([z.instanceof(File), z.any()]).optional(),
image2: z.union([z.instanceof(File), z.any()]).optional(), image2: z.union([z.instanceof(File), z.any()]).optional(),
image3: z.union([z.instanceof(File), z.any()]).optional(), image3: z.union([z.instanceof(File), z.any()]).optional(),
@ -67,7 +65,7 @@ export async function POST(request: NextRequest) {
image3: formData.get("image3"), image3: formData.get("image3"),
}); });
if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.errors[0].message }, 400); if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.issues[0].message }, 400);
const { name: uncensoredName, tags: uncensoredTags, description: uncensoredDescription, qrBytesRaw, image1, image2, image3 } = parsed.data; const { name: uncensoredName, tags: uncensoredTags, description: uncensoredDescription, qrBytesRaw, image1, image2, image3 } = parsed.data;
// Censor potential inappropriate words // Censor potential inappropriate words

View file

@ -25,11 +25,11 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
const { id: slugId } = await params; const { id: slugId } = await params;
const parsed = idSchema.safeParse(slugId); const parsed = idSchema.safeParse(slugId);
if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.errors[0].message }, 400); if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.issues[0].message }, 400);
const miiId = parsed.data; const miiId = parsed.data;
const searchParamsParsed = searchParamsSchema.safeParse(Object.fromEntries(request.nextUrl.searchParams)); const searchParamsParsed = searchParamsSchema.safeParse(Object.fromEntries(request.nextUrl.searchParams));
if (!searchParamsParsed.success) return rateLimit.sendResponse({ error: searchParamsParsed.error.errors[0].message }, 400); if (!searchParamsParsed.success) return rateLimit.sendResponse({ error: searchParamsParsed.error.issues[0].message }, 400);
const { type: imageType } = searchParamsParsed.data; const { type: imageType } = searchParamsParsed.data;
const fileExtension = imageType === "metadata" ? ".png" : ".webp"; const fileExtension = imageType === "metadata" ? ".png" : ".webp";

View file

@ -13,7 +13,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
const { id: slugId } = await params; const { id: slugId } = await params;
const parsed = idSchema.safeParse(slugId); const parsed = idSchema.safeParse(slugId);
if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.errors[0].message }, 400); if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.issues[0].message }, 400);
const userId = parsed.data; const userId = parsed.data;
const filePath = path.join(process.cwd(), "uploads", "user", `${userId}.webp`); const filePath = path.join(process.cwd(), "uploads", "user", `${userId}.webp`);

View file

@ -36,15 +36,15 @@ const searchSchema = z.object({
// todo: incorporate tagsSchema // todo: incorporate tagsSchema
// Pages // Pages
limit: z.coerce limit: z.coerce
.number({ message: "Limit must be a number" }) .number({ error: "Limit must be a number" })
.int({ message: "Limit must be an integer" }) .int({ error: "Limit must be an integer" })
.min(1, { message: "Limit must be at least 1" }) .min(1, { error: "Limit must be at least 1" })
.max(100, { message: "Limit cannot be more than 100" }) .max(100, { error: "Limit cannot be more than 100" })
.optional(), .optional(),
page: z.coerce page: z.coerce
.number({ message: "Page must be a number" }) .number({ error: "Page must be a number" })
.int({ message: "Page must be an integer" }) .int({ error: "Page must be an integer" })
.min(1, { message: "Page must be at least 1" }) .min(1, { error: "Page must be at least 1" })
.optional(), .optional(),
}); });
@ -52,7 +52,7 @@ export default async function MiiList({ searchParams, userId, inLikesPage }: Pro
const session = await auth(); const session = await auth();
const parsed = searchSchema.safeParse(searchParams); const parsed = searchSchema.safeParse(searchParams);
if (!parsed.success) return <h1>{parsed.error.errors[0].message}</h1>; if (!parsed.success) return <h1>{parsed.error.issues[0].message}</h1>;
const { q: query, sort, tags, page = 1, limit = 24 } = parsed.data; const { q: query, sort, tags, page = 1, limit = 24 } = parsed.data;

View file

@ -24,7 +24,7 @@ export default function ProfileSettings() {
const handleSubmitDisplayNameChange = async (close: () => void) => { const handleSubmitDisplayNameChange = async (close: () => void) => {
const parsed = displayNameSchema.safeParse(displayName); const parsed = displayNameSchema.safeParse(displayName);
if (!parsed.success) { if (!parsed.success) {
setDisplayNameChangeError(parsed.error.errors[0].message); setDisplayNameChangeError(parsed.error.issues[0].message);
return; return;
} }
@ -47,7 +47,7 @@ export default function ProfileSettings() {
const handleSubmitUsernameChange = async (close: () => void) => { const handleSubmitUsernameChange = async (close: () => void) => {
const parsed = usernameSchema.safeParse(username); const parsed = usernameSchema.safeParse(username);
if (!parsed.success) { if (!parsed.success) {
setUsernameChangeError(parsed.error.errors[0].message); setUsernameChangeError(parsed.error.issues[0].message);
return; return;
} }

View file

@ -44,12 +44,12 @@ export default function EditForm({ mii, likes }: Props) {
// Validate before sending request // Validate before sending request
const nameValidation = nameSchema.safeParse(name); const nameValidation = nameSchema.safeParse(name);
if (!nameValidation.success) { if (!nameValidation.success) {
setError(nameValidation.error.errors[0].message); setError(nameValidation.error.issues[0].message);
return; return;
} }
const tagsValidation = tagsSchema.safeParse(tags); const tagsValidation = tagsSchema.safeParse(tags);
if (!tagsValidation.success) { if (!tagsValidation.success) {
setError(tagsValidation.error.errors[0].message); setError(tagsValidation.error.issues[0].message);
return; return;
} }

View file

@ -49,12 +49,12 @@ export default function SubmitForm() {
// Validate before sending request // Validate before sending request
const nameValidation = nameSchema.safeParse(name); const nameValidation = nameSchema.safeParse(name);
if (!nameValidation.success) { if (!nameValidation.success) {
setError(nameValidation.error.errors[0].message); setError(nameValidation.error.issues[0].message);
return; return;
} }
const tagsValidation = tagsSchema.safeParse(tags); const tagsValidation = tagsSchema.safeParse(tags);
if (!tagsValidation.success) { if (!tagsValidation.success) {
setError(tagsValidation.error.errors[0].message); setError(tagsValidation.error.issues[0].message);
return; return;
} }

View file

@ -11,7 +11,7 @@ export default function UsernameForm() {
const handleSubmit = async () => { const handleSubmit = async () => {
const parsed = usernameSchema.safeParse(username); const parsed = usernameSchema.safeParse(username);
if (!parsed.success) setError(parsed.error.errors[0].message); if (!parsed.success) setError(parsed.error.issues[0].message);
const response = await fetch("/api/auth/username", { const response = await fetch("/api/auth/username", {
method: "PATCH", method: "PATCH",

View file

@ -5,39 +5,39 @@ import { z } from "zod";
export const querySchema = z export const querySchema = z
.string() .string()
.trim() .trim()
.min(2, { message: "Search query must be at least 2 characters long" }) .min(2, { error: "Search query must be at least 2 characters long" })
.max(64, { message: "Search query cannot be more than 64 characters long" }) .max(64, { error: "Search query cannot be more than 64 characters long" })
.regex(/^[a-zA-Z0-9-_. ']+$/, { .regex(/^[a-zA-Z0-9-_. ']+$/, {
message: "Search query can only contain letters, numbers, dashes, underscores, apostrophes, and spaces.", error: "Search query can only contain letters, numbers, dashes, underscores, apostrophes, and spaces.",
}); });
// Miis // Miis
export const nameSchema = z export const nameSchema = z
.string() .string()
.trim() .trim()
.min(2, { message: "Name must be at least 2 characters long" }) .min(2, { error: "Name must be at least 2 characters long" })
.max(64, { message: "Name cannot be more than 64 characters long" }) .max(64, { error: "Name cannot be more than 64 characters long" })
.regex(/^[a-zA-Z0-9-_. ']+$/, { .regex(/^[a-zA-Z0-9-_. ']+$/, {
message: "Name can only contain letters, numbers, dashes, underscores, apostrophes, and spaces.", error: "Name can only contain letters, numbers, dashes, underscores, apostrophes, and spaces.",
}); });
export const tagsSchema = z export const tagsSchema = z
.array( .array(
z z
.string() .string()
.min(2, { message: "Tags must be at least 2 characters long" }) .min(2, { error: "Tags must be at least 2 characters long" })
.max(64, { message: "Tags cannot be more than 20 characters long" }) .max(64, { error: "Tags cannot be more than 20 characters long" })
.regex(/^[a-z0-9-_]+$/, { .regex(/^[a-z0-9-_]+$/, {
message: "Tags can only contain lowercase letters, numbers, dashes, and underscores.", error: "Tags can only contain lowercase letters, numbers, dashes, and underscores.",
}) })
) )
.min(1, { message: "There must be at least 1 tag" }) .min(1, { error: "There must be at least 1 tag" })
.max(8, { message: "There cannot be more than 8 tags" }); .max(8, { error: "There cannot be more than 8 tags" });
export const idSchema = z.coerce export const idSchema = z.coerce
.number({ message: "ID must be a number" }) .number({ error: "ID must be a number" })
.int({ message: "ID must be an integer" }) .int({ error: "ID must be an integer" })
.positive({ message: "ID must be valid" }); .positive({ error: "ID must be valid" });
// Account Info // Account Info
export const usernameSchema = z export const usernameSchema = z
@ -50,8 +50,8 @@ export const usernameSchema = z
export const displayNameSchema = z export const displayNameSchema = z
.string() .string()
.trim() .trim()
.min(2, { message: "Display name must be at least 2 characters long" }) .min(2, { error: "Display name must be at least 2 characters long" })
.max(64, { message: "Display name cannot be more than 64 characters long" }) .max(64, { error: "Display name cannot be more than 64 characters long" })
.regex(/^[a-zA-Z0-9-_. ']+$/, { .regex(/^[a-zA-Z0-9-_. ']+$/, {
message: "Display name can only contain letters, numbers, dashes, underscores, apostrophes, and spaces.", error: "Display name can only contain letters, numbers, dashes, underscores, apostrophes, and spaces.",
}); });