diff --git a/next.config.ts b/next.config.ts index f7158ac..8114235 100644 --- a/next.config.ts +++ b/next.config.ts @@ -5,6 +5,17 @@ const nextConfig: NextConfig = { images: { unoptimized: true, }, + async headers() { + return [ + { + // Prevent Cloudflare from serving cached HTML for RSC navigation requests + source: "/:path*", + headers: [ + { key: "Vary", value: "RSC, Next-Router-State-Tree, Next-Router-Prefetch" }, + ], + }, + ]; + }, }; export default nextConfig; diff --git a/package.json b/package.json index 16787e4..05c95cf 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,6 @@ "react-image-crop": "^11.0.10", "redis": "^5.11.0", "satori": "^0.26.0", - "seedrandom": "^3.0.5", "sharp": "^0.34.5", "sjcl-with-all": "1.0.8", "swr": "^2.4.1", @@ -46,7 +45,6 @@ "@types/node": "^25.6.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", - "@types/seedrandom": "^3.0.8", "@types/sjcl": "^1.0.34", "eslint": "^10.2.0", "eslint-config-next": "16.2.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 13b6320..26a202f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -71,9 +71,6 @@ importers: satori: specifier: ^0.26.0 version: 0.26.0 - seedrandom: - specifier: ^3.0.5 - version: 3.0.5 sharp: specifier: ^0.34.5 version: 0.34.5 @@ -108,9 +105,6 @@ importers: '@types/react-dom': specifier: ^19.2.3 version: 19.2.3(@types/react@19.2.14) - '@types/seedrandom': - specifier: ^3.0.8 - version: 3.0.8 '@types/sjcl': specifier: ^1.0.34 version: 1.0.34 @@ -797,9 +791,6 @@ packages: '@types/react@19.2.14': resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} - '@types/seedrandom@3.0.8': - resolution: {integrity: sha512-TY1eezMU2zH2ozQoAFAQFOPpvP15g+ZgSfTZt31AUUH/Rxtnz3H+A/Sv1Snw2/amp//omibc+AEkTaA8KUeOLQ==} - '@types/sjcl@1.0.34': resolution: {integrity: sha512-bQHEeK5DTQRunIfQeUMgtpPsNNCcZyQ9MJuAfW1I7iN0LDunTc78Fu17STbLMd7KiEY/g2zHVApippa70h6HoQ==} @@ -2252,9 +2243,6 @@ packages: schema-dts@2.0.0: resolution: {integrity: sha512-t7NoCy3Rn5GHGx6p7s1qIYK/AeIb8ZxJNR9WUNFkwMv2CiiGZBmqqYWc2FlZVm5ZbiHMY4OvBWhj7QtyrFO2Jw==} - seedrandom@3.0.5: - resolution: {integrity: sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==} - semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -3111,8 +3099,6 @@ snapshots: dependencies: csstype: 3.2.3 - '@types/seedrandom@3.0.8': {} - '@types/sjcl@1.0.34': {} '@types/use-sync-external-store@0.0.6': {} @@ -4701,8 +4687,6 @@ snapshots: transitivePeerDependencies: - typescript - seedrandom@3.0.5: {} - semver@6.3.1: {} semver@7.7.4: {} diff --git a/prisma/migrations/20260416234406_pr_28/migration.sql b/prisma/migrations/20260416234406_pr_28/migration.sql new file mode 100644 index 0000000..da78f81 --- /dev/null +++ b/prisma/migrations/20260416234406_pr_28/migration.sql @@ -0,0 +1,2 @@ +-- CreateIndex +CREATE INDEX "miis_in_queue_quarantined_createdAt_idx" ON "miis"("in_queue", "quarantined", "createdAt" DESC); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index cfc142b..c3cc1cc 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -104,6 +104,7 @@ model Mii { @@index([gender]) @@index([makeup]) @@index([quarantined, id]) + @@index([in_queue, quarantined, createdAt(sort: Desc)]) @@map("miis") } diff --git a/src/app/api/mii/[id]/like/route.ts b/src/app/api/mii/[id]/like/route.ts index 3a1e213..5680e12 100644 --- a/src/app/api/mii/[id]/like/route.ts +++ b/src/app/api/mii/[id]/like/route.ts @@ -6,54 +6,54 @@ import { idSchema } from "@/lib/schemas"; import { RateLimit } from "@/lib/rate-limit"; export async function PATCH(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { - const session = await auth(); - if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + // const session = await auth(); + // if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - const rateLimit = new RateLimit(request, 100, "/api/mii/like"); - const check = await rateLimit.handle(); - if (check) return check; + // const rateLimit = new RateLimit(request, 100, "/api/mii/like"); + // const check = await rateLimit.handle(); + // if (check) return check; - const { id: slugId } = await params; - const parsed = idSchema.safeParse(slugId); - if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.issues[0].message }, 400); - const miiId = parsed.data; + // const { id: slugId } = await params; + // const parsed = idSchema.safeParse(slugId); + // if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.issues[0].message }, 400); + // const miiId = parsed.data; - const result = await prisma.$transaction(async (tx) => { - const existingLike = await tx.like.findUnique({ - where: { - userId_miiId: { - userId: Number(session.user?.id), - miiId, - }, - }, - }); + // const result = await prisma.$transaction(async (tx) => { + // const existingLike = await tx.like.findUnique({ + // where: { + // userId_miiId: { + // userId: Number(session.user?.id), + // miiId, + // }, + // }, + // }); - if (existingLike) { - // Remove the like if it exists - await tx.like.delete({ - where: { - userId_miiId: { - userId: Number(session.user?.id), - miiId, - }, - }, - }); - } else { - // Add a like if it doesn't exist - await tx.like.create({ - data: { - userId: Number(session.user?.id), - miiId, - }, - }); - } + // if (existingLike) { + // // Remove the like if it exists + // await tx.like.delete({ + // where: { + // userId_miiId: { + // userId: Number(session.user?.id), + // miiId, + // }, + // }, + // }); + // } else { + // // Add a like if it doesn't exist + // await tx.like.create({ + // data: { + // userId: Number(session.user?.id), + // miiId, + // }, + // }); + // } - const likeCount = await tx.like.count({ - where: { miiId }, - }); + // const likeCount = await tx.like.count({ + // where: { miiId }, + // }); - return { liked: !existingLike, count: likeCount }; - }); + // return { liked: !existingLike, count: likeCount }; + // }); - return rateLimit.sendResponse({ success: true, liked: result.liked, count: result.count }); + return NextResponse.json({ success: false }); } diff --git a/src/app/api/mii/has-liked/route.ts b/src/app/api/mii/has-liked/route.ts index ecb0e87..f064c4d 100644 --- a/src/app/api/mii/has-liked/route.ts +++ b/src/app/api/mii/has-liked/route.ts @@ -4,25 +4,26 @@ import { prisma } from "@/lib/prisma"; import { RateLimit } from "@/lib/rate-limit"; export async function GET(request: NextRequest) { - const session = await auth(); - if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + // const session = await auth(); + // if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - const rateLimit = new RateLimit(request, 50, "/api/mii/like_get"); - const check = await rateLimit.handle(); - if (check) return check; + // const rateLimit = new RateLimit(request, 50, "/api/mii/like_get"); + // const check = await rateLimit.handle(); + // if (check) return check; - const idsParam = new URL(request.url).searchParams.get("ids"); - if (!idsParam) return NextResponse.json({ error: "Missing IDs parameter" }, { status: 400 }); + // const idsParam = new URL(request.url).searchParams.get("ids"); + // if (!idsParam) return NextResponse.json({ error: "Missing IDs parameter" }, { status: 400 }); - const ids = idsParam.split(",").map(Number).filter(Boolean); - if (!ids.length) return NextResponse.json({ error: "No valid IDs provided" }, { status: 400 }); - if (ids.length > 100) return NextResponse.json({ error: "Too many IDs, maximum is 100" }, { status: 400 }); + // const ids = idsParam.split(",").map(Number).filter(Boolean); + // if (!ids.length) return NextResponse.json({ error: "No valid IDs provided" }, { status: 400 }); + // if (ids.length > 100) return NextResponse.json({ error: "Too many IDs, maximum is 100" }, { status: 400 }); - const liked = await prisma.like.findMany({ - where: { userId: Number(session.user?.id), miiId: { in: ids } }, - select: { miiId: true }, - }); + // const liked = await prisma.like.findMany({ + // where: { userId: Number(session.user?.id), miiId: { in: ids } }, + // select: { miiId: true }, + // }); - // Return only Miis that are liked - return NextResponse.json(liked.map((l) => l.miiId)); + // // Return only Miis that are liked + // return NextResponse.json(liked.map((l) => l.miiId)); + return NextResponse.json({ success: false }, { status: 500 }); } diff --git a/src/app/api/submit/route.ts b/src/app/api/submit/route.ts index 710c6d6..cf862d5 100644 --- a/src/app/api/submit/route.ts +++ b/src/app/api/submit/route.ts @@ -220,7 +220,8 @@ export async function POST(request: NextRequest) { // Download the image of the Mii (3DS) if (platform === "THREE_DS") { const studioUrl = conversion?.mii.studioUrl({ width: 512 }); - const studioResponse = await fetch(studioUrl!); + if (!studioUrl || new URL(studioUrl).hostname !== "studio.mii.nintendo.com") throw new Error("Invalid studio URL"); + const studioResponse = await fetch(studioUrl); if (!studioResponse.ok) { throw new Error(`Failed to fetch Mii image ${studioResponse.status}`); diff --git a/src/app/mii/[id]/image/route.ts b/src/app/mii/[id]/image/route.ts index ced1e92..da9a272 100644 --- a/src/app/mii/[id]/image/route.ts +++ b/src/app/mii/[id]/image/route.ts @@ -20,7 +20,7 @@ const searchParamsSchema = z.object({ export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { const rateLimit = new RateLimit(request, 200, "/mii/image"); - const check = await rateLimit.handle(); + const check = await rateLimit.handleByIp(); if (check) return check; const { id: slugId } = await params; @@ -107,9 +107,12 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ }); } + // mii, features are purged on edit; qr-code is immutable. imageN isn't purged, so keep its TTL short. + const isStableType = imageType === "mii" || imageType === "qr-code" || imageType === "features"; + return rateLimit.sendResponse(buffer, 200, { "Content-Type": "image/png", "X-Robots-Tag": "noindex, noimageindex, nofollow", - "Cache-Control": "public, max-age=60, stale-while-revalidate=30", + "Cache-Control": isStableType ? "public, max-age=3600, stale-while-revalidate=86400" : "public, max-age=60, stale-while-revalidate=30", }); } diff --git a/src/app/mii/[id]/page.tsx b/src/app/mii/[id]/page.tsx index 17863ac..ec31c1e 100644 --- a/src/app/mii/[id]/page.tsx +++ b/src/app/mii/[id]/page.tsx @@ -1,3 +1,4 @@ +import { cache } from "react"; import { Metadata } from "next"; import Image from "next/image"; import Link from "next/link"; @@ -5,7 +6,6 @@ import { redirect } from "next/navigation"; import { Icon } from "@iconify/react"; -import { auth } from "@/lib/auth"; import { prisma } from "@/lib/prisma"; import { MiiPlatform } from "@prisma/client"; @@ -25,24 +25,21 @@ interface Props { params: Promise<{ id: string }>; } +export const revalidate = 300; + +const getMii = cache(async (id: number) => + prisma.mii.findUnique({ + where: { id }, + include: { + user: { select: { name: true } }, + _count: { select: { likedBy: true } }, + }, + }), +); + export async function generateMetadata({ params }: Props): Promise { const { id } = await params; - - const mii = await prisma.mii.findUnique({ - where: { - id: Number(id), - }, - include: { - user: { - select: { - name: true, - }, - }, - _count: { - select: { likedBy: true }, // Get total like count - }, - }, - }); + const mii = await getMii(Number(id)); // Bots get redirected anyways if (!mii) return {}; @@ -90,31 +87,7 @@ export async function generateMetadata({ params }: Props): Promise { export default async function MiiPage({ params }: Props) { const { id } = await params; - const session = await auth(); - - const mii = await prisma.mii.findUnique({ - where: { - id: Number(id), - }, - include: { - user: { - select: { - name: true, - }, - }, - likedBy: session?.user - ? { - where: { - userId: Number(session.user.id), - }, - select: { userId: true }, - } - : false, - _count: { - select: { likedBy: true }, // Get total like count - }, - }, - }); + const mii = await getMii(Number(id)); if (!mii) redirect("/404"); @@ -333,7 +306,7 @@ export default async function MiiPage({ params }: Props) { {/* Submission name */}

{mii.name}

{/* Like button */} - 0} big /> + {/* Tags */}
diff --git a/src/components/like-button.tsx b/src/components/like-button.tsx index 7628beb..470864e 100644 --- a/src/components/like-button.tsx +++ b/src/components/like-button.tsx @@ -24,31 +24,31 @@ export default function LikeButton({ likes, isLiked, miiId, disabled, abbreviate const [isAnimating, setIsAnimating] = useState(false); const onClick = async () => { - if (disabled) return; - if (!session.data?.user) { - router.push("/login"); - return; - } + // if (disabled) return; + // if (!session.data?.user) { + // router.push("/login"); + // return; + // } - setIsLikedState(!isLikedState); - setLikesState(isLikedState ? likesState - 1 : likesState + 1); + // setIsLikedState(!isLikedState); + // setLikesState(isLikedState ? likesState - 1 : likesState + 1); - // Trigger animation - if (!isLikedState) { - setIsAnimating(true); - setTimeout(() => setIsAnimating(false), 1000); // match animation duration - } + // // Trigger animation + // if (!isLikedState) { + // setIsAnimating(true); + // setTimeout(() => setIsAnimating(false), 1000); // match animation duration + // } - const response = await fetch(`/api/mii/${miiId}/like`, { method: "PATCH" }); + // const response = await fetch(`/api/mii/${miiId}/like`, { method: "PATCH" }); - if (response.ok) { - const { liked, count } = await response.json(); - setIsLikedState(liked); - setLikesState(count); - } else { - setIsLikedState(isLikedState); - setLikesState(likesState); - } + // if (response.ok) { + // const { liked, count } = await response.json(); + // setIsLikedState(liked); + // setLikesState(count); + // } else { + // setIsLikedState(isLikedState); + // setLikesState(likesState); + // } }; // Preload like button icons @@ -56,9 +56,9 @@ export default function LikeButton({ likes, isLiked, miiId, disabled, abbreviate loadIcons(["icon-park-solid:like", "icon-park-outline:like"]); }, []); - useEffect(() => { - setIsLikedState(isLiked); - }, [isLiked]); + // useEffect(() => { + // setIsLikedState(isLiked); + // }, [isLiked]); return (