diff --git a/next.config.ts b/next.config.ts index a1a9f1a..58e8b27 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,19 +1,46 @@ import type { NextConfig } from "next"; +const s3Host = process.env.S3_ENDPOINT + ? new URL(process.env.S3_ENDPOINT).hostname + : ""; + +const securityHeaders = [ + { key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains; preload" }, + { key: "X-Frame-Options", value: "SAMEORIGIN" }, + { key: "X-Content-Type-Options", value: "nosniff" }, + { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" }, + { key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" }, + { + key: "Content-Security-Policy", + value: [ + "default-src 'self'", + "img-src 'self' data: blob: https:", + "font-src 'self' data:", + "style-src 'self' 'unsafe-inline'", + "script-src 'self' 'unsafe-inline' 'unsafe-eval'", + "connect-src 'self' https://*.clerk.accounts.dev https://api.clerk.com", + "frame-ancestors 'self'", + ].join("; "), + }, +]; + const nextConfig: NextConfig = { output: "standalone", + poweredByHeader: false, + compress: true, images: { - remotePatterns: [ + remotePatterns: s3Host + ? [{ protocol: "https", hostname: s3Host }] + : [], + }, + async headers() { + return [ { - protocol: "https", - hostname: process.env.S3_ENDPOINT?.replace("https://", "") || "", + source: "/:path*", + headers: securityHeaders, }, - { - protocol: "http", - hostname: process.env.S3_ENDPOINT?.replace("http://", "") || "", - }, - ], + ]; }, }; -export default nextConfig; \ No newline at end of file +export default nextConfig; diff --git a/src/components/ImageUploader.tsx b/src/components/ImageUploader.tsx index be8e4aa..eeb8590 100644 --- a/src/components/ImageUploader.tsx +++ b/src/components/ImageUploader.tsx @@ -41,31 +41,43 @@ export default function ImageUploader({ images, onImagesChange }: ImageUploaderP setUploading(true); try { + const baseOrder = images.length; + const results = await Promise.allSettled( + validFiles.map(async (file, idx) => { + const formData = new FormData(); + formData.append("file", file); + const res = await fetch("/api/upload", { + method: "POST", + body: formData, + }); + if (!res.ok) { + const data = await res.json().catch(() => ({})); + throw new Error(data.error || "Не успеа качувањето"); + } + const { key, url } = await res.json(); + return { key, url, order: baseOrder + idx + 1 }; + }) + ); + const newImages = [...images]; const newPreviews = [...previews]; - - for (const file of validFiles) { - const formData = new FormData(); - formData.append("file", file); - - const res = await fetch("/api/upload", { - method: "POST", - body: formData, - }); - - if (!res.ok) { - const data = await res.json(); - throw new Error(data.error || "Не успеа качувањето"); + const failures: string[] = []; + for (const r of results) { + if (r.status === "fulfilled") { + newImages.push({ key: r.value.key, order: r.value.order, url: r.value.url }); + newPreviews.push({ key: r.value.key, url: r.value.url, order: r.value.order }); + } else { + failures.push(r.reason instanceof Error ? r.reason.message : "Не успеа качувањето"); } - - const { key, url } = await res.json(); - const order = newImages.length + 1; - newImages.push({ key, order, url }); - newPreviews.push({ key, url, order }); } onImagesChange(newImages); setPreviews(newPreviews); + if (failures.length > 0) { + setError(failures.join("; ")); + } else { + setError(""); + } } catch (err) { setError(err instanceof Error ? err.message : "Не успеа качувањето"); } finally {