From a9584405503317c3d6b4b636a2277f5172bf74c0 Mon Sep 17 00:00:00 2001 From: Shemy Gan Date: Fri, 11 Oct 2024 14:28:09 -0400 Subject: [PATCH 01/15] - Add Dialog component - Create delete all monitors and clear all stats windows using dialog component --- Client/src/Components/Dialog/index.jsx | 80 ++++++++++++++++++++++++++ Client/src/Pages/Settings/index.jsx | 44 ++++++++++++-- 2 files changed, 118 insertions(+), 6 deletions(-) create mode 100644 Client/src/Components/Dialog/index.jsx diff --git a/Client/src/Components/Dialog/index.jsx b/Client/src/Components/Dialog/index.jsx new file mode 100644 index 000000000..a4b3e282d --- /dev/null +++ b/Client/src/Components/Dialog/index.jsx @@ -0,0 +1,80 @@ +import LoadingButton from "@mui/lab/LoadingButton"; +import { Button, Modal, Stack, Typography } from "@mui/material"; +import PropTypes from "prop-types"; +const Dialog = ({ arialabelledby, ariadescribedby, + open, onClose, bdy1, yBtnLbl, yBtnOnClick, nBtnLbl, nBtnOnClick, theme, isLoading, bdy2 }) => { + return + + + {bdy1} + + {bdy2 && + {bdy2} + } + + + + {yBtnLbl} + + + + +} + +Dialog.prototypes = { + arialabelledby: PropTypes.string.isRequired, + ariadescribedby: PropTypes.string.isRequired, + open: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired, + bdy1: PropTypes.string.isRequired, + nBtnLbl: PropTypes.string.isRequired, + nBtnOnClick: PropTypes.func.isRequired, + yBtnLbl: PropTypes.string.isRequired, + yBtnOnClick: PropTypes.func.isRequired, + theme: PropTypes.object.isRequired, + isLoading: PropTypes.bool.isRequired, + bdy2: PropTypes.string +} + +export default Dialog \ No newline at end of file diff --git a/Client/src/Pages/Settings/index.jsx b/Client/src/Pages/Settings/index.jsx index 9aba1c792..cb2e3e6d1 100644 --- a/Client/src/Pages/Settings/index.jsx +++ b/Client/src/Pages/Settings/index.jsx @@ -22,6 +22,7 @@ import { ConfigBox } from "./styled"; import { networkService } from "../../main"; import { settingsValidation } from "../../Validation/validation"; import { useNavigate } from "react-router"; +import Dialog from "../../Components/Dialog" const SECONDS_PER_DAY = 86400; @@ -37,6 +38,7 @@ const Settings = ({ isAdmin }) => { ttl: checkTTL ? (checkTTL / SECONDS_PER_DAY).toString() : 0, }); const [errors, setErrors] = useState({}); + const [isOpen, setIsOpen] = useState({delMntrs: false, delSts: false}); const dispatch = useDispatch(); const navigate = useNavigate(); @@ -118,6 +120,8 @@ const Settings = ({ isAdmin }) => { } catch (error) { logger.error(error); createToast({ body: "Failed to clear stats" }); + } finally { + setIsOpen({delMntrs: false, delSts:false}) } }; @@ -146,6 +150,8 @@ const Settings = ({ isAdmin }) => { } catch (error) { logger.error(error); createToast({ Body: "Failed to delete all monitors" }); + } finally { + setIsOpen({ delMntrs: false, delSts: false }) } }; @@ -202,17 +208,30 @@ const Settings = ({ isAdmin }) => { /> Clear all stats. This is irreversible. - setIsOpen({delSts: true, delMntrs: false})} sx={{ mt: theme.spacing(4) }} > Clear all stats - + + setIsOpen({ delSts: false, delMntrs: false })} + bdy1="Do you want to clear all stats?" + yBtnLbl="Yes, clear all stats" + yBtnOnClick={handleClearStats} + nBtnLbl="Cancel" + nBtnOnClick={() => setIsOpen({ delSts: false, delMntrs: false })} + theme={theme} + isLoading={isLoading || authIsLoading || checksIsLoading} + > + )} {isAdmin && ( @@ -242,13 +261,26 @@ const Settings = ({ isAdmin }) => { variant="contained" color="error" loading={isLoading || authIsLoading || checksIsLoading} - onClick={handleDeleteAllMonitors} + onClick={()=>setIsOpen({delMntrs: true, delSts: false})} sx={{ mt: theme.spacing(4) }} > Remove all monitors + setIsOpen({delMntrs:false, delSts: false})} + bdy1="Do you want to remove all monitors?" + yBtnLbl="Yes, clear all monitors" + yBtnOnClick={handleDeleteAllMonitors} + nBtnLbl="Cancel" + nBtnOnClick={() => setIsOpen({delMntrs:false, delSts: false})} + theme={theme} + > + )} {isAdmin && ( From 7e4ac8eacce83b461c4aadbb19460665e63d86ab Mon Sep 17 00:00:00 2001 From: Shemy Gan Date: Fri, 11 Oct 2024 14:32:47 -0400 Subject: [PATCH 02/15] 901 update --- Client/src/Pages/Settings/index.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/Client/src/Pages/Settings/index.jsx b/Client/src/Pages/Settings/index.jsx index cb2e3e6d1..38d94397a 100644 --- a/Client/src/Pages/Settings/index.jsx +++ b/Client/src/Pages/Settings/index.jsx @@ -279,6 +279,7 @@ const Settings = ({ isAdmin }) => { nBtnLbl="Cancel" nBtnOnClick={() => setIsOpen({delMntrs:false, delSts: false})} theme={theme} + isLoading={isLoading || authIsLoading || checksIsLoading} > From 05c051ae3abd55dc46e5ef6842481f3cef3202a9 Mon Sep 17 00:00:00 2001 From: Rushi Gandhi Date: Tue, 15 Oct 2024 12:20:55 +0530 Subject: [PATCH 03/15] Generate refresh token & add it to response --- Server/controllers/authController.js | 24 ++++++++++++++++++++++-- Server/service/settingsService.js | 2 ++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/Server/controllers/authController.js b/Server/controllers/authController.js index 136a05a7a..bde728e32 100644 --- a/Server/controllers/authController.js +++ b/Server/controllers/authController.js @@ -33,6 +33,24 @@ const issueToken = (payload, appSettings) => { } }; +/** + * Creates and returns a JWT refresh token with an arbitrary payload + * @function + * @param {Object} payload - Here we have passed MongoDB assigned _id. + * @param {Object} appSettings + * @returns {String} + * @throws {Error} + */ +const issueRefreshToken = (payload, appSettings) => { + try { + const tokenTTL = appSettings.refreshTokenTTL ? appSettings.refreshTokenTTL : "7d"; + return jwt.sign({ id: payload }, appSettings.refreshTokenSecret, { expiresIn: tokenTTL }); + } catch (error) { + throw handleError(error, SERVICE_NAME, "issueRefreshToken"); + } +}; + + /** * Registers a new user. If the user is the first account, a JWT secret is created. If not, an invite token is required. * @async @@ -81,6 +99,7 @@ const registerUser = async (req, res, next) => { const appSettings = await req.settingsService.getSettings(); const token = issueToken(userForToken, appSettings); + const refreshToken = issueRefreshToken(userForToken._id, appSettings); req.emailService .buildAndSendEmail( @@ -99,7 +118,7 @@ const registerUser = async (req, res, next) => { return res.status(200).json({ success: true, msg: successMessages.AUTH_CREATE_USER, - data: { user: newUser, token: token }, + data: { user: newUser, token: token, refreshToken: refreshToken }, }); } catch (error) { console.log("ERROR", error); @@ -147,13 +166,14 @@ const loginUser = async (req, res, next) => { // Happy path, return token const appSettings = await req.settingsService.getSettings(); const token = issueToken(userWithoutPassword, appSettings); + const refreshToken = issueRefreshToken(userWithoutPassword._id, appSettings); // reset avatar image userWithoutPassword.avatarImage = user.avatarImage; return res.status(200).json({ success: true, msg: successMessages.AUTH_LOGIN_USER, - data: { user: userWithoutPassword, token: token }, + data: { user: userWithoutPassword, token: token, refreshToken: refreshToken }, }); } catch (error) { next(handleError(error, SERVICE_NAME, "loginController")); diff --git a/Server/service/settingsService.js b/Server/service/settingsService.js index 24e890d64..764c24db3 100644 --- a/Server/service/settingsService.js +++ b/Server/service/settingsService.js @@ -6,11 +6,13 @@ const envConfig = { apiBaseUrl: undefined, clientHost: process.env.CLIENT_HOST, jwtSecret: process.env.JWT_SECRET, + refreshTokenSecret: process.env.REFRESH_TOKEN_SECRET, dbType: process.env.DB_TYPE, dbConnectionString: process.env.DB_CONNECTION_STRING, redisHost: process.env.REDIS_HOST, redisPort: process.env.REDIS_PORT, jwtTTL: process.env.TOKEN_TTL, + refreshTokenTTL: process.env.REFRESH_TOKEN_TTL, pagespeedApiKey: process.env.PAGESPEED_API_KEY, systemEmailHost: process.env.SYSTEM_EMAIL_HOST, systemEmailPort: process.env.SYSTEM_EMAIL_PORT, From 581b6217a992fcf2135ae7f4395f3672c487fb7b Mon Sep 17 00:00:00 2001 From: Rushi Gandhi Date: Tue, 15 Oct 2024 14:11:47 +0530 Subject: [PATCH 04/15] Update appSetting for refresh token & Update documentation --- Server/db/models/AppSettings.js | 12 +++++++++++- guides/users-guide/quickstart.md | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Server/db/models/AppSettings.js b/Server/db/models/AppSettings.js index f95d58f4b..bbd863af8 100644 --- a/Server/db/models/AppSettings.js +++ b/Server/db/models/AppSettings.js @@ -22,6 +22,11 @@ const AppSettingsSchema = mongoose.Schema( required: true, default: "my_secret", }, + refreshTokenSecret: { + type: String, + required: true, + default: "my_refresh_secret", + }, dbType: { type: String, required: true, @@ -44,7 +49,12 @@ const AppSettingsSchema = mongoose.Schema( jwtTTL: { type: String, required: true, - default: "99d", + default: "2h", + }, + refreshTokenTTL: { + type: String, + required: true, + default: "7d", }, pagespeedApiKey: { type: String, diff --git a/guides/users-guide/quickstart.md b/guides/users-guide/quickstart.md index 225f4fdb5..2c469bdaa 100644 --- a/guides/users-guide/quickstart.md +++ b/guides/users-guide/quickstart.md @@ -79,7 +79,7 @@ Make sure you change the directory to the specified directories, as paths in com Configure the server with the following environmental variables: -
ENV Variable NameRequired/OptionalTypeDescriptionAccepted Values
CLIENT_HOSTRequiredstringFrontend Host
JWT_SECRETRequiredstringJWT secret
DB_TYPEOptionalstringSpecify DB to useMongoDB | FakeDB
DB_CONNECTION_STRINGRequiredstringSpecifies URL for MongoDB Database
PORTOptionalintegerSpecifies Port for Server
LOGIN_PAGE_URLRequiredstringLogin url to be used in emailing service
REDIS_HOSTRequiredstringHost address for Redis database
REDIS_PORTRequiredintegerPort for Redis database
TOKEN_TTLOptionalstringTime for token to liveIn vercel/ms format https://github.com/vercel/ms
PAGESPEED_API_KEYOptionalstringAPI Key for PageSpeed requests
SYSTEM_EMAIL_HOSTRequiredstringHost to send System Emails From
SYSTEM_EMAIL_PORTRequirednumberPort for System Email Host
SYSTEM_EMAIL_ADDRESSRequiredstringSystem Email Address
SYSTEM_EMAIL_PASSWORDRequiredstringSystem Email Password
+
ENV Variable NameRequired/OptionalTypeDescriptionAccepted Values
CLIENT_HOSTRequiredstringFrontend Host
JWT_SECRETRequiredstringJWT secret
REFRESH_TOKEN_SECRETRequiredstringRefresh JWT secret
DB_TYPEOptionalstringSpecify DB to useMongoDB | FakeDB
DB_CONNECTION_STRINGRequiredstringSpecifies URL for MongoDB Database
PORTOptionalintegerSpecifies Port for Server
LOGIN_PAGE_URLRequiredstringLogin url to be used in emailing service
REDIS_HOSTRequiredstringHost address for Redis database
REDIS_PORTRequiredintegerPort for Redis database
TOKEN_TTLOptionalstringTime for token to liveIn vercel/ms format https://github.com/vercel/ms
REFRESH_TOKEN_TTLOptionalstringTime for refresh token to live
PAGESPEED_API_KEYOptionalstringAPI Key for PageSpeed requests
SYSTEM_EMAIL_HOSTRequiredstringHost to send System Emails From
SYSTEM_EMAIL_PORTRequirednumberPort for System Email Host
SYSTEM_EMAIL_ADDRESSRequiredstringSystem Email Address
SYSTEM_EMAIL_PASSWORDRequiredstringSystem Email Password
--- From cd0160f421d5fb669a8fad97a501cac3298707da Mon Sep 17 00:00:00 2001 From: Rushi Gandhi Date: Tue, 15 Oct 2024 15:55:29 +0530 Subject: [PATCH 05/15] Pass whole user object instead of just _id --- Server/controllers/authController.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Server/controllers/authController.js b/Server/controllers/authController.js index bde728e32..7eefc6722 100644 --- a/Server/controllers/authController.js +++ b/Server/controllers/authController.js @@ -44,7 +44,7 @@ const issueToken = (payload, appSettings) => { const issueRefreshToken = (payload, appSettings) => { try { const tokenTTL = appSettings.refreshTokenTTL ? appSettings.refreshTokenTTL : "7d"; - return jwt.sign({ id: payload }, appSettings.refreshTokenSecret, { expiresIn: tokenTTL }); + return jwt.sign(payload, appSettings.refreshTokenSecret, { expiresIn: tokenTTL }); } catch (error) { throw handleError(error, SERVICE_NAME, "issueRefreshToken"); } @@ -99,7 +99,7 @@ const registerUser = async (req, res, next) => { const appSettings = await req.settingsService.getSettings(); const token = issueToken(userForToken, appSettings); - const refreshToken = issueRefreshToken(userForToken._id, appSettings); + const refreshToken = issueRefreshToken(userForToken, appSettings); req.emailService .buildAndSendEmail( @@ -166,7 +166,7 @@ const loginUser = async (req, res, next) => { // Happy path, return token const appSettings = await req.settingsService.getSettings(); const token = issueToken(userWithoutPassword, appSettings); - const refreshToken = issueRefreshToken(userWithoutPassword._id, appSettings); + const refreshToken = issueRefreshToken(userWithoutPassword, appSettings); // reset avatar image userWithoutPassword.avatarImage = user.avatarImage; From cbb4dc79581b392e8f4cbf1ef86f52109376cc63 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 15 Oct 2024 21:35:17 +0530 Subject: [PATCH 06/15] Added the latest repo version in about section of Settings page --- Client/src/Pages/Settings/index.jsx | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/Client/src/Pages/Settings/index.jsx b/Client/src/Pages/Settings/index.jsx index db3fe232b..e957d020f 100644 --- a/Client/src/Pages/Settings/index.jsx +++ b/Client/src/Pages/Settings/index.jsx @@ -17,7 +17,7 @@ import PropTypes from "prop-types"; import LoadingButton from "@mui/lab/LoadingButton"; import { setTimezone, setMode } from "../../Features/UI/uiSlice"; import timezones from "../../Utils/timezones.json"; -import { useState } from "react"; +import { useState,useEffect } from "react"; import { ConfigBox } from "./styled"; import { networkService } from "../../main"; import { settingsValidation } from "../../Validation/validation"; @@ -37,10 +37,30 @@ const Settings = ({ isAdmin }) => { const [form, setForm] = useState({ ttl: checkTTL ? (checkTTL / SECONDS_PER_DAY).toString() : 0, }); + const [version,setVersion]=useState(""); const [errors, setErrors] = useState({}); const dispatch = useDispatch(); const navigate = useNavigate(); + //Fetching latest release version from github + useEffect(() => { + const fetchLatestVersion = async () => { + try { + const response = await fetch( + "https://api.github.com/repos/bluewave-labs/bluewave-uptime/releases/latest" + ); + if (!response.ok) { + throw new Error("Failed to fetch latest version"); + } + const data = await response.json(); + setVersion(data.tag_name); // Set the latest version number + } catch (error) { + createToast({ body: error.message || "Error fetching latest version" }); // Set error message + } + }; + fetchLatestVersion(); + }); + const handleChange = (event) => { const { value, id } = event.target; const { error } = settingsValidation.validate( @@ -300,6 +320,7 @@ const Settings = ({ isAdmin }) => { BlueWave Uptime v1.0.0 + Latest Version: {version} @@ -307,8 +328,8 @@ const Settings = ({ isAdmin }) => { From 33dd78defd90e715d652a4730d58853d0f8c7a9d Mon Sep 17 00:00:00 2001 From: navishmonga <141874091+navishmonga@users.noreply.github.com> Date: Tue, 15 Oct 2024 21:44:41 +0530 Subject: [PATCH 07/15] Update index.jsx --- Client/src/Pages/Settings/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Client/src/Pages/Settings/index.jsx b/Client/src/Pages/Settings/index.jsx index e957d020f..34df4b528 100644 --- a/Client/src/Pages/Settings/index.jsx +++ b/Client/src/Pages/Settings/index.jsx @@ -59,7 +59,7 @@ const Settings = ({ isAdmin }) => { } }; fetchLatestVersion(); - }); + },[]); const handleChange = (event) => { const { value, id } = event.target; From 21caca1ffbc9f624d9bc9a68283495f4278083af Mon Sep 17 00:00:00 2001 From: Shemy Gan Date: Tue, 15 Oct 2024 14:16:47 -0400 Subject: [PATCH 08/15] - Update based on review feedbacks --- Client/src/Components/Dialog/index.jsx | 42 +++++----- Client/src/Pages/Monitors/Configure/index.jsx | 77 ++++------------- .../src/Pages/Monitors/Home/actionsMenu.jsx | 83 ++++--------------- Client/src/Pages/Settings/index.jsx | 41 ++++----- 4 files changed, 72 insertions(+), 171 deletions(-) diff --git a/Client/src/Components/Dialog/index.jsx b/Client/src/Components/Dialog/index.jsx index a4b3e282d..0e3bdad54 100644 --- a/Client/src/Components/Dialog/index.jsx +++ b/Client/src/Components/Dialog/index.jsx @@ -1,11 +1,11 @@ import LoadingButton from "@mui/lab/LoadingButton"; import { Button, Modal, Stack, Typography } from "@mui/material"; import PropTypes from "prop-types"; -const Dialog = ({ arialabelledby, ariadescribedby, - open, onClose, bdy1, yBtnLbl, yBtnOnClick, nBtnLbl, nBtnOnClick, theme, isLoading, bdy2 }) => { +const Dialog = ({ modelTitle, modelDescription, + open, onClose, title, confirmationBtnLbl, confirmationBtnOnClick, cancelBtnLbl, cancelBtnOnClick, theme, isLoading, description }) => { return - - {bdy1} + {title} - {bdy2 && - {bdy2} + {description && + {description} } - {nBtnLbl} + {cancelBtnLbl} - {yBtnLbl} + {confirmationBtnLbl} } -Dialog.prototypes = { - arialabelledby: PropTypes.string.isRequired, - ariadescribedby: PropTypes.string.isRequired, +Dialog.PropTypes = { + modelTitle: PropTypes.string.isRequired, + modelDescription: PropTypes.string.isRequired, open: PropTypes.bool.isRequired, onClose: PropTypes.func.isRequired, - bdy1: PropTypes.string.isRequired, - nBtnLbl: PropTypes.string.isRequired, - nBtnOnClick: PropTypes.func.isRequired, - yBtnLbl: PropTypes.string.isRequired, - yBtnOnClick: PropTypes.func.isRequired, + title: PropTypes.string.isRequired, + confirmationBtnLbl: PropTypes.string.isRequired, + confirmationBtnOnClick: PropTypes.func.isRequired, + cancelBtnLbl: PropTypes.string.isRequired, + cancelBtnOnClick: PropTypes.func.isRequired, theme: PropTypes.object.isRequired, isLoading: PropTypes.bool.isRequired, - bdy2: PropTypes.string + description: PropTypes.string } export default Dialog \ No newline at end of file diff --git a/Client/src/Pages/Monitors/Configure/index.jsx b/Client/src/Pages/Monitors/Configure/index.jsx index cd55837a7..d10d6edc3 100644 --- a/Client/src/Pages/Monitors/Configure/index.jsx +++ b/Client/src/Pages/Monitors/Configure/index.jsx @@ -2,7 +2,7 @@ import { useNavigate, useParams } from "react-router"; import { useTheme } from "@emotion/react"; import { useDispatch, useSelector } from "react-redux"; import { useEffect, useState } from "react"; -import { Box, Button, Modal, Stack, Tooltip, Typography } from "@mui/material"; +import { Box, Stack, Tooltip, Typography } from "@mui/material"; import { monitorValidation } from "../../../Validation/validation"; import { createToast } from "../../../Utils/toastUtils"; import { logger } from "../../../Utils/Logger"; @@ -24,6 +24,7 @@ import PulseDot from "../../../Components/Animated/PulseDot"; import SkeletonLayout from "./skeleton"; import LoadingButton from "@mui/lab/LoadingButton"; import "./index.css"; +import Dialog from "../../../Components/Dialog"; /** * Parses a URL string and returns a URL object. @@ -447,71 +448,21 @@ const Configure = () => { )} - setIsOpen(false)} - disablePortal + title="Do you really want to delete this monitor?" + confirmationBtnLbl="Delete" + confirmationBtnOnClick={handleRemove} + cancelBtnLbl="Cancel" + cancelBtnOnClick={() => setIsOpen(false)} + theme={theme} + isLoading={isLoading} + description="Once deleted, this monitor cannot be retrieved." > - - - Do you really want to delete this monitor? - - - Once deleted, this monitor cannot be retrieved. - - - - - Delete - - - - + ); }; diff --git a/Client/src/Pages/Monitors/Home/actionsMenu.jsx b/Client/src/Pages/Monitors/Home/actionsMenu.jsx index d2ccc0b88..fabb74a1b 100644 --- a/Client/src/Pages/Monitors/Home/actionsMenu.jsx +++ b/Client/src/Pages/Monitors/Home/actionsMenu.jsx @@ -5,13 +5,9 @@ import { useNavigate } from "react-router-dom"; import { createToast } from "../../../Utils/toastUtils"; import { logger } from "../../../Utils/Logger"; import { - Button, IconButton, Menu, - MenuItem, - Modal, - Stack, - Typography, + MenuItem } from "@mui/material"; import { deleteUptimeMonitor, @@ -20,6 +16,7 @@ import { } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice"; import Settings from "../../../assets/icons/settings-bold.svg?react"; import PropTypes from "prop-types"; +import Dialog from "../../../Components/Dialog" const ActionsMenu = ({ monitor, isAdmin, updateCallback }) => { const [anchorEl, setAnchorEl] = useState(null); @@ -29,7 +26,7 @@ const ActionsMenu = ({ monitor, isAdmin, updateCallback }) => { const theme = useTheme(); const authState = useSelector((state) => state.auth); const authToken = authState.authToken; - + const { isLoading } = useSelector((state) => state.uptimeMonitors); const handleRemove = async (event) => { event.preventDefault(); @@ -199,69 +196,21 @@ const ActionsMenu = ({ monitor, isAdmin, updateCallback }) => { )} - { - e.stopPropagation(); - setIsOpen(false); - }} + onClose={() => setIsOpen(false)} + title="Do you really want to delete this monitor?" + confirmationBtnLbl="Delete" + confirmationBtnOnClick={e => { e.stopPropagation(); handleRemove(e) }} + cancelBtnLbl="Cancel" + cancelBtnOnClick={e => { e.stopPropagation(); setIsOpen(false) }} + theme={theme} + isLoading={isLoading} + description="Once deleted, this monitor cannot be retrieved." > - - - Do you really want to delete this monitor? - - - Once deleted, this monitor cannot be retrieved. - - - - - - - + ); }; diff --git a/Client/src/Pages/Settings/index.jsx b/Client/src/Pages/Settings/index.jsx index bf7465349..67bca8db0 100644 --- a/Client/src/Pages/Settings/index.jsx +++ b/Client/src/Pages/Settings/index.jsx @@ -39,7 +39,8 @@ const Settings = ({ isAdmin }) => { ttl: checkTTL ? (checkTTL / SECONDS_PER_DAY).toString() : 0, }); const [errors, setErrors] = useState({}); - const [isOpen, setIsOpen] = useState({delMntrs: false, delSts: false}); + const delStatMntrInitState = { delMntrs: false, delSts: false }; + const [isOpen, setIsOpen] = useState(delStatMntrInitState); const dispatch = useDispatch(); const navigate = useNavigate(); @@ -122,7 +123,7 @@ const Settings = ({ isAdmin }) => { logger.error(error); createToast({ body: "Failed to clear stats" }); } finally { - setIsOpen({delMntrs: false, delSts:false}) + setIsOpen(delStatMntrInitState) } }; @@ -152,10 +153,10 @@ const Settings = ({ isAdmin }) => { logger.error(error); createToast({ Body: "Failed to delete all monitors" }); } finally { - setIsOpen({ delMntrs: false, delSts: false }) + setIsOpen(delStatMntrInitState) } }; - + return ( { setIsOpen({ delSts: false, delMntrs: false })} - bdy1="Do you want to clear all stats?" - yBtnLbl="Yes, clear all stats" - yBtnOnClick={handleClearStats} - nBtnLbl="Cancel" - nBtnOnClick={() => setIsOpen({ delSts: false, delMntrs: false })} + onClose={() => setIsOpen(delStatMntrInitState)} + title="Do you want to clear all stats?" + confirmationBtnLbl="Yes, clear all stats" + confirmationBtnOnClick={handleClearStats} + cancelBtnLbl="Cancel" + cancelBtnOnClick={() => setIsOpen(delStatMntrInitState)} theme={theme} isLoading={isLoading || authIsLoading || checksIsLoading} > @@ -290,15 +291,15 @@ const Settings = ({ isAdmin }) => { setIsOpen({delMntrs:false, delSts: false})} - bdy1="Do you want to remove all monitors?" - yBtnLbl="Yes, clear all monitors" - yBtnOnClick={handleDeleteAllMonitors} - nBtnLbl="Cancel" - nBtnOnClick={() => setIsOpen({delMntrs:false, delSts: false})} + onClose={() => setIsOpen(delStatMntrInitState)} + title="Do you want to remove all monitors?" + confirmationBtnLbl="Yes, clear all monitors" + confirmationBtnOnClick={handleDeleteAllMonitors} + cancelBtnLbl="Cancel" + cancelBtnOnClick={() => setIsOpen(delStatMntrInitState)} theme={theme} isLoading={isLoading || authIsLoading || checksIsLoading} > From 94e287658082b424500061f0f335e53cfd138d03 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 16 Oct 2024 12:13:18 +0530 Subject: [PATCH 09/15] Fixed the default version profile and the network calls to be made using NetworkService --- Client/src/Pages/Settings/index.jsx | 16 ++++++++-------- Client/src/Utils/NetworkService.js | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/Client/src/Pages/Settings/index.jsx b/Client/src/Pages/Settings/index.jsx index 34df4b528..2ffec346e 100644 --- a/Client/src/Pages/Settings/index.jsx +++ b/Client/src/Pages/Settings/index.jsx @@ -37,7 +37,7 @@ const Settings = ({ isAdmin }) => { const [form, setForm] = useState({ ttl: checkTTL ? (checkTTL / SECONDS_PER_DAY).toString() : 0, }); - const [version,setVersion]=useState(""); + const [version,setVersion]=useState("unknown"); const [errors, setErrors] = useState({}); const dispatch = useDispatch(); const navigate = useNavigate(); @@ -45,18 +45,18 @@ const Settings = ({ isAdmin }) => { //Fetching latest release version from github useEffect(() => { const fetchLatestVersion = async () => { + let version="unknown"; try { - const response = await fetch( - "https://api.github.com/repos/bluewave-labs/bluewave-uptime/releases/latest" - ); - if (!response.ok) { + const response = await networkService.fetchGithubLatestRelease(); + if (!response.status===200) { throw new Error("Failed to fetch latest version"); } - const data = await response.json(); - setVersion(data.tag_name); // Set the latest version number + version=response.data.tag_name; } catch (error) { createToast({ body: error.message || "Error fetching latest version" }); // Set error message - } + } finally{ + setVersion(version); + } }; fetchLatestVersion(); },[]); diff --git a/Client/src/Utils/NetworkService.js b/Client/src/Utils/NetworkService.js index 573651f58..79e5b701e 100644 --- a/Client/src/Utils/NetworkService.js +++ b/Client/src/Utils/NetworkService.js @@ -832,6 +832,20 @@ class NetworkService { } ); } + + /** + * ************************************ + * Fetcher github latest release version + * ************************************ + * + * @async + * @returns {Promise} The response from the axios GET request. + * + */ + async fetchGithubLatestRelease() { + return this.axiosInstance.get('https://api.github.com/repos/bluewave-labs/bluewave-uptime/releases/latest'); + } + } export default NetworkService; From 3b4bab707fc9fb38c0795ba361a56f983f4dcc60 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 16 Oct 2024 12:22:54 +0530 Subject: [PATCH 10/15] converted into a single version line --- Client/src/Pages/Settings/index.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Client/src/Pages/Settings/index.jsx b/Client/src/Pages/Settings/index.jsx index 2ffec346e..5ba4d676a 100644 --- a/Client/src/Pages/Settings/index.jsx +++ b/Client/src/Pages/Settings/index.jsx @@ -319,8 +319,7 @@ const Settings = ({ isAdmin }) => { About - BlueWave Uptime v1.0.0 - Latest Version: {version} + BlueWave Uptime {version} From 29ce8bb116c1a19a07db128bad3bfb808a14af8f Mon Sep 17 00:00:00 2001 From: Rushi Gandhi Date: Wed, 16 Oct 2024 12:45:31 +0530 Subject: [PATCH 11/15] Refactor issueToken function for issueing the refresh token --- Server/controllers/authController.js | 48 ++++++++++++---------------- Server/utils/utils.js | 6 ++++ 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/Server/controllers/authController.js b/Server/controllers/authController.js index 7eefc6722..cb816fde6 100644 --- a/Server/controllers/authController.js +++ b/Server/controllers/authController.js @@ -12,7 +12,7 @@ require("dotenv").config(); const { errorMessages, successMessages } = require("../utils/messages"); const jwt = require("jsonwebtoken"); const SERVICE_NAME = "AuthController"; -const { getTokenFromHeaders } = require("../utils/utils"); +const { getTokenFromHeaders, tokenType } = require("../utils/utils"); const crypto = require("crypto"); const { handleValidationError, handleError } = require("./controllerUtils"); @@ -20,37 +20,31 @@ const { handleValidationError, handleError } = require("./controllerUtils"); * Creates and returns JWT token with an arbitrary payload * @function * @param {Object} payload + * @param {tokenType} typeOfToken - Whether to generate refresh token with long TTL or access token with short TTL. * @param {Object} appSettings * @returns {String} * @throws {Error} */ -const issueToken = (payload, appSettings) => { +const issueToken = (payload, typeOfToken, appSettings) => { try { - const tokenTTL = appSettings.jwtTTL ? appSettings.jwtTTL : "2h"; - return jwt.sign(payload, appSettings.jwtSecret, { expiresIn: tokenTTL }); + let tokenSecret, tokenTTL, payloadData = {}; + + if(typeOfToken === tokenType.REFRESH_TOKEN) { + tokenSecret = appSettings.refreshTokenSecret; + tokenTTL = appSettings.refreshTokenTTL ? appSettings.refreshTokenTTL : "7d"; + } + else { + tokenSecret = appSettings.jwtSecret; + tokenTTL = appSettings.jwtTTL ? appSettings.jwtTTL : "2h"; + payloadData = payload; + } + + return jwt.sign(payloadData, tokenSecret, { expiresIn: tokenTTL }); } catch (error) { throw handleError(error, SERVICE_NAME, "issueToken"); } }; -/** - * Creates and returns a JWT refresh token with an arbitrary payload - * @function - * @param {Object} payload - Here we have passed MongoDB assigned _id. - * @param {Object} appSettings - * @returns {String} - * @throws {Error} - */ -const issueRefreshToken = (payload, appSettings) => { - try { - const tokenTTL = appSettings.refreshTokenTTL ? appSettings.refreshTokenTTL : "7d"; - return jwt.sign(payload, appSettings.refreshTokenSecret, { expiresIn: tokenTTL }); - } catch (error) { - throw handleError(error, SERVICE_NAME, "issueRefreshToken"); - } -}; - - /** * Registers a new user. If the user is the first account, a JWT secret is created. If not, an invite token is required. * @async @@ -98,8 +92,8 @@ const registerUser = async (req, res, next) => { const appSettings = await req.settingsService.getSettings(); - const token = issueToken(userForToken, appSettings); - const refreshToken = issueRefreshToken(userForToken, appSettings); + const token = issueToken(userForToken, tokenType.ACCESS_TOKEN, appSettings); + const refreshToken = issueToken({}, tokenType.REFRESH_TOKEN, appSettings); req.emailService .buildAndSendEmail( @@ -165,8 +159,8 @@ const loginUser = async (req, res, next) => { // Happy path, return token const appSettings = await req.settingsService.getSettings(); - const token = issueToken(userWithoutPassword, appSettings); - const refreshToken = issueRefreshToken(userWithoutPassword, appSettings); + const token = issueToken(userWithoutPassword, tokenType.ACCESS_TOKEN, appSettings); + const refreshToken = issueToken({}, tokenType.REFRESH_TOKEN, appSettings); // reset avatar image userWithoutPassword.avatarImage = user.avatarImage; @@ -380,7 +374,7 @@ const resetPassword = async (req, res, next) => { const user = await req.db.resetPassword(req, res); const appSettings = await req.settingsService.getSettings(); - const token = issueToken(user._doc, appSettings); + const token = issueToken(user._doc, tokenType.ACCESS_TOKEN, appSettings); res.status(200).json({ success: true, msg: successMessages.AUTH_RESET_PASSWORD, diff --git a/Server/utils/utils.js b/Server/utils/utils.js index 4331a4610..f839eb464 100644 --- a/Server/utils/utils.js +++ b/Server/utils/utils.js @@ -27,7 +27,13 @@ const getTokenFromHeaders = (headers) => { return parts[1]; }; +const tokenType = Object.freeze({ + ACCESS_TOKEN: "Access token", + REFRESH_TOKEN: "Refresh token" +}); + module.exports = { ParseBoolean, getTokenFromHeaders, + tokenType, }; From 7a3d2619f60b60042d07f83c54df0b553188f04c Mon Sep 17 00:00:00 2001 From: Rushi Gandhi Date: Wed, 16 Oct 2024 12:46:44 +0530 Subject: [PATCH 12/15] add testcases for refresh token --- .../tests/controllers/authController.test.js | 53 +++++++++++++++---- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/Server/tests/controllers/authController.test.js b/Server/tests/controllers/authController.test.js index 842959415..c33d13fcd 100644 --- a/Server/tests/controllers/authController.test.js +++ b/Server/tests/controllers/authController.test.js @@ -14,29 +14,61 @@ const jwt = require("jsonwebtoken"); const { errorMessages, successMessages } = require("../../utils/messages"); const sinon = require("sinon"); const logger = require("../../utils/logger"); +const { tokenType } = require("../../utils/utils"); describe("Auth Controller - issueToken", () => { + let stub; + + afterEach(() => { + sinon.restore(); // Restore stubs after each test + }); + it("should reject with an error if jwt.sign fails", () => { const error = new Error("jwt.sign error"); stub = sinon.stub(jwt, "sign").throws(error); const payload = { id: "123" }; const appSettings = { jwtSecret: "my_secret" }; - expect(() => issueToken(payload, appSettings)).to.throw(error); - stub.restore(); + expect(() => issueToken(payload, tokenType.ACCESS_TOKEN, appSettings)).to.throw(error); }); - it("should return a token if jwt.sign is successful and appSettings.jtwTTL is not defined", () => { + it("should return a token if jwt.sign is successful and appSettings.jwtTTL is not defined", () => { const payload = { id: "123" }; const appSettings = { jwtSecret: "my_secret" }; - const token = issueToken(payload, appSettings); - expect(token).to.be.a("string"); + const expectedToken = "mockToken"; + + stub = sinon.stub(jwt, "sign").returns(expectedToken); + const token = issueToken(payload, tokenType.ACCESS_TOKEN, appSettings); + expect(token).to.equal(expectedToken); }); it("should return a token if jwt.sign is successful and appSettings.jwtTTL is defined", () => { const payload = { id: "123" }; const appSettings = { jwtSecret: "my_secret", jwtTTL: "1s" }; - const token = issueToken(payload, appSettings); - expect(token).to.be.a("string"); + const expectedToken = "mockToken"; + + stub = sinon.stub(jwt, "sign").returns(expectedToken); + const token = issueToken(payload, tokenType.ACCESS_TOKEN, appSettings); + expect(token).to.equal(expectedToken); + }); + + it("should return a refresh token if jwt.sign is successful and appSettings.refreshTokenTTL is not defined", () => { + const payload = {}; + const appSettings = { refreshTokenSecret: "my_refresh_secret" }; + const expectedToken = "mockRefreshToken"; + + stub = sinon.stub(jwt, "sign").returns(expectedToken); + const token = issueToken(payload, tokenType.REFRESH_TOKEN, appSettings); + expect(token).to.equal(expectedToken); + }); + + it("should return a refresh token if jwt.sign is successful and appSettings.refreshTokenTTL is defined", () => { + const payload = {}; + const appSettings = { refreshTokenSecret: "my_refresh_secret", refreshTokenTTL: "7d" }; + const expectedToken = "mockRefreshToken"; + + stub = sinon.stub(jwt, "sign").returns(expectedToken); + const token = issueToken(payload, tokenType.REFRESH_TOKEN, appSettings); + expect(token).to.equal(expectedToken); }); }); @@ -61,6 +93,7 @@ describe("Auth Controller - registerUser", () => { settingsService: { getSettings: sinon.stub().resolves({ jwtSecret: "my_secret", + refreshTokenSecret: "my_refresh_secret", }), }, emailService: { @@ -140,7 +173,6 @@ describe("Auth Controller - registerUser", () => { req.db.checkSuperadmin.resolves(false); req.db.updateAppSettings.resolves(); req.db.insertUser.returns({ _id: "123" }); - req.settingsService.getSettings.returns({ jwtSecret: "my_secret" }); req.emailService.buildAndSendEmail.rejects(new Error("emailService error")); await registerUser(req, res, next); expect(logger.error.calledOnce).to.be.true; @@ -151,7 +183,6 @@ describe("Auth Controller - registerUser", () => { req.db.checkSuperadmin.resolves(false); req.db.updateAppSettings.resolves(); req.db.insertUser.returns(user); - req.settingsService.getSettings.returns({ jwtSecret: "my_secret" }); req.emailService.buildAndSendEmail.resolves("message-id"); await registerUser(req, res, next); expect(res.status.calledWith(200)).to.be.true; @@ -159,7 +190,7 @@ describe("Auth Controller - registerUser", () => { res.json.calledWith({ success: true, msg: successMessages.AUTH_CREATE_USER, - data: { user, token: sinon.match.string }, + data: { user, token: sinon.match.string, refreshToken: sinon.match.string }, }) ).to.be.true; expect(next.notCalled).to.be.true; @@ -176,6 +207,7 @@ describe("Auth Controller - loginUser", () => { settingsService: { getSettings: sinon.stub().resolves({ jwtSecret: "my_secret", + refreshTokenSecret: "my_refresh_token", }), }, }; @@ -220,6 +252,7 @@ describe("Auth Controller - loginUser", () => { avatarImage: undefined, }, token: sinon.match.string, + refreshToken: sinon.match.string, }, }) ).to.be.true; From 111f7d5a676621b08eeb32057f4f89f8e56f2901 Mon Sep 17 00:00:00 2001 From: Rushi Gandhi Date: Wed, 16 Oct 2024 14:13:48 +0530 Subject: [PATCH 13/15] remove if/else by using ternary operator --- Server/controllers/authController.js | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/Server/controllers/authController.js b/Server/controllers/authController.js index cb816fde6..0f386bc4b 100644 --- a/Server/controllers/authController.js +++ b/Server/controllers/authController.js @@ -27,17 +27,9 @@ const { handleValidationError, handleError } = require("./controllerUtils"); */ const issueToken = (payload, typeOfToken, appSettings) => { try { - let tokenSecret, tokenTTL, payloadData = {}; - - if(typeOfToken === tokenType.REFRESH_TOKEN) { - tokenSecret = appSettings.refreshTokenSecret; - tokenTTL = appSettings.refreshTokenTTL ? appSettings.refreshTokenTTL : "7d"; - } - else { - tokenSecret = appSettings.jwtSecret; - tokenTTL = appSettings.jwtTTL ? appSettings.jwtTTL : "2h"; - payloadData = payload; - } + const tokenTTL = (typeOfToken === tokenType.REFRESH_TOKEN) ? (appSettings?.refreshTokenTTL?? "7d") : (appSettings?.jwtTTL?? "2h"); + const tokenSecret = (typeOfToken === tokenType.REFRESH_TOKEN) ? appSettings?.refreshTokenSecret : appSettings?.jwtSecret; + const payloadData = (typeOfToken === tokenType.REFRESH_TOKEN) ? {} : payload; return jwt.sign(payloadData, tokenSecret, { expiresIn: tokenTTL }); } catch (error) { From ee5fe21688c85dd941434bc5da30272375c911ad Mon Sep 17 00:00:00 2001 From: Shemy Gan Date: Wed, 16 Oct 2024 09:52:59 -0400 Subject: [PATCH 14/15] - Change to use more verbose names and fixed lint error in typo --- Client/src/Components/Dialog/index.jsx | 2 +- Client/src/Pages/Settings/index.jsx | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Client/src/Components/Dialog/index.jsx b/Client/src/Components/Dialog/index.jsx index 0e3bdad54..389d3a6e8 100644 --- a/Client/src/Components/Dialog/index.jsx +++ b/Client/src/Components/Dialog/index.jsx @@ -62,7 +62,7 @@ const Dialog = ({ modelTitle, modelDescription, } -Dialog.PropTypes = { +Dialog.propTypes = { modelTitle: PropTypes.string.isRequired, modelDescription: PropTypes.string.isRequired, open: PropTypes.bool.isRequired, diff --git a/Client/src/Pages/Settings/index.jsx b/Client/src/Pages/Settings/index.jsx index 67bca8db0..e3dba341d 100644 --- a/Client/src/Pages/Settings/index.jsx +++ b/Client/src/Pages/Settings/index.jsx @@ -39,8 +39,8 @@ const Settings = ({ isAdmin }) => { ttl: checkTTL ? (checkTTL / SECONDS_PER_DAY).toString() : 0, }); const [errors, setErrors] = useState({}); - const delStatMntrInitState = { delMntrs: false, delSts: false }; - const [isOpen, setIsOpen] = useState(delStatMntrInitState); + const deleteStatsMonitorsInitState = { deleteMonitors: false, deleteStats: false }; + const [isOpen, setIsOpen] = useState(deleteStatsMonitorsInitState); const dispatch = useDispatch(); const navigate = useNavigate(); @@ -123,7 +123,7 @@ const Settings = ({ isAdmin }) => { logger.error(error); createToast({ body: "Failed to clear stats" }); } finally { - setIsOpen(delStatMntrInitState) + setIsOpen(deleteStatsMonitorsInitState) } }; @@ -153,7 +153,7 @@ const Settings = ({ isAdmin }) => { logger.error(error); createToast({ Body: "Failed to delete all monitors" }); } finally { - setIsOpen(delStatMntrInitState) + setIsOpen(deleteStatsMonitorsInitState) } }; @@ -233,7 +233,7 @@ const Settings = ({ isAdmin }) => {