diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..edca4ed --- /dev/null +++ b/.env.example @@ -0,0 +1,21 @@ +# Copy this file to .env and fill values before running docker-compose + +# Postgres DB credentials (used by the DB container) +POSTGRES_USER= +POSTGRES_PASSWORD= +POSTGRES_DB= +DB_HOST_PORT= + +# Server settings (the server container will read DB_* vars) +NODE_ENV= +NODE_PORT= +DB_HOST= +DB_PORT= +DB_NAME= +DB_USER= +DB_PASSWORD= +CLIENT_HOST= + +# Client (Vite) settings +CLIENT_PORT= +HOST= diff --git a/.gitignore b/.gitignore index d011d0e..4975d06 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .DS_Store .env.local -.env.development \ No newline at end of file +.env.development +.env \ No newline at end of file diff --git a/README.md b/README.md index 57f27f3..7b3ee70 100644 --- a/README.md +++ b/README.md @@ -71,4 +71,23 @@ To run client and server together use the following commands: In the custom-3D folder - Run the command `npm run install:all` -- Run the command `npm run start:all` \ No newline at end of file +- Run the command `npm run start:all` + +**Alternatively using Docker:** + +- Copy the example env and fill values (this repository gitignores `.env`): +```bash +cp .env.example .env +``` + +Then start services with docker-compose: +- Build and start all services +```bash +docker-compose up --build +``` + +- Seed/run server tasks +```bash +docker-compose exec server npm run knex:dev -- migrate:latest +docker-compose exec server npm run seed +``` \ No newline at end of file diff --git a/client/.dockerignore b/client/.dockerignore new file mode 100644 index 0000000..dc5d4c8 --- /dev/null +++ b/client/.dockerignore @@ -0,0 +1,4 @@ +node_modules +dist +.DS_Store +.env diff --git a/client/Dockerfile b/client/Dockerfile new file mode 100644 index 0000000..f109e60 --- /dev/null +++ b/client/Dockerfile @@ -0,0 +1,7 @@ +FROM node:20 +WORKDIR /usr/src/app +COPY package*.json ./ +RUN npm install +COPY . . +EXPOSE 9000 +CMD ["npm", "run", "dev"] diff --git a/client/vite.config.js b/client/vite.config.js index 684b947..b3c58a9 100644 --- a/client/vite.config.js +++ b/client/vite.config.js @@ -15,6 +15,7 @@ export default defineConfig({ plugins: [react()], server: { port: 9000, + host: "0.0.0.0", }, build: { // Relative to the root diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..6b60f72 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,58 @@ +version: "3.8" +services: + db: + image: postgres:14 + restart: unless-stopped + env_file: + - .env + environment: + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} + volumes: + - db-data:/var/lib/postgresql/data + ports: + - "${DB_HOST_PORT:-5432}:5432" + + server: + build: ./server + command: npm run start:dev + volumes: + - ./server:/usr/src/app + - /usr/src/app/node_modules + environment: + NODE_ENV: ${NODE_ENV} + NODE_PORT: ${NODE_PORT} + FILE_PATH: /usr/src/app/src + DB_HOST: db + DB_PORT: ${DB_PORT} + DB_NAME: ${DB_NAME} + DB_USER: ${DB_USER} + DB_PASSWORD: ${DB_PASSWORD} + CLIENT_HOST: ${CLIENT_HOST} + env_file: + - .env + depends_on: + - db + ports: + - "3000:3000" + + client: + build: ./client + command: npm run dev + volumes: + - ./client:/usr/src/app + - /usr/src/app/node_modules + environment: + PORT: ${CLIENT_PORT} + HOST: ${HOST} + env_file: + - .env + - ./client/.env + depends_on: + - server + ports: + - "9000:9000" + +volumes: + db-data: diff --git a/server/.dockerignore b/server/.dockerignore new file mode 100644 index 0000000..1ec5429 --- /dev/null +++ b/server/.dockerignore @@ -0,0 +1,6 @@ +node_modules +.env +dist +coverage +uploads +.DS_Store diff --git a/server/.prettierrc b/server/.prettierrc new file mode 100644 index 0000000..b5b527d --- /dev/null +++ b/server/.prettierrc @@ -0,0 +1,10 @@ +{ + "singleQuote": false, + "trailingComma": "all", + "semi": true, + "tabWidth": 2, + "arrowParens": "always", + "useTabs": false, + "bracketSpacing": true, + "printWidth": 150 +} \ No newline at end of file diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..cbe56ab --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,7 @@ +FROM node:18 +WORKDIR /usr/src/app +COPY package*.json ./ +RUN npm install +COPY . . +EXPOSE 3000 +CMD ["npm", "run", "start:dev"] diff --git a/server/src/api/objects.js b/server/src/api/objects.js index db68faf..08ac8f0 100644 --- a/server/src/api/objects.js +++ b/server/src/api/objects.js @@ -5,6 +5,7 @@ const { errorHandler } = require("../utils"); const { HttpError } = require("../error"); const uploadFile = require("../middleware/upload"); const fs = require("fs"); +const path = require("path"); /** * Get Object by id @@ -90,13 +91,18 @@ const uploadFileAndSaveObject = errorHandler(async (req, res) => { // Uploads the file in the server folder await uploadFile(req, res); - if (req.file === undefined) { + // Get the file from formidable (req.files is an object with arrays) + const fileArray = req.files?.file; + if (!fileArray || fileArray.length === 0) { throw new HttpError(400, "Please upload a file!"); } - const filepath = req.params.projectId + "/" + req.file.originalname; - const objectId = req.body.id; - const objectName = req.body.objectName; + const file = fileArray[0]; + // formidable v3 uses 'newFilename', v2 uses path, filepath, newFilename + const filename = file.newFilename || path.basename(file.filepath) || file.filename; + const filepath = req.params.projectId + "/" + filename; + const objectId = Array.isArray(req.body.id) ? req.body.id[0] : req.body.id; + const objectName = Array.isArray(req.body.objectName) ? req.body.objectName[0] : req.body.objectName; const projectId = req.params.projectId; // Checks if the object is present in the database @@ -107,7 +113,7 @@ const uploadFileAndSaveObject = errorHandler(async (req, res) => { await ObjectsController.saveObject(objectId, objectName, projectId, filepath); } - return { message: "Uploaded the file successfully: " + req.file.originalname }; + return { message: "Uploaded the file successfully: " + filename }; }); module.exports = { getObjectById, getObjectsPathByProjectId, deleteObject, uploadFileAndSaveObject, getObjectsByProjectId }; diff --git a/server/src/index.js b/server/src/index.js index f15253b..30d67e2 100644 --- a/server/src/index.js +++ b/server/src/index.js @@ -68,6 +68,9 @@ app.get("/", (req, res) => { res.send("Hello World"); }); +// Serve static files before auth middleware +app.use("/uploads", express.static(path.join(__dirname, "/public/uploads"))); + app.use("/api/", require("./routes/auth")); app.use(verifyToken); app.use("/api/", require("./routes/users")); diff --git a/server/src/middleware/upload.js b/server/src/middleware/upload.js index 854a98f..e5628e8 100644 --- a/server/src/middleware/upload.js +++ b/server/src/middleware/upload.js @@ -2,26 +2,36 @@ const formidable = require("formidable"); const fs = require("fs"); const path = require("path"); -const uploadFileMiddleware = (req, res, next) => { - const dir = path.join(process.env.FILE_PATH, "/public/uploads/", req.params.projectId); +const uploadFileMiddleware = (req, res) => { + return new Promise((resolve, reject) => { + const baseDir = process.env.FILE_PATH || process.cwd(); + const projectDir = req.params.projectId; + const dir = path.join(baseDir, "public", "uploads", projectDir); - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); - } + // Ensure directory exists + if (!fs.existsSync(dir)) { + try { + fs.mkdirSync(dir, { recursive: true }); + } catch (err) { + console.error("Failed to create directory:", err); + return reject(err); + } + } - const form = new formidable.IncomingForm({ - uploadDir: dir, - keepExtensions: true, // Keep original file extensions - }); + const form = new formidable.IncomingForm({ + uploadDir: dir, + keepExtensions: true, + }); - form.parse(req, (err, fields, files) => { - if (err) { - return next(err); - } + form.parse(req, (err, fields, files) => { + if (err) { + return reject(err); + } - req.files = files; // Attach files to the request object for further processing - req.body = fields; // Attach other form fields if needed - next(); + req.files = files; + req.body = fields; + resolve(); + }); }); };