From 16cf582e2c13d5ec3dc1a783b7bcec3cb652f614 Mon Sep 17 00:00:00 2001
From: Kailasdevdas
Date: Thu, 16 Apr 2026 15:33:22 +0530
Subject: [PATCH] refactor: move Bytescale upload logic to backend for security
---
backend/package-lock.json | 99 +++++++++++++++++++
backend/package.json | 2 +
backend/src/routes/upload.routes.js | 36 +++++--
.../BytescaleUploader/BytescaleUploader.tsx | 58 +++++++----
frontend/src/pages/Department.tsx | 22 ++---
5 files changed, 177 insertions(+), 40 deletions(-)
diff --git a/backend/package-lock.json b/backend/package-lock.json
index 3967c09..560722a 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -9,6 +9,7 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
+ "@bytescale/sdk": "^3.53.0",
"@editorjs/editorjs": "^2.31.4",
"@editorjs/header": "^2.8.8",
"@editorjs/list": "^2.0.9",
@@ -21,6 +22,7 @@
"express-session": "^1.19.0",
"jsonwebtoken": "^9.0.3",
"multer": "^2.1.1",
+ "node-fetch": "^3.3.2",
"postmark": "^4.0.7",
"prisma": "^6.19.2",
"slugify": "^1.6.9"
@@ -29,6 +31,12 @@
"nodemon": "^3.1.11"
}
},
+ "node_modules/@bytescale/sdk": {
+ "version": "3.53.0",
+ "resolved": "https://registry.npmjs.org/@bytescale/sdk/-/sdk-3.53.0.tgz",
+ "integrity": "sha512-qCeNup3pSjaklXuBrO9JeKbozZEs/PjQEvuqCiOAWLBRl6lDjd0V9gRVYqyttPimXYFoV+J/7dmPWtK6RfOABQ==",
+ "license": "MIT"
+ },
"node_modules/@codexteam/icons": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/@codexteam/icons/-/icons-0.0.5.tgz",
@@ -600,6 +608,15 @@
"url": "https://opencollective.com/express"
}
},
+ "node_modules/data-uri-to-buffer": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
+ "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -900,6 +917,29 @@
"node": ">=8.0.0"
}
},
+ "node_modules/fetch-blob": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
+ "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/jimmywarting"
+ },
+ {
+ "type": "paypal",
+ "url": "https://paypal.me/jimmywarting"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "node-domexception": "^1.0.0",
+ "web-streams-polyfill": "^3.0.3"
+ },
+ "engines": {
+ "node": "^12.20 || >= 14.13"
+ }
+ },
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -991,6 +1031,18 @@
"node": ">= 0.6"
}
},
+ "node_modules/formdata-polyfill": {
+ "version": "4.0.10",
+ "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
+ "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
+ "license": "MIT",
+ "dependencies": {
+ "fetch-blob": "^3.1.2"
+ },
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -1522,6 +1574,44 @@
"node": "^18 || ^20 || >= 21"
}
},
+ "node_modules/node-domexception": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
+ "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
+ "deprecated": "Use your platform's native DOMException instead",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/jimmywarting"
+ },
+ {
+ "type": "github",
+ "url": "https://paypal.me/jimmywarting"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.5.0"
+ }
+ },
+ "node_modules/node-fetch": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
+ "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
+ "license": "MIT",
+ "dependencies": {
+ "data-uri-to-buffer": "^4.0.0",
+ "fetch-blob": "^3.1.4",
+ "formdata-polyfill": "^4.0.10"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/node-fetch"
+ }
+ },
"node_modules/node-fetch-native": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
@@ -2216,6 +2306,15 @@
"node": ">= 0.8"
}
},
+ "node_modules/web-streams-polyfill": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
+ "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
diff --git a/backend/package.json b/backend/package.json
index b18dc4c..d208b7c 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -15,6 +15,7 @@
"license": "ISC",
"type": "module",
"dependencies": {
+ "@bytescale/sdk": "^3.53.0",
"@editorjs/editorjs": "^2.31.4",
"@editorjs/header": "^2.8.8",
"@editorjs/list": "^2.0.9",
@@ -27,6 +28,7 @@
"express-session": "^1.19.0",
"jsonwebtoken": "^9.0.3",
"multer": "^2.1.1",
+ "node-fetch": "^3.3.2",
"postmark": "^4.0.7",
"prisma": "^6.19.2",
"slugify": "^1.6.9"
diff --git a/backend/src/routes/upload.routes.js b/backend/src/routes/upload.routes.js
index c6a4556..9197686 100644
--- a/backend/src/routes/upload.routes.js
+++ b/backend/src/routes/upload.routes.js
@@ -1,15 +1,35 @@
import express from "express";
-import {upload} from "../controllers/upload.controller.js";
+import * as Bytescale from "@bytescale/sdk";
+import multer from "multer";
const router = express.Router();
-router.post("/image", upload.single("image"), (req, res) => {
- res.json({
- success: 1,
- file: {
- url: `http://localhost:3000/uploads/blog/${req.file.filename}`,
- },
- });
+const uploadManager = new Bytescale.UploadManager({
+ apiKey: process.env.BYTESCALE_SECRET_API_KEY,
+});
+
+const storage = multer.memoryStorage();
+const upload = multer({storage});
+
+router.post("/", upload.single("file"), async (req, res) => {
+ try {
+ const file = req.file;
+ const {folderPath} = req.body;
+
+ const result = await uploadManager.upload({
+ data: file.buffer,
+ name: file.originalname,
+ mime: file.mimetype,
+ path: {
+ folderPath: folderPath || "/general",
+ },
+ });
+
+ res.json({fileUrl: result.fileUrl});
+ } catch (error) {
+ console.error(error);
+ res.status(500).json({error: "Upload failed"});
+ }
});
export default router;
diff --git a/frontend/src/components/BytescaleUploader/BytescaleUploader.tsx b/frontend/src/components/BytescaleUploader/BytescaleUploader.tsx
index 7636aa7..792612f 100644
--- a/frontend/src/components/BytescaleUploader/BytescaleUploader.tsx
+++ b/frontend/src/components/BytescaleUploader/BytescaleUploader.tsx
@@ -1,7 +1,7 @@
-import { useState, useRef } from "react";
-import * as Bytescale from "@bytescale/sdk";
-import { Button } from "@/components/ui/button";
-import { User, X, Loader2 } from "lucide-react";
+import {useState, useRef} from "react";
+import {Button} from "@/components/ui/button";
+import {User, X, Loader2} from "lucide-react";
+import axios from "axios";
interface BytescaleUploaderProps {
value: string;
@@ -9,11 +9,11 @@ interface BytescaleUploaderProps {
folderPath: "/doctors" | "/departments" | "/news";
}
-const uploadManager = new Bytescale.UploadManager({
- apiKey: "public_223k2cv3MyaAxBeyALCfJa8EMLek",
-});
-
-export function BytescaleUploader({ value, onChange }: BytescaleUploaderProps) {
+export function BytescaleUploader({
+ value,
+ onChange,
+ folderPath,
+}: BytescaleUploaderProps) {
const [isUploading, setIsUploading] = useState(false);
const fileInputRef = useRef(null);
@@ -21,19 +21,35 @@ export function BytescaleUploader({ value, onChange }: BytescaleUploaderProps) {
const file = event.target.files?.[0];
if (!file) return;
- setIsUploading(true);
- try {
- const { fileUrl } = await uploadManager.upload({
- data: file,
- path: {
- folderPath: "/doctors",
- },
- });
+ if (file.size > 5 * 1024 * 1024) {
+ alert("File is too large (Max 5MB)");
+ return;
+ }
+ setIsUploading(true);
+
+ const formData = new FormData();
+ formData.append("file", file);
+ formData.append("folderPath", folderPath);
+
+ try {
+ const response = await axios.post(
+ "http://localhost:3000/api/upload",
+ formData,
+ {
+ headers: {
+ "Content-Type": "multipart/form-data",
+ },
+ },
+ );
+
+ const {fileUrl} = response.data;
onChange(fileUrl);
} catch (e: any) {
console.error("Upload Error:", e);
- alert(`Error:\n${e.message}`);
+ const errorMessage =
+ e.response?.data?.error || e.message || "Upload failed";
+ alert(`Upload Error: ${errorMessage}`);
} finally {
setIsUploading(false);
if (fileInputRef.current) fileInputRef.current.value = "";
@@ -49,7 +65,7 @@ export function BytescaleUploader({ value, onChange }: BytescaleUploaderProps) {
)}
diff --git a/frontend/src/pages/Department.tsx b/frontend/src/pages/Department.tsx
index 2e119b2..354c9e0 100644
--- a/frontend/src/pages/Department.tsx
+++ b/frontend/src/pages/Department.tsx
@@ -1,6 +1,6 @@
-import { useState, useEffect, useCallback } from "react";
-import { AxiosError } from "axios";
-import { BytescaleUploader } from "@/components/BytescaleUploader/BytescaleUploader";
+import {useState, useEffect, useCallback} from "react";
+import {AxiosError} from "axios";
+import {BytescaleUploader} from "@/components/BytescaleUploader/BytescaleUploader";
import {
getDepartmentsApi,
@@ -18,8 +18,8 @@ import {
TableRow,
} from "@/components/ui/table";
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
-import { Button } from "@/components/ui/button";
+import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
+import {Button} from "@/components/ui/button";
import {
Dialog,
@@ -29,8 +29,8 @@ import {
DialogFooter,
} from "@/components/ui/dialog";
-import { Input } from "@/components/ui/input";
-import { Textarea } from "@/components/ui/textarea";
+import {Input} from "@/components/ui/input";
+import {Textarea} from "@/components/ui/textarea";
import {
Loader2,
@@ -122,7 +122,7 @@ export default function DepartmentPage() {
);
function handleChange(e: any) {
- setForm({ ...form, [e.target.name]: e.target.value });
+ setForm({...form, [e.target.name]: e.target.value});
}
function truncate(text: string, limit = 60) {
@@ -159,7 +159,7 @@ export default function DepartmentPage() {
async function handleSubmit() {
try {
if (editing) {
- const { departmentId, ...updateData } = form;
+ const {departmentId, ...updateData} = form;
await updateDepartmentApi(editing.departmentId, updateData);
} else {
await createDepartmentApi(form);
@@ -402,7 +402,7 @@ export default function DepartmentPage() {
setForm({ ...form, image: url })}
+ onChange={(url) => setForm({...form, image: url})}
/>
--
2.43.0