diff --git a/backend/.env.example b/backend/.env.example index 864e59c..da79753 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -5,7 +5,8 @@ REDIS_URL="redis://localhost:6379/0" # Used for metadata, sitemaps, etc. NEXT_PUBLIC_BASE_URL=http://localhost:3000 -FRONTEND_URL=http://localhost:4321 +NEXT_PUBLIC_FRONTEND_URL=http://localhost:5173 +NEXT_PUBLIC_STATIC_URL= CLOUDFLARE_ZONE_ID=XXXXXXXXXXXXXXXX CLOUDFLARE_API_TOKEN=XXXXXXXXXXXXXXXX diff --git a/backend/src/app/api/mii/[id]/edit/route.ts b/backend/src/app/api/mii/[id]/edit/route.ts index d840838..7f1207f 100644 --- a/backend/src/app/api/mii/[id]/edit/route.ts +++ b/backend/src/app/api/mii/[id]/edit/route.ts @@ -246,8 +246,8 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ body: JSON.stringify({ files: [ `${process.env.NEXT_PUBLIC_BASE_URL}/mii/${miiId}`, - `${process.env.NEXT_PUBLIC_BASE_URL}/mii/${miiId}/image?type=mii`, - `${process.env.NEXT_PUBLIC_BASE_URL}/mii/${miiId}/image?type=features`, + `${process.env.NEXT_PUBLIC_STATIC_URL}/mii/${miiId}/mii.png`, + `${process.env.NEXT_PUBLIC_STATIC_URL}/mii/${miiId}/features.png`, ], }), }).catch((err) => { diff --git a/backend/src/app/mii/[id]/image/route.ts b/backend/src/app/mii/[id]/image/route.ts deleted file mode 100644 index 21b1782..0000000 --- a/backend/src/app/mii/[id]/image/route.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { NextRequest } from "next/server"; -import { Prisma } from "@prisma/client"; - -import fs from "fs/promises"; -import path from "path"; -import { z } from "zod"; - -import { idSchema } from "@tomodachi-share/shared/schemas"; -import { RateLimit } from "@/lib/rate-limit"; -import { generateMetadataImage } from "@/lib/images"; -import { prisma } from "@/lib/prisma"; - -const searchParamsSchema = z.object({ - type: z - .enum(["mii", "qr-code", "features", "image0", "image1", "image2", "metadata"], { - message: "Image type must be either 'mii', 'qr-code', 'features', 'image[number from 0 to 2]' or 'metadata'", - }) - .default("mii"), -}); - -export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { - const rateLimit = new RateLimit(request, 200, "/mii/image"); - 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 searchParamsParsed = searchParamsSchema.safeParse(Object.fromEntries(request.nextUrl.searchParams)); - if (!searchParamsParsed.success) return rateLimit.sendResponse({ error: searchParamsParsed.error.issues[0].message }, 400); - const { type: imageType } = searchParamsParsed.data; - - const filePath = path.join(process.cwd(), "uploads", "mii", miiId.toString(), `${imageType}.png`); - - let buffer: Buffer | undefined; - // Only find Mii if image type is 'metadata' - let mii: Prisma.MiiGetPayload<{ - include: { - user: { - select: { - name: true; - }; - }; - }; - }> | null = null; - - if (imageType === "metadata") { - mii = await prisma.mii.findUnique({ - where: { - id: miiId, - }, - include: { - user: { - select: { - name: true, - }, - }, - }, - }); - - if (!mii) { - return rateLimit.sendResponse({ error: "Mii not found" }, 404); - } - } - - try { - // Try to read file - buffer = await fs.readFile(filePath); - } catch { - // If the readFile() fails, that probably means it doesn't exist - if (imageType === "metadata" && mii) { - // Metadata images were added after 1274 Miis were submitted, so we generate it on-the-fly - console.log(`Metadata image not found for mii ID ${miiId}, generating metadata image...`); - const { buffer: metadataBuffer, error, status } = await generateMetadataImage(mii, mii.user.name!); - - if (error) { - return rateLimit.sendResponse({ error }, status); - } - - buffer = metadataBuffer; - } else { - return rateLimit.sendResponse({ error: "Image not found" }, 404); - } - } - - if (!buffer) return rateLimit.sendResponse({ error: "Image not found" }, 404); - - // Set the file name for the metadata image in the response for SEO - if (mii && imageType === "metadata") { - const slugify = (str: string) => - str - .toLowerCase() - .replace(/[^a-z0-9]+/g, "-") // replace non-alphanumeric with hyphens - .replace(/^-+|-+$/g, ""); - - const name = slugify(mii.name); - const tags = mii.tags.map(slugify).join("-"); - - const filename = `${name}-mii-${tags}.png`; - - return rateLimit.sendResponse(buffer, 200, { - "Content-Type": "image/png", - "Content-Disposition": `inline; filename="${filename}"`, - "Cache-Control": "public, max-age=31536000", - }); - } - - 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", - }); -} diff --git a/backend/src/app/profile/[id]/picture/route.ts b/backend/src/app/profile/[id]/picture/route.ts deleted file mode 100644 index ae755c7..0000000 --- a/backend/src/app/profile/[id]/picture/route.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; - -import fs from "fs/promises"; -import path from "path"; - -import { idSchema } from "@tomodachi-share/shared/schemas"; -import { RateLimit } from "@/lib/rate-limit"; - -export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { - const rateLimit = new RateLimit(request, 16, "/profile/picture"); - 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 userId = parsed.data; - - const filePath = path.join(process.cwd(), "uploads", "user", `${userId}.png`); - - try { - const buffer = await fs.readFile(filePath); - return new NextResponse(new Uint8Array(buffer)); // convert to Uint8Array due to weird types issue - } catch { - return rateLimit.sendResponse({ error: "Image not found" }, 404); - } -} diff --git a/backend/src/app/sitemap.ts b/backend/src/app/sitemap.ts index 5d57800..19be510 100644 --- a/backend/src/app/sitemap.ts +++ b/backend/src/app/sitemap.ts @@ -7,8 +7,9 @@ export const revalidate = 43200; // update every 12 hours export default async function sitemap(): Promise { const baseUrl = process.env.NEXT_PUBLIC_BASE_URL; - if (!baseUrl) { - console.error("NEXT_PUBLIC_BASE_URL environment variable missing"); + const staticUrl = process.env.NEXT_PUBLIC_STATIC_URL; + if (!baseUrl || !staticUrl) { + console.error("NEXT_PUBLIC_BASE_URL or NEXT_PUBLIC_STATIC_URL environment variable missing"); return []; } @@ -34,7 +35,7 @@ export default async function sitemap(): Promise { lastModified: mii.createdAt, changeFrequency: "weekly", priority: 0.7, - images: [`${baseUrl}/mii/${mii.id}/image?type=metadata`], + images: [`${staticUrl}/mii/${mii.id}/metadata.png`], }) as SitemapRoute, ), ...users.map( diff --git a/frontend/src/components/mii/delete-mii-button.tsx b/frontend/src/components/mii/delete-mii-button.tsx index 199ae3b..121bf39 100644 --- a/frontend/src/components/mii/delete-mii-button.tsx +++ b/frontend/src/components/mii/delete-mii-button.tsx @@ -85,7 +85,7 @@ export default function DeleteMiiButton({ miiId, miiName, likes, inMiiPage }: Pr

