fix: upload permissions in Docker and improve file validation

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
orohi
2026-06-06 15:53:16 +03:00
parent 5c5aa6caec
commit 43f477ead9
4 changed files with 81 additions and 18 deletions
+5 -4
View File
@@ -1,5 +1,5 @@
FROM node:22-alpine AS base
RUN apk add --no-cache libc6-compat openssl
RUN apk add --no-cache libc6-compat openssl su-exec
WORKDIR /app
FROM base AS deps
@@ -18,7 +18,7 @@ ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs
RUN mkdir -p public/uploads && chown nextjs:nodejs public/uploads
RUN mkdir -p public/uploads
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next ./.next
@@ -26,10 +26,11 @@ COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/prisma ./prisma
COPY --from=builder /app/next.config.ts ./next.config.ts
COPY docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh && chown -R nextjs:nodejs /app
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["sh", "-c", "npx prisma db push --skip-generate && npm start"]
ENTRYPOINT ["/docker-entrypoint.sh"]
+9
View File
@@ -0,0 +1,9 @@
#!/bin/sh
set -e
mkdir -p /app/public/uploads
chown -R nextjs:nodejs /app/public/uploads
cd /app
exec su-exec nextjs sh -c "npx prisma db push --skip-generate && exec npm start"
+5
View File
@@ -1,6 +1,11 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
experimental: {
serverActions: {
bodySizeLimit: "10mb",
},
},
images: {
remotePatterns: [
{
+62 -14
View File
@@ -1,5 +1,6 @@
import { NextRequest, NextResponse } from "next/server";
import { writeFile, mkdir } from "fs/promises";
import { writeFile, mkdir, access } from "fs/promises";
import { constants } from "fs";
import path from "path";
import { prisma } from "@/lib/prisma";
@@ -10,7 +11,44 @@ const ALLOWED_TYPES = [
"image/webp",
"image/svg+xml",
];
const MIME_BY_EXT: Record<string, string> = {
jpg: "image/jpeg",
jpeg: "image/jpeg",
png: "image/png",
gif: "image/gif",
webp: "image/webp",
svg: "image/svg+xml",
};
const MAX_SIZE = 10 * 1024 * 1024;
const UPLOAD_DIR = path.join(process.cwd(), "public", "uploads");
function resolveMimeType(file: File): string | null {
if (file.type && ALLOWED_TYPES.includes(file.type)) {
return file.type;
}
const ext = file.name.split(".").pop()?.toLowerCase() || "";
return MIME_BY_EXT[ext] ?? null;
}
function safeExtension(filename: string, mimeType: string): string {
const ext = filename.split(".").pop()?.toLowerCase();
if (ext && MIME_BY_EXT[ext]) {
return ext;
}
const byMime: Record<string, string> = {
"image/jpeg": "jpg",
"image/png": "png",
"image/gif": "gif",
"image/webp": "webp",
"image/svg+xml": "svg",
};
return byMime[mimeType] || "jpg";
}
export async function POST(request: NextRequest) {
try {
@@ -18,13 +56,11 @@ export async function POST(request: NextRequest) {
const file = formData.get("file") as File | null;
if (!file) {
return NextResponse.json(
{ error: "Файл не найден" },
{ status: 400 }
);
return NextResponse.json({ error: "Файл не найден" }, { status: 400 });
}
if (!ALLOWED_TYPES.includes(file.type)) {
const mimeType = resolveMimeType(file);
if (!mimeType) {
return NextResponse.json(
{ error: "Недопустимый тип файла" },
{ status: 400 }
@@ -38,15 +74,24 @@ export async function POST(request: NextRequest) {
);
}
const ext = file.name.split(".").pop()?.toLowerCase() || "jpg";
const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 9)}.${ext}`;
const uploadDir = path.join(process.cwd(), "public", "uploads");
await mkdir(UPLOAD_DIR, { recursive: true });
await mkdir(uploadDir, { recursive: true });
try {
await access(UPLOAD_DIR, constants.W_OK);
} catch {
console.error("Upload dir not writable:", UPLOAD_DIR);
return NextResponse.json(
{ error: "Каталог загрузок недоступен для записи" },
{ status: 500 }
);
}
const ext = safeExtension(file.name, mimeType);
const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 9)}.${ext}`;
const filepath = path.join(UPLOAD_DIR, filename);
const bytes = await file.arrayBuffer();
const buffer = Buffer.from(bytes);
await writeFile(path.join(uploadDir, filename), buffer);
await writeFile(filepath, Buffer.from(bytes));
const url = `/uploads/${filename}`;
@@ -54,13 +99,16 @@ export async function POST(request: NextRequest) {
data: {
filename,
originalName: file.name,
mimeType: file.type,
mimeType,
size: file.size,
url,
},
});
return NextResponse.json(photo);
return NextResponse.json({
...photo,
createdAt: photo.createdAt.toISOString(),
});
} catch (error) {
console.error("Upload error:", error);
return NextResponse.json(