refactor: replace server action with API route for image uploads and update Nginx rate limiting configuration

This commit is contained in:
2026-05-30 01:06:52 +07:00 Unverified
parent bf215bb9cf
commit 96c52e8ad6
4 changed files with 76 additions and 46 deletions
-42
View File
@@ -1,42 +0,0 @@
"use server";
import { writeFile, mkdir } from "fs/promises";
import path from "path";
import { getServerSession } from "next-auth";
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
export async function uploadImage(formData: FormData): Promise<string> {
const session = await getServerSession(authOptions);
const adminIds = (process.env.ADMIN_DISCORD_IDS ?? "")
.split(",")
.map((s) => s.trim())
.filter(Boolean);
const discordId = (session?.user as { discordId?: string } | undefined)
?.discordId;
if (!discordId || !adminIds.includes(discordId)) {
throw new Error("Unauthorized");
}
const file = formData.get("file") as File;
if (!file) throw new Error("No file provided");
if (!file.type.startsWith("image/")) {
throw new Error("Only image files are allowed");
}
const allowedExtensions = ["png", "jpg", "jpeg", "webp", "gif"];
const originalExt = (file.name.split(".").pop() || "").toLowerCase();
if (!allowedExtensions.includes(originalExt)) {
throw new Error("Invalid image extension");
}
const filename = `${crypto.randomUUID()}.${originalExt}`;
const uploadDir = path.join(process.cwd(), "public", "form");
await mkdir(uploadDir, { recursive: true });
const buffer = Buffer.from(await file.arrayBuffer());
await writeFile(path.join(uploadDir, filename), buffer);
return `/form/${filename}`;
}
+51
View File
@@ -0,0 +1,51 @@
import { NextResponse } from "next/server";
import { writeFile, mkdir } from "fs/promises";
import path from "path";
import { getServerSession } from "next-auth";
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
export async function POST(req: Request) {
try {
const session = await getServerSession(authOptions);
const adminIds = (process.env.ADMIN_DISCORD_IDS ?? "")
.split(",")
.map((s) => s.trim())
.filter(Boolean);
const discordId = (session?.user as { discordId?: string } | undefined)
?.discordId;
if (!discordId || !adminIds.includes(discordId)) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const formData = await req.formData();
const file = formData.get("file") as File | null;
if (!file) {
return NextResponse.json({ error: "No file provided" }, { status: 400 });
}
if (!file.type.startsWith("image/")) {
return NextResponse.json({ error: "Only image files are allowed" }, { status: 400 });
}
const allowedExtensions = ["png", "jpg", "jpeg", "webp", "gif"];
const originalExt = (file.name.split(".").pop() || "").toLowerCase();
if (!allowedExtensions.includes(originalExt)) {
return NextResponse.json({ error: "Invalid image extension" }, { status: 400 });
}
const filename = `${crypto.randomUUID()}.${originalExt}`;
const uploadDir = path.join(process.cwd(), "public", "form");
await mkdir(uploadDir, { recursive: true });
const buffer = Buffer.from(await file.arrayBuffer());
await writeFile(path.join(uploadDir, filename), buffer);
return NextResponse.json({ url: `/form/${filename}` });
} catch (error: any) {
console.error("Upload error:", error);
return NextResponse.json({ error: error.message || "Internal server error" }, { status: 500 });
}
}
+13 -2
View File
@@ -18,7 +18,6 @@ import {
deleteQuestion,
reorderQuestions,
} from "@/app/actions/questions";
import { uploadImage } from "@/app/actions/upload";
import { Plus, Trash, ArrowUp, ArrowDown, Loader2, Save, Image as ImageIcon, X } from "lucide-react";
import Image from 'next/image';
@@ -146,7 +145,19 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC
setUploadingId(questionId);
toast.promise(
uploadImage(formData).then(async (imageUrl) => {
new Promise<string>(async (resolve, reject) => {
try {
const res = await fetch("/api/upload", {
method: "POST",
body: formData,
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || "Failed to upload image");
resolve(data.url);
} catch (error) {
reject(error);
}
}).then(async (imageUrl) => {
await updateQuestion(questionId, form.id, { imageUrl });
setQuestions((prev) =>
prev.map((q) => (q.id === questionId ? { ...q, imageUrl } : q))
+12 -2
View File
@@ -15,9 +15,19 @@ http {
server {
listen 80;
# Bypass rate limit for static assets and Next.js internal files
location /_next/ {
proxy_pass http://erika:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
# Apply rate limit to actual page loads and API requests
location / {
# Apply rate limit: allow burst of up to 10 requests without delay
limit_req zone=ratelimit burst=10 nodelay;
# Increased burst to 30 to allow normal page loads that fetch multiple APIs/images
limit_req zone=ratelimit burst=30 nodelay;
# Proxy to the Next.js app
proxy_pass http://erika:3000;