Merge remote-tracking branch 'upstream/master' into feat/notifications

This commit is contained in:
M M committed 2024-06-12 12:09:04 -07:00
commit 134fe39bd4
8 files changed
+217 -26

No files matched your search

+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={{
-1
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";
+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
+8 -2
View File
@@ -4,6 +4,9 @@ const {
loginValidation,
editUserParamValidation,
editUserBodyValidation,
recoveryValidation,
recoveryTokenValidation,
newPasswordValidation,
} = require("../validation/joi");
const logger = require("../utils/logger");
require("dotenv").config();
@@ -161,13 +164,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 +198,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 +224,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) {
+8 -1
View File
@@ -136,6 +136,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 +153,9 @@ const resetPassword = async (req, res) => {
} else {
throw new Error("User not found");
}
} catch (error) {}
} catch (error) {
throw error;
}
};
//****************************************
+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,