From 7a3c17eff30e7bbf5e3bc475ff00f563764485b9 Mon Sep 17 00:00:00 2001 From: slawk0 Date: Thu, 21 Nov 2024 17:12:11 +0100 Subject: [PATCH] implementing uploading attachments --- .gitignore | 1 - .idea/.gitignore | 5 + .idea/inspectionProfiles/Project_Default.xml | 6 + .idea/modules.xml | 8 ++ .idea/relay.iml | 12 ++ .idea/vcs.xml | 6 + client/src/components/chat/MessageForm.tsx | 74 +++++++++- client/src/pages/Chat.tsx | 1 + client/vite.config.ts | 16 ++- server/db/db.js | 1 + server/package-lock.json | 140 +++++++++++++++++++ server/package.json | 1 + server/server.js | 41 ++++++ 13 files changed, 299 insertions(+), 13 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/relay.iml create mode 100644 .idea/vcs.xml diff --git a/.gitignore b/.gitignore index 2d2b47d..b512c09 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ -.idea node_modules \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..b58b603 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,5 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..03d9549 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..f405dfb --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/relay.iml b/.idea/relay.iml new file mode 100644 index 0000000..24643cc --- /dev/null +++ b/.idea/relay.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/client/src/components/chat/MessageForm.tsx b/client/src/components/chat/MessageForm.tsx index 14a53bb..cce4b7e 100644 --- a/client/src/components/chat/MessageForm.tsx +++ b/client/src/components/chat/MessageForm.tsx @@ -1,13 +1,17 @@ -import { useRef, useCallback, useEffect } from 'react'; +import { useRef, useCallback, useEffect, useState } from 'react'; import { useForm, SubmitHandler } from 'react-hook-form'; import type { KeyboardEventHandler } from 'react'; import { socket } from '../../socket/socket.tsx'; import { customAlphabet } from 'nanoid'; import { useOutletContext } from 'react-router-dom'; import { ChatMessages } from '../../pages/Chat.tsx'; +import { axiosClient } from '../../App.tsx'; + const nanoid = customAlphabet('1234567890', 5); + type Input = { message: string; + file: FileList | null; }; type MessageFormProps = { @@ -18,7 +22,10 @@ type MessageFormProps = { const MessageForm = ({ contact, setMessages }: MessageFormProps) => { const { username }: { username: string } = useOutletContext(); - const { register, handleSubmit, reset, watch } = useForm({ + const [file, setFile] = useState(null); + const [isUploading, setIsUploading] = useState(false); + + const { register, handleSubmit, reset, watch, setValue } = useForm({ mode: 'onChange', }); @@ -44,15 +51,48 @@ const MessageForm = ({ contact, setMessages }: MessageFormProps) => { return; } + const uploadFile = async (file: File) => { + const formData = new FormData(); + formData.append('attachment', file); + try { + setIsUploading(true); + const response = await axiosClient.post( + '/api/chat/attachment', + formData, + { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }, + ); + setIsUploading(false); + return response.data.attachmentUrl; + } catch (e) { + setIsUploading(false); + console.error('Failed to upload attachment: ', e); + return null; + } + }; + // Sending message - const submitMessage: SubmitHandler = (data) => { - if (!data.message) return; + const submitMessage: SubmitHandler = async (data) => { + if (!data.message && !file) return; if (!socket) { console.error('Socket not initialized'); return; } + let attachmentUrl: string | null = null; + + if (file) { + attachmentUrl = await uploadFile(file); + if (!attachmentUrl) { + console.error('Failed to upload attachment'); + return; + } + } + const tempId: string = nanoid(); // Temporary ID for unsent message const tempMessage: ChatMessages = { @@ -62,6 +102,7 @@ const MessageForm = ({ contact, setMessages }: MessageFormProps) => { message_id: 0, // Set to 0 because of key={msg.message_id || msg.tempId} in messages list pending: true, tempId: tempId, + attachment: attachmentUrl || null, }; setMessages((prevMessages) => [...prevMessages, tempMessage]); // Display as gray @@ -72,6 +113,7 @@ const MessageForm = ({ contact, setMessages }: MessageFormProps) => { message: data.message.trim(), recipient: contact, tempId: tempId, + attachmentUrl: attachmentUrl || undefined, }, (response: { status: string; message_id: number; tempId: string }) => { if (response.status === 'ok') { @@ -84,6 +126,8 @@ const MessageForm = ({ contact, setMessages }: MessageFormProps) => { ); reset({ message: '' }); + setValue('file', null); + setFile(null); } console.log(response.status, response.tempId); }, @@ -93,6 +137,7 @@ const MessageForm = ({ contact, setMessages }: MessageFormProps) => { message: data.message.trim(), recipient: contact, tempId: tempId, + attachmentUrl: attachmentUrl, }); }; @@ -104,6 +149,13 @@ const MessageForm = ({ contact, setMessages }: MessageFormProps) => { } }; + const handleFileChange = (e: React.ChangeEvent) => { + if (e.target.files && e.target.files.length > 0) { + setFile(e.target.files[0]); + setValue('file', e.target.files); // Update form state with file + } + }; + const { ref, ...messageRegister } = register('message', { maxLength: { value: 2000, @@ -148,11 +200,21 @@ const MessageForm = ({ contact, setMessages }: MessageFormProps) => { -
+
+ + {isUploading && ( + Uploading... + )} diff --git a/client/src/pages/Chat.tsx b/client/src/pages/Chat.tsx index ca130a5..39a76b9 100644 --- a/client/src/pages/Chat.tsx +++ b/client/src/pages/Chat.tsx @@ -16,6 +16,7 @@ export type ChatMessages = { message_id: number; tempId: string; pending: boolean; + attachment: string | null; }; type ContactsProps = { usernamecontact: string; diff --git a/client/vite.config.ts b/client/vite.config.ts index 84ca7fb..a90898a 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -1,18 +1,22 @@ -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; // https://vitejs.dev/config/ export default defineConfig({ server: { proxy: { - "/api": { - target: "http://localhost:3000", + '/api': { + target: 'http://localhost:3000', changeOrigin: true, }, - "/socket.io": { - target: "ws://localhost:3000", + '/socket.io': { + target: 'ws://localhost:3000', ws: true, }, + '/attachments': { + target: 'http://localhost:3000', + changeOrigin: true, + }, }, }, plugins: [react()], diff --git a/server/db/db.js b/server/db/db.js index ad1df99..163cf4e 100644 --- a/server/db/db.js +++ b/server/db/db.js @@ -48,6 +48,7 @@ async function createTables() { message VARCHAR(10000) NOT NULL, timestamp TIMESTAMPTZ DEFAULT NOW(), message_id SERIAL PRIMARY KEY, + attachment VARCHAR(1000), UNIQUE (sender, recipient, message_id) ); CREATE INDEX IF NOT EXISTS idx_messages_conversation diff --git a/server/package-lock.json b/server/package-lock.json index 2242fb9..4a4c565 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -12,6 +12,7 @@ "dotenv": "^16.4.5", "express": "^4.21.0", "jsonwebtoken": "^9.0.2", + "multer": "^1.4.5-lts.1", "nanoid": "^5.0.7", "pg": "^8.13.0", "socket.io": "^4.8.0", @@ -126,6 +127,12 @@ "node": ">= 8" } }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, "node_modules/aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", @@ -262,6 +269,23 @@ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "license": "BSD-3-Clause" }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -339,6 +363,51 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "license": "MIT" }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/concat-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/concat-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", @@ -403,6 +472,12 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -1055,6 +1130,12 @@ "node": ">=0.12.0" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -1236,6 +1317,15 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minipass": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", @@ -1288,6 +1378,36 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/multer": { + "version": "1.4.5-lts.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", + "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/multer/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/nanoid": { "version": "5.0.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.7.tgz", @@ -1631,6 +1751,12 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -1959,6 +2085,14 @@ "node": ">= 0.8" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -2075,6 +2209,12 @@ "node": ">= 0.6" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", diff --git a/server/package.json b/server/package.json index 470ab20..90cbe5b 100644 --- a/server/package.json +++ b/server/package.json @@ -11,6 +11,7 @@ "dotenv": "^16.4.5", "express": "^4.21.0", "jsonwebtoken": "^9.0.2", + "multer": "^1.4.5-lts.1", "nanoid": "^5.0.7", "pg": "^8.13.0", "socket.io": "^4.8.0", diff --git a/server/server.js b/server/server.js index ef3ac38..641713b 100644 --- a/server/server.js +++ b/server/server.js @@ -10,6 +10,9 @@ const crypto = require("crypto"); const saltRounds = 10; const { Server } = require("socket.io"); const io = new Server(server); +const multer = require("multer"); +const { join } = require("path"); +const { existsSync, mkdirSync } = require("fs"); require("dotenv").config(); const PORT = process.env.SERVER_PORT; const { @@ -34,6 +37,7 @@ const { const { generateJwtToken, verifyJwtToken } = require("./auth/jwt"); const { initializeSocket } = require("./socket/socket"); const { getContacts, insertContact } = require("./db/db"); +const { extname } = require("node:path"); const corsOptions = { origin: process.env.ORIGIN, @@ -41,8 +45,27 @@ const corsOptions = { credentials: true, }; +const storage = multer.diskStorage({ + destination: function (req, file, cb) { + const dir = join(__dirname, "attachments"); + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }); + } + cb(null, dir); + }, + filename: function (req, file, cb) { + const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9); + const extension = extname(file.originalname); + const finalName = uniqueSuffix + extension; + cb(null, finalName); + }, +}); + +const upload = multer({ storage: storage }); + // Serve socket.io js app.use("/socket.io", express.static("./node_modules/socket.io/client-dist/")); +app.use("/attachments", express.static("./attachments")); app.use(cors(corsOptions)); app.use(bodyParser.json()); app.use(cookieParser()); @@ -259,6 +282,24 @@ app.post("/api/chat/sendmessage", authorizeUser, async (req, res) => { return res.status(500).json({ message: "HUJ!" }); }); +app.post( + "/api/chat/attachment", + upload.single("attachment"), + authorizeUser, + async (req, res) => { + if (!req.file) { + return res.status(400).json({ message: "No file specified" }); + } + const { finalName } = req.file; + const url = process.env.ORIGIN; + res.json({ + message: "File uploaded successfully", + attachmentUrl: req.file, + url: url, + }); + }, +); + initializeSocket(io); server.listen(PORT, () => {