From 15ac2ac48d44a060bed134db9a96ace10928b1d6 Mon Sep 17 00:00:00 2001 From: subnub Date: Sat, 12 Oct 2024 14:47:02 -0400 Subject: [PATCH] continued settings page, validation and user improvements --- backend/express-routers/user-router.ts | 36 +++-- backend/middleware/files/files-middleware.ts | 10 +- .../middleware/folders/folder-middleware.ts | 20 ++- backend/middleware/user/user-middleware.ts | 23 +++ backend/models/file-model.ts | 7 + backend/models/folder-model.ts | 10 +- backend/models/thumbnail-model.ts | 7 + backend/services/user-service/user-service.ts | 3 +- src/api/user.ts | 23 +++ src/components/FileItem/FileItem.tsx | 2 +- .../QuickAccessItem/QuickAccessItem.tsx | 2 +- .../SettingsPage/SettingsAccountSection.tsx | 89 ++++++++++- .../SettingsChangePasswordPopup.tsx | 147 ++++++++++++++++++ src/components/SettingsPage/SettingsPage.tsx | 2 + 14 files changed, 350 insertions(+), 31 deletions(-) create mode 100644 backend/middleware/user/user-middleware.ts create mode 100644 src/components/SettingsPage/SettingsChangePasswordPopup.tsx diff --git a/backend/express-routers/user-router.ts b/backend/express-routers/user-router.ts index acf4469..e10ec77 100644 --- a/backend/express-routers/user-router.ts +++ b/backend/express-routers/user-router.ts @@ -4,25 +4,24 @@ import authNoEmailVerification from "../middleware/authNoEmailVerificication"; import UserController from "../controllers/user-controller"; import authRefresh from "../middleware/authRefresh"; import authLogout from "../middleware/authLogout"; +import { changePasswordValidationRules } from "../middleware/user/user-middleware"; const userController = new UserController(); const router = Router(); +// GET + router.get( "/user-service/user", authNoEmailVerification, userController.getUser ); -router.patch( - "/user-service/refresh-storage-size", - auth, - userController.refreshStorageSize -); - router.get("/user-service/user-detailed", auth, userController.getUserDetailed); +// POST + router.post("/user-service/login", userController.login); router.post("/user-service/logout", authLogout, userController.logout); @@ -31,12 +30,6 @@ router.post("/user-service/logout-all", authLogout, userController.logoutAll); router.post("/user-service/create", userController.createUser); -router.post( - "/user-service/change-password", - auth, - userController.changePassword -); - router.post("/user-service/verify-email", userController.verifyEmail); router.post( @@ -52,8 +45,23 @@ router.post( router.post("/user-service/reset-password", userController.resetPassword); -router.patch("/user-service/add-name", auth, userController.addName); - router.post("/user-service/get-token", authRefresh, userController.getToken); +// PATCH + +router.patch( + "/user-service/refresh-storage-size", + auth, + userController.refreshStorageSize +); + +router.patch("/user-service/add-name", auth, userController.addName); + +router.patch( + "/user-service/change-password", + auth, + changePasswordValidationRules, + userController.changePassword +); + export default router; diff --git a/backend/middleware/files/files-middleware.ts b/backend/middleware/files/files-middleware.ts index 73336c8..52ffe8c 100644 --- a/backend/middleware/files/files-middleware.ts +++ b/backend/middleware/files/files-middleware.ts @@ -79,7 +79,15 @@ export const downloadFileValidationRules = [ export const renameFileValidationRules = [ body("id").isString().withMessage("ID must be a string"), - body("title").isString().withMessage("Title must be a string"), + body("title") + .exists() + .withMessage("Title is required") + .isString() + .withMessage("Title must be a string") + .isLength({ min: 1, max: 256 }) + .withMessage( + "Title must be at least 1 character and at most 256 characters" + ), middlewareValidationFunction, ]; diff --git a/backend/middleware/folders/folder-middleware.ts b/backend/middleware/folders/folder-middleware.ts index 00f011d..c4b8d6a 100644 --- a/backend/middleware/folders/folder-middleware.ts +++ b/backend/middleware/folders/folder-middleware.ts @@ -30,7 +30,15 @@ export const getFolderListValidationRules = [ export const renameFolderValidationRules = [ body("id").isString().withMessage("ID must be a string"), - body("title").isString().withMessage("Title must be a string"), + body("title") + .exists() + .withMessage("Title is required") + .isString() + .withMessage("Title must be a string") + .isLength({ min: 1, max: 256 }) + .withMessage( + "Title must be at least 1 character and at most 256 characters" + ), middlewareValidationFunction, ]; @@ -60,7 +68,15 @@ export const deleteFolderValidationRules = [ // POST export const createFolderValidationRules = [ - body("name").isString().withMessage("Name must be a string"), + body("name") + .exists() + .withMessage("Name is required") + .isString() + .withMessage("Name must be a string") + .isLength({ min: 1, max: 256 }) + .withMessage( + "Name must be at least 1 character and at most 256 characters" + ), body("parent").optional().isString().withMessage("Parent must be a string"), middlewareValidationFunction, ]; diff --git a/backend/middleware/user/user-middleware.ts b/backend/middleware/user/user-middleware.ts new file mode 100644 index 0000000..c22efda --- /dev/null +++ b/backend/middleware/user/user-middleware.ts @@ -0,0 +1,23 @@ +import { body, param, query, validationResult } from "express-validator"; +import { Request, Response, NextFunction } from "express"; +import { middlewareValidationFunction } from "../utils/middleware-utils"; + +// PATCH + +export const changePasswordValidationRules = [ + body("oldPassword") + .exists() + .withMessage("Old password is required") + .isString() + .withMessage("Old password must be a string"), + body("newPassword") + .exists() + .withMessage("New password is required") + .isString() + .withMessage("New password must be a string") + .isLength({ min: 1, max: 256 }) + .withMessage( + "New password must be at least 6 characters and at most 256 characters" + ), + middlewareValidationFunction, +]; diff --git a/backend/models/file-model.ts b/backend/models/file-model.ts index 127238f..3e64802 100644 --- a/backend/models/file-model.ts +++ b/backend/models/file-model.ts @@ -16,6 +16,13 @@ const fileSchema = new mongoose.Schema({ filename: { type: String, required: true, + validate(value: any) { + if (!value || value.length === 0 || value.length >= 256) { + throw new Error( + "Filename is required and length must be greater than 0 and 256 characters max" + ); + } + }, }, metadata: { type: { diff --git a/backend/models/folder-model.ts b/backend/models/folder-model.ts index 58624c9..f1c33cb 100644 --- a/backend/models/folder-model.ts +++ b/backend/models/folder-model.ts @@ -5,18 +5,22 @@ const folderSchema = new mongoose.Schema( name: { type: String, required: true, + validate(value: any) { + if (!value || value.length === 0 || value.length >= 256) { + throw new Error( + "Name is required and length must be greater than 0 and 256 characters max" + ); + } + }, }, - parent: { type: String, required: true, }, - owner: { type: String, required: true, }, - parentList: { type: Array, required: true, diff --git a/backend/models/thumbnail-model.ts b/backend/models/thumbnail-model.ts index ac92448..a7cf2c7 100644 --- a/backend/models/thumbnail-model.ts +++ b/backend/models/thumbnail-model.ts @@ -5,6 +5,13 @@ const thumbnailSchema = new mongoose.Schema( name: { type: String, required: true, + validate(value: any) { + if (!value || value.length === 0 || value.length >= 256) { + throw new Error( + "Name is required and length must be greater than 0 and 256 characters max" + ); + } + }, }, owner: { type: String, diff --git a/backend/services/user-service/user-service.ts b/backend/services/user-service/user-service.ts index 79ef21c..e3b44fa 100644 --- a/backend/services/user-service/user-service.ts +++ b/backend/services/user-service/user-service.ts @@ -121,8 +121,7 @@ class UserService { const isMatch = await bcrypt.compare(oldPassword, user.password); - if (!isMatch) - throw new ForbiddenError("Change Passwords Do Not Match Error"); + if (!isMatch) throw new ForbiddenError("Old Password Is Incorrect"); const encryptionKey = user.getEncryptionKey(); diff --git a/src/api/user.ts b/src/api/user.ts index b62139a..6df69ce 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -34,3 +34,26 @@ export const createAccountAPI = async (email: string, password: string) => { }); return response.data; }; + +export const logoutAPI = async () => { + const response = await axios.post("/user-service/logout"); + return response.data; +}; + +export const logoutAllAPI = async () => { + const response = await axios.post("/user-service/logout-all"); + return response.data; +}; + +// PATCH + +export const changePasswordAPI = async ( + oldPassword: string, + newPassword: string +) => { + const response = await axios.patch("/user-service/change-password", { + oldPassword, + newPassword, + }); + return response.data; +}; diff --git a/src/components/FileItem/FileItem.tsx b/src/components/FileItem/FileItem.tsx index 0f38f40..c31a518 100644 --- a/src/components/FileItem/FileItem.tsx +++ b/src/components/FileItem/FileItem.tsx @@ -204,7 +204,7 @@ const FileItem: React.FC = memo((props) => { > {!!thumbnail ? (
- + {file.metadata.isVideo && (
diff --git a/src/components/QuickAccessItem/QuickAccessItem.tsx b/src/components/QuickAccessItem/QuickAccessItem.tsx index e5fd2b0..7bdd3b0 100644 --- a/src/components/QuickAccessItem/QuickAccessItem.tsx +++ b/src/components/QuickAccessItem/QuickAccessItem.tsx @@ -126,7 +126,7 @@ const QuickAccessItem = memo((props: QuickAccessItemProps) => { > {!!thumbnail ? (
- + {file.metadata.isVideo && (
diff --git a/src/components/SettingsPage/SettingsAccountSection.tsx b/src/components/SettingsPage/SettingsAccountSection.tsx index 4b05aa6..bb6fafb 100644 --- a/src/components/SettingsPage/SettingsAccountSection.tsx +++ b/src/components/SettingsPage/SettingsAccountSection.tsx @@ -1,4 +1,8 @@ -import React from "react"; +import React, { useState } from "react"; +import SettingsChangePasswordPopup from "./SettingsChangePasswordPopup"; +import { toast } from "react-toastify"; +import { logoutAPI } from "../../api/user"; +import Swal from "sweetalert2"; interface SettingsPageAccountProps { user: { @@ -8,8 +12,70 @@ interface SettingsPageAccountProps { } const SettingsPageAccount: React.FC = ({ user }) => { + const [showChangePasswordPopup, setShowChangePasswordPopup] = useState(false); + + const logoutClick = async () => { + try { + const result = await Swal.fire({ + title: "Logout?", + icon: "info", + showCancelButton: true, + confirmButtonColor: "#3085d6", + cancelButtonColor: "#d33", + confirmButtonText: "Yes, logout", + }); + + if (!result.value) return; + + await toast.promise(logoutAPI(), { + pending: "Logging out...", + success: "Logged out", + error: "Error Logging Out", + }); + + window.localStorage.removeItem("hasPreviouslyLoggedIn"); + + window.location.assign("/"); + } catch (e) { + console.log("Error logging out", e); + } + }; + + const logoutAllClick = async () => { + try { + const result = await Swal.fire({ + title: "Logout all?", + icon: "info", + showCancelButton: true, + confirmButtonColor: "#3085d6", + cancelButtonColor: "#d33", + confirmButtonText: "Yes, logout all", + }); + + if (!result.value) return; + + await toast.promise(logoutAPI(), { + pending: "Logging out all...", + success: "Logged out all", + error: "Error Logging Out Al", + }); + + window.localStorage.removeItem("hasPreviouslyLoggedIn"); + + window.location.assign("/"); + } catch (e) { + console.log("Error logging out", e); + } + }; + return (
+ {showChangePasswordPopup && ( + setShowChangePasswordPopup(false)} + /> + )} +

Account

@@ -20,21 +86,30 @@ const SettingsPageAccount: React.FC = ({ user }) => {

Change password

-

+

Logout account

-

+

Logout all accounts

-

+

diff --git a/src/components/SettingsPage/SettingsChangePasswordPopup.tsx b/src/components/SettingsPage/SettingsChangePasswordPopup.tsx new file mode 100644 index 0000000..5861f0d --- /dev/null +++ b/src/components/SettingsPage/SettingsChangePasswordPopup.tsx @@ -0,0 +1,147 @@ +import { useMemo, useState } from "react"; +import CloseIcon from "../../icons/CloseIcon"; +import classNames from "classnames"; +import { toast } from "react-toastify"; +import { changePasswordAPI } from "../../api/user"; + +interface SettingsChangePasswordPopupProps { + closePopup: () => void; +} + +const SettingsChangePasswordPopup: React.FC< + SettingsChangePasswordPopupProps +> = ({ closePopup }) => { + const [currentPassword, setCurrentPassword] = useState(""); + const [newPassword, setNewPassword] = useState(""); + const [verifyNewPassword, setVerifyNewPassword] = useState(""); + const [loadingChangePassword, setLoadingChangePassword] = useState(false); + + const inputDisabled = useMemo(() => { + if ( + loadingChangePassword || + currentPassword.length === 0 || + newPassword.length === 0 || + verifyNewPassword.length === 0 + ) { + return true; + } + + if (newPassword !== verifyNewPassword) { + return true; + } + + if (newPassword.length < 6) { + return true; + } + + return false; + }, [currentPassword, newPassword, verifyNewPassword, loadingChangePassword]); + + const errorMessage = useMemo(() => { + if (newPassword.length === 0) { + return ""; + } + + if (newPassword.length < 6) { + return "Password must be at least 6 characters"; + } + + if (verifyNewPassword.length !== 0 && newPassword !== verifyNewPassword) { + return "Passwords do not match"; + } + + return ""; + }, [newPassword, verifyNewPassword]); + + const submitPasswordChange = async (e: any) => { + e.preventDefault(); + setLoadingChangePassword(true); + try { + await toast.promise(changePasswordAPI(currentPassword, newPassword), { + pending: "Changing password...", + success: "Password Changed", + error: "Error Changing Password", + }); + closePopup(); + } catch (e) { + console.log("Error changing password", e); + } finally { + setLoadingChangePassword(false); + } + }; + + const outterWrapperClick = (e: any) => { + if (e.target.id !== "outer-wrapper") return; + closePopup(); + }; + + return ( +
+
+
+

Change password

+ +
+
+ + + + + + + {errorMessage && ( +
+

{errorMessage}

+
+ )} + +
+ +
+
+
+
+ ); +}; + +export default SettingsChangePasswordPopup; diff --git a/src/components/SettingsPage/SettingsPage.tsx b/src/components/SettingsPage/SettingsPage.tsx index 4c16c2d..28bbbec 100644 --- a/src/components/SettingsPage/SettingsPage.tsx +++ b/src/components/SettingsPage/SettingsPage.tsx @@ -12,6 +12,7 @@ import Swal from "sweetalert2"; import SettingsGeneralSection from "./SettingsGeneralSection"; import { useClickOutOfBounds } from "../../hooks/utils"; import MenuIcon from "../../icons/MenuIcon"; +import { ToastContainer } from "react-toastify"; const SettingsPage = () => { const [user, setUser] = useState(null); @@ -125,6 +126,7 @@ const SettingsPage = () => { {tab === "general" && }
+ ); };