Are you sure? This will delete your Mii permanently. This action cannot be undone.

- mii image + mii image

{miiName} diff --git a/frontend/src/components/mii/list/index.tsx b/frontend/src/components/mii/list/index.tsx index 4c9f84e..e7a22b8 100644 --- a/frontend/src/components/mii/list/index.tsx +++ b/frontend/src/components/mii/list/index.tsx @@ -126,7 +126,7 @@ export default function MiiList({ parentPage, userId }: Props) { {parentPage !== "admin" ? ( mii image {[ - `${import.meta.env.VITE_API_URL}/mii/${mii.id}/image?type=mii`, + `${import.meta.env.VITE_STATIC_URL}/mii/${mii.id}/mii.png`, mii.platform === "THREE_DS" - ? `${import.meta.env.VITE_API_URL}/mii/${mii.id}/image?type=qr-code` - : `${import.meta.env.VITE_API_URL}/mii/${mii.id}/image?type=features`, - ...Array.from({ length: mii.imageCount }, (_, i) => `${import.meta.env.VITE_API_URL}/mii/${mii.id}/image?type=image${i}`), + ? `${import.meta.env.VITE_STATIC_URL}/mii/${mii.id}/qr-code.png` + : `${import.meta.env.VITE_STATIC_URL}/mii/${mii.id}/features.png`, + ...Array.from({ length: mii.imageCount }, (_, i) => `${import.meta.env.VITE_STATIC_URL}/mii/${mii.id}/image${i}.png`), ].map((src, i) => ( mii image ))} diff --git a/frontend/src/components/mii/share-mii-button.tsx b/frontend/src/components/mii/share-mii-button.tsx index 8ce513b..eacefd3 100644 --- a/frontend/src/components/mii/share-mii-button.tsx +++ b/frontend/src/components/mii/share-mii-button.tsx @@ -28,7 +28,7 @@ export default function ShareMiiButton({ miiId }: Props) { }; const handleCopyImage = async () => { - const response = await fetch(`${import.meta.env.VITE_API_URL}/mii/${miiId}/image?type=metadata`); + const response = await fetch(`${import.meta.env.VITE_STATIC_URL}/mii/${miiId}/metadata.png`); const blob = await response.blob(); await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]); @@ -118,7 +118,7 @@ export default function ShareMiiButton({ miiId }: Props) {

mii 'metadata' image {/* Save button */} { - const path = `${API_URL}/mii/${mii.id}/image?type=image${index}`; + const path = `${STATIC_URL}/mii/${mii.id}/image${index}.png`; const response = await fetch(path); const blob = await response.blob(); @@ -167,6 +167,7 @@ export default function EditMiiPage() { }, [mii, mii?.id, mii?.imageCount]); const API_URL = import.meta.env.VITE_API_URL; + const STATIC_URL = import.meta.env.VITE_STATIC_URL; useEffect(() => { fetch(`${API_URL}/api/mii/${id}/info`) @@ -181,8 +182,8 @@ export default function EditMiiPage() { setDescription(data.description); setGender(data.gender ?? "MALE"); setMakeup(data.makeup ?? "PARTIAL"); - setMiiPortraitUri(`${API_URL}/mii/${data.id}/image?type=mii`); - setMiiFeaturesUri(`${API_URL}/mii/${data.id}/image?type=features`); + setMiiPortraitUri(`${STATIC_URL}/mii/${data.id}/mii.png`); + setMiiFeaturesUri(`${STATIC_URL}/mii/${data.id}/features.png`); setYouTubeId(data.youtubeId ?? ""); setQuarantined(data.quarantined); instructions.current = deepMerge(defaultInstructions, (data.instructions as object) ?? {}); @@ -208,10 +209,8 @@ export default function EditMiiPage() {
URL.createObjectURL(file)), ]} /> diff --git a/frontend/src/pages/mii.tsx b/frontend/src/pages/mii.tsx index cf73c47..bec20ab 100644 --- a/frontend/src/pages/mii.tsx +++ b/frontend/src/pages/mii.tsx @@ -23,6 +23,7 @@ export default function MiiPage() { const [isLiked, setIsLiked] = useState(false); const API_URL = import.meta.env.VITE_API_URL; + const STATIC_URL = import.meta.env.VITE_STATIC_URL; useEffect(() => { fetch(`${API_URL}/api/mii/${id}/info`) @@ -50,7 +51,7 @@ export default function MiiPage() { }, [id]); if (loading || !mii) return
Loading...
; - const images = [...Array.from({ length: mii.imageCount ?? 0 }, (_, index) => `${API_URL}/mii/${mii.id}/image?type=image${index}`)]; + const images = [...Array.from({ length: mii.imageCount ?? 0 }, (_, index) => `${STATIC_URL}/mii/${mii.id}/image${index}.png`)]; return (
@@ -76,7 +77,7 @@ export default function MiiPage() { {/* Mii Image */}
) : (
- mii image + mii image

{mii.name}