feat: change profile pictures

I keep forgetting to do things. The edit mii api route has been using
the public folder as an uploads directory... whoops...
This commit is contained in:
trafficlunar 2025-05-09 22:30:26 +01:00
parent c5437ed3e7
commit 5514f2ec39
12 changed files with 257 additions and 19 deletions

View file

@ -10,7 +10,7 @@ export async function PATCH(request: NextRequest) {
const session = await auth();
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const rateLimit = new RateLimit(request, 1);
const rateLimit = new RateLimit(request, 3);
const check = await rateLimit.handle();
if (check) return check;

View file

@ -0,0 +1,85 @@
import { NextRequest, NextResponse } from "next/server";
import dayjs from "dayjs";
import { z } from "zod";
import fs from "fs/promises";
import path from "path";
import sharp from "sharp";
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { RateLimit } from "@/lib/rate-limit";
import { validateImage } from "@/lib/images";
const uploadsDirectory = path.join(process.cwd(), "uploads", "user");
const formDataSchema = z.object({
image: z.union([z.instanceof(File), z.any()]).optional(),
});
export async function PATCH(request: NextRequest) {
const session = await auth();
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const rateLimit = new RateLimit(request, 3);
const check = await rateLimit.handle();
if (check) return check;
// Check if profile picture was updated in the last 30 days
const user = await prisma.user.findUnique({ where: { id: Number(session.user.id) } });
if (user && user.imageUpdatedAt) {
const timePeriod = dayjs().subtract(30, "days");
const lastUpdate = dayjs(user.imageUpdatedAt);
if (lastUpdate.isAfter(timePeriod)) return rateLimit.sendResponse({ error: "Profile picture was changed in the last 30 days" }, 400);
}
// Parse data
const formData = await request.formData();
const parsed = formDataSchema.safeParse({
image: formData.get("image"),
});
if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.errors[0].message }, 400);
const { image } = parsed.data;
// If there is no image, set the profile picture to the guest image
if (!image) {
await prisma.user.update({
where: { id: Number(session.user.id) },
data: { image: `/guest.webp`, imageUpdatedAt: new Date() },
});
return rateLimit.sendResponse({ success: true });
}
// Validate image contents
const imageValidation = await validateImage(image);
if (!imageValidation.valid) return rateLimit.sendResponse({ error: imageValidation.error }, imageValidation.status ?? 400);
// Ensure directories exist
await fs.mkdir(uploadsDirectory, { recursive: true });
try {
const buffer = Buffer.from(await image.arrayBuffer());
const webpBuffer = await sharp(buffer).resize({ width: 128, height: 128 }).webp({ quality: 85 }).toBuffer();
const fileLocation = path.join(uploadsDirectory, `${session.user.id}.webp`);
await fs.writeFile(fileLocation, webpBuffer);
} catch (error) {
console.error("Error uploading profile picture:", error);
return rateLimit.sendResponse({ error: "Failed to store profile picture" }, 500);
}
try {
await prisma.user.update({
where: { id: Number(session.user.id) },
data: { image: `/profile/${session.user.id}/picture`, imageUpdatedAt: new Date() },
});
} catch (error) {
console.error("Failed to update profile picture:", error);
return rateLimit.sendResponse({ error: "Failed to update profile picture" }, 500);
}
return rateLimit.sendResponse({ success: true });
}

View file

@ -12,7 +12,7 @@ export async function PATCH(request: NextRequest) {
const session = await auth();
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const rateLimit = new RateLimit(request, 1);
const rateLimit = new RateLimit(request, 3);
const check = await rateLimit.handle();
if (check) return check;

View file

@ -14,7 +14,7 @@ import { idSchema, nameSchema, tagsSchema } from "@/lib/schemas";
import { validateImage } from "@/lib/images";
import { RateLimit } from "@/lib/rate-limit";
const uploadsDirectory = path.join(process.cwd(), "public", "mii");
const uploadsDirectory = path.join(process.cwd(), "uploads", "mii");
const editSchema = z.object({
name: nameSchema.optional(),

View file

@ -19,7 +19,7 @@ import { convertQrCode } from "@/lib/qr-codes";
import Mii from "@/lib/mii.js/mii";
import { TomodachiLifeMii } from "@/lib/tomodachi-life-mii";
const uploadsDirectory = path.join(process.cwd(), "uploads");
const uploadsDirectory = path.join(process.cwd(), "uploads", "mii");
const submitSchema = z.object({
name: nameSchema,

View file

@ -29,7 +29,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
if (!searchParamsParsed.success) return rateLimit.sendResponse({ error: searchParamsParsed.error.errors[0].message }, 400);
const { type: imageType } = searchParamsParsed.data;
const filePath = path.join(process.cwd(), "uploads", miiId.toString(), `${imageType}.webp`);
const filePath = path.join(process.cwd(), "uploads", "mii", miiId.toString(), `${imageType}.webp`);
try {
const buffer = await fs.readFile(filePath);

View file

@ -0,0 +1,27 @@
import { NextRequest, NextResponse } from "next/server";
import fs from "fs/promises";
import path from "path";
import { idSchema } from "@/lib/schemas";
import { RateLimit } from "@/lib/rate-limit";
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const rateLimit = new RateLimit(request, 16);
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.errors[0].message }, 400);
const userId = parsed.data;
const filePath = path.join(process.cwd(), "uploads", "user", `${userId}.webp`);
try {
const buffer = await fs.readFile(filePath);
return new NextResponse(buffer);
} catch {
return rateLimit.sendResponse({ error: "Image not found" }, 404);
}
}