diff --git a/Client/src/Components/BasicTable/index.jsx b/Client/src/Components/BasicTable/index.jsx index fbd8759e5..93eba2024 100644 --- a/Client/src/Components/BasicTable/index.jsx +++ b/Client/src/Components/BasicTable/index.jsx @@ -91,7 +91,7 @@ const BasicTable = ({ data, paginated, reversed }) => { } let paginationComponent = <>; - if (paginated === true && displayData.length > rowsPerPage) { + if (paginated === true && data.rows.length > rowsPerPage) { paginationComponent = ( { +const Check = ({ text, variant = "info", outlined = false }) => { const theme = useTheme(); return ( { }`} alignItems="center" > - + {outlined ? ( + + ) : ( + + )} {text} ); @@ -42,6 +48,7 @@ const Check = ({ text, variant = "info" }) => { Check.propTypes = { text: PropTypes.string.isRequired, variant: PropTypes.oneOf(["info", "error", "success"]), + outlined: PropTypes.bool, }; export default Check; diff --git a/Client/src/Components/Fallback/index.css b/Client/src/Components/Fallback/index.css new file mode 100644 index 000000000..226c7bc47 --- /dev/null +++ b/Client/src/Components/Fallback/index.css @@ -0,0 +1,28 @@ +[class*="fallback__"] { + width: fit-content; + margin: auto; + margin-top: 100px; +} +[class*="fallback__"] h1.MuiTypography-root { + font-size: var(--env-var-font-size-large); + font-weight: 600; +} +[class*="fallback__"] .check span.MuiTypography-root, +[class*="fallback__"] h1.MuiTypography-root { + color: var(--env-var-color-5); +} +[class*="fallback__"] button.MuiButtonBase-root, +[class*="fallback__"] .check { + width: max-content; +} +[class*="fallback__"] button.MuiButtonBase-root { + height: 34px; +} +[class*="fallback__"] .check span.MuiTypography-root, +[class*="fallback__"] button.MuiButtonBase-root { + font-size: var(--env-var-font-size-medium); +} + +.fallback__status > .MuiStack-root { + margin-left: var(--env-var-spacing-2); +} diff --git a/Client/src/Components/Fallback/index.jsx b/Client/src/Components/Fallback/index.jsx new file mode 100644 index 000000000..943c3aa5e --- /dev/null +++ b/Client/src/Components/Fallback/index.jsx @@ -0,0 +1,60 @@ +import PropTypes from "prop-types"; +import { useTheme } from "@emotion/react"; +import { Stack, Typography } from "@mui/material"; +import Skeleton from "../../assets/Images/create-placeholder.svg?react"; +import Button from "../Button"; +import Check from "../Check/Check"; +import { useNavigate } from "react-router-dom"; +import "./index.css"; + +/** + * Fallback component to display a fallback UI with a title, a list of checks, and a navigation button. + * + * @param {Object} props - The component props. + * @param {string} props.title - The title to be displayed in the fallback UI. + * @param {Array} props.checks - An array of strings representing the checks to display. + * @param {string} [props.link="/"] - The link to navigate to. + * + * @returns {JSX.Element} The rendered fallback UI. + */ + +const Fallback = ({ title, checks, link = "/" }) => { + const theme = useTheme(); + const navigate = useNavigate(); + + return ( + + + + + A {title} is used to: + + {checks.map((check, index) => ( + + ))} + + +
+
); }; diff --git a/Client/src/Pages/Status/index.jsx b/Client/src/Pages/Status/index.jsx index f0cf82b60..071948576 100644 --- a/Client/src/Pages/Status/index.jsx +++ b/Client/src/Pages/Status/index.jsx @@ -1,7 +1,26 @@ -import React from "react"; +import Fallback from "../../Components/Fallback"; +import { useTheme } from "@emotion/react"; const Status = () => { - return
Status
; + const theme = useTheme(); + + return ( +
+ +
+ ); }; export default Status; diff --git a/Client/src/assets/Images/create-placeholder.svg b/Client/src/assets/Images/create-placeholder.svg new file mode 100644 index 000000000..bdd9ef945 --- /dev/null +++ b/Client/src/assets/Images/create-placeholder.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Client/src/assets/icons/check-outlined.svg b/Client/src/assets/icons/check-outlined.svg new file mode 100644 index 000000000..d26657863 --- /dev/null +++ b/Client/src/assets/icons/check-outlined.svg @@ -0,0 +1,3 @@ + + + diff --git a/README.md b/README.md index 16d5232c9..eae2d9e8c 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ You can see the designs [here](https://www.figma.com/design/RPSfaw66HjzSwzntKcgD - POST [/api/v1/auth/register](#post-register) - POST [/api/v1/auth/login](#post-login) - POST [/api/v1/auth/user/{userId}](#post-auth-user-edit-id) + - GET [/api/v1/auth/users](#get-all-users-id) - POST [/api/v1/auth/recovery/request](#post-auth-recovery-request-id) - POST [/api/v1/auth/recovery/validate](#post-auth-recovery-validate-id) - POST [/api/v1/auth/recovery/reset](#post-auth-recovery-reset-id) @@ -453,6 +454,55 @@ curl --request POST \ +
+GET/api/v1/auth/users + +###### Method/Headers + +> | Method/Headers | Value | +> | -------------- | ----- | +> | Method | GET | + +###### Response Payload + +> | Type | Notes | +> | ------------- | ------------------------------------- | +> | `Array` | Returns an array containing all users | + +##### Sample CURL request + +``` +curl --request GET \ + --url http://localhost:5000/api/v1/auth/users \ + --header 'Authorization: \ + --header 'User-Agent: insomnia/2023.5.8' +``` + +##### Sample Resonse + +```json +{ + "success": true, + "msg": "Got all users", + "data": [ + { + "_id": "669e90072d5663d25808bc7b", + "firstName": "Alex", + "lastName": "Holliday", + "email": "test@test.com", + "isActive": true, + "isVerified": false, + "role": ["admin"], + "createdAt": "2024-07-22T16:59:51.695Z", + "updatedAt": "2024-07-22T16:59:51.695Z", + "__v": 0 + } + ] +} +``` + +
+
POST/api/v1/auth/recovery/request diff --git a/Server/controllers/authController.js b/Server/controllers/authController.js index 53fc9c384..28126c0a2 100644 --- a/Server/controllers/authController.js +++ b/Server/controllers/authController.js @@ -428,6 +428,18 @@ const deleteUserController = async (req, res, next) => { } }; +const getAllUsersController = async (req, res) => { + try { + const allUsers = await req.db.getAllUsers(req, res); + res + .status(200) + .json({ success: true, msg: "Got all users", data: allUsers }); + } catch (error) { + error.service = SERVICE_NAME; + next(error); + } +}; + module.exports = { registerController, loginController, @@ -438,4 +450,5 @@ module.exports = { validateRecoveryTokenController, resetPasswordController, deleteUserController, + getAllUsersController, }; diff --git a/Server/db/MongoDB.js b/Server/db/MongoDB.js index 644d446ac..104e49f38 100644 --- a/Server/db/MongoDB.js +++ b/Server/db/MongoDB.js @@ -158,6 +158,17 @@ const deleteUser = async (req, res) => { } }; +const getAllUsers = async (req, res) => { + try { + const users = await UserModel.find() + .select("-password") + .select("-profileImage"); + return users; + } catch (error) { + throw error; + } +}; + const requestInviteToken = async (req, res) => { try { await InviteToken.deleteMany({ email: req.body.email }); @@ -680,6 +691,7 @@ module.exports = { getUserByEmail, updateUser, deleteUser, + getAllUsers, requestInviteToken, requestRecoveryToken, validateRecoveryToken, diff --git a/Server/middleware/verifyAdmin.js b/Server/middleware/verifyAdmin.js new file mode 100644 index 000000000..7ef123451 --- /dev/null +++ b/Server/middleware/verifyAdmin.js @@ -0,0 +1,57 @@ +const jwt = require("jsonwebtoken"); +const logger = require("../utils/logger"); +const SERVICE_NAME = "verifyAdmin"; +const TOKEN_PREFIX = "Bearer "; +const { errorMessages } = require("../utils/messages"); +/** + * Verifies the JWT token + * @function + * @param {express.Request} req + * @param {express.Response} res + * @param {express.NextFunction} next + * @returns {express.Response} + */ +const verifyAdmin = (req, res, next) => { + const token = req.headers["authorization"]; + // Make sure a token is provided + if (!token) { + const error = new Error(errorMessages.NO_AUTH_TOKEN); + error.status = 401; + error.service = SERVICE_NAME; + next(error); + return; + } + // Make sure it is properly formatted + if (!token.startsWith(TOKEN_PREFIX)) { + const error = new Error(errorMessages.INVALID_AUTH_TOKEN); // Instantiate a new Error object for improperly formatted token + error.status = 400; + error.service = SERVICE_NAME; + next(error); + return; + } + + const parsedToken = token.slice(TOKEN_PREFIX.length, token.length); + // verify admin role is present + jwt.verify(parsedToken, process.env.JWT_SECRET, (err, decoded) => { + if (err) { + logger.error(errorMessages.INVALID_AUTH_TOKEN, { + service: SERVICE_NAME, + }); + return res + .status(401) + .json({ success: false, msg: errorMessages.INVALID_AUTH_TOKEN }); + } + + if (decoded.role.includes("admin") === false) { + logger.error(errorMessages.INVALID_AUTH_TOKEN, { + service: SERVICE_NAME, + }); + return res + .status(401) + .json({ success: false, msg: errorMessages.UNAUTHORIZED }); + } + next(); + }); +}; + +module.exports = { verifyAdmin }; diff --git a/Server/routes/authRoute.js b/Server/routes/authRoute.js index 219032e26..02a1a67dd 100644 --- a/Server/routes/authRoute.js +++ b/Server/routes/authRoute.js @@ -1,5 +1,6 @@ const router = require("express").Router(); const { verifyJWT } = require("../middleware/verifyJWT"); +const { verifyAdmin } = require("../middleware/verifyAdmin"); const { verifyOwnership } = require("../middleware/verifyOwnership"); const multer = require("multer"); const upload = multer(); @@ -13,6 +14,7 @@ const { validateRecoveryTokenController, resetPasswordController, checkAdminController, + getAllUsersController, deleteUserController, inviteController, } = require("../controllers/authController"); @@ -27,6 +29,7 @@ router.post( userEditController ); router.get("/users/admin", checkAdminController); +router.get("/users", verifyJWT, verifyAdmin, getAllUsersController); router.delete( "/user/:userId", verifyJWT, diff --git a/Server/utils/messages.js b/Server/utils/messages.js index 787adc8f6..9ef5c6938 100644 --- a/Server/utils/messages.js +++ b/Server/utils/messages.js @@ -2,6 +2,7 @@ const errorMessages = { // General Errors: FRIENDLY_ERROR: "Something went wrong...", UNKNOWN_ERROR: "An unknown error occurred", + // Auth Controller UNAUTHORIZED: "Unauthorized access", AUTH_ADMIN_EXISTS: "Admin already exists",