From cc1fc2f5f03303c2eb084bf85df86bc286c81a35 Mon Sep 17 00:00:00 2001 From: Shubham-Lal Date: Mon, 8 Apr 2024 11:39:09 +0530 Subject: [PATCH] use multipart/form-data for register --- client/src/components/modal/auth-modal.tsx | 11 +- client/src/components/modal/brief-info.tsx | 68 +++++--- client/src/pages/auth/index.tsx | 4 +- client/src/types.ts | 8 +- client/src/utils/handleAutoLogin.ts | 4 - server/avatars/README.md | 1 - server/controllers/auth.js | 53 ++---- server/package-lock.json | 183 +++++++++++++++++++++ server/package.json | 2 + server/routes/auth.js | 11 +- server/server.js | 41 +++-- server/utils/getMimeType.js | 10 -- 12 files changed, 281 insertions(+), 115 deletions(-) delete mode 100644 server/avatars/README.md delete mode 100644 server/utils/getMimeType.js diff --git a/client/src/components/modal/auth-modal.tsx b/client/src/components/modal/auth-modal.tsx index f043d27..b41cd09 100644 --- a/client/src/components/modal/auth-modal.tsx +++ b/client/src/components/modal/auth-modal.tsx @@ -7,12 +7,21 @@ import SignupTab from "./signup-tab"; import BriefInfo from "./brief-info"; import { IoClose } from "react-icons/io5"; +type RegisterData = { + email: string; + password: string; + avatar: string | File; + username: string; + first_name: string; + last_name: string; +}; + const AuthModal = () => { const location = useLocation(); const { authTab, setAuthTab } = useAuthStore(); const { tempUser } = useTempStore(); - const [registerData, setRegisterData] = useState(() => ({ + const [registerData, setRegisterData] = useState(() => ({ email: tempUser.email || "", password: "", avatar: tempUser.avatar || "", diff --git a/client/src/components/modal/brief-info.tsx b/client/src/components/modal/brief-info.tsx index 718a2cd..9164c3f 100644 --- a/client/src/components/modal/brief-info.tsx +++ b/client/src/components/modal/brief-info.tsx @@ -2,7 +2,6 @@ import { useState, useCallback } from "react"; import { useNavigate } from "react-router-dom"; import { AuthStatus, AuthTab, useAuthStore, useTempStore } from "../../store/useAuthStore"; import { RegisterDataProps } from "../../types"; -import useFileHandler from "../../hooks/useFileHandler"; import { toast } from "sonner"; import { LoadingSVG } from "../loading/svg"; import { IoPersonCircleOutline } from "react-icons/io5"; @@ -26,6 +25,7 @@ const BriefInfo: React.FC = ({ registerData, setRegisterData const handleInputChange = useCallback((e: React.ChangeEvent) => { const { name, value } = e.target; + // eslint-disable-next-line no-useless-escape const specialCharRegex = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/; if (specialCharRegex.test(value)) { return toast.warning('Special characters not allowed.'); @@ -42,15 +42,22 @@ const BriefInfo: React.FC = ({ registerData, setRegisterData })); }, [setRegisterData]); - const handleAvatarChange = useFileHandler(5); - const handleFileInputChange = async (e: React.ChangeEvent) => { - const base64String = await handleAvatarChange(e); - setRegisterData(prevState => ({ - ...prevState, - avatar: base64String || "" - })); + const handleFileInputChange = (event: React.ChangeEvent) => { + if (event.target.files && event.target.files.length > 0) { + const file = event.target.files[0]; + if (file) { + setRegisterData(prevState => ({ + ...prevState, + avatar: file + })); + } + } }; + const handleKeyPress = useCallback((e: React.KeyboardEvent) => { + if (e.key === ' ') e.preventDefault(); + }, []); + const handleFormSubmit = async (e: React.FormEvent) => { e.preventDefault(); setIsSubmitted(true); @@ -76,19 +83,18 @@ const BriefInfo: React.FC = ({ registerData, setRegisterData if (trimmedUsername && trimmedFirstName && trimmedLastName) { setLoading(true); + + const formData = new FormData(); + formData.append('email', registerData.email); + formData.append('password', registerData.password); + formData.append('username', trimmedUsername); + formData.append('first_name', trimmedFirstName); + formData.append('last_name', trimmedLastName); + formData.append('avatar', registerData.avatar); + await fetch(`${import.meta.env.VITE_SERVER_URL}/api/auth/register`, { method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - email: registerData.email, - password: registerData.password, - avatar: registerData.avatar, - username: trimmedUsername, - first_name: trimmedFirstName, - last_name: trimmedLastName - }) + body: formData }) .then(res => res.json()) .then(response => { @@ -128,8 +134,7 @@ const BriefInfo: React.FC = ({ registerData, setRegisterData
document.getElementById('user-avatar')?.click()} - onClick={() => toast.warning('Feature coming soon.')} + onClick={() => document.getElementById('user-avatar')?.click()} > = ({ registerData, setRegisterData onChange={handleFileInputChange} /> {registerData.avatar ? ( - Avatar + typeof registerData.avatar === 'string' ? ( + Avatar + ) : ( + Avatar + ) ) : }
{registerData.avatar ? : } @@ -155,6 +168,7 @@ const BriefInfo: React.FC = ({ registerData, setRegisterData name="username" value={registerData.username} onChange={handleInputChange} + onKeyPress={handleKeyPress} className={`${isSubmitted && !validationState.isUsernameValid ? "shake" : ""}`} style={{ borderColor: isSubmitted && !validationState.isUsernameValid ? "red" : "" }} placeholder={isSubmitted && !validationState.isUsernameValid ? 'Required' : ''} @@ -168,6 +182,7 @@ const BriefInfo: React.FC = ({ registerData, setRegisterData name="first_name" value={registerData.first_name} onChange={handleInputChange} + onKeyPress={handleKeyPress} className={`${isSubmitted && !validationState.isFirstNameValid ? "shake" : ""}`} style={{ borderColor: isSubmitted && !validationState.isFirstNameValid ? "red" : "" }} placeholder={isSubmitted && !validationState.isFirstNameValid ? 'Required' : ''} @@ -180,6 +195,7 @@ const BriefInfo: React.FC = ({ registerData, setRegisterData name="last_name" value={registerData.last_name} onChange={handleInputChange} + onKeyPress={handleKeyPress} className={`${isSubmitted && !validationState.isLastNameValid ? "shake" : ""}`} style={{ borderColor: isSubmitted && !validationState.isLastNameValid ? "red" : "" }} placeholder={isSubmitted && !validationState.isLastNameValid ? 'Required' : ''} diff --git a/client/src/pages/auth/index.tsx b/client/src/pages/auth/index.tsx index 442e7a0..87f5f5a 100644 --- a/client/src/pages/auth/index.tsx +++ b/client/src/pages/auth/index.tsx @@ -16,7 +16,7 @@ export default function AuthPage() { setAuthTab(type === 'login' ? AuthTab.Login : type === 'signup' ? AuthTab.Signup : AuthTab.Login); } else if (isAuthenticated === AuthStatus.Authenticated) { - navigate(route === '/auth' || route === '/login' || route === '/signup' ? '/' : route) + navigate(route === '/auth' || route === '/login' || route === '/signup' ? '/' : route, { replace: true }); } else if (isAuthenticated === AuthStatus.Failed) { setAuthTab(type === 'login' ? AuthTab.Login : type === 'signup' ? AuthTab.Signup : AuthTab.Login); @@ -36,7 +36,7 @@ export default function AuthPage() { avatar: user.picture || "" }); setAuthTab(AuthTab.Signup); - navigate('/auth', { replace: true }); + navigate('/auth?type=signup', { replace: true }); } }, [isAuthenticated, location.search, navigate, route, setAuthTab, setTempUser]); diff --git a/client/src/types.ts b/client/src/types.ts index dd9b6ab..1bf8915 100644 --- a/client/src/types.ts +++ b/client/src/types.ts @@ -1,10 +1,8 @@ -import React from "react"; - export interface RegisterDataProps { registerData: { email: string; password: string; - avatar: string; + avatar: string | File; username: string; first_name: string; last_name: string; @@ -12,9 +10,9 @@ export interface RegisterDataProps { setRegisterData: React.Dispatch>; -} \ No newline at end of file +} diff --git a/client/src/utils/handleAutoLogin.ts b/client/src/utils/handleAutoLogin.ts index f41b0a8..8e9ea76 100644 --- a/client/src/utils/handleAutoLogin.ts +++ b/client/src/utils/handleAutoLogin.ts @@ -1,4 +1,3 @@ -import { toast } from 'sonner'; import { User, AuthStatus, AuthTab } from '../store/useAuthStore'; type SetRoute = (navigate: string) => void; @@ -24,19 +23,16 @@ const handleAutoLogin = (setRoute: SetRoute, setUser: SetUser, setIsAuthenticate .then(res => res.json()) .then(response => { if (response.success) { - toast.success(response.message); setUser(response.data.user); setIsAuthenticated(AuthStatus.Authenticated); setAuthTab(AuthTab.Closed); } else { - toast.success('Session logged out.'); setIsAuthenticated(AuthStatus.Failed); localStorage.removeItem('token'); } }) .catch(() => { - toast.success('Session logged out.'); setIsAuthenticated(AuthStatus.Failed); localStorage.removeItem('token'); }); diff --git a/server/avatars/README.md b/server/avatars/README.md deleted file mode 100644 index ff8af6c..0000000 --- a/server/avatars/README.md +++ /dev/null @@ -1 +0,0 @@ -This dir is where the user avatar are stored \ No newline at end of file diff --git a/server/controllers/auth.js b/server/controllers/auth.js index a160259..1cd9830 100644 --- a/server/controllers/auth.js +++ b/server/controllers/auth.js @@ -1,8 +1,5 @@ const bcrypt = require('bcrypt'); const jwt = require('jsonwebtoken'); -const fs = require('fs'); -const path = require('path'); -const { getMimeType } = require('../utils/getMimeType'); const ErrorHandler = require('../utils/ErrorHandler'); exports.handleGoogleAuth = async function (fastify, request, reply) { @@ -26,7 +23,15 @@ exports.handleGoogleAuth = async function (fastify, request, reply) { exports.register = async function (fastify, request, reply) { try { - const { email, password, avatar, username, first_name, last_name } = request.body; + const { email, password, username, first_name, last_name } = request.body; + let avatar = null; + + if (request.body.avatar) { + avatar = request.body.avatar; + } + else if (request.file) { + avatar = request.file; + } const [emailExists] = await fastify.mysql.query('SELECT * FROM users WHERE email = ?', [email]); if (emailExists.length > 0) throw new ErrorHandler(400, false, 'Account already exists.'); @@ -34,27 +39,9 @@ exports.register = async function (fastify, request, reply) { const [usernameExists] = await fastify.mysql.query('SELECT * FROM users WHERE username = ?', [username]); if (usernameExists.length > 0) throw new ErrorHandler(400, false, 'Username already exists.'); - let avatarPath = null; - if (avatar) { - const isBase64 = avatar.startsWith('data:image'); - if (!isBase64) avatarPath = avatar; - else { - const mimeType = getMimeType(avatar); - if (!mimeType) throw new ErrorHandler(400, false, 'Invalid avatar format.'); - - const base64Data = avatar.replace(/^data:image\/\w+;base64,/, ''); - const extension = mimeType.split('/')[1]; - const timestamp = new Date().toISOString().replace(/:/g, '-').replace('T', '_').split('.')[0]; - const uniqueFilename = `${username}_${timestamp}.${extension}`; - const fullPath = path.resolve(__dirname, '..', 'avatars', uniqueFilename); - fs.writeFileSync(fullPath, base64Data, 'base64'); - avatarPath = uniqueFilename; - } - } - const hashedPassword = await bcrypt.hash(password, 10); - const [result] = await fastify.mysql.query('INSERT INTO users (email, password, username, first_name, last_name, avatar) VALUES (?, ?, ?, ?, ?, ?)', [email, hashedPassword, username, first_name, last_name, avatarPath]); + const [result] = await fastify.mysql.query('INSERT INTO users (email, password, username, first_name, last_name, avatar) VALUES (?, ?, ?, ?, ?, ?)', [email, hashedPassword, username, first_name, last_name, null]); if (result.affectedRows > 0) { const token = jwt.sign({ userId: username }, process.env.JWT_SECRET, { expiresIn: '12h' }); @@ -62,7 +49,7 @@ exports.register = async function (fastify, request, reply) { success: true, message: 'Account created successfully.', data: { - user: { email, avatar: avatarPath, username, first_name, last_name }, + user: { email, avatar: null, username, first_name, last_name }, token } }); @@ -98,12 +85,6 @@ exports.login = async function (fastify, request, reply) { if (!isPasswordValid) throw new ErrorHandler(400, false, 'Incorrect password.'); - let avatarUrl = null; - if (userData.avatar) { - if (userData.avatar.startsWith('http')) avatarUrl = userData.avatar; - else avatarUrl = `${process.env.SERVER_URL}/avatars/${userData.avatar}`; - } - const token = jwt.sign({ userId: userData.username }, process.env.JWT_SECRET, { expiresIn: '12h' }); return reply.code(200).send({ @@ -111,8 +92,7 @@ exports.login = async function (fastify, request, reply) { message: 'Login successful.', data: { user: { - ...(({ password, ...rest }) => rest)(userData), - avatar: avatarUrl + ...(({ password, ...rest }) => rest)(userData) }, token } @@ -143,19 +123,12 @@ exports.autoLogin = async function (fastify, request, reply) { const userData = user[0]; - let avatarUrl = null; - if (userData.avatar) { - if (userData.avatar.startsWith('http')) avatarUrl = userData.avatar; - else avatarUrl = `${process.env.SERVER_URL}/avatars/${userData.avatar}`; - } - return reply.code(200).send({ success: true, message: 'Login successful.', data: { user: { - ...(({ password, ...rest }) => rest)(userData), - avatar: avatarUrl + ...(({ password, ...rest }) => rest)(userData) } } }); diff --git a/server/package-lock.json b/server/package-lock.json index 725438a..9aea305 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -9,11 +9,13 @@ "version": "1.0.0", "dependencies": { "@fastify/cors": "^9.0.1", + "@fastify/multipart": "^8.2.0", "@fastify/oauth2": "^7.8.0", "@fastify/static": "^7.0.2", "bcrypt": "^5.1.1", "dotenv": "^16.4.5", "fastify": "^4.26.2", + "fastify-multer": "^2.0.3", "jsonwebtoken": "^9.0.2", "mysql2": "^3.9.3" }, @@ -39,6 +41,17 @@ "fast-uri": "^2.0.0" } }, + "node_modules/@fastify/busboy": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-1.2.1.tgz", + "integrity": "sha512-7PQA7EH43S0CxcOa9OeAnaeA0oQ+e/DHNPZwSQM9CQHW76jle5+OvLdibRp/Aafs9KXbLhxyjOTkRjWUbQEd3Q==", + "dependencies": { + "text-decoding": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/@fastify/cookie": { "version": "9.3.1", "resolved": "https://registry.npmjs.org/@fastify/cookie/-/cookie-9.3.1.tgz", @@ -57,6 +70,11 @@ "mnemonist": "0.39.6" } }, + "node_modules/@fastify/deepmerge": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@fastify/deepmerge/-/deepmerge-1.3.0.tgz", + "integrity": "sha512-J8TOSBq3SoZbDhM9+R/u77hP93gz/rajSA+K2kGyijPpORPWUXHUpTaleoj+92As0S9uPRP7Oi8IqMf0u+ro6A==" + }, "node_modules/@fastify/error": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/@fastify/error/-/error-3.4.1.tgz", @@ -78,6 +96,27 @@ "fast-deep-equal": "^3.1.3" } }, + "node_modules/@fastify/multipart": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@fastify/multipart/-/multipart-8.2.0.tgz", + "integrity": "sha512-OZ8nsyyoS2TV7Yeu3ZdrdDGsKUTAbfjrKC9jSxGgT2qdgek+BxpWX31ZubTrWMNZyU5xwk4ox6AvTjAbYWjrWg==", + "dependencies": { + "@fastify/busboy": "^2.1.0", + "@fastify/deepmerge": "^1.0.0", + "@fastify/error": "^3.0.0", + "fastify-plugin": "^4.0.0", + "secure-json-parse": "^2.4.0", + "stream-wormhole": "^1.1.0" + } + }, + "node_modules/@fastify/multipart/node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "engines": { + "node": ">=14" + } + }, "node_modules/@fastify/oauth2": { "version": "7.8.0", "resolved": "https://registry.npmjs.org/@fastify/oauth2/-/oauth2-7.8.0.tgz", @@ -445,6 +484,11 @@ "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==" + }, "node_modules/aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", @@ -597,6 +641,11 @@ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" }, + "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==" + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -658,6 +707,33 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", @@ -772,6 +848,11 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -881,6 +962,32 @@ "toad-cache": "^3.3.0" } }, + "node_modules/fastify-multer": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/fastify-multer/-/fastify-multer-2.0.3.tgz", + "integrity": "sha512-QnFqrRgxmUwWHTgX9uyQSu0C/hmVCfcxopqjApZ4uaZD5W9MJ+nHUlW4+9q7Yd3BRxDIuHvgiM5mjrh6XG8cAA==", + "dependencies": { + "@fastify/busboy": "^1.0.0", + "append-field": "^1.0.0", + "concat-stream": "^2.0.0", + "fastify-plugin": "^2.0.1", + "mkdirp": "^1.0.4", + "on-finished": "^2.3.0", + "type-is": "~1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/fastify-multer/node_modules/fastify-plugin": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-2.3.4.tgz", + "integrity": "sha512-I+Oaj6p9oiRozbam30sh39BiuiqBda7yK2nmSPVwDCfIBlKnT8YB3MY+pRQc2Fcd07bf6KPGklHJaQ2Qu81TYQ==", + "dependencies": { + "semver": "^7.3.2" + } + }, "node_modules/fastify-plugin": { "version": "4.5.1", "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz", @@ -1381,6 +1488,14 @@ "semver": "bin/semver.js" } }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mime": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", @@ -1392,6 +1507,25 @@ "node": ">=10.0.0" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -1611,6 +1745,17 @@ "node": ">=14.0.0" } }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -1986,6 +2131,14 @@ "node": ">= 0.8" } }, + "node_modules/stream-wormhole": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stream-wormhole/-/stream-wormhole-1.1.0.tgz", + "integrity": "sha512-gHFfL3px0Kctd6Po0M8TzEvt3De/xu6cnRrjlfYNhwbhLPLwigI2t1nc6jrzNuaYg5C4YF78PPFuQPzRiqn9ew==", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -2072,6 +2225,11 @@ "node": ">=10" } }, + "node_modules/text-decoding": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-decoding/-/text-decoding-1.0.0.tgz", + "integrity": "sha512-/0TJD42KDnVwKmDK6jj3xP7E2MG7SHAOG4tyTgyUCRPdHwvkquYNLEQltmdMa3owq3TkddCVcTsoctJI8VQNKA==" + }, "node_modules/thread-stream": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.4.1.tgz", @@ -2125,6 +2283,23 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "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==" + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -2278,6 +2453,14 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/server/package.json b/server/package.json index e8c737f..bd5c843 100644 --- a/server/package.json +++ b/server/package.json @@ -8,11 +8,13 @@ }, "dependencies": { "@fastify/cors": "^9.0.1", + "@fastify/multipart": "^8.2.0", "@fastify/oauth2": "^7.8.0", "@fastify/static": "^7.0.2", "bcrypt": "^5.1.1", "dotenv": "^16.4.5", "fastify": "^4.26.2", + "fastify-multer": "^2.0.3", "jsonwebtoken": "^9.0.2", "mysql2": "^3.9.3" }, diff --git a/server/routes/auth.js b/server/routes/auth.js index fba5701..c439cd4 100644 --- a/server/routes/auth.js +++ b/server/routes/auth.js @@ -3,15 +3,15 @@ const verifyToken = require('../middleware/verifyToken'); module.exports = async function (fastify, opts) { const registerSchema = { + consumes: ['multipart/form-data'], body: { type: 'object', properties: { email: { type: 'string', format: 'email', minLength: 1 }, password: { type: 'string', minLength: 6 }, - avatar: { type: 'string', nullable: true }, - username: { type: 'string', minLength: 1, pattern: '^[a-zA-Z0-9]+$' }, - first_name: { type: 'string', minLength: 1, pattern: '^[a-zA-Z0-9]+$' }, - last_name: { type: 'string', minLength: 1, pattern: '^[a-zA-Z0-9]+$' } + username: { type: 'string', minLength: 1 }, + first_name: { type: 'string', minLength: 1 }, + last_name: { type: 'string', minLength: 1 } }, required: ['email', 'password', 'username', 'first_name', 'last_name'] } @@ -34,7 +34,8 @@ module.exports = async function (fastify, opts) { fastify.post('/register', { schema: registerSchema, - attachValidation: true + attachValidation: true, + preValidation: fastify.upload.single('avatar'), }, (request, reply) => { if (request.validationError) { const errors = request.validationError.validation.map(error => { diff --git a/server/server.js b/server/server.js index 04349b8..13c9bb8 100644 --- a/server/server.js +++ b/server/server.js @@ -1,21 +1,17 @@ -require('dotenv').config(); -const path = require('path'); +require('dotenv').config() +const path = require('path') -const http = require('http'); -let server; +const http = require('http') +let server const serverFactory = (handler, opts) => { - server = http.createServer((req, res) => { - handler(req, res); - }); - - return server; -}; + return server = http.createServer((req, res) => handler(req, res)) +} const fastify = require('fastify')({ serverFactory: serverFactory, bodyLimit: 7 * 1024 * 1024 -}); +}) fastify.register(require('@fastify/cors'), { origin: "*" }) @@ -24,7 +20,12 @@ fastify.register(require('@fastify/static'), { prefix: '/avatars/', wildcard: false, redirect: false, -}); +}) + +fastify.register(require('@fastify/multipart')) +const multer = require('fastify-multer') +const upload = multer({ storage: multer.memoryStorage() }) +fastify.decorate('upload', upload) fastify.decorate('mysql', require('./db.js')) @@ -42,19 +43,17 @@ fastify.register(require('@fastify/oauth2'), { discovery: { issuer: 'https://accounts.google.com' } -}); +}) fastify.register(require('./routes/auth'), { prefix: '/api/auth' }) fastify.setNotFoundHandler((request, reply) => { - const url = request.raw.url; - if (url.startsWith('/avatars/')) return - reply.redirect(process.env.FRONTEND_URL); -}); + reply.redirect(process.env.FRONTEND_URL) +}) fastify.ready(() => { server.listen({ port: process.env.PORT }, (err) => { - if (err) throw err; - console.log(`Server running on port ${process.env.PORT}`); - }); -}); \ No newline at end of file + if (err) throw err + console.log(`Server running on port ${process.env.PORT}`) + }) +}) \ No newline at end of file diff --git a/server/utils/getMimeType.js b/server/utils/getMimeType.js deleted file mode 100644 index 5058597..0000000 --- a/server/utils/getMimeType.js +++ /dev/null @@ -1,10 +0,0 @@ -// utils/mimeUtils.js - -function getMimeType(base64String) { - const mimeType = base64String.match(/data:([a-zA-Z0-9]+\/[a-zA-Z0-9-.+]+).*,.*/); - return mimeType ? mimeType[1] : null; -} - -module.exports = { - getMimeType -}; \ No newline at end of file