continued settings page, validation and user improvements

This commit is contained in:
subnub
2024-10-12 14:47:02 -04:00
parent c38c15f63f
commit 15ac2ac48d
14 changed files with 350 additions and 31 deletions
+22 -14
View File
@@ -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;
+9 -1
View File
@@ -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,
];
@@ -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,
];
@@ -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,
];
+7
View File
@@ -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: {
+7 -3
View File
@@ -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,
+7
View File
@@ -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,
@@ -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();
+23
View File
@@ -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;
};
+1 -1
View File
@@ -204,7 +204,7 @@ const FileItem: React.FC<FileItemProps> = memo((props) => {
>
{!!thumbnail ? (
<div className="w-full min-h-[88px] max-h-[88px] h-full flex">
<img className="object-cover" src={thumbnail} />
<img className="object-cover w-full" src={thumbnail} />
{file.metadata.isVideo && (
<div className="w-full h-full absolute flex justify-center items-center text-white">
<PlayButtonIcon className="w-[50px] h-[50px]" />
@@ -126,7 +126,7 @@ const QuickAccessItem = memo((props: QuickAccessItemProps) => {
>
{!!thumbnail ? (
<div className="w-full min-h-[88px] max-h-[88px] h-full flex">
<img className=" object-cover" src={thumbnail} />
<img className=" object-cover w-full" src={thumbnail} />
{file.metadata.isVideo && (
<div className="w-full h-full absolute flex justify-center items-center text-white">
<PlayButtonIcon className="w-[50px] h-[50px]" />
@@ -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<SettingsPageAccountProps> = ({ 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 (
<div>
{showChangePasswordPopup && (
<SettingsChangePasswordPopup
closePopup={() => setShowChangePasswordPopup(false)}
/>
)}
<div className="bg-white-hover p-3 flex items-center w-full rounded-md">
<p className="text-base">Account</p>
</div>
@@ -20,21 +86,30 @@ const SettingsPageAccount: React.FC<SettingsPageAccountProps> = ({ user }) => {
</div>
<div className="px-3 py-4 flex flex-row justify-between items-center border-b border-gray-secondary">
<p className="text-gray-primary">Change password</p>
<p className="text-primary hover:text-primary-hover cursor-pointer">
<button
className="text-primary hover:text-primary-hover cursor-pointer"
onClick={() => setShowChangePasswordPopup(true)}
>
Change
</p>
</button>
</div>
<div className="px-3 py-4 flex flex-row justify-between items-center border-b border-gray-secondary">
<p className="text-gray-primary">Logout account</p>
<p className="text-primary hover:text-primary-hover cursor-pointer">
<button
className="text-primary hover:text-primary-hover cursor-pointer"
onClick={logoutClick}
>
Logout
</p>
</button>
</div>
<div className="px-3 py-4 flex flex-row justify-between items-center border-b border-gray-secondary">
<p className="text-gray-primary">Logout all accounts</p>
<p className="text-primary hover:text-primary-hover cursor-pointer">
<button
className="text-primary hover:text-primary-hover cursor-pointer"
onClick={logoutAllClick}
>
Logout all
</p>
</button>
</div>
</div>
</div>
@@ -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 (
<div
id="outer-wrapper"
className="w-screen h-screen bg-black bg-opacity-80 absolute top-0 left-0 right-0 bottom-0 z-50 flex justify-center items-center flex-col"
onClick={outterWrapperClick}
>
<div className="w-[300px] sm:w-[440px] bg-white rounded-md animate">
<div className="flex justify-between p-4 border-b border-gray-secondary">
<p className="text-md ">Change password</p>
<CloseIcon className="w-6 h-6 cursor-pointer" onClick={closePopup} />
</div>
<form className="mt-2 p-4" onSubmit={submitPasswordChange}>
<label>
<p className="text-sm text-gray-primary mb-1">Current password</p>
<input
className="border border-[#BEC9D3] rounded-md py-2 px-3 text-black w-full text-sm mb-3"
type="password"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
/>
</label>
<label>
<p className="text-sm text-gray-primary mb-1">New password</p>
<input
className="border border-[#BEC9D3] rounded-md py-2 px-3 text-black w-full mb-3"
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
/>
</label>
<label>
<p className="text-sm text-gray-primary mb-1">
Verify new password
</p>
<input
className="border border-[#BEC9D3] rounded-md py-2 px-3 text-black w-full mb-3"
type="password"
value={verifyNewPassword}
onChange={(e) => setVerifyNewPassword(e.target.value)}
/>
</label>
{errorMessage && (
<div className="p-2">
<p className="text-sm text-red-500 text-center">{errorMessage}</p>
</div>
)}
<div className="flex justify-center mt-2">
<input
type="submit"
value="Submit"
className={classNames(
"bg-primary text-white px-4 py-2 rounded-md w-32 cursor-pointer",
{
"opacity-50 cursor-not-allowed": inputDisabled,
}
)}
disabled={inputDisabled}
/>
</div>
</form>
</div>
</div>
);
};
export default SettingsChangePasswordPopup;
@@ -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" && <SettingsGeneralSection />}
</div>
</div>
<ToastContainer position="bottom-left" />
</div>
);
};