From e8d4a5d86875b4420bfa8151b14939b8c6b4dab2 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 1 Jul 2024 11:48:56 -0700 Subject: [PATCH 1/6] Add profileImage to schema, controllers, and routes --- Server/db/MongoDB.js | 10 +- Server/middleware/handleErrors.js | 1 + Server/models/user.js | 5 +- Server/package-lock.json | 149 ++++++++++++++++++++++++++++++ Server/package.json | 1 + Server/routes/authRoute.js | 4 +- Server/validation/joi.js | 1 + 7 files changed, 167 insertions(+), 4 deletions(-) diff --git a/Server/db/MongoDB.js b/Server/db/MongoDB.js index ef53863ee..0c9061594 100644 --- a/Server/db/MongoDB.js +++ b/Server/db/MongoDB.js @@ -32,7 +32,15 @@ const connect = async () => { */ const insertUser = async (req, res) => { try { - const newUser = new UserModel({ ...req.body }); + const userData = { ...req.body }; + console.log(req.file); + if (req.file) { + userData.profileImage = { + data: req.file.buffer, + contentType: req.file.mimetype, + }; + } + const newUser = new UserModel(userData); await newUser.save(); return await UserModel.findOne({ _id: newUser._id }).select("-password"); // .select() doesn't work with create, need to save then find } catch (error) { diff --git a/Server/middleware/handleErrors.js b/Server/middleware/handleErrors.js index 1e1e955c5..106100f41 100644 --- a/Server/middleware/handleErrors.js +++ b/Server/middleware/handleErrors.js @@ -2,6 +2,7 @@ const logger = require("../utils/logger"); const { errorMessages } = require("../utils/messages"); const handleErrors = (error, req, res, next) => { + console.log(error); const status = error.status || 500; const message = error.message || errorMessages.FRIENDLY_ERROR; const service = error.service || errorMessages.UNKNOWN_SERVICE; diff --git a/Server/models/user.js b/Server/models/user.js index a40824399..f13665cc1 100644 --- a/Server/models/user.js +++ b/Server/models/user.js @@ -20,8 +20,9 @@ const UserSchema = mongoose.Schema( type: String, required: true, }, - profilePicUrl: { - type: String, + profileImage: { + data: Buffer, + contentType: String, }, isActive: { type: Boolean, diff --git a/Server/package-lock.json b/Server/package-lock.json index edfb2ef44..bef07d2cf 100644 --- a/Server/package-lock.json +++ b/Server/package-lock.json @@ -21,6 +21,7 @@ "jsonwebtoken": "9.0.2", "mailersend": "^2.2.0", "mongoose": "^8.3.3", + "multer": "1.4.5-lts.1", "ping": "0.4.4", "winston": "^3.13.0" }, @@ -333,6 +334,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", @@ -476,6 +483,12 @@ "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/bullmq": { "version": "5.7.15", "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.7.15.tgz", @@ -491,6 +504,17 @@ "uuid": "^9.0.0" } }, + "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", @@ -634,6 +658,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", @@ -676,6 +745,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", @@ -1460,6 +1535,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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/isomorphic-unfetch": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/isomorphic-unfetch/-/isomorphic-unfetch-3.1.0.tgz", @@ -1743,6 +1824,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", @@ -1954,6 +2044,36 @@ "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.2" } }, + "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/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -2215,6 +2335,12 @@ "node": ">=4.0.0" } }, + "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", @@ -2559,6 +2685,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", @@ -2702,6 +2836,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", @@ -2834,6 +2974,15 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "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 3df37cda6..2133afd4d 100644 --- a/Server/package.json +++ b/Server/package.json @@ -23,6 +23,7 @@ "jsonwebtoken": "9.0.2", "mailersend": "^2.2.0", "mongoose": "^8.3.3", + "multer": "1.4.5-lts.1", "ping": "0.4.4", "winston": "^3.13.0" }, diff --git a/Server/routes/authRoute.js b/Server/routes/authRoute.js index 1049659fa..0ccddc3d2 100644 --- a/Server/routes/authRoute.js +++ b/Server/routes/authRoute.js @@ -1,5 +1,7 @@ const router = require("express").Router(); const { verifyJWT } = require("../middleware/verifyJWT"); +const multer = require("multer"); +const upload = multer(); const { registerController, @@ -11,7 +13,7 @@ const { } = require("../controllers/authController"); //Auth routes -router.post("/register", registerController); +router.post("/register", upload.single("profileImage"), registerController); router.post("/login", loginController); router.post("/user/:userId", verifyJWT, userEditController); diff --git a/Server/validation/joi.js b/Server/validation/joi.js index 4316939b4..9f0a97510 100644 --- a/Server/validation/joi.js +++ b/Server/validation/joi.js @@ -14,6 +14,7 @@ const registerValidation = joi.object({ lastname: joi.string().required(), email: joi.string().email().required(), password: joi.string().min(8).required(), + profileImage: joi.any(), }); const editUserParamValidation = joi.object({ From 20301d19333aad2d3a521a7846a55429251929f8 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 1 Jul 2024 11:50:35 -0700 Subject: [PATCH 2/6] removed stub method --- Client/src/Features/Auth/authSlice.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/Client/src/Features/Auth/authSlice.js b/Client/src/Features/Auth/authSlice.js index cf808b431..ef41c1b46 100644 --- a/Client/src/Features/Auth/authSlice.js +++ b/Client/src/Features/Auth/authSlice.js @@ -63,15 +63,6 @@ export const update = createAsyncThunk( } ); -export const deleteAccount = createAsyncThunk( - "auth/delete", - async (form, thunkApi) => { - try { - //TODO - } catch (error) {} - } -); - const handleAuthFulfilled = (state, action) => { state.isLoading = false; state.success = action.payload.success; From d7d6effdda6c00dffe783d9d080595a89567c0c8 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 2 Jul 2024 10:38:02 -0700 Subject: [PATCH 3/6] Add route and controller --- Server/controllers/authController.js | 19 +++++++++++++++++++ Server/db/MongoDB.js | 10 ++++++++++ Server/routes/authRoute.js | 2 ++ 3 files changed, 31 insertions(+) diff --git a/Server/controllers/authController.js b/Server/controllers/authController.js index 16eb6eb00..be39851ab 100644 --- a/Server/controllers/authController.js +++ b/Server/controllers/authController.js @@ -151,6 +151,24 @@ const userEditController = async (req, res, next) => { } }; +/** + * Checks to see if an admin account exists + * @async + * @param {Express.Request} req + * @param {Express.Response} res + * @returns {Promise} + */ + +const checkAdminController = async (req, res) => { + try { + await req.db.checkAdmin(req, res); + } catch (error) { + error.service = SERVICE_NAME; + next(error); + return; + } +}; + /** * Returns a recovery token * @async @@ -238,6 +256,7 @@ module.exports = { registerController, loginController, userEditController, + checkAdminController, recoveryRequestController, validateRecoveryTokenController, resetPasswordController, diff --git a/Server/db/MongoDB.js b/Server/db/MongoDB.js index ef53863ee..0b7aa0c87 100644 --- a/Server/db/MongoDB.js +++ b/Server/db/MongoDB.js @@ -163,6 +163,15 @@ const resetPassword = async (req, res) => { } }; +const checkAdmin = async (req, res) => { + try { + const admin = await UserModel.findOne({ role: "admin" }); + console.log(admin); + } catch (error) { + throw error; + } +}; + //**************************************** // Monitors //**************************************** @@ -502,6 +511,7 @@ module.exports = { requestRecoveryToken, validateRecoveryToken, resetPassword, + checkAdmin, getAllMonitors, getMonitorById, getMonitorsByUserId, diff --git a/Server/routes/authRoute.js b/Server/routes/authRoute.js index 1049659fa..492f0d534 100644 --- a/Server/routes/authRoute.js +++ b/Server/routes/authRoute.js @@ -8,12 +8,14 @@ const { recoveryRequestController, validateRecoveryTokenController, resetPasswordController, + checkAdminController, } = require("../controllers/authController"); //Auth routes router.post("/register", registerController); router.post("/login", loginController); router.post("/user/:userId", verifyJWT, userEditController); +router.get("/users/admin", checkAdminController); //Recovery routes router.post("/recovery/request", recoveryRequestController); From ec632812022503ca132a184c6b2773d58afa87b4 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 2 Jul 2024 10:45:38 -0700 Subject: [PATCH 4/6] Return admin account status on check --- Server/controllers/authController.js | 7 ++++++- Server/db/MongoDB.js | 5 ++++- Server/utils/messages.js | 1 + Server/validation/joi.js | 2 ++ 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Server/controllers/authController.js b/Server/controllers/authController.js index be39851ab..bbf676e8a 100644 --- a/Server/controllers/authController.js +++ b/Server/controllers/authController.js @@ -161,7 +161,12 @@ const userEditController = async (req, res, next) => { const checkAdminController = async (req, res) => { try { - await req.db.checkAdmin(req, res); + const adminExists = await req.db.checkAdmin(req, res); + return res.status(200).json({ + success: true, + msg: successMessages.AUTH_ADMIN_EXISTS, + data: adminExists, + }); } catch (error) { error.service = SERVICE_NAME; next(error); diff --git a/Server/db/MongoDB.js b/Server/db/MongoDB.js index 0b7aa0c87..a789966d9 100644 --- a/Server/db/MongoDB.js +++ b/Server/db/MongoDB.js @@ -166,7 +166,10 @@ const resetPassword = async (req, res) => { const checkAdmin = async (req, res) => { try { const admin = await UserModel.findOne({ role: "admin" }); - console.log(admin); + if (admin !== null) { + return true; + } + return false; } catch (error) { throw error; } diff --git a/Server/utils/messages.js b/Server/utils/messages.js index cb435757c..9ed03fdb6 100644 --- a/Server/utils/messages.js +++ b/Server/utils/messages.js @@ -56,6 +56,7 @@ const successMessages = { AUTH_CREATE_RECOVERY_TOKEN: "Recovery token created successfully", AUTH_VERIFY_RECOVERY_TOKEN: "Recovery token verified successfully", AUTH_RESET_PASSWORD: "Password reset successfully", + AUTH_ADMIN_CHECK: "Admin check completed successfully", // Check Controller CHECK_CREATE: "Check created successfully", diff --git a/Server/validation/joi.js b/Server/validation/joi.js index 4316939b4..64865a6c2 100644 --- a/Server/validation/joi.js +++ b/Server/validation/joi.js @@ -14,6 +14,7 @@ const registerValidation = joi.object({ lastname: joi.string().required(), email: joi.string().email().required(), password: joi.string().min(8).required(), + role: joi.string().required(), }); const editUserParamValidation = joi.object({ @@ -24,6 +25,7 @@ const editUserBodyValidation = joi.object({ firstname: joi.string(), lastname: joi.string(), profilePicUrl: joi.string(), + role: joi.string(), }); const recoveryValidation = joi.object({ From 92c78166236d6033b0b7ca935aa54ca53dcae5bc Mon Sep 17 00:00:00 2001 From: Daniel Cojocea Date: Tue, 2 Jul 2024 14:51:31 -0400 Subject: [PATCH 5/6] Added validation to picture uploads --- Client/src/Components/ProgressBars/index.css | 27 +++- Client/src/Components/ProgressBars/index.jsx | 107 +++++++++----- .../TabPanels/Account/ProfilePanel.jsx | 41 +++++- .../src/Components/TextFields/Image/index.css | 18 ++- .../src/Components/TextFields/Image/index.jsx | 134 +++++++++--------- Client/src/Validation/validation.js | 16 +++ 6 files changed, 222 insertions(+), 121 deletions(-) diff --git a/Client/src/Components/ProgressBars/index.css b/Client/src/Components/ProgressBars/index.css index 37317228f..8bd3fc966 100644 --- a/Client/src/Components/ProgressBars/index.css +++ b/Client/src/Components/ProgressBars/index.css @@ -1,6 +1,6 @@ -.progress-bar-container{ - background-color: #fafafa; - padding: var(--env-var-spacing-1-plus); +.progress-bar-container { + background-color: #fafafa; + padding: var(--env-var-spacing-1-plus); } .progress-bar-container h2.MuiTypography-root, .progress-bar-container p.MuiTypography-root { @@ -10,9 +10,22 @@ .progress-bar-container p.MuiTypography-root { opacity: 0.6; } -.progress-bar-container p.MuiTypography-root:has(span){ - font-size: 12px; +.progress-bar-container p.MuiTypography-root:has(span) { + font-size: 12px; } .progress-bar-container p.MuiTypography-root span { - padding-left: 2px; -} \ No newline at end of file + padding-left: 2px; +} +.progress-bar-container:has(p.input-error){ + border-color: var(--env-var-color-24); + padding: 8px var(--env-var-spacing-1-plus); +} +.progress-bar-container p.input-error{ + color: var(--env-var-color-24); + opacity: 0.8; +} +.progress-bar-container:has(p.input-error)>.MuiStack-root>svg{ + fill: var(--env-var-color-24); + width: 20px; + height: 20px; +} diff --git a/Client/src/Components/ProgressBars/index.jsx b/Client/src/Components/ProgressBars/index.jsx index 211777080..0f0affd0d 100644 --- a/Client/src/Components/ProgressBars/index.jsx +++ b/Client/src/Components/ProgressBars/index.jsx @@ -9,6 +9,7 @@ import { Typography, } from "@mui/material"; import CloseIcon from "@mui/icons-material/Close"; +import ErrorOutlineOutlinedIcon from "@mui/icons-material/ErrorOutlineOutlined"; import "./index.css"; /** @@ -21,7 +22,14 @@ import "./index.css"; * @returns {JSX.Element} The rendered component. */ -const ProgressUpload = ({ icon, label, size, progress = 0, onClick }) => { +const ProgressUpload = ({ + icon, + label, + size, + progress = 0, + onClick, + error, +}) => { const theme = useTheme(); return ( { border: `solid 1px ${theme.palette.otherColors.graishWhite}`, }} > - - {icon ? ( + + {error ? ( + + ) : icon ? ( { ) : ( "" )} - - - {label} + {error ? ( + + {error} - - {size} - - + ) : ( + + + {error ? error : label} + + {!error && size} + + )} { /> - - - - - - {progress} - % - - + {!error ? ( + + + + + + {progress} + % + + + ) : ( + "" + )} ); }; diff --git a/Client/src/Components/TabPanels/Account/ProfilePanel.jsx b/Client/src/Components/TabPanels/Account/ProfilePanel.jsx index dd6398911..2208fd646 100644 --- a/Client/src/Components/TabPanels/Account/ProfilePanel.jsx +++ b/Client/src/Components/TabPanels/Account/ProfilePanel.jsx @@ -7,7 +7,10 @@ import Button from "../../Button"; import EmailTextField from "../../TextFields/Email/EmailTextField"; import StringTextField from "../../TextFields/Text/TextField"; import Avatar from "../../Avatar"; -import { editProfileValidation } from "../../../Validation/validation"; +import { + editProfileValidation, + imageValidation, +} from "../../../Validation/validation"; import { useDispatch, useSelector } from "react-redux"; import { update } from "../../../Features/Auth/authSlice"; import ImageField from "../../TextFields/Image"; @@ -76,12 +79,30 @@ const ProfilePanel = () => { const [uploadProgress, setUploadProgress] = useState(0); const [isUploading, setIsUploading] = useState(false); const handlePicture = (event) => { + clearError("picture"); const pic = event.target.files[0]; console.log(pic); //TODO - add setPicture state to localData setPicture({ name: pic.name, type: pic.type, size: formatBytes(pic.size) }); setIsUploading(true); + const { error } = imageValidation.validate( + { + type: pic.type, + size: pic.size, + }, + { abortEarly: false } + ); + + if (error) { + setErrors((prev) => { + const updatedErrors = { ...prev }; + updatedErrors["picture"] = error.details[0].message; + return updatedErrors; + }); + return; + } + setTimeout(() => { //TODO - add setPicture state to localData setPicture((prev) => ({ ...prev, src: URL.createObjectURL(pic) })); @@ -98,20 +119,28 @@ const ProfilePanel = () => { }; const formatBytes = (bytes) => { if (bytes === 0) return "0 Bytes"; - const k = 1024; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " MB"; + const megabytes = bytes / (1024 * 1024); + return megabytes.toFixed(2) + " MB"; + }; + const clearError = (err) => { + setErrors((prev) => { + const updatedErrors = { ...prev }; + if (updatedErrors[err]) delete updatedErrors[err]; + return updatedErrors; + }); }; const handleCancelUpload = () => { //TODO - add setPicture state to localData - setPicture(null); + setPicture(); setUploadProgress(0); setIsUploading(false); + clearError("picture"); }; const handleClosePictureModal = () => { setIsOpen(""); setUploadProgress(0); setIsUploading(false); + clearError("picture"); }; //TODO - revisit once localData is set up properly @@ -353,7 +382,6 @@ const ProfilePanel = () => { aria-describedby="update-profile-picture" open={isModalOpen("picture")} onClose={handleClosePictureModal} - disablePortal > { size={picture?.size} progress={uploadProgress} onClick={handleCancelUpload} + error={errors["picture"]} /> ) : ( "" diff --git a/Client/src/Components/TextFields/Image/index.css b/Client/src/Components/TextFields/Image/index.css index 48b1a82ff..96a730bf0 100644 --- a/Client/src/Components/TextFields/Image/index.css +++ b/Client/src/Components/TextFields/Image/index.css @@ -1,13 +1,27 @@ .image-field-wrapper h2.MuiTypography-root, -.image-field-wrapper p.MuiTypography-root { +.image-field-wrapper p.MuiTypography-root, +.image-field-wrapper + p.MuiTypography-root, +.MuiStack-root:has(#modal-update-picture) h1.MuiTypography-root { color: var(--env-var-color-2); +} +.MuiStack-root:has(#modal-update-picture) h1.MuiTypography-root { + font-weight: 600; +} +.image-field-wrapper h2.MuiTypography-root, +.MuiStack-root:has(#modal-update-picture) button, +.MuiStack-root:has(#modal-update-picture) h1.MuiTypography-root { font-size: var(--env-var-font-size-medium); } .image-field-wrapper h2.MuiTypography-root { margin-top: 10px; } +.image-field-wrapper + p.MuiTypography-root { + margin-top: 8px; +} +.image-field-wrapper + p.MuiTypography-root, .image-field-wrapper p.MuiTypography-root { - opacity: 0.8; + opacity: 0.6; + font-size: var(--env-var-font-size-small-plus); } .image-field-wrapper h2.MuiTypography-root span { color: var(--env-var-color-3); diff --git a/Client/src/Components/TextFields/Image/index.jsx b/Client/src/Components/TextFields/Image/index.jsx index 5a3a35089..ae0a50c7d 100644 --- a/Client/src/Components/TextFields/Image/index.jsx +++ b/Client/src/Components/TextFields/Image/index.jsx @@ -27,77 +27,78 @@ const ImageField = ({ id, picture, onChange }) => { return ( <> {!picture ? ( - - + - - + - - - - Click to upload or drag and drop - - - (max. 800x400px) - - - + + + + + Click to upload or drag and drop + + (maximum size: 3MB) + + + Supported formats: JPG, PNG + ) : ( { borderRadius: "50%", overflow: "hidden", backgroundImage: `url(${picture})`, - backgroundSize: "cover" + backgroundSize: "cover", }} - > - + > )} diff --git a/Client/src/Validation/validation.js b/Client/src/Validation/validation.js index 4aea7e48e..00903fdb7 100644 --- a/Client/src/Validation/validation.js +++ b/Client/src/Validation/validation.js @@ -150,7 +150,23 @@ const createMonitorValidation = joi.object({ }), }); +const imageValidation = joi.object({ + type: joi.string().valid("image/jpeg", "image/png").messages({ + "any.only": "Invalid file format.", + "string.empty": "File type required.", + }), + size: joi + .number() + .max(3 * 1024 * 1024) + .messages({ + "number.base": "File size must be a number.", + "number.max": "File size must be less than 3 MB.", + "number.empty": "File size required.", + }), +}); + export { + imageValidation, createMonitorValidation, registerValidation, loginValidation, From 5cbe916a946831713064c987e45eabce5a9827eb Mon Sep 17 00:00:00 2001 From: Daniel Cojocea Date: Tue, 2 Jul 2024 14:56:02 -0400 Subject: [PATCH 6/6] Added missing proptype and jsdocs --- Client/src/Components/ProgressBars/index.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Client/src/Components/ProgressBars/index.jsx b/Client/src/Components/ProgressBars/index.jsx index 0f0affd0d..2d60683ef 100644 --- a/Client/src/Components/ProgressBars/index.jsx +++ b/Client/src/Components/ProgressBars/index.jsx @@ -19,6 +19,7 @@ import "./index.css"; * @param {string} props.size - The size information for the progress item. * @param {number} props.progress - The current progress value (0-100). * @param {function} props.onClick - The function to handle click events on the remove button. + * @param {string} props.error - Error message to display if there's an error (optional). * @returns {JSX.Element} The rendered component. */ @@ -136,6 +137,7 @@ ProgressUpload.propTypes = { size: PropTypes.string.isRequired, // Size information for the progress item progress: PropTypes.number.isRequired, // Current progress value (0-100) onClick: PropTypes.func.isRequired, // Function to handle click events on the remove button + error: PropTypes.string, // Error message to display if there's an error (optional) }; export default ProgressUpload;