Merge branch 'develop' into feat/be/esm
This commit is contained in:
commit
81f1fe4694
16 files changed
+305
-186
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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 <Modal
|
||||
aria-labelledby={modelTitle}
|
||||
aria-describedby={modelDescription}
|
||||
open={open}
|
||||
onClose={onClose} >
|
||||
<Stack
|
||||
gap={theme.spacing(2)}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: 400,
|
||||
bgcolor: theme.palette.background.main,
|
||||
border: 1,
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
boxShadow: 24,
|
||||
p: theme.spacing(15),
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography id={modelTitle} component="h2"
|
||||
fontSize={16}
|
||||
color={theme.palette.text.primary}
|
||||
fontWeight={600}>
|
||||
{title}
|
||||
</Typography>
|
||||
{description && <Typography id={modelDescription} color={theme.palette.text.tertiary}>
|
||||
{description}
|
||||
</Typography>}
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(4)}
|
||||
mt={theme.spacing(12)}
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Button
|
||||
variant="text"
|
||||
color="info"
|
||||
onClick={cancelBtnOnClick}
|
||||
>
|
||||
{cancelBtnLbl}
|
||||
</Button>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="error"
|
||||
loading= {isLoading}
|
||||
onClick={confirmationBtnOnClick}
|
||||
>
|
||||
{confirmationBtnLbl}
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
}
|
||||
|
||||
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
|
||||
@@ -223,6 +223,7 @@ const TeamPanel = () => {
|
||||
|
||||
return (
|
||||
<TabPanel
|
||||
className="team-panel table-container"
|
||||
value="team"
|
||||
sx={{
|
||||
"& h1": {
|
||||
|
||||
@@ -61,7 +61,7 @@ const Incidents = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack className="incidents" pt={theme.spacing(6)} gap={theme.spacing(12)}>
|
||||
<Stack className="incidents table-container" pt={theme.spacing(6)} gap={theme.spacing(12)}>
|
||||
{loading ? (
|
||||
<SkeletonLayout />
|
||||
) : (
|
||||
|
||||
@@ -46,7 +46,7 @@ const Maintenance = ({ isAdmin }) => {
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="maintenance"
|
||||
className="maintenance table-container"
|
||||
sx={{
|
||||
':has(> [class*="fallback__"])': {
|
||||
position: "relative",
|
||||
|
||||
@@ -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 = () => {
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
<Modal
|
||||
aria-labelledby="modal-delete-monitor"
|
||||
aria-describedby="delete-monitor-confirmation"
|
||||
<Dialog
|
||||
modelTitle="modal-delete-monitor"
|
||||
modelDescription="delete-monitor-confirmation"
|
||||
open={isOpen}
|
||||
onClose={() => 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."
|
||||
>
|
||||
<Stack
|
||||
gap={theme.spacing(2)}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: 400,
|
||||
bgcolor: theme.palette.background.main,
|
||||
border: 1,
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
boxShadow: 24,
|
||||
p: theme.spacing(15),
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
id="modal-delete-monitor"
|
||||
component="h2"
|
||||
fontSize={16}
|
||||
color={theme.palette.text.primary}
|
||||
fontWeight={600}
|
||||
>
|
||||
Do you really want to delete this monitor?
|
||||
</Typography>
|
||||
<Typography
|
||||
id="delete-monitor-confirmation"
|
||||
color={theme.palette.text.tertiary}
|
||||
>
|
||||
Once deleted, this monitor cannot be retrieved.
|
||||
</Typography>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(4)}
|
||||
mt={theme.spacing(12)}
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Button
|
||||
variant="text"
|
||||
color="info"
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="error"
|
||||
loading={isLoading}
|
||||
onClick={handleRemove}
|
||||
>
|
||||
Delete
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</Dialog>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 }) => {
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
<Modal
|
||||
aria-labelledby="modal-delete-monitor"
|
||||
aria-describedby="delete-monitor-confirmation"
|
||||
<Dialog
|
||||
modelTitle="modal-delete-monitor"
|
||||
modelDescription="delete-monitor-confirmation"
|
||||
open={isOpen}
|
||||
onClose={(e) => {
|
||||
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."
|
||||
>
|
||||
<Stack
|
||||
gap={theme.spacing(2)}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: 400,
|
||||
bgcolor: theme.palette.background.main,
|
||||
border: 1,
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
boxShadow: 24,
|
||||
p: theme.spacing(15),
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography id="modal-delete-monitor" component="h2" variant="h2">
|
||||
Do you really want to delete this monitor?
|
||||
</Typography>
|
||||
<Typography id="delete-monitor-confirmation" variant="body1">
|
||||
Once deleted, this monitor cannot be retrieved.
|
||||
</Typography>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(4)}
|
||||
mt={theme.spacing(12)}
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Button
|
||||
variant="text"
|
||||
color="info"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(e);
|
||||
handleRemove(e);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -45,7 +45,7 @@ const Monitors = ({ isAdmin }) => {
|
||||
monitorState?.monitorsSummary?.monitors?.length === 0;
|
||||
|
||||
return (
|
||||
<Stack className="monitors" gap={theme.spacing(8)}>
|
||||
<Stack className="monitors table-container" gap={theme.spacing(8)}>
|
||||
{loading ? (
|
||||
<SkeletonLayout />
|
||||
) : (
|
||||
|
||||
@@ -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 (
|
||||
<Box
|
||||
className="settings"
|
||||
@@ -223,17 +250,30 @@ const Settings = ({ isAdmin }) => {
|
||||
/>
|
||||
<Box>
|
||||
<Typography>Clear all stats. This is irreversible.</Typography>
|
||||
<LoadingButton
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
loading={isLoading || authIsLoading || checksIsLoading}
|
||||
onClick={handleClearStats}
|
||||
color="error"
|
||||
onClick={()=>setIsOpen({...deleteStatsMonitorsInitState, deleteStats: true})}
|
||||
sx={{ mt: theme.spacing(4) }}
|
||||
>
|
||||
Clear all stats
|
||||
</LoadingButton>
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Dialog
|
||||
modelTitle="model-clear-stats"
|
||||
modelDescription="clear-stats-confirmation"
|
||||
open={isOpen.deleteStats}
|
||||
onClose={() => 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}
|
||||
>
|
||||
</Dialog>
|
||||
</ConfigBox>
|
||||
)}
|
||||
{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
|
||||
</LoadingButton>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Dialog
|
||||
modelTitle="model-delete-all-monitors"
|
||||
modelDescription="delete-all-monitors-confirmation"
|
||||
open={isOpen.deleteMonitors}
|
||||
onClose={() => 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}
|
||||
>
|
||||
</Dialog>
|
||||
</ConfigBox>
|
||||
)}
|
||||
{isAdmin && (
|
||||
@@ -299,7 +353,7 @@ const Settings = ({ isAdmin }) => {
|
||||
<Typography component="h1">About</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography component="h2">BlueWave Uptime v1.0.0</Typography>
|
||||
<Typography component="h2">BlueWave Uptime {version}</Typography>
|
||||
<Typography
|
||||
sx={{ mt: theme.spacing(2), mb: theme.spacing(6), opacity: 0.6 }}
|
||||
>
|
||||
@@ -307,8 +361,8 @@ const Settings = ({ isAdmin }) => {
|
||||
</Typography>
|
||||
<Link
|
||||
level="secondary"
|
||||
url="https://github.com/bluewave-labs"
|
||||
label="https://github.com/bluewave-labs"
|
||||
url="https://github.com/bluewave-labs/bluewave-uptime"
|
||||
label="https://github.com/bluewave-labs/bluewave-uptime"
|
||||
/>
|
||||
</Box>
|
||||
</ConfigBox>
|
||||
|
||||
@@ -832,6 +832,20 @@ class NetworkService {
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* ************************************
|
||||
* Fetcher github latest release version
|
||||
* ************************************
|
||||
*
|
||||
* @async
|
||||
* @returns {Promise<AxiosResponse>} 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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
+22
-17
@@ -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 };
|
||||
@@ -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:
|
||||
|
||||
<table><thead><tr><th width="239">ENV Variable Name</th><th width="149">Required/Optional</th><th width="116">Type</th><th>Description</th><th>Accepted Values</th></tr></thead><tbody><tr><td>CLIENT_HOST</td><td>Required</td><td><code>string</code></td><td>Frontend Host</td><td></td></tr><tr><td>JWT_SECRET</td><td>Required</td><td><code>string</code></td><td>JWT secret</td><td></td></tr><tr><td>DB_TYPE</td><td>Optional</td><td><code>string</code></td><td>Specify DB to use</td><td><code>MongoDB | FakeDB</code></td></tr><tr><td>DB_CONNECTION_STRING</td><td>Required</td><td><code>string</code></td><td>Specifies URL for MongoDB Database</td><td></td></tr><tr><td>PORT</td><td>Optional</td><td><code>integer</code></td><td>Specifies Port for Server</td><td></td></tr><tr><td>LOGIN_PAGE_URL</td><td>Required</td><td><code>string</code></td><td>Login url to be used in emailing service</td><td></td></tr><tr><td>REDIS_HOST</td><td>Required</td><td><code>string</code></td><td>Host address for Redis database</td><td></td></tr><tr><td>REDIS_PORT</td><td>Required</td><td><code>integer</code></td><td>Port for Redis database</td><td></td></tr><tr><td>TOKEN_TTL</td><td>Optional</td><td><code>string</code></td><td>Time for token to live</td><td>In vercel/ms format https://github.com/vercel/ms</td></tr><tr><td>PAGESPEED_API_KEY</td><td>Optional</td><td><code>string</code></td><td>API Key for PageSpeed requests</td><td></td></tr><tr><td>SYSTEM_EMAIL_HOST</td><td>Required</td><td><code>string</code></td><td>Host to send System Emails From</td><td></td></tr><tr><td>SYSTEM_EMAIL_PORT</td><td>Required</td><td><code>number</code></td><td>Port for System Email Host</td><td></td></tr><tr><td>SYSTEM_EMAIL_ADDRESS</td><td>Required</td><td><code>string</code></td><td>System Email Address</td><td></td></tr><tr><td>SYSTEM_EMAIL_PASSWORD</td><td>Required</td><td><code>string</code></td><td>System Email Password</td><td></td></tr></tbody></table>
|
||||
<table><thead><tr><th width="239">ENV Variable Name</th><th width="149">Required/Optional</th><th width="116">Type</th><th>Description</th><th>Accepted Values</th></tr></thead><tbody><tr><td>CLIENT_HOST</td><td>Required</td><td><code>string</code></td><td>Frontend Host</td><td></td></tr><tr><td>JWT_SECRET</td><td>Required</td><td><code>string</code></td><td>JWT secret</td><td></td></tr><tr><td>REFRESH_TOKEN_SECRET</td><td>Required</td><td><code>string</code></td><td>Refresh JWT secret</td><td></td></tr><tr><td>DB_TYPE</td><td>Optional</td><td><code>string</code></td><td>Specify DB to use</td><td><code>MongoDB | FakeDB</code></td></tr><tr><td>DB_CONNECTION_STRING</td><td>Required</td><td><code>string</code></td><td>Specifies URL for MongoDB Database</td><td></td></tr><tr><td>PORT</td><td>Optional</td><td><code>integer</code></td><td>Specifies Port for Server</td><td></td></tr><tr><td>LOGIN_PAGE_URL</td><td>Required</td><td><code>string</code></td><td>Login url to be used in emailing service</td><td></td></tr><tr><td>REDIS_HOST</td><td>Required</td><td><code>string</code></td><td>Host address for Redis database</td><td></td></tr><tr><td>REDIS_PORT</td><td>Required</td><td><code>integer</code></td><td>Port for Redis database</td><td></td></tr><tr><td>TOKEN_TTL</td><td>Optional</td><td><code>string</code></td><td>Time for token to live</td><td>In vercel/ms format https://github.com/vercel/ms</td></tr><tr><td>REFRESH_TOKEN_TTL</td><td>Optional</td><td><code>string</code></td><td>Time for refresh token to live</td><td></td></tr><tr><td>PAGESPEED_API_KEY</td><td>Optional</td><td><code>string</code></td><td>API Key for PageSpeed requests</td><td></td></tr><tr><td>SYSTEM_EMAIL_HOST</td><td>Required</td><td><code>string</code></td><td>Host to send System Emails From</td><td></td></tr><tr><td>SYSTEM_EMAIL_PORT</td><td>Required</td><td><code>number</code></td><td>Port for System Email Host</td><td></td></tr><tr><td>SYSTEM_EMAIL_ADDRESS</td><td>Required</td><td><code>string</code></td><td>System Email Address</td><td></td></tr><tr><td>SYSTEM_EMAIL_PASSWORD</td><td>Required</td><td><code>string</code></td><td>System Email Password</td><td></td></tr></tbody></table>
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in new issue
Block a user