Merge branch 'bluewave-labs:master' into monitors
This commit is contained in:
15 files changed
+319
-144
No files matched your search
@@ -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">
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
//****************************************
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
|
||||
@@ -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