mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-05-21 00:48:45 -05:00
Merge pull request #123 from bluewave-labs/feat/forgot-password
Feat/forgot password, resolves #121
This commit is contained in:
+1
-1
@@ -34,7 +34,7 @@ function App() {
|
||||
<Route path="/playground" element={<PlayGround />} />
|
||||
<Route path="/forgot-password" element={<ForgotPassword />} />
|
||||
<Route path="/check-email" element={<CheckEmail />} />
|
||||
<Route path="/set-new-password" element={<SetNewPassword />} />
|
||||
<Route path="/set-new-password/:token" element={<SetNewPassword />} />
|
||||
<Route
|
||||
path="/new-password-confirmed"
|
||||
element={<NewPasswordConfirmed />}
|
||||
|
||||
@@ -6,8 +6,10 @@ import PasswordTextField from "../../Components/TextFields/Password/PasswordText
|
||||
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";
|
||||
|
||||
const SetNewPassword = () => {
|
||||
const { token } = useParams();
|
||||
return (
|
||||
<div className="set-new-password-page">
|
||||
<BackgroundPattern />
|
||||
|
||||
@@ -27,6 +27,9 @@ BlueWave uptime monitoring application
|
||||
- <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>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)
|
||||
###### Monitors
|
||||
- <code>GET</code> [/api/v1/monitors](#get-monitors)
|
||||
- <code>GET</code> [/api/v1/monitor/{id}](#get-monitor-id)
|
||||
@@ -80,6 +83,7 @@ Configure the server with the following environmental variables:
|
||||
|
||||
| ENV Variable Name | Required/Optional | Type | Description | Accepted Values |
|
||||
| -------------------- | ----------------- | --------- | ------------------------------------------------------------------------------------------- | ------------------- |
|
||||
| CLIENT_HOST | Required | `string` | Frontend Host | |
|
||||
| JWT_SECRET | Required | `string` | JWT secret | |
|
||||
| DB_TYPE | Optional | `string` | Specify DB to use | `MongoDB \| FakeDB` |
|
||||
| DB_CONNECTION_STRING | Required | `string` | Specifies URL for MongoDB Database | |
|
||||
@@ -382,6 +386,166 @@ curl --request POST \
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary id='post-auth-recovery-request-id'><code>POST</code><b>/api/v1/auth/recovery/request</b></summary>
|
||||
|
||||
###### Method/Headers
|
||||
|
||||
> | Method/Headers | Value |
|
||||
> | -------------- | ----- |
|
||||
> | Method | POST |
|
||||
|
||||
##### Body
|
||||
|
||||
> | Name | Type | Notes |
|
||||
> | ----- | -------- | ------------ |
|
||||
> | email | `string` | User's email |
|
||||
|
||||
###### Response Payload
|
||||
|
||||
> | Type | Notes |
|
||||
> | --------------- | --------------------------------------- |
|
||||
> | `RecoveryToken` | Returns a recovery token if email found |
|
||||
|
||||
##### Sample CURL request
|
||||
|
||||
```
|
||||
curl --request POST \
|
||||
--url http://localhost:5000/api/v1/auth/recovery/request \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"email" : "ajhollid@gmail.com"
|
||||
}'
|
||||
```
|
||||
|
||||
###### Sample Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"msg": "Created recovery token",
|
||||
"data": {
|
||||
"email": "your_email@gmail.com",
|
||||
"token": "f519da5e4a9be40cfc3c0fde97e60c0e6d17bdaa613f5ba537a45073f3865193",
|
||||
"_id": "6668878263587f30748e968e",
|
||||
"expiry": "2024-06-11T17:21:06.984Z",
|
||||
"createdAt": "2024-06-11T17:21:06.985Z",
|
||||
"updatedAt": "2024-06-11T17:21:06.985Z",
|
||||
"__v": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary id='post-auth-recovery-validate-id'><code>POST</code><b>/api/v1/auth/recovery/validate</b></summary>
|
||||
|
||||
###### Method/Headers
|
||||
|
||||
> | Method/Headers | Value |
|
||||
> | -------------- | ----- |
|
||||
> | Method | POST |
|
||||
|
||||
##### Body
|
||||
|
||||
> | Name | Type | Notes |
|
||||
> | ------------- | -------- | ----------------------------------- |
|
||||
> | recoveryToken | `string` | Token issued in `/recovery/request` |
|
||||
|
||||
###### Response Payload
|
||||
|
||||
> | Type | Notes |
|
||||
> | --------------- | -------------------------- |
|
||||
> | `RecoveryToken` | Returns the recovery token |
|
||||
|
||||
##### Sample CURL request
|
||||
|
||||
```
|
||||
curl --request POST \
|
||||
--url http://localhost:5000/api/v1/auth/recovery/validate \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"recoveryToken" : "f519da5e4a9be40cfc3c0fde97e60c0e6d17bdaa613f5ba537a45073f3865193"
|
||||
}'
|
||||
```
|
||||
|
||||
###### Sample Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"msg": "Token is valid",
|
||||
"data": {
|
||||
"_id": "6668894263587f30748e969a",
|
||||
"email": "ajhollid@gmail.com",
|
||||
"token": "457d9926b24dedf613f120eeb524ef00ac45b3f0fc5c70bd25b1cc8aa83a64a0",
|
||||
"expiry": "2024-06-11T17:28:34.349Z",
|
||||
"createdAt": "2024-06-11T17:28:34.349Z",
|
||||
"updatedAt": "2024-06-11T17:28:34.349Z",
|
||||
"__v": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary id='post-auth-recovery-reset-id'><code>POST</code><b>/api/v1/auth/recovery/reset</b></summary>
|
||||
|
||||
###### Method/Headers
|
||||
|
||||
> | Method/Headers | Value |
|
||||
> | -------------- | ----- |
|
||||
> | Method | POST |
|
||||
|
||||
##### Body
|
||||
|
||||
> | Name | Type | Notes |
|
||||
> | ------------- | -------- | --------------------------------------------- |
|
||||
> | recoveryToken | `string` | Token issued returned by `/recovery/validate` |
|
||||
> | password | `string` | User's new password` |
|
||||
|
||||
###### Response Payload
|
||||
|
||||
> | Type | Notes |
|
||||
> | ------ | ------------------------ |
|
||||
> | `User` | Returns the updated user |
|
||||
|
||||
##### Sample CURL request
|
||||
|
||||
```
|
||||
curl --request POST \
|
||||
--url http://localhost:5000/api/v1/auth/recovery/reset \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"recoveryToken" : "f519da5e4a9be40cfc3c0fde97e60c0e6d17bdaa613f5ba537a45073f3865193",
|
||||
"password": "testtest"
|
||||
}'
|
||||
```
|
||||
|
||||
###### Sample Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"msg": "Password reset",
|
||||
"data": {
|
||||
"_id": "66675891cb17336d84c25d9f",
|
||||
"firstname": "User First Name",
|
||||
"lastname": "User Last Name",
|
||||
"email": "your_email@gmail.com",
|
||||
"isActive": true,
|
||||
"isVerified": false,
|
||||
"createdAt": "2024-06-10T19:48:33.863Z",
|
||||
"updatedAt": "2024-06-11T17:21:22.289Z",
|
||||
"__v": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
###### Monitors
|
||||
|
||||
@@ -87,7 +87,6 @@ const registerController = async (req, res, next) => {
|
||||
* @param {Express.Request} req
|
||||
* @param {Express.Response} res
|
||||
* @returns {Promise<Express.Response>}
|
||||
* @throws {Error}
|
||||
*/
|
||||
const loginController = async (req, res, next) => {
|
||||
try {
|
||||
@@ -97,10 +96,6 @@ const loginController = async (req, res, next) => {
|
||||
// Check if user exists
|
||||
const user = await req.db.getUserByEmail(req, res);
|
||||
|
||||
// If user not found, throw an error
|
||||
if (!user) {
|
||||
throw new Error("User not found!");
|
||||
}
|
||||
// Compare password
|
||||
const match = await user.comparePassword(req.body.password);
|
||||
if (!match) {
|
||||
@@ -145,7 +140,7 @@ const userEditController = async (req, res, next) => {
|
||||
|
||||
try {
|
||||
const updatedUser = await req.db.updateUser(req, res);
|
||||
res
|
||||
return res
|
||||
.status(200)
|
||||
.json({ success: true, msg: "User updated", data: updatedUser });
|
||||
} catch (error) {
|
||||
@@ -155,4 +150,87 @@ const userEditController = async (req, res, next) => {
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { registerController, loginController, userEditController };
|
||||
/**
|
||||
* Returns a recovery token
|
||||
* @async
|
||||
* @param {Express.Request} req
|
||||
* @property {Object} req.body
|
||||
* @property {string} req.body.email
|
||||
* @param {Express.Response} res
|
||||
* @returns {Promise<Express.Response>}
|
||||
*/
|
||||
const recoveryRequestController = async (req, res, next) => {
|
||||
try {
|
||||
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>`,
|
||||
`Recovery token: ${recoveryToken.token}`
|
||||
);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: "Created recovery token",
|
||||
});
|
||||
}
|
||||
// TODO Email token to user
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a recovery token
|
||||
* @async
|
||||
* @param {Express.Request} req
|
||||
* @property {Object} req.body
|
||||
* @property {string} req.body.token
|
||||
* @param {Express.Response} res
|
||||
* @returns {Promise<Express.Response>}
|
||||
*/
|
||||
const validateRecoveryTokenController = async (req, res, next) => {
|
||||
try {
|
||||
const recoveryToken = await req.db.validateRecoveryToken(req, res);
|
||||
// TODO Redirect user to reset password after validating token
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: "Token is valid",
|
||||
});
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns an updated user
|
||||
* @async
|
||||
* @param {Express.Request} req
|
||||
* @property {Object} req.body
|
||||
* @property {string} req.body.token
|
||||
* @property {string} req.body.password
|
||||
* @param {Express.Response} res
|
||||
* @returns {Promise<Express.Response>}
|
||||
*/
|
||||
const resetPasswordController = async (req, res, next) => {
|
||||
try {
|
||||
user = await req.db.resetPassword(req, res);
|
||||
res.status(200).json({ success: true, msg: "Password reset", data: user });
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
module.exports = {
|
||||
registerController,
|
||||
loginController,
|
||||
userEditController,
|
||||
recoveryRequestController,
|
||||
validateRecoveryTokenController,
|
||||
resetPasswordController,
|
||||
};
|
||||
|
||||
+74
-6
@@ -3,10 +3,8 @@ const mongoose = require("mongoose");
|
||||
const UserModel = require("../models/user");
|
||||
const Check = require("../models/Check");
|
||||
const Alert = require("../models/Alert");
|
||||
|
||||
const verifyId = (userId, monitorId) => {
|
||||
return userId.toString() === monitorId.toString();
|
||||
};
|
||||
const RecoveryToken = require("../models/RecoveryToken");
|
||||
const crypto = require("crypto");
|
||||
|
||||
const connect = async () => {
|
||||
try {
|
||||
@@ -44,6 +42,7 @@ const insertUser = async (req, res) => {
|
||||
* Get User by Email
|
||||
* Gets a user by Email. Not sure if we'll ever need this except for login.
|
||||
* If not needed except for login, we can move password comparison here
|
||||
* Throws error if user not found
|
||||
* @async
|
||||
* @param {Express.Request} req
|
||||
* @param {Express.Response} res
|
||||
@@ -52,10 +51,14 @@ const insertUser = async (req, res) => {
|
||||
*/
|
||||
const getUserByEmail = async (req, res) => {
|
||||
try {
|
||||
// Returns null if no user is found
|
||||
// Need the password to be able to compare, removed .select()
|
||||
// We can strip the hash before returing the user
|
||||
return await UserModel.findOne({ email: req.body.email });
|
||||
const user = await UserModel.findOne({ email: req.body.email });
|
||||
if (user) {
|
||||
return user;
|
||||
} else {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
@@ -86,6 +89,68 @@ const updateUser = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Request a recovery token
|
||||
* @async
|
||||
* @param {Express.Request} req
|
||||
* @param {Express.Response} res
|
||||
* @returns {Promise<UserModel>}
|
||||
* @throws {Error}
|
||||
*/
|
||||
const requestRecoveryToken = async (req, res) => {
|
||||
try {
|
||||
// Delete any existing tokens
|
||||
await RecoveryToken.deleteMany({ email: req.body.email });
|
||||
let recoveryToken = new RecoveryToken({
|
||||
email: req.body.email,
|
||||
token: crypto.randomBytes(32).toString("hex"),
|
||||
});
|
||||
await recoveryToken.save();
|
||||
return recoveryToken;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const validateRecoveryToken = async (req, res) => {
|
||||
try {
|
||||
const candidateToken = req.body.recoveryToken;
|
||||
const recoveryToken = await RecoveryToken.findOne({
|
||||
token: candidateToken,
|
||||
});
|
||||
if (recoveryToken !== null) {
|
||||
return recoveryToken;
|
||||
} else {
|
||||
throw new Error("Token not found");
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const resetPassword = async (req, res) => {
|
||||
try {
|
||||
const newPassword = req.body.password;
|
||||
|
||||
// Validate token again
|
||||
const recoveryToken = await validateRecoveryToken(req, res);
|
||||
const user = await UserModel.findOne({ email: recoveryToken.email });
|
||||
|
||||
if (user !== null) {
|
||||
user.password = newPassword;
|
||||
await user.save();
|
||||
await RecoveryToken.deleteMany({ email: recoveryToken.email });
|
||||
// Fetch the user again without the password
|
||||
const userWithoutPassword = await UserModel.findOne({
|
||||
email: recoveryToken.email,
|
||||
}).select("-password");
|
||||
return userWithoutPassword;
|
||||
} else {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
//****************************************
|
||||
// Monitors
|
||||
//****************************************
|
||||
@@ -393,6 +458,9 @@ module.exports = {
|
||||
insertUser,
|
||||
getUserByEmail,
|
||||
updateUser,
|
||||
requestRecoveryToken,
|
||||
validateRecoveryToken,
|
||||
resetPassword,
|
||||
getAllMonitors,
|
||||
getMonitorById,
|
||||
getMonitorsByUserId,
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
const RecoveryTokenSchema = mongoose.Schema(
|
||||
{
|
||||
email: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
},
|
||||
token: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
expiry: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
expires: 600,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
}
|
||||
);
|
||||
|
||||
module.exports = mongoose.model("RecoveryToken", RecoveryTokenSchema);
|
||||
@@ -5,10 +5,19 @@ const {
|
||||
registerController,
|
||||
loginController,
|
||||
userEditController,
|
||||
recoveryRequestController,
|
||||
validateRecoveryTokenController,
|
||||
resetPasswordController,
|
||||
} = require("../controllers/authController");
|
||||
|
||||
//Auth routes
|
||||
router.post("/register", registerController);
|
||||
router.post("/login", loginController);
|
||||
router.post("/user/:userId", verifyJWT, userEditController);
|
||||
|
||||
//Recovery routes
|
||||
router.post("/recovery/request", recoveryRequestController);
|
||||
router.post("/recovery/validate", validateRecoveryTokenController);
|
||||
router.post("/recovery/reset/", resetPasswordController);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -14,7 +14,6 @@ class JobQueue {
|
||||
* @throws {Error}
|
||||
*/
|
||||
constructor() {
|
||||
console.log(process.env.REDIS_PORT);
|
||||
this.queue = new Queue(QUEUE_NAME, {
|
||||
connection,
|
||||
});
|
||||
|
||||
+34
-25
@@ -1,7 +1,7 @@
|
||||
const sgMail = require('@sendgrid/mail')
|
||||
const logger = require('./logger')
|
||||
sgMail.setApiKey(process.env.SENDGRID_API_KEY)
|
||||
const SERVICE_NAME = 'Email_Service';
|
||||
const sgMail = require("@sendgrid/mail");
|
||||
const logger = require("./logger");
|
||||
sgMail.setApiKey(process.env.SENDGRID_API_KEY);
|
||||
const SERVICE_NAME = "Email_Service";
|
||||
/**
|
||||
* @async
|
||||
* @function
|
||||
@@ -12,25 +12,34 @@ const SERVICE_NAME = 'Email_Service';
|
||||
* @example
|
||||
* await sendEmail(['veysel@bluewavelabs.ca','alex@bluewavelabs.ca'],'Testing Email Service','<h1>BlueWaveLabs</h1>','Testing Email Service')
|
||||
*/
|
||||
const sendEmail = async (receivers,subject, contentHTML, contentText = null ) => {
|
||||
const msg = {
|
||||
to: receivers,
|
||||
from: {
|
||||
name: 'Uptime System',
|
||||
email: process.env.SYSTEM_EMAIL_ADDRESS // must be verified email by sendgrid
|
||||
},
|
||||
subject: subject,
|
||||
text: contentText || contentHTML,
|
||||
html: contentHTML,
|
||||
}
|
||||
try {
|
||||
await sgMail.send(msg);
|
||||
logger.info(`Emails sent to receivers:${receivers} with the subject:${subject}`,{service:SERVICE_NAME})
|
||||
} catch (error) {
|
||||
logger.error(`Sending Email action failed, ERROR:${error}`, { service: SERVICE_NAME });
|
||||
}
|
||||
}
|
||||
const sendEmail = async (
|
||||
receivers,
|
||||
subject,
|
||||
contentHTML,
|
||||
contentText = null
|
||||
) => {
|
||||
const msg = {
|
||||
to: receivers,
|
||||
from: {
|
||||
name: "Uptime System",
|
||||
email: process.env.SYSTEM_EMAIL_ADDRESS, // must be verified email by sendgrid
|
||||
},
|
||||
subject: subject,
|
||||
text: contentText || contentHTML,
|
||||
html: contentHTML,
|
||||
};
|
||||
try {
|
||||
await sgMail.send(msg);
|
||||
logger.info(
|
||||
`Emails sent to receivers:${receivers} with the subject:${subject}`,
|
||||
{ service: SERVICE_NAME }
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(`Sending Email action failed, ERROR:${error}`, {
|
||||
service: SERVICE_NAME,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
module.exports = {sendEmail}
|
||||
module.exports = { sendEmail };
|
||||
|
||||
Reference in New Issue
Block a user