Merge branch 'bluewave-labs:master' into monitors

This commit is contained in:
Mohammad Khalilzadeh authored and GitHub committed 2024-06-13 10:47:15 +03:30
commit 67394991b2
15 files changed
+319 -144

No files matched your search

+5 -2
View File
@@ -36,6 +36,7 @@ const levelConfig = {
* @component
* @param {Object} props
* @param {'primary' | 'secondary' | 'tertiary' | 'error' | 'imagePrimary' | 'imageSecondary' | 'imageTertiary'} props.level - The level of the button
* @param {string} props.type - The type of the button
* @param {string} props.label - The label of the button
* @param {React.ReactNode} props.img - Image for button
* @param {boolean} [props.disabled] - Whether the button is disabled
@@ -44,13 +45,14 @@ const levelConfig = {
* @returns {JSX.Element}
* @example
* // Render an error button
* <Button level="error" label="Error" disabled sx={{marginTop: "1rem"}}/>
* <Button type="submit" level="error" label="Error" disabled sx={{marginTop: "1rem"}}/>
*/
const Button = ({ level, label, disabled, img, onClick, sx }) => {
const Button = ({ type, level, label, disabled, img, onClick, sx }) => {
const { variant, color } = levelConfig[level];
return (
<MuiButton
type={type}
variant={variant}
color={color}
disabled={disabled}
@@ -67,6 +69,7 @@ const Button = ({ level, label, disabled, img, onClick, sx }) => {
};
Button.propTypes = {
type: PropTypes.string,
level: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
img: PropTypes.node,
@@ -32,6 +32,7 @@ const EmailTextField = ({
label = "Email",
variant,
placeholder,
autoComplete,
icon,
helperText,
error,
@@ -60,6 +61,7 @@ const EmailTextField = ({
id={id}
variant={variant}
placeholder={placeholder}
autoComplete={autoComplete}
InputProps={{
endAdornment: (
<InputAdornment position="end">
@@ -22,6 +22,7 @@ const PasswordTextField = ({
onChange,
id,
label = "Password",
autoComplete,
variant,
placeholder,
icon,
@@ -48,6 +49,7 @@ const PasswordTextField = ({
<TextField
onChange={onChange}
type="password"
autoComplete={autoComplete}
error={error}
className="password-text-field-input"
id={id}
@@ -19,6 +19,7 @@ import { useTheme } from "@mui/material";
*/
const StringTextField = ({
onChange,
autoComplete,
id,
label,
variant,
@@ -51,6 +52,7 @@ const StringTextField = ({
id={id}
variant={variant}
placeholder={placeholder}
autoComplete={autoComplete}
InputProps={{
endAdornment: (
<InputAdornment position="end">
+57 -1
View File
@@ -5,8 +5,60 @@ import Logomark from "../../assets/Images/key-password.png";
import EmailTextField from "../../Components/TextFields/Email/EmailTextField";
import Button from "../../Components/Button";
import LeftArrow from "../../assets/Images/arrow-left.png";
import { useState, useEffect } from "react";
import { recoveryValidation } from "../../Validation/validation";
import axios from "axios";
import { useNavigate } from "react-router-dom";
const BASE_URL = import.meta.env.VITE_APP_API_BASE_URL;
const ForgotPassword = () => {
const navigate = useNavigate();
const [isLoading, setIsLoading] = useState(false); // Used to disable the button while loading so user doesn't submit multiple times
const [errors, setErrors] = useState({});
const [form, setForm] = useState({
email: "",
});
const idMap = {
"forgot-password-email-input": "email",
};
useEffect(() => {
const { error } = recoveryValidation.validate(form);
if (error === undefined) {
setErrors({});
} else {
const validationErrors = error.details.reduce((acc, err) => {
return { ...acc, [err.path[0]]: err.message };
}, {});
setErrors(validationErrors);
}
}, [form]);
const handleInput = (e) => {
const fieldName = idMap[e.target.id];
setForm({ ...form, [fieldName]: e.target.value });
};
const handleSubmit = async () => {
//TODO show loading spinner
setIsLoading(true);
try {
const { error } = recoveryValidation.validate(form);
if (error !== undefined) {
throw error;
}
await axios.post(`${BASE_URL}/auth/recovery/request`, form);
navigate("/check-email");
} catch (error) {
//TODO display error (Toast?)
alert(error);
} finally {
setIsLoading(false);
}
};
return (
<div className="forgot-password-page">
<BackgroundPattern></BackgroundPattern>
@@ -27,12 +79,16 @@ const ForgotPassword = () => {
<div className="forgot-password-v-gap-large"></div>
<div className="forgot-password-body">
<EmailTextField
error={false}
onChange={handleInput}
error={errors.email !== undefined ? true : false}
helperText={errors.email !== undefined ? errors.email : ""}
placeholder="Enter your email"
id="forgot-password-email-input"
/>
<div className="forgot-password-v-gap-medium"></div>
<Button
disabled={errors.email !== undefined || isLoading === true}
onClick={handleSubmit}
level="primary"
label="Reset password"
sx={{
+8 -14
View File
@@ -44,9 +44,10 @@ const Login = () => {
} else {
setErrors({});
}
}, []);
}, [form]);
const handleSubmit = async () => {
const handleSubmit = async (e) => {
e.preventDefault();
try {
await loginValidation.validateAsync(form, { abortEarly: false });
const action = await dispatch(login(form));
@@ -77,15 +78,6 @@ const Login = () => {
};
const handleInput = (e) => {
const fieldName = idMap[e.target.id];
// Extract and validate individual fields as input changes
const fieldSchema = loginValidation.extract(fieldName);
const { error } = fieldSchema.validate(e.target.value);
let errMsg = "";
if (error) {
errMsg = error.message;
}
setErrors({ ...errors, [fieldName]: errMsg });
const newForm = { ...form, [idMap[e.target.id]]: e.target.value };
setForm(newForm);
};
@@ -93,7 +85,7 @@ const Login = () => {
return (
<div className="login-page">
<BackgroundPattern></BackgroundPattern>
<div className="login-form">
<form className="login-form" onSubmit={handleSubmit}>
<div className="login-form-header">
<img
className="login-form-header-logo"
@@ -110,6 +102,7 @@ const Login = () => {
error={errors.email ? true : false}
helperText={errors.email ? errors.email : ""}
placeholder="Enter your email"
autoComplete="email"
id="login-email-input"
/>
<div className="login-form-v2-spacing" />
@@ -118,6 +111,7 @@ const Login = () => {
error={errors.password ? true : false}
helperText={errors.password ? errors.password : ""}
placeholder="Password"
autoComplete="current-password"
id="login-password-input"
/>
</div>
@@ -129,10 +123,10 @@ const Login = () => {
<div className="login-form-v3-spacing" />
<div className="login-form-actions">
<Button
type="submit"
level="primary"
label="Sign in"
sx={{ width: "100%" }}
onClick={handleSubmit}
/>
<div className="login-form-v-spacing" />
<Button
@@ -147,7 +141,7 @@ const Login = () => {
Don’t have an account?
<span className="new-account-option-span">Sign up</span>
</div>
</div>
</form>
</div>
);
};
+77 -86
View File
@@ -1,7 +1,6 @@
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import axios from "axios";
import "./index.css";
import BackgroundPattern from "../../Components/BackgroundPattern/BackgroundPattern";
import Logomark from "../../assets/Images/Logomark.png";
@@ -53,23 +52,15 @@ const Register = () => {
} else {
setErrors({});
}
}, []);
}, [form]);
const handleInput = (e) => {
const fieldName = idMap[e.target.id];
// Extract and validate individual fields as input changes
const fieldSchema = registerValidation.extract(fieldName);
const { error } = fieldSchema.validate(e.target.value);
let errMsg = "";
if (error) {
errMsg = error.message;
}
setErrors({ ...errors, [fieldName]: errMsg });
const newForm = { ...form, [idMap[e.target.id]]: e.target.value };
setForm(newForm);
};
const handleSubmit = async () => {
const handleSubmit = async (e) => {
e.preventDefault();
try {
await registerValidation.validateAsync(form, { abortEarly: false });
const action = await dispatch(register(form));
@@ -104,82 +95,82 @@ const Register = () => {
return (
<div className="register-page">
<BackgroundPattern></BackgroundPattern>
<form>
<div className="register-form">
<div className="register-form-header">
<img
className="register-form-header-logo"
src={Logomark}
alt="Logomark"
/>
<div className="register-form-v-spacing-large" />
<div className="register-form-heading">Create an account</div>
<div className="register-form-v-spacing-large"></div>
</div>
<div className="register-form-v-spacing-40px" />
<div className="register-form-inputs">
<StringTextField
onChange={handleInput}
error={errors.firstname ? true : false}
helperText={errors.firstname ? errors.firstname : ""}
label="First name*"
placeholder="Enter your first name"
id="register-firstname-input"
/>
<div className="login-form-v2-spacing" />
<StringTextField
onChange={handleInput}
error={errors.lastname ? true : false}
helperText={errors.lastname ? errors.lastname : ""}
label="Last name*"
placeholder="Enter your last name"
id="register-lastname-input"
/>
<div className="login-form-v2-spacing" />
<EmailTextField
onChange={handleInput}
label="Email*"
error={errors.email ? true : false}
helperText={errors.email ? errors.email : ""}
placeholder="Enter your email"
id="register-email-input"
/>
<div className="login-form-v2-spacing" />
<PasswordTextField
onChange={handleInput}
label="Password*"
error={errors.password ? true : false}
helperText={errors.password ? errors.password : ""}
placeholder="Create a password"
id="register-password-input"
/>
</div>
<div className="login-form-v2-spacing" />
<div className="register-form-checks">
<Check text="Must be at least 8 characters" />
<div className="register-form-v-spacing-small"></div>
<Check text="Must contain one special character" />
</div>
<div className="login-form-v2-spacing" />
<div className="register-form-actions">
<Button
onClick={handleSubmit}
level="primary"
label="Get started"
sx={{ width: "100%" }}
/>
<div className="login-form-v-spacing" />
<Button
disabled={true}
level="secondary"
label="Sign up with Google"
sx={{ width: "100%", color: "#344054", fontWeight: "700" }}
img={<img className="google-enter" src={Google} alt="Google" />}
/>
</div>
<form className="register-form" onSubmit={handleSubmit}>
<div className="register-form-header">
<img
className="register-form-header-logo"
src={Logomark}
alt="Logomark"
/>
<div className="register-form-v-spacing-large" />
<div className="register-form-heading">Create an account</div>
<div className="register-form-v-spacing-large"></div>
</div>
<div className="register-form-v-spacing-40px" />
<div className="register-form-inputs">
<StringTextField
onChange={handleInput}
error={errors.firstname ? true : false}
helperText={errors.firstname ? errors.firstname : ""}
label="First name*"
placeholder="Enter your first name"
id="register-firstname-input"
/>
<div className="login-form-v2-spacing" />
<StringTextField
onChange={handleInput}
error={errors.lastname ? true : false}
helperText={errors.lastname ? errors.lastname : ""}
label="Last name*"
placeholder="Enter your last name"
id="register-lastname-input"
/>
<div className="login-form-v2-spacing" />
<EmailTextField
onChange={handleInput}
label="Email*"
error={errors.email ? true : false}
helperText={errors.email ? errors.email : ""}
placeholder="Enter your email"
autoComplete="email"
id="register-email-input"
/>
<div className="login-form-v2-spacing" />
<PasswordTextField
onChange={handleInput}
label="Password*"
error={errors.password ? true : false}
helperText={errors.password ? errors.password : ""}
placeholder="Create a password"
autoComplete="current-password"
id="register-password-input"
/>
</div>
<div className="login-form-v2-spacing" />
<div className="register-form-checks">
<Check text="Must be at least 8 characters" />
<div className="register-form-v-spacing-small"></div>
<Check text="Must contain one special character" />
</div>
<div className="login-form-v2-spacing" />
<div className="register-form-actions">
<Button
type="submit"
level="primary"
label="Get started"
sx={{ width: "100%" }}
/>
<div className="login-form-v-spacing" />
<Button
disabled={true}
level="secondary"
label="Sign up with Google"
sx={{ width: "100%", color: "#344054", fontWeight: "700" }}
img={<img className="google-enter" src={Google} alt="Google" />}
/>
</div>
<div className="register-bottom-spacing"></div>
</form>
<div className="register-bottom-spacing"></div>
</div>
);
};
+66 -5
View File
@@ -1,14 +1,69 @@
import BackgroundPattern from "../../Components/BackgroundPattern/BackgroundPattern";
import "./index.css";
import React from "react";
import LockIcon from "../../assets/Images/lock-icon.png";
import PasswordTextField from "../../Components/TextFields/Password/PasswordTextField";
import Check from "../../Components/Check/Check";
import Button from "../../Components/Button";
import LeftArrow from "../../assets/Images/arrow-left.png";
import { useParams } from "react-router-dom";
import { useState, useEffect } from "react";
import { newPasswordValidation } from "../../Validation/validation";
import axios from "axios";
const BASE_URL = import.meta.env.VITE_APP_API_BASE_URL;
import { useNavigate } from "react-router-dom";
const SetNewPassword = () => {
const navigate = useNavigate();
const [errors, setErrors] = useState({});
const [form, setForm] = useState({});
const [isLoading, setIsLoading] = useState(false);
const idMap = {
"register-password-input": "password",
"confirm-password-input": "confirm",
};
const handleInput = (e) => {
const fieldName = idMap[e.target.id];
setForm({ ...form, [fieldName]: e.target.value });
console.log(errors);
};
const handleSubmit = async () => {
// TODO show loading spinner
setIsLoading(true);
try {
await axios.post(`${BASE_URL}/auth/recovery/validate`, {
recoveryToken: token,
});
await axios.post(`${BASE_URL}/auth/recovery/reset`, {
...form,
recoveryToken: token,
});
navigate("/new-password-confirmed");
} catch (error) {
// TODO display error (Toast?)
alert(error.response.data.msg);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
const { error } = newPasswordValidation.validate(form, {
abortEarly: false,
});
if (error) {
const validationErrors = error.details.reduce((acc, err) => {
return { ...acc, [err.path[0]]: err.message };
}, {});
setErrors(validationErrors);
} else {
setErrors({});
}
}, [form]);
const { token } = useParams();
return (
<div className="set-new-password-page">
@@ -31,16 +86,20 @@ const SetNewPassword = () => {
<div className="set-new-password-form-content">
<PasswordTextField
label="Password"
error={false}
onChange={handleInput}
error={errors.password !== undefined ? true : false}
helperText={errors.password !== undefined ? errors.password : ""}
placeholder="••••••••"
id="register-password-input"
/>
<div className="set-new-password-form-gap-medium"></div>
<PasswordTextField
label="Confirm password"
error={false}
onChange={handleInput}
error={errors.confirm !== undefined ? true : false}
helperText={errors.confirm !== undefined ? errors.confirm : ""}
placeholder="••••••••"
id="register-password-input"
id="confirm-password-input"
/>
<div className="set-new-password-form-gap-medium"></div>
<div className="set-new-password-form-checks">
@@ -50,8 +109,10 @@ const SetNewPassword = () => {
</div>
<div className="set-new-password-form-gap-medium"></div>
<Button
disabled={Object.keys(errors).length !== 0 || isLoading === true}
onClick={handleSubmit}
level="primary"
label="Reset passwprd"
label="Reset password"
sx={{
width: "100%",
fontSize: "13px",
+29 -1
View File
@@ -40,4 +40,32 @@ const loginValidation = joi.object({
}),
});
export { registerValidation, loginValidation };
const recoveryValidation = joi.object({
email: joi
.string()
.email({ tlds: { allow: false } })
.required()
.messages({
"string.email": "Email must be a valid email",
"string.empty": "Email is required",
}),
});
const newPasswordValidation = joi.object({
password: joi.string().min(8).required().messages({
"string.min": "Password must be at least 8 characters",
"string.empty": "Password is required",
}),
confirm: joi.string().valid(joi.ref("password")).min(8).required().messages({
"string.min": "Password must be at least 8 characters",
"string.empty": "Password is required",
"any.only": "Passwords do not match",
}),
});
export {
registerValidation,
loginValidation,
recoveryValidation,
newPasswordValidation,
};
+29 -15
View File
@@ -17,12 +17,14 @@ BlueWave uptime monitoring application
- Clone this repository to your local machine
1. [Installation (Client)](#client)
2. [Installation (Server)](#server)
3. [Configuration(Server)](#config-server)
- [Environment](#environmental-variables)
2. [Configuration(Client)](#config-client)
- [Environment](#env-vars-client)
3. [Installation (Server)](#server)
4. [Configuration(Server)](#config-server)
- [Environment](#env-vars-server)
- [Database](#databases)
- [Docker Images](#docker-images)
4. [Endpoints](#endpoints)
5. [Endpoints](#endpoints)
###### Auth
- <code>POST</code> [/api/v1/auth/register](#post-register)
- <code>POST</code> [/api/v1/auth/login](#post-login)
@@ -48,19 +50,31 @@ BlueWave uptime monitoring application
- <code>GET</code> [/api/v1/alerts/{alertId}](#get-alert-alert-id)
- <code>POST</code> [/api/v1/alerts/edit/{alertId}](#edit-alert)
- <code>POST</code> [/api/v1/alerts/delete/{alertId}](#delete-alert)
5. [Error Handling](#error-handling)
6. [Contributors](#contributors)
6. [Error Handling](#error-handling)
7. [Contributors](#contributors)
---
### Client
#### Installation
#### <u>Installation</u>
1. Change directory to the `Client` directory
2. Install all dependencies by running `npm install`
#### Starting Development Server
<br/>
#### <u>Configuration</u> <a id="config-client"></a>
##### Environmental Variables <a id="env-vars-client"></a>
| ENV Variable Name | Required/Optional | Type | Description | Accepted Values |
| --------------------- | ----------------- | -------- | ------------------ | --------------- |
| VITE_APP_API_BASE_URL | Required | `string` | Base URL of server | {host}/api/v1 |
<br/>
#### <u>Starting Development Server</u>
1. Run `npm run dev` to start the development server.
@@ -68,16 +82,16 @@ BlueWave uptime monitoring application
### Server
#### Installation
#### <u>Installation</u>
1. Change directory to the `Server` directory
2. Install all dependencies by running `npm install`
---
<br/>
#### Configuration <a id="config-server"></a>
#### <u>Configuration</u> <a id="config-server"></a>
##### Environmental Variables
##### Environmental Variables <a id="env-vars-server"></a>
Configure the server with the following environmental variables:
@@ -94,7 +108,7 @@ Configure the server with the following environmental variables:
| REDIS_HOST | Required | `string` | Host address for Redis database | |
| REDIS_PORT | Required | `integer` | Port for Redis database | |
---
<br/>
##### Databases
@@ -133,9 +147,9 @@ From the `Redis` folder run
2. Run the image: `docker run -d -p 6379:6379 -v $(pwd)/../redis/redis_data:/data --name uptime_redis uptime_redis`
</details>
---
<br/>
#### Starting the Development Server
#### <u>Starting the Development Server</u>
1. run `npm run dev` to start the development server
-2
View File
@@ -1,4 +1,3 @@
const express = require("express");
const logger = require("../utils/logger");
require("dotenv").config();
const {
@@ -11,7 +10,6 @@ const {
editAlertBodyValidation,
deleteAlertParamValidation,
} = require("../validation/joi");
const { get } = require("mongoose");
const SERVICE_NAME = "alerts";
+10 -16
View File
@@ -4,6 +4,9 @@ const {
loginValidation,
editUserParamValidation,
editUserBodyValidation,
recoveryValidation,
recoveryTokenValidation,
newPasswordValidation,
} = require("../validation/joi");
const logger = require("../utils/logger");
require("dotenv").config();
@@ -42,18 +45,6 @@ const registerController = async (req, res, next) => {
return;
}
// Check if the user exists
try {
const isUser = await req.db.getUserByEmail(req, res);
if (isUser) {
throw new Error("User already exists");
}
} catch (error) {
error.service = SERVICE_NAME;
next(error);
return;
}
// Create a new user
try {
const newUser = await req.db.insertUser(req, res);
@@ -98,8 +89,8 @@ const loginController = async (req, res, next) => {
// Compare password
const match = await user.comparePassword(req.body.password);
if (!match) {
throw new Error("Password does not match!");
if (match !== true) {
throw new Error("Incorrect password");
}
// Remove password from user object. Should this be abstracted to DB layer?
@@ -161,13 +152,14 @@ const userEditController = async (req, res, next) => {
*/
const recoveryRequestController = async (req, res, next) => {
try {
await recoveryValidation.validateAsync(req.body);
const user = await req.db.getUserByEmail(req, res);
if (user) {
const recoveryToken = await req.db.requestRecoveryToken(req, res);
await sendEmail(
[req.body.email],
"Uptime Monitor Password Recovery",
`<a href='${process.env.CLIENT_HOST}/set-new-password/${recoveryToken.token}'>Click here to reset your password</a>`,
`<a clicktracking="off" href='${process.env.CLIENT_HOST}/set-new-password/${recoveryToken.token}'>Click here to reset your password</a>`,
`Recovery token: ${recoveryToken.token}`
);
return res.status(200).json({
@@ -194,7 +186,8 @@ const recoveryRequestController = async (req, res, next) => {
*/
const validateRecoveryTokenController = async (req, res, next) => {
try {
const recoveryToken = await req.db.validateRecoveryToken(req, res);
await recoveryTokenValidation.validateAsync(req.body);
await req.db.validateRecoveryToken(req, res);
// TODO Redirect user to reset password after validating token
return res.status(200).json({
success: true,
@@ -219,6 +212,7 @@ const validateRecoveryTokenController = async (req, res, next) => {
*/
const resetPasswordController = async (req, res, next) => {
try {
await newPasswordValidation.validateAsync(req.body);
user = await req.db.resetPassword(req, res);
res.status(200).json({ success: true, msg: "Password reset", data: user });
} catch (error) {
+12 -1
View File
@@ -5,6 +5,7 @@ const Check = require("../models/Check");
const Alert = require("../models/Alert");
const RecoveryToken = require("../models/RecoveryToken");
const crypto = require("crypto");
const DUPLICATE_KEY_CODE = 11000; // MongoDB error code for duplicate key
const connect = async () => {
try {
@@ -34,6 +35,9 @@ const insertUser = async (req, res) => {
await newUser.save();
return await UserModel.findOne({ _id: newUser._id }).select("-password"); // .select() doesn't work with create, need to save then find
} catch (error) {
if (error.code === DUPLICATE_KEY_CODE) {
throw new Error("Email already exists");
}
throw error;
}
};
@@ -136,6 +140,11 @@ const resetPassword = async (req, res) => {
const recoveryToken = await validateRecoveryToken(req, res);
const user = await UserModel.findOne({ email: recoveryToken.email });
const match = await user.comparePassword(newPassword);
if (match === true) {
throw new Error("New password must be different from old password");
}
if (user !== null) {
user.password = newPassword;
await user.save();
@@ -148,7 +157,9 @@ const resetPassword = async (req, res) => {
} else {
throw new Error("User not found");
}
} catch (error) {}
} catch (error) {
throw error;
}
};
//****************************************
-1
View File
@@ -4,7 +4,6 @@ const handleErrors = (error, req, res, next) => {
const status = error.status || 500;
const message = error.message || "Something went wrong";
const service = error.service || "Unknown service";
logger.error(error.message, { service: service });
res.status(status).json({ success: false, msg: message });
};
+20
View File
@@ -26,6 +26,23 @@ const editUserBodyValidation = joi.object({
profilePicUrl: joi.string(),
});
const recoveryValidation = joi.object({
email: joi
.string()
.email({ tlds: { allow: false } })
.required(),
});
const recoveryTokenValidation = joi.object({
recoveryToken: joi.string().required(),
});
const newPasswordValidation = joi.object({
recoveryToken: joi.string().required(),
password: joi.string().min(8).required(),
confirm: joi.string(),
});
//****************************************
// Monitors
//****************************************
@@ -120,6 +137,9 @@ const deleteChecksParamValidation = joi.object({
module.exports = {
loginValidation,
registerValidation,
recoveryValidation,
recoveryTokenValidation,
newPasswordValidation,
getMonitorByIdValidation,
getMonitorsByUserIdValidation,
monitorValidation,