diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 8cdcf26..d92ce0c 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -80,6 +80,8 @@ For Discord, create an application in the developer portal, go to 'OAuth2', copy For GitHub, navigate to your profile settings, then 'Developer Settings', and create a new application. Set the homepage URL to `http://localhost:3000` and copy the Client ID and generate a new client secret. Finally, add in a callback URL with the value `http://localhost:3000/api/auth/callback/github`. +Google is annoying so I'm not explaining it. + After configuring the environment variables, you can run a development server. ```bash diff --git a/src/app/api/mii/[id]/edit/route.ts b/src/app/api/mii/[id]/edit/route.ts index 17bcb38..54bb48f 100644 --- a/src/app/api/mii/[id]/edit/route.ts +++ b/src/app/api/mii/[id]/edit/route.ts @@ -110,26 +110,28 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise< parsed.data; // Validate image files - let wasImagesModerated = false; - const images: File[] = []; + const customImages: File[] = []; for (const img of [image1, image2, image3]) { if (!img) continue; const validation = await validateImage(img); - if (!validation.valid) wasImagesModerated = true; - images.push(img); + if (validation.valid) { + customImages.push(img); + } else { + return rateLimit.sendResponse({ error: `Failed to verify custom image: ${validation.error}` }, validation.status ?? 400); + } } // Check Mii portrait & features image (Switch) if (mii.platform === "SWITCH") { if (miiPortraitImage) { const validation = await validateImage(miiPortraitImage); - if (!validation.valid) wasImagesModerated = true; + if (!validation.valid) return rateLimit.sendResponse({ error: `Failed to verify portrait: ${validation.error}` }, validation.status ?? 400); } if (miiFeaturesImage) { const validation = await validateImage(miiFeaturesImage); - if (!validation.valid) wasImagesModerated = true; + if (!validation.valid) return rateLimit.sendResponse({ error: `Failed to verify features: ${validation.error}` }, validation.status ?? 400); } } @@ -147,10 +149,10 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise< if (makeup !== undefined) updateData.makeup = makeup; if (youtubeId !== undefined) updateData.youtubeId = youtubeId; if (instructions !== undefined) updateData.instructions = instructions; - if (images.length > 0) updateData.imageCount = images.length; + if (customImages.length > 0) updateData.imageCount = customImages.length; - const imagesChanged = images.length > 0 || miiPortraitImage || miiFeaturesImage; - if ((settings.queueEnabled && imagesChanged) || wasImagesModerated) updateData.in_queue = true; + const imagesChanged = customImages.length > 0 || miiPortraitImage || miiFeaturesImage; + if (settings.queueEnabled && imagesChanged) updateData.in_queue = true; if (Object.keys(updateData).length === 0) return rateLimit.sendResponse({ error: "Nothing was changed" }, 400); const updatedMii = await prisma.mii.update({ @@ -172,7 +174,7 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise< await fs.mkdir(miiUploadsDirectory, { recursive: true }); // Only touch files if new images were uploaded - if (images.length > 0) { + if (customImages.length > 0) { // Delete all custom images const files = await fs.readdir(miiUploadsDirectory); await Promise.all(files.filter((file) => file.startsWith("image")).map((file) => fs.unlink(path.join(miiUploadsDirectory, file)))); @@ -180,7 +182,7 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise< // Compress and upload new images try { await Promise.all( - images.map(async (image, index) => { + customImages.map(async (image, index) => { const buffer = Buffer.from(await image.arrayBuffer()); const pngBuffer = await sharp(buffer).resize({ height: 800, fit: "inside", withoutEnlargement: true }).png({ quality: 85 }).toBuffer(); const fileLocation = path.join(miiUploadsDirectory, `image${index}.png`); @@ -198,17 +200,6 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise< // Only save portrait & features for Switch Miis when they are provided if (mii.platform === "SWITCH" && (miiPortraitImage || miiFeaturesImage)) { try { - // Delete existing portrait/features if they're being replaced - await Promise.all( - ["mii.png", "features.png"] - .filter((file) => { - if (file === "mii.png") return miiPortraitImage; - if (file === "features.png") return miiFeaturesImage; - return false; - }) - .map((file) => fs.unlink(path.join(miiUploadsDirectory, file))), - ); - await Promise.all( [ miiPortraitImage && diff --git a/src/app/api/submit/route.ts b/src/app/api/submit/route.ts index b48ddeb..4238698 100644 --- a/src/app/api/submit/route.ts +++ b/src/app/api/submit/route.ts @@ -63,13 +63,13 @@ const submitSchema = z (data) => { // If platform is Switch, gender, miiPortraitImage, and miiFeaturesImage must be present if (data.platform === "SWITCH") { - return data.gender !== undefined && data.miiPortraitImage !== undefined; + return data.gender !== undefined && data.miiPortraitImage !== undefined && data.miiFeaturesImage !== undefined; } return true; }, { - message: "Gender, Mii portrait & features image, and instructions are required for Switch platform", - path: ["gender", "miiPortraitImage", "miiFeaturesImage", "instructions"], + message: "Gender, Mii portrait & features image are required for Switch platform", + path: ["gender", "miiPortraitImage", "miiFeaturesImage"], }, ); @@ -161,23 +161,27 @@ export async function POST(request: NextRequest) { const description = uncensoredDescription && profanity.censor(uncensoredDescription); // Validate image files - let wasImagesModerated = false; const customImages: File[] = []; for (const img of [image1, image2, image3]) { if (!img) continue; const validation = await validateImage(img); - if (!validation.valid) wasImagesModerated = true; - customImages.push(img); + if (validation.valid) { + customImages.push(img); + } else { + return rateLimit.sendResponse({ error: `Failed to verify custom image: ${validation.error}` }, validation.status ?? 400); + } } // Check Mii portrait & features image (Switch) if (platform === "SWITCH") { const portraitValidation = await validateImage(miiPortraitImage); const featuresValidation = await validateImage(miiFeaturesImage); - if (!portraitValidation.valid) wasImagesModerated = true; - if (!featuresValidation.valid) wasImagesModerated = true; + if (!portraitValidation.valid) + return rateLimit.sendResponse({ error: `Failed to verify portrait: ${portraitValidation.error}` }, portraitValidation.status ?? 400); + if (!featuresValidation.valid) + return rateLimit.sendResponse({ error: `Failed to verify features: ${featuresValidation.error}` }, featuresValidation.status ?? 400); } const qrBytes = new Uint8Array(qrBytesRaw ?? []); @@ -202,7 +206,7 @@ export async function POST(request: NextRequest) { tags, description, gender: gender ?? "MALE", - in_queue: wasImagesModerated || settings.queueEnabled, + in_queue: settings.queueEnabled, // Automatically detect certain information if on 3DS ...(platform === "THREE_DS" @@ -340,5 +344,5 @@ export async function POST(request: NextRequest) { return rateLimit.sendResponse({ error: "Failed to store user images" }, 500); } - return rateLimit.sendResponse({ success: true, id: miiRecord.id, inQueue: wasImagesModerated }); + return rateLimit.sendResponse({ success: true, id: miiRecord.id }); } diff --git a/src/components/submit-form/edit-form.tsx b/src/components/submit-form/edit-form.tsx index ce14cc6..5634070 100644 --- a/src/components/submit-form/edit-form.tsx +++ b/src/components/submit-form/edit-form.tsx @@ -50,6 +50,11 @@ export default function EditForm({ mii, likes }: Props) { const session = useSession(); const [files, setFiles] = useState([]); + const handleFilesChange: React.Dispatch> = (updater) => { + hasCustomImagesChanged.current = true; + setFiles(updater); + }; + const handleDrop = useCallback( (acceptedFiles: FileWithPath[]) => { if (files.length >= 3) return; @@ -439,7 +444,7 @@ export default function EditForm({ mii, likes }: Props) { - +
diff --git a/src/lib/images.tsx b/src/lib/images.tsx index eced945..01b4013 100644 --- a/src/lib/images.tsx +++ b/src/lib/images.tsx @@ -52,26 +52,6 @@ export async function validateImage(file: File): Promise<{ valid: boolean; error return { valid: false, error: "Image dimensions are invalid. Resolution must be between 128x128 and 8000x8000" }; } - // Check for inappropriate content - // https://github.com/trafficlunar/api-moderation - try { - const blob = new Blob([buffer]); - const formData = new FormData(); - formData.append("image", blob); - - const headers = new Headers(); - headers.append("token", process.env.TOKEN ?? ""); - const moderationResponse = await fetch("https://api.trafficlunar.net/moderate/image", { method: "POST", body: formData, headers }); - const result = await moderationResponse.json(); - if (result.error) { - return { valid: false, error: result.error }; - } - } catch (moderationError) { - console.error("Error fetching moderation API:", moderationError); - Sentry.captureException(moderationError, { extra: { stage: "moderation-api-fetch" } }); - return { valid: false, error: "Moderation API is down", status: 503 }; - } - return { valid: true }; } catch (error) { console.error("Error validating image:", error);