refactor: replace server action with API route for image uploads and update Nginx rate limiting configuration
This commit is contained in:
@@ -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}`;
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user