diff --git a/Client/src/Components/BasicTable/index.css b/Client/src/Components/BasicTable/index.css index 5defa124d..06ea3e3ce 100644 --- a/Client/src/Components/BasicTable/index.css +++ b/Client/src/Components/BasicTable/index.css @@ -120,7 +120,7 @@ opacity: 0.4; } -.monitors .MuiTable-root .MuiTableHead-root .MuiTableCell-root { +.table-container .MuiTable-root .MuiTableHead-root .MuiTableCell-root { text-transform: uppercase; opacity: 0.8; font-size: var(--env-var-font-size-small-plus); diff --git a/Client/src/Components/Dialog/index.jsx b/Client/src/Components/Dialog/index.jsx new file mode 100644 index 000000000..389d3a6e8 --- /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 = ({ modelTitle, modelDescription, + open, onClose, title, confirmationBtnLbl, confirmationBtnOnClick, cancelBtnLbl, cancelBtnOnClick, theme, isLoading, description }) => { + return + + + {title} + + {description && + {description} + } + + + + {confirmationBtnLbl} + + + + +} + +Dialog.propTypes = { + modelTitle: PropTypes.string.isRequired, + modelDescription: PropTypes.string.isRequired, + open: PropTypes.bool.isRequired, + onClose: 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, + description: PropTypes.string +} + +export default Dialog \ No newline at end of file diff --git a/Client/src/Components/TabPanels/Account/TeamPanel.jsx b/Client/src/Components/TabPanels/Account/TeamPanel.jsx index e5fd5d02b..f5a6e86b5 100644 --- a/Client/src/Components/TabPanels/Account/TeamPanel.jsx +++ b/Client/src/Components/TabPanels/Account/TeamPanel.jsx @@ -223,6 +223,7 @@ const TeamPanel = () => { return ( { }; return ( - + {loading ? ( ) : ( diff --git a/Client/src/Pages/Maintenance/index.jsx b/Client/src/Pages/Maintenance/index.jsx index e69711eec..c5440cba4 100644 --- a/Client/src/Pages/Maintenance/index.jsx +++ b/Client/src/Pages/Maintenance/index.jsx @@ -46,7 +46,7 @@ const Maintenance = ({ isAdmin }) => { return ( [class*="fallback__"])': { position: "relative", 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/Monitors/Home/index.jsx b/Client/src/Pages/Monitors/Home/index.jsx index 151a04bbd..a0f72b827 100644 --- a/Client/src/Pages/Monitors/Home/index.jsx +++ b/Client/src/Pages/Monitors/Home/index.jsx @@ -45,7 +45,7 @@ const Monitors = ({ isAdmin }) => { monitorState?.monitorsSummary?.monitors?.length === 0; return ( - + {loading ? ( ) : ( diff --git a/Client/src/Pages/Settings/index.jsx b/Client/src/Pages/Settings/index.jsx index db3fe232b..10f34fa23 100644 --- a/Client/src/Pages/Settings/index.jsx +++ b/Client/src/Pages/Settings/index.jsx @@ -17,11 +17,12 @@ 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"; import { useNavigate } from "react-router"; +import Dialog from "../../Components/Dialog" const SECONDS_PER_DAY = 86400; @@ -37,10 +38,32 @@ const Settings = ({ isAdmin }) => { const [form, setForm] = useState({ ttl: checkTTL ? (checkTTL / SECONDS_PER_DAY).toString() : 0, }); + const [version,setVersion]=useState("unknown"); const [errors, setErrors] = useState({}); + const deleteStatsMonitorsInitState = { deleteMonitors: false, deleteStats: false }; + const [isOpen, setIsOpen] = useState(deleteStatsMonitorsInitState); const dispatch = useDispatch(); const navigate = useNavigate(); + //Fetching latest release version from github + useEffect(() => { + const fetchLatestVersion = async () => { + let version="unknown"; + try { + const response = await networkService.fetchGithubLatestRelease(); + if (!response.status===200) { + throw new Error("Failed to fetch latest version"); + } + version=response.data.tag_name; + } catch (error) { + createToast({ body: error.message || "Error fetching latest version" }); // Set error message + } finally{ + setVersion(version); + } + }; + fetchLatestVersion(); + },[]); + const handleChange = (event) => { const { value, id } = event.target; const { error } = settingsValidation.validate( @@ -119,6 +142,8 @@ const Settings = ({ isAdmin }) => { } catch (error) { logger.error(error); createToast({ body: "Failed to clear stats" }); + } finally { + setIsOpen(deleteStatsMonitorsInitState) } }; @@ -147,9 +172,11 @@ const Settings = ({ isAdmin }) => { } catch (error) { logger.error(error); createToast({ Body: "Failed to delete all monitors" }); + } finally { + setIsOpen(deleteStatsMonitorsInitState) } }; - + return ( { /> Clear all stats. This is irreversible. - setIsOpen({...deleteStatsMonitorsInitState, deleteStats: true})} sx={{ mt: theme.spacing(4) }} > Clear all stats - + + setIsOpen(deleteStatsMonitorsInitState)} + title="Do you want to clear all stats?" + confirmationBtnLbl="Yes, clear all stats" + confirmationBtnOnClick={handleClearStats} + cancelBtnLbl="Cancel" + cancelBtnOnClick={() => setIsOpen(deleteStatsMonitorsInitState)} + theme={theme} + isLoading={isLoading || authIsLoading || checksIsLoading} + > + )} {isAdmin && ( @@ -263,13 +303,27 @@ const Settings = ({ isAdmin }) => { variant="contained" color="error" loading={isLoading || authIsLoading || checksIsLoading} - onClick={handleDeleteAllMonitors} + onClick={()=>setIsOpen({...deleteStatsMonitorsInitState, deleteMonitors: true})} sx={{ mt: theme.spacing(4) }} > Remove all monitors + setIsOpen(deleteStatsMonitorsInitState)} + title="Do you want to remove all monitors?" + confirmationBtnLbl="Yes, clear all monitors" + confirmationBtnOnClick={handleDeleteAllMonitors} + cancelBtnLbl="Cancel" + cancelBtnOnClick={() => setIsOpen(deleteStatsMonitorsInitState)} + theme={theme} + isLoading={isLoading || authIsLoading || checksIsLoading} + > + )} {isAdmin && ( @@ -299,7 +353,7 @@ const Settings = ({ isAdmin }) => { About - BlueWave Uptime v1.0.0 + BlueWave Uptime {version} @@ -307,8 +361,8 @@ const Settings = ({ isAdmin }) => { 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; diff --git a/Server/controllers/authController.js b/Server/controllers/authController.js index deb210d32..7ba0ec831 100644 --- a/Server/controllers/authController.js +++ b/Server/controllers/authController.js @@ -11,7 +11,7 @@ import logger from "../utils/logger.js"; import dotenv from "dotenv"; import { errorMessages, successMessages } from "../utils/messages.js"; import jwt from "jsonwebtoken"; -import { getTokenFromHeaders } from "../utils/utils.js"; +import { getTokenFromHeaders, tokenType } from "../utils/utils.js"; import crypto from "crypto"; import { handleValidationError, handleError } from "./controllerUtils.js"; const SERVICE_NAME = "authController"; @@ -21,14 +21,24 @@ dotenv.config(); * 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 }); + 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) { throw handleError(error, SERVICE_NAME, "issueToken"); } @@ -81,7 +91,8 @@ const registerUser = async (req, res, next) => { const appSettings = await req.settingsService.getSettings(); - const token = issueToken(userForToken, appSettings); + const token = issueToken(userForToken, tokenType.ACCESS_TOKEN, appSettings); + const refreshToken = issueToken({}, tokenType.REFRESH_TOKEN, appSettings); req.emailService .buildAndSendEmail( @@ -100,9 +111,10 @@ 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); next(handleError(error, SERVICE_NAME, "registerController")); } }; @@ -147,14 +159,15 @@ const loginUser = async (req, res, next) => { // Happy path, return token const appSettings = await req.settingsService.getSettings(); - const token = issueToken(userWithoutPassword, appSettings); + const token = issueToken(userWithoutPassword, tokenType.ACCESS_TOKEN, appSettings); + const refreshToken = issueToken({}, tokenType.REFRESH_TOKEN, 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, "loginUser")); @@ -361,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/db/models/AppSettings.js b/Server/db/models/AppSettings.js index 399c0331d..547af5f78 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/Server/service/settingsService.js b/Server/service/settingsService.js index b73f7caa8..bac92cc12 100644 --- a/Server/service/settingsService.js +++ b/Server/service/settingsService.js @@ -5,11 +5,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, diff --git a/Server/tests/controllers/authController.test.js b/Server/tests/controllers/authController.test.js index d06a0c61f..e4f78750f 100644 --- a/Server/tests/controllers/authController.test.js +++ b/Server/tests/controllers/authController.test.js @@ -13,30 +13,67 @@ import { import jwt from "jsonwebtoken"; import { errorMessages, successMessages } from "../../utils/messages.js"; import sinon from "sinon"; +import { tokenType } from "../../utils/utils.js"; import logger from "../../utils/logger.js"; describe("Auth Controller - issueToken", () => { - it("should reject with an error if jwt.sign fails", () => { - const error = new Error("jwt.sign error"); - const 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(); + let stub; + + afterEach(() => { + sinon.restore(); // Restore stubs after each test }); - it("should return a token if jwt.sign is successful and appSettings.jtwTTL is not defined", () => { + 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" }; - const token = issueToken(payload, appSettings); - expect(token).to.be.a("string"); + expect(() => issueToken(payload, tokenType.ACCESS_TOKEN, appSettings)).to.throw( + error + ); + }); + + 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 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); }); }); @@ -62,6 +99,7 @@ describe("Auth Controller - registerUser", () => { settingsService: { getSettings: sinon.stub().resolves({ jwtSecret: "my_secret", + refreshTokenSecret: "my_refresh_secret", }), }, emailService: { @@ -172,7 +210,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; @@ -190,6 +228,7 @@ describe("Auth Controller - loginUser", () => { settingsService: { getSettings: sinon.stub().resolves({ jwtSecret: "my_secret", + refreshTokenSecret: "my_refresh_token", }), }, }; @@ -234,6 +273,7 @@ describe("Auth Controller - loginUser", () => { avatarImage: undefined, }, token: sinon.match.string, + refreshToken: sinon.match.string, }, }) ).to.be.true; diff --git a/Server/utils/utils.js b/Server/utils/utils.js index da61a69c3..5bd9258f9 100644 --- a/Server/utils/utils.js +++ b/Server/utils/utils.js @@ -4,27 +4,32 @@ * @returns {boolean} */ const ParseBoolean = (value) => { - if (value === true || value === "true") { - return true; - } else if ( - value === false || - value === "false" || - value === null || - value === undefined - ) { - return false; - } + if (value === true || value === "true") { + return true; + } else if ( + value === false || + value === "false" || + value === null || + value === undefined + ) { + return false; + } }; const getTokenFromHeaders = (headers) => { - const authorizationHeader = headers.authorization; - if (!authorizationHeader) throw new Error("No auth headers"); + const authorizationHeader = headers.authorization; + if (!authorizationHeader) throw new Error("No auth headers"); - const parts = authorizationHeader.split(" "); - if (parts.length !== 2 || parts[0] !== "Bearer") - throw new Error("Invalid auth headers"); + const parts = authorizationHeader.split(" "); + if (parts.length !== 2 || parts[0] !== "Bearer") + throw new Error("Invalid auth headers"); - return parts[1]; + return parts[1]; }; -export { ParseBoolean, getTokenFromHeaders }; +const tokenType = Object.freeze({ + ACCESS_TOKEN: "Access token", + REFRESH_TOKEN: "Refresh token", +}); + +export { ParseBoolean, getTokenFromHeaders, tokenType }; diff --git a/guides/users-guide/quickstart.md b/guides/users-guide/quickstart.md index 6d2ec2242..6bc6975c4 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
---