mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-01-14 21:59:42 -06:00
Merge remote-tracking branch 'upstream/develop' into feat/incidents
This commit is contained in:
@@ -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 = (
|
||||
<Pagination
|
||||
count={Math.ceil(data.rows.length / rowsPerPage)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import "./check.css";
|
||||
import PropTypes from "prop-types";
|
||||
import CheckGrey from "../../assets/icons/check.svg?react";
|
||||
import CheckOutlined from "../../assets/icons/check-outlined.svg?react";
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
@@ -11,6 +12,7 @@ import { useTheme } from "@emotion/react";
|
||||
* @param {Object} props - The properties that define the `Check` component.
|
||||
* @param {string} props.text - The text to be displayed as the label next to the check icon.
|
||||
* @param {'info' | 'error' | 'success'} [props.variant='info'] - The variant of the check component, affecting its styling.
|
||||
* @param {boolean} [props.outlined] - Whether the check icon should be outlined or not.
|
||||
*
|
||||
* @example
|
||||
* // To use this component, import it and use it in your JSX like this:
|
||||
@@ -18,12 +20,12 @@ import { useTheme } from "@emotion/react";
|
||||
*
|
||||
* @returns {React.Element} The `Check` component with a check icon and a label, defined by the `text` prop.
|
||||
*/
|
||||
const Check = ({ text, variant = "info" }) => {
|
||||
const Check = ({ text, variant = "info", outlined = false }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.gap.small}
|
||||
gap={outlined ? theme.gap.medium : theme.gap.small}
|
||||
className={`check${
|
||||
variant === "error"
|
||||
? " check-error"
|
||||
@@ -33,7 +35,11 @@ const Check = ({ text, variant = "info" }) => {
|
||||
}`}
|
||||
alignItems="center"
|
||||
>
|
||||
<CheckGrey alt="form checks" />
|
||||
{outlined ? (
|
||||
<CheckOutlined alt="check" />
|
||||
) : (
|
||||
<CheckGrey alt="form checks" />
|
||||
)}
|
||||
<Typography component="span">{text}</Typography>
|
||||
</Stack>
|
||||
);
|
||||
@@ -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;
|
||||
|
||||
28
Client/src/Components/Fallback/index.css
Normal file
28
Client/src/Components/Fallback/index.css
Normal file
@@ -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);
|
||||
}
|
||||
60
Client/src/Components/Fallback/index.jsx
Normal file
60
Client/src/Components/Fallback/index.jsx
Normal file
@@ -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<string>} 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 (
|
||||
<Stack
|
||||
className={`fallback__${title.trim().split(" ")[0]}`}
|
||||
alignItems="center"
|
||||
gap={theme.gap.xl}
|
||||
>
|
||||
<Skeleton />
|
||||
<Stack gap={theme.gap.small}>
|
||||
<Typography component="h1" marginY={theme.gap.medium}>
|
||||
A {title} is used to:
|
||||
</Typography>
|
||||
{checks.map((check, index) => (
|
||||
<Check
|
||||
text={check}
|
||||
key={`${title.trim().split(" ")[0]}-${index}`}
|
||||
outlined={true}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
<Button
|
||||
level="primary"
|
||||
label={`Let's create your ${title}`}
|
||||
sx={{ alignSelf: "center" }}
|
||||
onClick={() => navigate(link)}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
Fallback.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
checks: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
link: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Fallback;
|
||||
@@ -1,37 +1,25 @@
|
||||
import React from "react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import Fallback from "../../Components/Fallback";
|
||||
import "./index.css";
|
||||
import WindowFrame from "./../../assets/Images/maintenance_window_frame.svg";
|
||||
import { Button } from "@mui/material";
|
||||
import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline";
|
||||
|
||||
const Maintenance = () => {
|
||||
const maintenanceItems = [
|
||||
{ id: 1, text: "Mark your maintenance periods" },
|
||||
{ id: 2, text: "Eliminate any misunderstandings" },
|
||||
{ id: 3, text: "Stop sending alerts in maintenance windows" },
|
||||
];
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<div className="maintenance-checklist-main">
|
||||
<img className="maintenance-image" src={WindowFrame} alt="WindowFrame" />
|
||||
<div className="maintenance-title">Create a maintenance window to</div>
|
||||
<div>
|
||||
{maintenanceItems.map((item) => (
|
||||
<div key={item.id} className="checklist-item">
|
||||
<CheckCircleOutlineIcon color="primary" />
|
||||
<div className="checklist-item-text">{item.text}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
className="maintenance-checklist-button"
|
||||
sx={{ textTransform: "none" }}
|
||||
>
|
||||
Let's create your maintenance window
|
||||
</Button>
|
||||
<div
|
||||
className="maintenance"
|
||||
style={{
|
||||
padding: `${theme.content.pY} ${theme.content.pX}`,
|
||||
}}
|
||||
>
|
||||
<Fallback
|
||||
title="maintenance window"
|
||||
checks={[
|
||||
"Mark your maintenance periods",
|
||||
"Eliminate any misunderstandings",
|
||||
"Stop sending alerts in maintenance windows",
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,26 @@
|
||||
import React from "react";
|
||||
import Fallback from "../../Components/Fallback";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
const Status = () => {
|
||||
return <div>Status</div>;
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="status"
|
||||
style={{
|
||||
padding: `${theme.content.pY} ${theme.content.pX}`,
|
||||
}}
|
||||
>
|
||||
<Fallback
|
||||
title="status page"
|
||||
checks={[
|
||||
"Share your uptime publicly",
|
||||
"Keep your users informed about incidents",
|
||||
"Build trust with your customers",
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Status;
|
||||
|
||||
29
Client/src/assets/Images/create-placeholder.svg
Normal file
29
Client/src/assets/Images/create-placeholder.svg
Normal file
@@ -0,0 +1,29 @@
|
||||
<svg width="256" height="170" viewBox="0 0 256 170" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="0.390244" y="0.390244" width="255.22" height="168.585" rx="7.41463" fill="white"/>
|
||||
<rect x="0.390244" y="0.390244" width="255.22" height="168.585" rx="7.41463" stroke="#EBEBEB" stroke-width="0.780488"/>
|
||||
<rect x="17.9512" y="16.3901" width="220.098" height="54.6341" rx="3.12195" fill="#EFF2FF"/>
|
||||
<rect x="17.9512" y="78.8293" width="85.0732" height="9.36585" rx="3.12195" fill="#D9D9D9"/>
|
||||
<rect x="17.9512" y="93.6584" width="220.098" height="9.36585" rx="3.12195" fill="#F3F3F3"/>
|
||||
<rect x="17.9512" y="108.488" width="220.098" height="9.36585" rx="3.12195" fill="#F3F3F3"/>
|
||||
<g filter="url(#filter0_d_28_7483)">
|
||||
<g clip-path="url(#clip0_28_7483)">
|
||||
<rect x="174.049" y="129.561" width="64" height="20.2927" rx="3.12195" fill="#E0F2FE"/>
|
||||
</g>
|
||||
<rect x="174.439" y="129.951" width="63.2195" height="19.5122" rx="2.73171" stroke="#D5D9EB" stroke-width="0.780488"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_28_7483" x="172.049" y="128.561" width="68" height="24.2927" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1"/>
|
||||
<feGaussianBlur stdDeviation="1"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.0627451 0 0 0 0 0.0941176 0 0 0 0 0.156863 0 0 0 0.05 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_28_7483"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_28_7483" result="shape"/>
|
||||
</filter>
|
||||
<clipPath id="clip0_28_7483">
|
||||
<rect x="174.049" y="129.561" width="64" height="20.2927" rx="3.12195" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
3
Client/src/assets/icons/check-outlined.svg
Normal file
3
Client/src/assets/icons/check-outlined.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.24935 10.0001L8.74935 12.5001L13.7493 7.50008M18.3327 10.0001C18.3327 14.6025 14.6017 18.3334 9.99935 18.3334C5.39698 18.3334 1.66602 14.6025 1.66602 10.0001C1.66602 5.39771 5.39698 1.66675 9.99935 1.66675C14.6017 1.66675 18.3327 5.39771 18.3327 10.0001Z" stroke="#1570EF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 454 B |
50
README.md
50
README.md
@@ -42,6 +42,7 @@ You can see the designs [here](https://www.figma.com/design/RPSfaw66HjzSwzntKcgD
|
||||
- <code>POST</code> [/api/v1/auth/register](#post-register)
|
||||
- <code>POST</code> [/api/v1/auth/login](#post-login)
|
||||
- <code>POST</code> [/api/v1/auth/user/{userId}](#post-auth-user-edit-id)
|
||||
- <code>GET</code> [/api/v1/auth/users](#get-all-users-id)
|
||||
- <code>POST</code> [/api/v1/auth/recovery/request](#post-auth-recovery-request-id)
|
||||
- <code>POST</code> [/api/v1/auth/recovery/validate](#post-auth-recovery-validate-id)
|
||||
- <code>POST</code> [/api/v1/auth/recovery/reset](#post-auth-recovery-reset-id)
|
||||
@@ -453,6 +454,55 @@ curl --request POST \
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary id='#get-all-users-id'><code>GET</code><b>/api/v1/auth/users</b></summary>
|
||||
|
||||
###### Method/Headers
|
||||
|
||||
> | Method/Headers | Value |
|
||||
> | -------------- | ----- |
|
||||
> | Method | GET |
|
||||
|
||||
###### Response Payload
|
||||
|
||||
> | Type | Notes |
|
||||
> | ------------- | ------------------------------------- |
|
||||
> | `Array<User>` | Returns an array containing all users |
|
||||
|
||||
##### Sample CURL request
|
||||
|
||||
```
|
||||
curl --request GET \
|
||||
--url http://localhost:5000/api/v1/auth/users \
|
||||
--header 'Authorization: <bearer_token>\
|
||||
--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
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary id='post-auth-recovery-request-id'><code>POST</code><b>/api/v1/auth/recovery/request</b></summary>
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
57
Server/middleware/verifyAdmin.js
Normal file
57
Server/middleware/verifyAdmin.js
Normal file
@@ -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 };
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user