From 63dbaf13fa0d539c96e870940f1b660f00fbdca2 Mon Sep 17 00:00:00 2001 From: trafficlunar Date: Sat, 18 Apr 2026 18:25:10 +0100 Subject: [PATCH] feat: add back edit page and fix profile settings --- backend/next.config.ts | 3 +- backend/src/app/api/admin/accept-mii/route.ts | 2 +- backend/src/app/api/admin/can-submit/route.ts | 4 +- backend/src/app/api/admin/queue/route.ts | 4 +- .../admin/regenerate-metadata-images/route.ts | 2 +- backend/src/app/api/auth/about-me/route.ts | 2 +- backend/src/app/api/auth/name/route.ts | 2 +- backend/src/app/api/auth/picture/route.ts | 2 +- backend/src/app/api/mii/[id]/edit/route.ts | 4 +- backend/src/app/api/mii/[id]/like/route.ts | 2 +- backend/src/app/api/submit/route.ts | 2 +- backend/src/app/out/page.tsx | 72 --- backend/src/lib/auth.ts | 1 + .../src/components/admin/control-center.tsx | 90 +-- .../components/admin/regenerate-images.tsx | 168 +++--- frontend/src/components/header-profile.tsx | 61 -- frontend/src/components/header.tsx | 62 +- frontend/src/components/like-button.tsx | 2 +- frontend/src/components/mii/list/index.tsx | 36 +- .../profile-settings/delete-account.tsx | 2 +- .../src/components/profile-settings/index.tsx | 4 +- .../profile-settings/profile-picture.tsx | 2 +- .../src/components/submit-form/edit-form.tsx | 456 --------------- frontend/src/components/submit-form/index.tsx | 529 ----------------- frontend/src/main.tsx | 2 + frontend/src/pages/admin.tsx | 2 +- frontend/src/pages/edit.tsx | 474 ++++++++++++++++ frontend/src/pages/mii.tsx | 8 +- frontend/src/pages/submit.tsx | 537 +++++++++++++++++- shared/src/index.ts | 1 + {backend/src/lib => shared/src}/utils.ts | 0 31 files changed, 1246 insertions(+), 1292 deletions(-) delete mode 100644 backend/src/app/out/page.tsx delete mode 100644 frontend/src/components/header-profile.tsx delete mode 100644 frontend/src/components/submit-form/edit-form.tsx delete mode 100644 frontend/src/components/submit-form/index.tsx create mode 100644 frontend/src/pages/edit.tsx rename {backend/src/lib => shared/src}/utils.ts (100%) diff --git a/backend/next.config.ts b/backend/next.config.ts index 9253312..f9ef87e 100644 --- a/backend/next.config.ts +++ b/backend/next.config.ts @@ -9,7 +9,8 @@ const nextConfig: NextConfig = { headers: [ { key: "Access-Control-Allow-Origin", value: process.env.NEXT_PUBLIC_FRONTEND_URL || "http://localhost:4321" }, { key: "Access-Control-Allow-Credentials", value: "true" }, - { key: "Access-Control-Allow-Methods", value: "GET,POST,PATCH,DELETE,OPTIONS" }, + { key: "Access-Control-Allow-Methods", value: "GET,POST,DELETE,OPTIONS" }, + { key: "Access-Control-Allow-Headers", value: "Content-Type" }, ], }, ]; diff --git a/backend/src/app/api/admin/accept-mii/route.ts b/backend/src/app/api/admin/accept-mii/route.ts index 941f9df..06b966b 100644 --- a/backend/src/app/api/admin/accept-mii/route.ts +++ b/backend/src/app/api/admin/accept-mii/route.ts @@ -4,7 +4,7 @@ import { auth } from "@/lib/auth"; import { prisma } from "@/lib/prisma"; import { idSchema } from "@tomodachi-share/shared/schemas"; -export async function PATCH(request: NextRequest) { +export async function POST(request: NextRequest) { const session = await auth(); if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); diff --git a/backend/src/app/api/admin/can-submit/route.ts b/backend/src/app/api/admin/can-submit/route.ts index c5459b3..f164e7d 100644 --- a/backend/src/app/api/admin/can-submit/route.ts +++ b/backend/src/app/api/admin/can-submit/route.ts @@ -1,13 +1,13 @@ import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; import { auth } from "@/lib/auth"; -import { settings } from "@/lib/settings"; +import { settings } from "../../../../lib/settings"; export async function GET() { return NextResponse.json({ success: true, value: settings.canSubmit }); } -export async function PATCH(request: NextRequest) { +export async function POST(request: NextRequest) { const session = await auth(); if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); diff --git a/backend/src/app/api/admin/queue/route.ts b/backend/src/app/api/admin/queue/route.ts index 422fd7d..2f23490 100644 --- a/backend/src/app/api/admin/queue/route.ts +++ b/backend/src/app/api/admin/queue/route.ts @@ -1,13 +1,13 @@ import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; import { auth } from "@/lib/auth"; -import { settings } from "@/lib/settings"; +import { settings } from "../../../../lib/settings"; export async function GET() { return NextResponse.json({ success: true, value: settings.queueEnabled }); } -export async function PATCH(request: NextRequest) { +export async function POST(request: NextRequest) { const session = await auth(); if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); diff --git a/backend/src/app/api/admin/regenerate-metadata-images/route.ts b/backend/src/app/api/admin/regenerate-metadata-images/route.ts index 69cb2d3..10237d5 100644 --- a/backend/src/app/api/admin/regenerate-metadata-images/route.ts +++ b/backend/src/app/api/admin/regenerate-metadata-images/route.ts @@ -3,7 +3,7 @@ import { auth } from "@/lib/auth"; import { prisma } from "@/lib/prisma"; import { generateMetadataImage } from "@/lib/images"; -export async function PATCH() { +export async function POST() { const session = await auth(); if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); diff --git a/backend/src/app/api/auth/about-me/route.ts b/backend/src/app/api/auth/about-me/route.ts index 5269d0c..587daf5 100644 --- a/backend/src/app/api/auth/about-me/route.ts +++ b/backend/src/app/api/auth/about-me/route.ts @@ -6,7 +6,7 @@ import { auth } from "@/lib/auth"; import { prisma } from "@/lib/prisma"; import { RateLimit } from "@/lib/rate-limit"; -export async function PATCH(request: NextRequest) { +export async function POST(request: NextRequest) { const session = await auth(); if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); diff --git a/backend/src/app/api/auth/name/route.ts b/backend/src/app/api/auth/name/route.ts index 2e79541..8a9af28 100644 --- a/backend/src/app/api/auth/name/route.ts +++ b/backend/src/app/api/auth/name/route.ts @@ -6,7 +6,7 @@ import { prisma } from "@/lib/prisma"; import { userNameSchema } from "@tomodachi-share/shared/schemas"; import { RateLimit } from "@/lib/rate-limit"; -export async function PATCH(request: NextRequest) { +export async function POST(request: NextRequest) { const session = await auth(); if (!session || !session.user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); diff --git a/backend/src/app/api/auth/picture/route.ts b/backend/src/app/api/auth/picture/route.ts index 7f5d5fa..c5f5775 100644 --- a/backend/src/app/api/auth/picture/route.ts +++ b/backend/src/app/api/auth/picture/route.ts @@ -17,7 +17,7 @@ const formDataSchema = z.object({ image: z.union([z.instanceof(File), z.any()]).optional(), }); -export async function PATCH(request: NextRequest) { +export async function POST(request: NextRequest) { const session = await auth(); if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); diff --git a/backend/src/app/api/mii/[id]/edit/route.ts b/backend/src/app/api/mii/[id]/edit/route.ts index 930bc8e..d840838 100644 --- a/backend/src/app/api/mii/[id]/edit/route.ts +++ b/backend/src/app/api/mii/[id]/edit/route.ts @@ -14,7 +14,7 @@ import { idSchema, nameSchema, switchMiiInstructionsSchema, tagsSchema } from "@ import { generateMetadataImage, validateImage } from "@/lib/images"; import { RateLimit } from "@/lib/rate-limit"; import { minifyInstructions, SwitchMiiInstructions } from "@tomodachi-share/shared"; -import { settings } from "@/lib/settings"; +import { settings } from "../../../../../lib/settings"; const uploadsDirectory = path.join(process.cwd(), "uploads", "mii"); @@ -41,7 +41,7 @@ const editSchema = z.object({ image3: z.union([z.instanceof(File), z.any()]).optional(), }); -export async function PATCH(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { +export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { const session = await auth(); if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); diff --git a/backend/src/app/api/mii/[id]/like/route.ts b/backend/src/app/api/mii/[id]/like/route.ts index 9464bad..332f5bb 100644 --- a/backend/src/app/api/mii/[id]/like/route.ts +++ b/backend/src/app/api/mii/[id]/like/route.ts @@ -5,7 +5,7 @@ import { prisma } from "@/lib/prisma"; import { idSchema } from "@tomodachi-share/shared/schemas"; import { RateLimit } from "@/lib/rate-limit"; -export async function PATCH(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { +export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { const session = await auth(); if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); diff --git a/backend/src/app/api/submit/route.ts b/backend/src/app/api/submit/route.ts index c062842..178d776 100644 --- a/backend/src/app/api/submit/route.ts +++ b/backend/src/app/api/submit/route.ts @@ -18,7 +18,7 @@ import Mii from "../../../../../shared/src/mii.js/mii"; import { convertQrCode, minifyInstructions, ThreeDsTomodachiLifeMii } from "@tomodachi-share/shared"; import { SwitchMiiInstructions } from "@tomodachi-share/shared"; -import { settings } from "@/lib/settings"; +import { settings } from "../../../lib/settings"; const uploadsDirectory = path.join(process.cwd(), "uploads", "mii"); diff --git a/backend/src/app/out/page.tsx b/backend/src/app/out/page.tsx deleted file mode 100644 index d72c91b..0000000 --- a/backend/src/app/out/page.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { Metadata } from "next"; -import Link from "next/link"; -import { redirect } from "next/navigation"; -import { Icon } from "@iconify/react"; - -export const metadata: Metadata = { - title: "Leaving TomodachiShare", - description: "Warning: You are leaving TomodachiShare, proceed with caution", -}; - -interface Props { - searchParams: Promise<{ [key: string]: string | string[] | undefined }>; -} - -export default async function LinkOutPage({ searchParams }: Props) { - const url = (await searchParams).url; - if (!url || Array.isArray(url)) redirect("/"); - - let parsed: URL; - try { - parsed = new URL(url); - } catch { - redirect("/"); // redirect if URL is invalid - } - - // Next.js doesn't allow attacks like these but you can never be too safe - if (!["http:", "https:"].includes(parsed.protocol)) redirect("/"); - - const isSafe = Array.from(SAFE_LINKS).some((domain) => parsed.hostname === domain || parsed.hostname.endsWith(`.${domain}`)); - if (isSafe) redirect(url); - - return ( -
-
-

- - Warning -

-

You're attempting to leave TomodachiShare island! The destination website is potentially dangerous.

- -
- {url} -
- -
- - - Travel Back - - - - Continue - -
-
-
- ); -} - -const SAFE_LINKS = new Set([ - "tomodachishare.com", - "trafficlunar.net", - "youtube.com", - "youtu.be", - "twitter.com", - "x.com", - "reddit.com", - "tiktok.com", - "tumblr.com", - "instagram.com", - "wikipedia.org", -]); diff --git a/backend/src/lib/auth.ts b/backend/src/lib/auth.ts index 9a04006..71f2774 100644 --- a/backend/src/lib/auth.ts +++ b/backend/src/lib/auth.ts @@ -18,6 +18,7 @@ export const { handlers, signIn, signOut, auth } = NextAuth({ sameSite: "none", path: "/", secure: true, + domain: process.env.NODE_ENV === "production" ? ".tomodachishare.com" : "localhost", }, }, }, diff --git a/frontend/src/components/admin/control-center.tsx b/frontend/src/components/admin/control-center.tsx index 4616a07..320690d 100644 --- a/frontend/src/components/admin/control-center.tsx +++ b/frontend/src/components/admin/control-center.tsx @@ -1,45 +1,45 @@ -// import { settings } from "@/lib/settings"; -// import { useState } from "react"; - -// export default function ControlCenter() { -// const [canSubmit, setCanSubmit] = useState(settings.canSubmit); -// const [isQueueEnabled, setIsQeueueEnabled] = useState(settings.queueEnabled); - -// const onClickSet = async () => { -// await fetch("/api/admin/can-submit", { method: "PATCH", body: JSON.stringify(canSubmit) }); -// await fetch("/api/admin/queue", { method: "PATCH", body: JSON.stringify(isQueueEnabled) }); -// }; - -// return ( -//
-//
-// setCanSubmit(e.target.checked)} -// /> -// -//
-//
-// setIsQeueueEnabled(e.target.checked)} -// /> -// -//
- -//
-// -//
-//
-// ); -// } +// import { settings } from "@/lib/settings"; +// import { useState } from "react"; + +// export default function ControlCenter() { +// const [canSubmit, setCanSubmit] = useState(settings.canSubmit); +// const [isQueueEnabled, setIsQeueueEnabled] = useState(settings.queueEnabled); + +// const onClickSet = async () => { +// await fetch("/api/admin/can-submit", { method: "POST", body: JSON.stringify(canSubmit) }); +// await fetch("/api/admin/queue", { method: "POST", body: JSON.stringify(isQueueEnabled) }); +// }; + +// return ( +//
+//
+// setCanSubmit(e.target.checked)} +// /> +// +//
+//
+// setIsQeueueEnabled(e.target.checked)} +// /> +// +//
+ +//
+// +//
+//
+// ); +// } diff --git a/frontend/src/components/admin/regenerate-images.tsx b/frontend/src/components/admin/regenerate-images.tsx index fda6a53..8e15372 100644 --- a/frontend/src/components/admin/regenerate-images.tsx +++ b/frontend/src/components/admin/regenerate-images.tsx @@ -1,84 +1,84 @@ -import { useEffect, useState } from "react"; -import { createPortal } from "react-dom"; - -import { Icon } from "@iconify/react"; -import SubmitButton from "../submit-button"; - -export default function RegenerateImagesButton() { - const [isOpen, setIsOpen] = useState(false); - const [isVisible, setIsVisible] = useState(false); - - const [error, setError] = useState(undefined); - - const handleSubmit = async () => { - const response = await fetch("/api/admin/regenerate-metadata-images", { method: "PATCH" }); - - if (!response.ok) { - const data = await response.json(); - setError(data.error); - - return; - } - - close(); - }; - - const close = () => { - setIsVisible(false); - setTimeout(() => { - setIsOpen(false); - }, 300); - }; - - useEffect(() => { - if (isOpen) { - // slight delay to trigger animation - setTimeout(() => setIsVisible(true), 10); - } - }, [isOpen]); - - return ( - <> - - - {isOpen && - createPortal( -
-
- -
-
-

Regenerate Images

- -
- -

Are you sure? This will delete and regenerate every metadata image.

- - {error && Error: {error}} - -
- - -
-
-
, - document.body, - )} - - ); -} +import { useEffect, useState } from "react"; +import { createPortal } from "react-dom"; + +import { Icon } from "@iconify/react"; +import SubmitButton from "../submit-button"; + +export default function RegenerateImagesButton() { + const [isOpen, setIsOpen] = useState(false); + const [isVisible, setIsVisible] = useState(false); + + const [error, setError] = useState(undefined); + + const handleSubmit = async () => { + const response = await fetch("/api/admin/regenerate-metadata-images", { method: "POST" }); + + if (!response.ok) { + const data = await response.json(); + setError(data.error); + + return; + } + + close(); + }; + + const close = () => { + setIsVisible(false); + setTimeout(() => { + setIsOpen(false); + }, 300); + }; + + useEffect(() => { + if (isOpen) { + // slight delay to trigger animation + setTimeout(() => setIsVisible(true), 10); + } + }, [isOpen]); + + return ( + <> + + + {isOpen && + createPortal( +
+
+ +
+
+

Regenerate Images

+ +
+ +

Are you sure? This will delete and regenerate every metadata image.

+ + {error && Error: {error}} + +
+ + +
+
+
, + document.body, + )} + + ); +} diff --git a/frontend/src/components/header-profile.tsx b/frontend/src/components/header-profile.tsx deleted file mode 100644 index 4853946..0000000 --- a/frontend/src/components/header-profile.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { Icon } from "@iconify/react"; -import { useEffect } from "react"; -import { useStore } from "@nanostores/react"; -import { session } from "../session"; -import { Link } from "react-router"; - -export default function HeaderProfile() { - const API_BASE_URL = import.meta.env.VITE_API_URL; - const $session = useStore(session); - - useEffect(() => { - fetch(`${API_BASE_URL}/api/auth/session`, { credentials: "include" }) - .then((res) => { - if (!res.ok) throw new Error("Failed to get session"); - return res.json(); - }) - .then((data) => { - session.set(data); - }) - .catch((err) => { - console.error(err); - }); - }, []); - - return ( - <> - {!$session?.user ? ( -
  • - - Login - -
  • - ) : ( - <> -
  • - - profile picture - {$session?.user?.name ?? "unknown"} - -
  • -
  • - - - -
  • - - )} - - ); -} diff --git a/frontend/src/components/header.tsx b/frontend/src/components/header.tsx index 0c515bf..c3d8493 100644 --- a/frontend/src/components/header.tsx +++ b/frontend/src/components/header.tsx @@ -1,9 +1,27 @@ import { Icon } from "@iconify/react"; import SearchBar from "./search-bar"; -import HeaderProfile from "./header-profile"; import { Link } from "react-router"; +import { useStore } from "@nanostores/react"; +import { session } from "../session"; +import { useEffect } from "react"; export default function Header() { + const $session = useStore(session); + + useEffect(() => { + fetch(`${import.meta.env.VITE_API_URL}/api/auth/session`, { credentials: "include" }) + .then((res) => { + if (!res.ok) throw new Error("Failed to get session"); + return res.json(); + }) + .then((data) => { + session.set(data); + }) + .catch((err) => { + console.error(err); + }); + }, []); + return (
    - + {!$session?.user ? ( +
  • + + Login + +
  • + ) : ( + <> +
  • + + { + e.currentTarget.onerror = null; // Prevent infinite loops + e.currentTarget.src = "/guest.png"; + }} + alt="profile picture" + width={40} + height={40} + className="rounded-full aspect-square object-cover h-full bg-white outline-2 outline-orange-400" + /> + {$session?.user?.name ?? "unknown"} + +
  • +
  • + + + +
  • + + )}
    ); diff --git a/frontend/src/components/like-button.tsx b/frontend/src/components/like-button.tsx index 9082918..e764f24 100644 --- a/frontend/src/components/like-button.tsx +++ b/frontend/src/components/like-button.tsx @@ -29,7 +29,7 @@ export default function LikeButton({ likes, isLiked, disabled, abbreviate, big } // 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: "POST" }); // if (response.ok) { // const { liked, count } = await response.json(); // setIsLikedState(liked); diff --git a/frontend/src/components/mii/list/index.tsx b/frontend/src/components/mii/list/index.tsx index 5cfaa97..4b8cb9d 100644 --- a/frontend/src/components/mii/list/index.tsx +++ b/frontend/src/components/mii/list/index.tsx @@ -80,21 +80,27 @@ export default function MiiList({ parentPage, userId }: Props) {
    )} - {parentPage !== "admin" ? - mii image + mii image + + ) : ( + `${import.meta.env.VITE_API_URL}/mii/${mii.id}/image?type=image${index}`), + ]} /> - : `${import.meta.env.VITE_API_URL}/mii/${mii.id}/image?type=image${index}`), - ]} - />} + )}
    @@ -141,7 +147,7 @@ export default function MiiList({ parentPage, userId }: Props) {