Merge remote-tracking branch 'upstream/master' into feat/notifications
This commit is contained in:
commit
134fe39bd4
8 files changed
+217
-26
No files matched your search
@@ -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,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";
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
//****************************************
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user