continued settings page, validation and user improvements
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user