diff --git a/README.es.md b/README.es.md new file mode 100644 index 000000000..21909b2ed --- /dev/null +++ b/README.es.md @@ -0,0 +1,172 @@ +

bluewave-labs%2Fcheckmate | Trendshift

+ +![](https://img.shields.io/github/license/bluewave-labs/checkmate) +![](https://img.shields.io/github/repo-size/bluewave-labs/checkmate) +![](https://img.shields.io/github/commit-activity/m/bluewave-labs/checkmate) +![](https://img.shields.io/github/last-commit/bluewave-labs/checkmate) +![](https://img.shields.io/github/languages/top/bluewave-labs/checkmate) +![](https://img.shields.io/github/issues/bluewave-labs/checkmate) +![](https://img.shields.io/github/issues-pr/bluewave-labs/checkmate) +[![OpenSSF Mejores Prácticas](https://www.bestpractices.dev/projects/9901/badge)](https://www.bestpractices.dev/projects/9901) +[![Pregunta en DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/bluewave-labs/checkmate) + +

Checkmate

+ +

Una aplicación de código abierto para monitoreo de infraestructura y tiempo de actividad

+ +image + +Este repositorio contiene tanto el frontend como el backend de Checkmate, una herramienta de monitoreo de código abierto y autoalojada para rastrear hardware de servidores, tiempo de actividad, tiempos de respuesta e incidentes en tiempo real con visualizaciones atractivas. Checkmate revisa regularmente si un servidor o sitio web es accesible y funciona de manera óptima, proporcionando alertas e informes en tiempo real sobre la disponibilidad, el tiempo de inactividad y el tiempo de respuesta de los servicios monitoreados. + +Checkmate también tiene un agente llamado [Capture](https://github.com/bluewave-labs/capture), para recuperar datos de servidores remotos. Aunque Capture no es obligatorio para ejecutar Checkmate, proporciona información adicional sobre el estado de la CPU, RAM, disco y temperatura de tus servidores. + +Checkmate ha sido probado con más de 1000 monitores activos sin problemas ni cuellos de botella de rendimiento. + +**Si deseas patrocinar una función, [visita este enlace](https://checkmate.so/sponsored-features).** + +## 📚 Tabla de contenidos + +- [📦 Demo](#demo) +- [🔗 Guía del usuario](#guía-del-usuario) +- [🛠️ Instalación](#instalación) +- [🏁 Traducciones](#traducciones) +- [🚀 Rendimiento](#rendimiento) +- [💚 Preguntas e ideas](#preguntas-e-ideas) +- [🧩 Características](#características) +- [🏗️ Capturas de pantalla](#capturas-de-pantalla) +- [🏗️ Tecnologías](#tecnologías) +- [🔗 Enlaces útiles](#enlaces-útiles) +- [🤝 Contribuciones](#contribuciones) +- [💰 Patrocinadores](#patrocinadores) + +... + +**[Texto truncado para mantener la longitud del mensaje manejable]** + +## Demo + +Puedes ver la última versión de [Checkmate](https://checkmate-demo.bluewavelabs.ca/) en acción. El usuario es uptimedemo@demo.com y la contraseña es Demouser1! (ten en cuenta que actualizamos el servidor de demostración de vez en cuando, así que si no funciona para ti, por favor contáctanos en el canal de Discusiones). + +## Guía del usuario + +Las instrucciones de uso se pueden encontrar [aquí](https://docs.checkmate.so/checkmate-2.1). Todavía está en desarrollo y parte de la información puede estar desactualizada ya que continuamente añadimos funciones cada semana. ¡Ten por seguro que estamos haciendo lo mejor posible! :) + +## Instalación + +Consulta las instrucciones de instalación en el [portal de documentación de Checkmate](https://docs.checkmate.so/checkmate-2.1/users-guide/quickstart). + +Alternativamente, también puedes usar [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md) o [Pikapods](https://www.pikapods.com/) para desplegar rápidamente una instancia de Checkmate. Si deseas monitorear tu infraestructura de servidores, necesitarás el agente [Capture](https://github.com/bluewave-labs/capture). El repositorio de Capture también contiene las instrucciones de instalación. + +## Traducciones + +Si deseas usar Checkmate en tu idioma, por favor [ve a esta página](https://poeditor.com/join/project/lRUoGZFCsJ) y regístrate para el idioma al que te gustaría traducir Checkmate. + +## Rendimiento + +Gracias a extensas optimizaciones, Checkmate opera con un uso de memoria excepcionalmente bajo, requiriendo recursos mínimos de memoria y CPU. Aquí está el uso de memoria de una instancia de Node.js ejecutándose en un servidor que monitorea 323 servidores cada minuto: + +![image](https://github.com/user-attachments/assets/37e04a75-d83a-488f-b25c-025511b492c9) + +También puedes ver el consumo de memoria de MongoDB y Redis en el mismo servidor (398Mb y 15Mb) para la misma cantidad de servidores: + +![image](https://github.com/user-attachments/assets/3b469e85-e675-4040-a162-3f24c1afc751) + +## Preguntas e Ideas + +Si tienes alguna pregunta, sugerencia o comentario, tienes varias opciones: + +- [Canal de Discord](https://discord.gg/NAb6H3UTjK) +- [Discusiones en GitHub](https://github.com/bluewave-labs/bluewave-uptime/discussions) +- [Grupo en Reddit](https://www.reddit.com/r/CheckmateMonitoring/) + +¡No dudes en hacer preguntas o compartir tus ideas, nos encantaría saber de ti! + +## Características + +- Completamente de código abierto, desplegable en tus propios servidores o dispositivos personales (por ejemplo, Raspberry Pi 4 o 5) +- Monitoreo de sitios web +- Monitoreo de velocidad de carga +- Monitoreo de infraestructura (memoria, uso de disco, rendimiento de CPU, etc) - requiere el agente [Capture](https://github.com/bluewave-labs/capture) +- Monitoreo de Docker +- Monitoreo con ping +- Monitoreo de certificados SSL +- Monitoreo de puertos +- Incidentes en una sola vista +- Páginas de estado +- Notificaciones por correo, Webhooks, Discord, Telegram, Slack +- Mantenimiento programado +- Monitoreo de consultas JSON +- Soporte para múltiples idiomas + +**Hoja de ruta a corto plazo:** ([Milestone 2.2](https://github.com/bluewave-labs/Checkmate/milestone/8)) + +- Mejores notificaciones +- Monitoreo de red +- ...y algunas funciones más + +## Capturas de pantalla + +

+image +

+

+ image +

+

+image +

+

+image +

+ +## Tecnologías + +- [ReactJs](https://react.dev/) +- [MUI (framework de React)](https://mui.com/) +- [Node.js](https://nodejs.org/en) +- [MongoDB](https://mongodb.com) +- [Recharts](https://recharts.org) +- ¡Y muchos otros componentes de código abierto! + +## Enlaces útiles + +- Si deseas apoyarnos, por favor considera darle una ⭐ y haz clic en "watch". +- ¿Tienes una pregunta o sugerencia para la hoja de ruta? Revisa nuestro [canal de Discord](https://discord.gg/NAb6H3UTjK) o el foro de [Discusiones](https://github.com/bluewave-labs/checkmate/discussions). +- ¿Quieres saber cuándo hay una nueva versión? Usa [Newreleases](https://newreleases.io/), un servicio gratuito para seguir lanzamientos. +- Mira un video de instalación y uso de Checkmate [aquí](https://www.youtube.com/watch?v=GfFOc0xHIwY) + +## Contribuciones + +Somos [Alex](http://github.com/ajhollid) (líder de equipo), [Vishnu](http://github.com/vishnusn77), [Mohadeseh](http://github.com/mohicody), [Gorkem](http://github.com/gorkem-bwl/), [Owaise](http://github.com/Owaiseimdad), [Aryaman](https://github.com/Br0wnHammer) y [Mert](https://github.com/mertssmnoglu), ayudando a personas y empresas a monitorear su infraestructura y servidores. + +Nos enorgullecemos de construir conexiones fuertes con contribuyentes de todos los niveles. A pesar de ser un proyecto joven, Checkmate ya ha ganado más de 7000 estrellas y ha atraído a más de 90 contribuyentes de todo el mundo. + +Nuestro repositorio ha sido marcado con estrella por empleados de **Google, Microsoft, Intel, Cisco, Tencent, Electronic Arts, ByteDance, JP Morgan Chase, Deloitte, Accenture, Foxconn, Broadcom, China Telecom, Barclays, Capgemini, Wipro, Cloudflare, Dassault Systèmes y NEC**, ¡así que no te detengas — participa, contribuye y aprende con nosotros! + +Cómo contribuir: + +0. Dale una estrella al repositorio :) +1. Revisa la [guía para contribuidores](https://github.com/bluewave-labs/Checkmate/blob/develop/CONTRIBUTING.md). Se anima a los nuevos a revisar las etiquetas `good-first-issue`. +2. Consulta la [estructura del proyecto](https://docs.checkmate.so/checkmate-2.1/developers-guide/general-project-structure) y la [visión general](https://bluewavelabs.gitbook.io/checkmate/developers-guide/high-level-overview). +3. Lee una estructura detallada de [Checkmate](https://deepwiki.com/bluewave-labs/Checkmate) si deseas profundizar en la arquitectura. +4. Abre un issue si crees que has encontrado un error. +5. Revisa los issues con la etiqueta `good-first-issue` si eres nuevo. +6. Haz un pull request para añadir nuevas funciones, mejoras o correcciones. + + + + + +[![Historial de estrellas](https://api.star-history.com/svg?repos=bluewave-labs/checkmate&type=Date)](https://star-history.com/#bluewave-labs/bluewave-uptime&Date) + +## Patrocinadores + +Gracias a [Gitbook](https://gitbook.io/) por darnos una cuenta gratuita para su plataforma de documentación, y a [Poeditor](https://poeditor.com/) por proporcionarnos una cuenta gratuita para servicios de traducción. Si deseas patrocinar Checkmate, por favor envía un correo a hello@bluewavelabs.ca + +Si deseas patrocinar una función, [visita esta página](https://checkmate.so/sponsored-features). + +También puedes revisar otros proyectos de BlueWave orientados a desarrolladores y contribuidores: + +- [VerifyWise](https://github.com/bluewave-labs/verifywise), la primera plataforma de gobernanza de IA de código abierto. +- [DataRoom](https://github.com/bluewave-labs/bluewave-dataroom), una aplicación de intercambio de archivos seguro, también conocida como dataroom. +- [Guidefox](https://github.com/bluewave-labs/guidefox), una app que ayuda a nuevos usuarios a aprender a usar tu producto mediante pistas, tours, popups y banners. diff --git a/README.md b/README.md index 757e70493..d584d832d 100644 --- a/README.md +++ b/README.md @@ -26,41 +26,40 @@ Checkmate has been stress-tested with 1000+ active monitors without any particul ## 📚 Table of contents -- [📦 Demo](#-demo) -- [🔗 User's guide](#-users-guide) -- [🛠️ Installation](#️-installation) -- [🚀 Deploying Checkmate with Helm](#-deploying-checkmate-with-helm) -- [🏁 Translations](#-translations) -- [🚀 Performance](#-performance) -- [💚 Questions & Ideas](#-questions--ideas) -- [🧩 Features](#-features) -- [🏗️ Screenshots](#-screenshots) -- [🏗️ Tech stack](#-tech-stack) +- [📦 Demo](#demo) +- [🔗 User's guide](#users-guide) +- [🛠️ Installation](#installation) +- [🏁 Translations](#translations) +- [🚀 Performance](#performance) +- [💚 Questions & Ideas](#questions--ideas) +- [🧩 Features](#features) +- [🏗️ Screenshots](#screenshots) +- [🏗️ Tech stack](#tech-stack) - [🔗 A few links](#a-few-links) -- [🤝 Contributing](#-contributing) -- [💰 Our sponsors](#-our-sponsors) +- [🤝 Contributing](#contributing) +- [💰 Our sponsors](#our-sponsors) -## 📦 Demo +## Demo You can see the latest build of [Checkmate](https://checkmate-demo.bluewavelabs.ca/) in action. The username is uptimedemo@demo.com and the password is Demouser1! (just a note that we update the demo server from time to time, so if it doesn't work for you, please ping us on the Discussions channel). -## 🔗 User's guide +## User's guide Usage instructions can be found [here](https://docs.checkmate.so/checkmate-2.1). It's still WIP and some of the information there might be outdated as we continuously add features weekly. Rest assured, we are doing our best! :) -## 🛠️ Installation +## Installation See installation instructions in [Checkmate documentation portal](https://docs.checkmate.so/checkmate-2.1/users-guide/quickstart). Alternatively, you can also use [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md) or [Pikapods](https://www.pikapods.com/) to quickly spin off a Checkmate instance. If you would like to monitor your server infrastructure, you'll need [Capture agent](https://github.com/bluewave-labs/capture). Capture repository also contains the installation instructions. -## 🏁 Translations +## Translations If you would like to use Checkmate in your language, please [go to this page](https://poeditor.com/join/project/lRUoGZFCsJ) and register for the language you would like to translate Checkmate to. -## 🚀 Performance +## Performance Thanks to extensive optimizations, Checkmate operates with an exceptionally small memory footprint, requiring minimal memory and CPU resources. Here’s the memory usage of a Node.js instance running on a server that monitors 323 servers every minute: @@ -70,16 +69,22 @@ You can see the memory footprint of MongoDB and Redis on the same server (398Mb ![image](https://github.com/user-attachments/assets/3b469e85-e675-4040-a162-3f24c1afc751) -## 💚 Questions & Ideas +## Questions & Ideas -If you have any questions, suggestions or comments, please use our [Discord channel](https://discord.gg/NAb6H3UTjK). We've also launched our [Discussions](https://github.com/bluewave-labs/bluewave-uptime/discussions) page! Feel free to ask questions or share your ideas—we'd love to hear from you! +If you have any questions, suggestions or comments, you have several options: -## 🧩 Features +- [Discord channel](https://discord.gg/NAb6H3UTjK) +- [GitHub Discussions](https://github.com/bluewave-labs/bluewave-uptime/discussions) +- [Reddit group](https://www.reddit.com/r/CheckmateMonitoring/) + +Feel free to ask questions or share your ideas - we'd love to hear from you! + +## Features - Completely open source, deployable on your servers or home devices (e.g Raspberry Pi 4 or 5) - Website monitoring - Page speed monitoring -- Infrastructure monitoring (memory, disk usage, CPU performance etc) - requires [Capture](https://github.com/bluewave-labs/capture) +- Infrastructure monitoring (memory, disk usage, CPU performance etc) - requires [Capture](https://github.com/bluewave-labs/capture) agent - Docker monitoring - Ping monitoring - SSL monitoring @@ -97,7 +102,7 @@ If you have any questions, suggestions or comments, please use our [Discord chan - Network monitoring - ..and a few more features -## 🏗️ Screenshots +## Screenshots

image @@ -114,7 +119,7 @@ If you have any questions, suggestions or comments, please use our [Discord chan -## 🏗️ Tech stack +## Tech stack - [ReactJs](https://react.dev/) - [MUI (React framework)](https://mui.com/) @@ -130,11 +135,11 @@ If you have any questions, suggestions or comments, please use our [Discord chan - Need a ping when there's a new release? Use [Newreleases](https://newreleases.io/), a free service to track releases. - Watch a Checkmate [installation and usage video](https://www.youtube.com/watch?v=GfFOc0xHIwY) -## 🤝 Contributing +## Contributing We are [Alex](http://github.com/ajhollid) (team lead), [Vishnu](http://github.com/vishnusn77), [Mohadeseh](http://github.com/mohicody), [Gorkem](http://github.com/gorkem-bwl/), [Owaise](http://github.com/Owaiseimdad), [Aryaman](https://github.com/Br0wnHammer) and [Mert](https://github.com/mertssmnoglu) helping individuals and businesses monitor their infra and servers. -We pride ourselves on building strong connections with contributors at every level. Despite being a young project, Checkmate has already earned 6000+ stars and attracted 80+ contributors from around the globe. +We pride ourselves on building strong connections with contributors at every level. Despite being a young project, Checkmate has already earned 7000+ stars and attracted 90+ contributors from around the globe. Our repo is starred by employees from **Google, Microsoft, Intel, Cisco, Tencent, Electronic Arts, ByteDance, JP Morgan Chase, Deloitte, Accenture, Foxconn, Broadcom, China Telecom, Barclays, Capgemini, Wipro, Cloudflare, Dassault Systèmes and NEC**, so don’t hold back — jump in, contribute and learn with us! @@ -154,7 +159,7 @@ Here's how you can contribute: [![Star History Chart](https://api.star-history.com/svg?repos=bluewave-labs/checkmate&type=Date)](https://star-history.com/#bluewave-labs/bluewave-uptime&Date) -## 💰 Our sponsors +## Our sponsors Thanks to [Gitbook](https://gitbook.io/) for giving us a free tier for their documentation platform, and [Poeditor](https://poeditor.com/) providing us a free account to use their i18n services. If you would like to sponsor Checkmate, please send an email to hello@bluewavelabs.ca diff --git a/charts/helm/checkmate/templates/secrets.yaml b/charts/helm/checkmate/templates/secrets.yaml index 42e7c6047..0a3f1ace5 100644 --- a/charts/helm/checkmate/templates/secrets.yaml +++ b/charts/helm/checkmate/templates/secrets.yaml @@ -14,6 +14,6 @@ metadata: name: checkmate-secrets type: Opaque stringData: -{{- range $key, $value := := $secrets }} +{{- range $key, $value := $secrets }} {{ $key }}: {{ $value | quote }} {{- end }} \ No newline at end of file diff --git a/charts/helm/checkmate/templates/server-deployment.yaml b/charts/helm/checkmate/templates/server-deployment.yaml index 8548495dd..6f21f86f3 100644 --- a/charts/helm/checkmate/templates/server-deployment.yaml +++ b/charts/helm/checkmate/templates/server-deployment.yaml @@ -20,11 +20,3 @@ spec: envFrom: - secretRef: name: checkmate-secrets - volumeMounts: - - name: docker-sock - mountPath: /var/run/docker.sock - volumes: - - name: docker-sock - hostPath: - path: /var/run/docker.sock - type: Socket diff --git a/charts/helm/checkmate/templates/server-ingress.yaml b/charts/helm/checkmate/templates/server-ingress.yaml index acf64f95f..c1982d451 100644 --- a/charts/helm/checkmate/templates/server-ingress.yaml +++ b/charts/helm/checkmate/templates/server-ingress.yaml @@ -5,6 +5,11 @@ metadata: name: server-ingress annotations: nginx.ingress.kubernetes.io/rewrite-target: / + nginx.ingress.kubernetes.io/enable-cors: "true" + nginx.ingress.kubernetes.io/cors-allow-origin: "http://{{ .Values.client.ingress.host }},https://{{ .Values.client.ingress.host }}" + nginx.ingress.kubernetes.io/cors-allow-methods: "GET, PUT, POST, DELETE, PATCH, OPTIONS" + nginx.ingress.kubernetes.io/cors-allow-headers: "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization" + nginx.ingress.kubernetes.io/cors-allow-credentials: "true" spec: rules: - host: {{ .Values.server.ingress.host }} diff --git a/client/src/Components/Charts/CustomGauge/index.jsx b/client/src/Components/Charts/CustomGauge/index.jsx index 3c7e13610..d9718d72c 100644 --- a/client/src/Components/Charts/CustomGauge/index.jsx +++ b/client/src/Components/Charts/CustomGauge/index.jsx @@ -1,8 +1,10 @@ import { useTheme } from "@emotion/react"; import { useEffect, useState, useMemo } from "react"; +import Stack from "@mui/material/Stack"; import { Box, Typography } from "@mui/material"; import PropTypes from "prop-types"; import "./index.css"; +import CircularProgress from "@mui/material/CircularProgress"; const MINIMUM_VALUE = 0; const MAXIMUM_VALUE = 100; @@ -16,6 +18,8 @@ const MAXIMUM_VALUE = 100; * @param {number} [props.radius=60] - Radius of the gauge circle * @param {number} [props.strokeWidth=15] - Width of the gauge stroke * @param {number} [props.threshold=50] - Threshold for color change + * @param {number} [props.precision=1] - Precision of the progress percentage + * @param {string} [props.unit="%"] - Unit of progress * * @example * { +const CustomGauge = ({ + isLoading = false, + progress = 0, + radius = 70, + strokeWidth = 15, + threshold = 50, + precision = 1, + unit = "%", +}) => { const theme = useTheme(); // Calculate the length of the stroke for the circle const { circumference, totalSize, strokeLength } = useMemo( @@ -54,8 +66,22 @@ const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, threshold = const fillColor = progressWithinRange > threshold - ? theme.palette.error.lowContrast // CAIO_REVIEW - : theme.palette.accent.main; // CAIO_REVIEW + ? theme.palette.error.lowContrast + : theme.palette.accent.main; + + if (isLoading) { + return ( + + + + ); + } return ( - {`${progressWithinRange.toFixed(1)}%`} + {`${progressWithinRange.toFixed(precision)}${unit}`} ); @@ -112,8 +138,11 @@ const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, threshold = export default CustomGauge; CustomGauge.propTypes = { + isLoading: PropTypes.bool, progress: PropTypes.number, radius: PropTypes.number, strokeWidth: PropTypes.number, threshold: PropTypes.number, + precision: PropTypes.number, + unit: PropTypes.string, }; diff --git a/client/src/Components/ConfigBox/index.jsx b/client/src/Components/ConfigBox/index.jsx index 44ae38472..e26e49e8d 100644 --- a/client/src/Components/ConfigBox/index.jsx +++ b/client/src/Components/ConfigBox/index.jsx @@ -4,15 +4,14 @@ const ConfigBox = styled(Stack)(({ theme }) => ({ display: "flex", flexDirection: "row", justifyContent: "space-between", - gap: theme.spacing(20), backgroundColor: theme.palette.primary.main, border: 1, borderStyle: "solid", borderColor: theme.palette.primary.lowContrast, borderRadius: theme.spacing(2), "& > *": { - paddingTop: theme.spacing(12), - paddingBottom: theme.spacing(18), + paddingTop: theme.spacing(15), + paddingBottom: theme.spacing(15), }, "& > div:first-of-type": { flex: 0.7, @@ -25,7 +24,7 @@ const ConfigBox = styled(Stack)(({ theme }) => ({ "& > div:last-of-type": { flex: 1, paddingRight: theme.spacing(20), - paddingLeft: theme.spacing(18), + paddingLeft: theme.spacing(20), }, "& h1, & h2": { color: theme.palette.primary.contrastTextSecondary, diff --git a/client/src/Components/Fallback/FallBackActionButtons.jsx b/client/src/Components/Fallback/FallBackActionButtons.jsx new file mode 100644 index 000000000..fbe8fe807 --- /dev/null +++ b/client/src/Components/Fallback/FallBackActionButtons.jsx @@ -0,0 +1,43 @@ +import { Button, Stack } from "@mui/material"; +import { useTheme } from "@mui/material/styles"; +import { useNavigate } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import PropTypes from "prop-types"; + +const FallbackActionButtons = ({ link, type }) => { + const theme = useTheme(); + const navigate = useNavigate(); + const { t } = useTranslation(); + + return ( + + + {type === "uptimeMonitor" && ( + + )} + + ); +}; +FallbackActionButtons.propTypes = { + title: PropTypes.string.isRequired, + link: PropTypes.string.isRequired, + type: PropTypes.string.isRequired, +}; + +export default FallbackActionButtons; diff --git a/client/src/Components/Fallback/FallbackBackground.jsx b/client/src/Components/Fallback/FallbackBackground.jsx new file mode 100644 index 000000000..e01659f49 --- /dev/null +++ b/client/src/Components/Fallback/FallbackBackground.jsx @@ -0,0 +1,31 @@ +import { useTheme } from "@emotion/react"; +import Box from "@mui/material/Box"; +import Skeleton from "../../assets/Images/create-placeholder.svg?react"; +import Background from "../../assets/Images/background-grid.svg?react"; +import SkeletonDark from "../../assets/Images/create-placeholder-dark.svg?react"; +import { useSelector } from "react-redux"; +const FallbackBackground = () => { + const theme = useTheme(); + const mode = useSelector((state) => state.ui.mode); + return ( + <> + {mode === "light" ? ( + + ) : ( + + )} + + + + + ); +}; + +export default FallbackBackground; diff --git a/client/src/Components/Fallback/FallbackCheckList.jsx b/client/src/Components/Fallback/FallbackCheckList.jsx new file mode 100644 index 000000000..4d13dead4 --- /dev/null +++ b/client/src/Components/Fallback/FallbackCheckList.jsx @@ -0,0 +1,35 @@ +import PropTypes from "prop-types"; +import { useTheme } from "@mui/material/styles"; +import Box from "@mui/material/Box"; +import Stack from "@mui/material/Stack"; +import Check from "../Check/Check"; +const FallbackCheckList = ({ checks, type }) => { + const theme = useTheme(); + return ( + + {checks?.map((check, index) => ( + + ))} + + ); +}; + +FallbackCheckList.propTypes = { + checks: PropTypes.arrayOf(PropTypes.string).isRequired, + title: PropTypes.string.isRequired, + type: PropTypes.string.isRequired, +}; + +export default FallbackCheckList; diff --git a/client/src/Components/Fallback/FallbackContainer.jsx b/client/src/Components/Fallback/FallbackContainer.jsx new file mode 100644 index 000000000..65d927190 --- /dev/null +++ b/client/src/Components/Fallback/FallbackContainer.jsx @@ -0,0 +1,44 @@ +import { useTheme } from "@emotion/react"; +import { Box, Stack } from "@mui/material"; +import PropTypes from "prop-types"; + +const FallbackContainer = ({ children, type }) => { + const theme = useTheme(); + return ( + + + {children} + + + ); +}; + +FallbackContainer.propTypes = { + children: PropTypes.node, + type: PropTypes.string, +}; + +export default FallbackContainer; diff --git a/client/src/Components/Fallback/FallbackPageSpeedWarning.jsx b/client/src/Components/Fallback/FallbackPageSpeedWarning.jsx new file mode 100644 index 000000000..bcec7300d --- /dev/null +++ b/client/src/Components/Fallback/FallbackPageSpeedWarning.jsx @@ -0,0 +1,69 @@ +import Box from "@mui/material/Box"; +import { useTheme } from "@mui/material/styles"; +import Link from "@mui/material/Link"; +import { Link as RouterLink } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import Alert from "../Alert"; +import PropTypes from "prop-types"; + +const renderWarningMessage = (t) => { + return ( + <> + {t("pageSpeedWarning")}{" "} + + {t("pageSpeedLearnMoreLink")} + {" "} + {t("pageSpeedAddApiKey")} + + ); +}; + +const FallbackPageSpeedWarning = ({ settingsData }) => { + const theme = useTheme(); + const { t } = useTranslation(); + return ( + + svg": { + color: theme.palette.warningSecondary.contrastText, + }, + }, + }} + > + {settingsData?.pagespeedKeySet === false && ( + + )} + + + ); +}; + +FallbackPageSpeedWarning.propTypes = { + settingsData: PropTypes.shape({ + pagespeedKeySet: PropTypes.bool, + }), +}; + +export default FallbackPageSpeedWarning; diff --git a/client/src/Components/Fallback/FallbackTitle.jsx b/client/src/Components/Fallback/FallbackTitle.jsx new file mode 100644 index 000000000..f7f42f2ca --- /dev/null +++ b/client/src/Components/Fallback/FallbackTitle.jsx @@ -0,0 +1,21 @@ +import { useTheme } from "@mui/material/styles"; +import { Typography } from "@mui/material"; +import PropTypes from "prop-types"; + +const FallbackTitle = ({ title }) => { + const theme = useTheme(); + return ( + + {title} + + ); +}; +FallbackTitle.propTypes = { + title: PropTypes.string.isRequired, +}; +export default FallbackTitle; diff --git a/client/src/Components/Fallback/index.css b/client/src/Components/Fallback/index.css index 357988b07..6988fa5bf 100644 --- a/client/src/Components/Fallback/index.css +++ b/client/src/Components/Fallback/index.css @@ -38,7 +38,3 @@ background-size: cover; background-repeat: no-repeat; } - -.fallback__status > .MuiStack-root { - margin-left: var(--env-var-spacing-2); -} diff --git a/client/src/Components/Fallback/index.jsx b/client/src/Components/Fallback/index.jsx index dd6043697..5fbb958e7 100644 --- a/client/src/Components/Fallback/index.jsx +++ b/client/src/Components/Fallback/index.jsx @@ -1,72 +1,26 @@ import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; -import { Box, Button, Stack, Typography, Link } from "@mui/material"; -import { Link as RouterLink } from "react-router-dom"; -import Skeleton from "../../assets/Images/create-placeholder.svg?react"; -import SkeletonDark from "../../assets/Images/create-placeholder-dark.svg?react"; -import Background from "../../assets/Images/background-grid.svg?react"; -import Check from "../Check/Check"; -import { useNavigate } from "react-router-dom"; -import { useSelector } from "react-redux"; -import Alert from "../Alert"; -import { useTranslation } from "react-i18next"; +import { Box, Stack } from "@mui/material"; import "./index.css"; -import { useFetchSettings } from "../../Hooks/settingsHooks"; -import { useState } from "react"; +import FallbackTitle from "./FallbackTitle"; +import FallbackCheckList from "./FallbackCheckList"; +import FallbackActionButtons from "./FallBackActionButtons"; +import FallbackBackground from "./FallbackBackground"; +import FallbackContainer from "./FallbackContainer"; /** * Fallback component to display a fallback UI with a title, a list of checks, and a navigation button. * * @param {Object} props - The component props. * @param {string} props.title - The title to be displayed in the fallback UI. + * @param {string} props.type - The type of the fallback (e.g., "pageSpeed", "notifications"). * @param {Array} props.checks - An array of strings representing the checks to display. * @param {string} [props.link="/"] - The link to navigate to. - * @param {boolean} [props.vowelStart=false] - Whether the title starts with a vowel. * @param {boolean} [props.showPageSpeedWarning=false] - Whether to show the PageSpeed API warning. * @returns {JSX.Element} The rendered fallback UI. */ -const Fallback = ({ - title, - checks, - link = "/", - isAdmin, - vowelStart = false, - showPageSpeedWarning = false, -}) => { +const Fallback = ({ title, checks, link = "/", isAdmin, type, children }) => { const theme = useTheme(); - const navigate = useNavigate(); - const mode = useSelector((state) => state.ui.mode); - const { t } = useTranslation(); - const [settingsData, setSettingsData] = useState(undefined); - - const [isLoading, error] = useFetchSettings({ - setSettingsData, - setIsApiKeySet: () => {}, - setIsEmailPasswordSet: () => {}, - }); - // Custom warning message with clickable link - const renderWarningMessage = () => { - return ( - <> - {t("pageSpeedWarning")}{" "} - - {t("pageSpeedLearnMoreLink")} - {" "} - {t("pageSpeedAddApiKey")} - - ); - }; return ( - + + - {mode === "light" ? ( - - ) : ( - - )} - - - - - - {vowelStart ? "An" : "A"} {title} is used to: - - {checks?.map((check, index) => ( - - ))} - - {/* TODO - display a different fallback if user is not an admin*/} - {isAdmin && ( - - - {/* Bulk create of uptime monitors */} - {title === "uptime monitor" && ( - - )} - - {/* Warning box for PageSpeed monitor */} - {title === "pagespeed monitor" && showPageSpeedWarning && ( - - svg": { - color: theme.palette.warningSecondary.contrastText, - }, - }, - }} - > - {settingsData?.pagespeedKeySet === false && ( - - )} - - - )} - - )} + + - + {isAdmin && ( + + + {children} + + )} + ); }; - Fallback.propTypes = { title: PropTypes.string.isRequired, checks: PropTypes.arrayOf(PropTypes.string).isRequired, link: PropTypes.string, isAdmin: PropTypes.bool, - vowelStart: PropTypes.bool, showPageSpeedWarning: PropTypes.bool, + type: PropTypes.string.isRequired, + children: PropTypes.node, }; - export default Fallback; diff --git a/client/src/Components/Inputs/FieldWrapper/index.jsx b/client/src/Components/Inputs/FieldWrapper/index.jsx new file mode 100644 index 000000000..cc5061af8 --- /dev/null +++ b/client/src/Components/Inputs/FieldWrapper/index.jsx @@ -0,0 +1,51 @@ +import { Stack, Typography } from "@mui/material"; +import PropTypes from "prop-types"; +import { useTheme } from "@emotion/react"; + +const DEFAULT_GAP = 6; +const FieldWrapper = ({ + label, + children, + gap, + labelMb, + labelFontWeight = 500, + labelVariant = "h3", + labelSx = {}, + sx = {}, +}) => { + const theme = useTheme(); + return ( + + {label && ( + + {label} + + )} + {children} + + ); +}; + +FieldWrapper.propTypes = { + label: PropTypes.node, + children: PropTypes.node.isRequired, + gap: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.object]), + labelMb: PropTypes.number, + labelFontWeight: PropTypes.number, + labelVariant: PropTypes.string, + labelSx: PropTypes.object, + sx: PropTypes.object, +}; + +export default FieldWrapper; diff --git a/client/src/Components/Inputs/Radio/index.jsx b/client/src/Components/Inputs/Radio/index.jsx index 646b38e93..4c687de1e 100644 --- a/client/src/Components/Inputs/Radio/index.jsx +++ b/client/src/Components/Inputs/Radio/index.jsx @@ -24,7 +24,17 @@ import "./index.css"; * @returns {JSX.Element} - The rendered Radio component. */ -const Radio = ({ name, checked, value, id, size, title, desc, onChange }) => { +const Radio = ({ + name, + checked, + value, + id, + size, + title, + desc, + onChange, + labelSpacing, +}) => { const theme = useTheme(); return ( @@ -53,7 +63,14 @@ const Radio = ({ name, checked, value, id, size, title, desc, onChange }) => { onChange={onChange} label={ <> - {title} + + {title} + { ); }; -//TODO keep search state inside of component +//TODO keep search state inside of component. const Search = ({ label, id, @@ -68,6 +70,14 @@ const Search = ({ startAdornment, endAdornment, onBlur, + //FieldWrapper's props + gap, + labelMb, + labelFontWeight, + labelVariant, + labelSx = {}, + unit = "option", + maxWidth = "100%", }) => { const theme = useTheme(); const { t } = useTranslation(); @@ -139,15 +149,17 @@ const Search = ({ getOptionLabel={(option) => option[filteredBy]} isOptionEqualToValue={(option, value) => option._id === value._id} // Compare by unique identifier renderInput={(params) => ( - - - {label} - + )} - + )} filterOptions={(options, { inputValue }) => { if (inputValue.trim() === "" && multiple && isAdorned) { @@ -186,7 +198,12 @@ const Search = ({ ); if (filtered.length === 0) { - return [{ [filteredBy]: "No monitors found", noOptions: true }]; + return [ + { + [filteredBy]: t("general.noOptionsFound", { unit: unit }), + noOptions: true, + }, + ]; } return filtered; }} @@ -281,7 +298,7 @@ Search.propTypes = { options: PropTypes.array.isRequired, filteredBy: PropTypes.string.isRequired, secondaryLabel: PropTypes.string, - value: PropTypes.array, + value: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), inputValue: PropTypes.string.isRequired, handleInputChange: PropTypes.func.isRequired, handleChange: PropTypes.func, @@ -292,6 +309,7 @@ Search.propTypes = { startAdornment: PropTypes.object, endAdornment: PropTypes.object, onBlur: PropTypes.func, + unit: PropTypes.string, }; export default Search; diff --git a/client/src/Components/Inputs/Select/index.jsx b/client/src/Components/Inputs/Select/index.jsx index 679a5b3b5..a438b6bb2 100644 --- a/client/src/Components/Inputs/Select/index.jsx +++ b/client/src/Components/Inputs/Select/index.jsx @@ -2,6 +2,7 @@ import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; import { MenuItem, Select as MuiSelect, Stack, Typography } from "@mui/material"; import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; +import FieldWrapper from "../FieldWrapper"; import "./index.css"; @@ -50,8 +51,14 @@ const Select = ({ onBlur, sx, name = "", - labelControlSpacing = 2, + labelControlSpacing = 6, maxWidth, + //FieldWrapper's props + labelMb, + labelFontWeight, + labelVariant, + labelSx = {}, + fieldWrapperSx = {}, }) => { const theme = useTheme(); const itemStyles = { @@ -69,20 +76,17 @@ const Select = ({ }; return ( - - {label && ( - - {label} - - )} { @@ -151,7 +162,7 @@ const Select = ({ ))} - + ); }; diff --git a/client/src/Components/Inputs/TextInput/index.jsx b/client/src/Components/Inputs/TextInput/index.jsx index bb2df5d21..469791704 100644 --- a/client/src/Components/Inputs/TextInput/index.jsx +++ b/client/src/Components/Inputs/TextInput/index.jsx @@ -2,6 +2,7 @@ import { Stack, TextField, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; import { forwardRef, useState, cloneElement } from "react"; import PropTypes from "prop-types"; +import FieldWrapper from "../FieldWrapper"; const getSx = (theme, type, maxWidth) => { const sx = { @@ -85,31 +86,43 @@ const TextInput = forwardRef( marginLeft, disabled = false, hidden = false, + //FieldWrapper's props + gap, + labelMb, + labelFontWeight, + labelVariant, + labelSx = {}, + sx = {}, }, ref ) => { const [fieldType, setFieldType] = useState(type); const theme = useTheme(); + const labelContent = label && ( + <> + {label} + {isRequired && } + {isOptional && } + + ); return ( - + ); } ); diff --git a/client/src/Components/LanguageSelector.jsx b/client/src/Components/LanguageSelector.jsx index 284d0db05..56a9a898d 100644 --- a/client/src/Components/LanguageSelector.jsx +++ b/client/src/Components/LanguageSelector.jsx @@ -6,6 +6,13 @@ import { useSelector } from "react-redux"; import { useDispatch } from "react-redux"; import { setLanguage } from "../Features/UI/uiSlice"; +const langMap = { + cs: "cz", + ja: "jp", + uk: "ua", + vi: "vn", +}; + const LanguageSelector = () => { const { i18n } = useTranslation(); const theme = useTheme(); @@ -28,14 +35,12 @@ const LanguageSelector = () => { {languages.map((lang) => { let parsedLang = lang === "en" ? "gb" : lang; - // Fix for Czech - if (parsedLang === "cs") { - parsedLang = "cz"; - } if (parsedLang.includes("-")) { parsedLang = parsedLang.split("-")[1].toLowerCase(); } + parsedLang = langMap[parsedLang] || parsedLang; + const flag = parsedLang ? `fi fi-${parsedLang}` : null; return ( diff --git a/client/src/Components/NotificationConfig/index.jsx b/client/src/Components/NotificationConfig/index.jsx index d3707eae4..c37889eff 100644 --- a/client/src/Components/NotificationConfig/index.jsx +++ b/client/src/Components/NotificationConfig/index.jsx @@ -10,7 +10,18 @@ import { useState, useEffect } from "react"; import { useTheme } from "@mui/material/styles"; import PropTypes from "prop-types"; -const NotificationConfig = ({ notifications, setMonitor, setNotifications }) => { +const NotificationConfig = ({ + notifications, + setMonitor, + setNotifications, + //FieldWrapper's props + gap, + labelMb, + labelFontWeight, + labelVariant, + labelSx = {}, + sx = {}, +}) => { // Local state const [notificationsSearch, setNotificationsSearch] = useState(""); const [selectedNotifications, setSelectedNotifications] = useState([]); @@ -66,6 +77,14 @@ const NotificationConfig = ({ notifications, setMonitor, setNotifications }) => handleChange={(value) => { handleSearch(value); }} + labelMb={labelMb} + labelVariant={labelVariant} + labelFontWeight={labelFontWeight} + labelSx={labelSx} + gap={gap} + sx={{ + ...sx, + }} /> { return ( { + const theme = useTheme(); + + if (Array.isArray(body)) { + return ( + + {body.map((item, idx) => ( + + {item} + + ))} + + ); + } else if (typeof body === "string") { + return {body}; + } + + return null; +}; + +ToastBody.propTypes = { + body: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), +}; + +export default ToastBody; diff --git a/client/src/Components/Toast/index.jsx b/client/src/Components/Toast/index.jsx new file mode 100644 index 000000000..36df0e631 --- /dev/null +++ b/client/src/Components/Toast/index.jsx @@ -0,0 +1,96 @@ +import Stack from "@mui/material/Stack"; +import IconButton from "@mui/material/IconButton"; +import Typography from "@mui/material/Typography"; +import Button from "@mui/material/Button"; +import ToastBody from "./body"; +import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; +import ErrorOutlineOutlinedIcon from "@mui/icons-material/ErrorOutlineOutlined"; +import WarningAmberOutlinedIcon from "@mui/icons-material/WarningAmberOutlined"; +import CloseIcon from "@mui/icons-material/Close"; + +// Utils +import { useTheme } from "@emotion/react"; +import PropTypes from "prop-types"; + +const icons = { + info: , + error: , + warning: , +}; + +const Toast = ({ variant, title, body, onClick, hasDismiss, hasIcon }) => { + const theme = useTheme(); + const icon = icons[variant]; + + return ( + + + {hasIcon && icon} + {title && ( + + {title} + + )} + {title && ( + + + + )} + + + + + {!title && ( + + + + )} + + {hasDismiss && ( + + )} + + ); +}; + +export default Toast; + +Toast.propTypes = { + variant: PropTypes.string.isRequired, + title: PropTypes.string, + body: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), + hasDismiss: PropTypes.bool, + hasIcon: PropTypes.bool, + onClick: PropTypes.func, +}; diff --git a/client/src/Features/Auth/authSlice.js b/client/src/Features/Auth/authSlice.js index 2fde49202..5ee31944a 100644 --- a/client/src/Features/Auth/authSlice.js +++ b/client/src/Features/Auth/authSlice.js @@ -44,7 +44,6 @@ export const login = createAsyncThunk("auth/login", async (form, thunkApi) => { export const update = createAsyncThunk("auth/update", async (data, thunkApi) => { const { localData: form } = data; - const user = thunkApi.getState().auth.user; try { const fd = new FormData(); form.firstName && fd.append("firstName", form.firstName); @@ -60,7 +59,6 @@ export const update = createAsyncThunk("auth/update", async (data, thunkApi) => form.deleteProfileImage && fd.append("deleteProfileImage", form.deleteProfileImage); const res = await networkService.updateUser({ - userId: user._id, form: fd, }); return res.data; @@ -77,10 +75,8 @@ export const update = createAsyncThunk("auth/update", async (data, thunkApi) => }); export const deleteUser = createAsyncThunk("auth/delete", async (_, thunkApi) => { - const user = thunkApi.getState().auth.user; - try { - const res = await networkService.deleteUser({ userId: user._id }); + const res = await networkService.deleteUser(); return res.data; } catch (error) { if (error.response && error.response.data) { diff --git a/client/src/Hooks/checkHooks.js b/client/src/Hooks/checkHooks.js index f23e1eaea..bdb1a73b7 100644 --- a/client/src/Hooks/checkHooks.js +++ b/client/src/Hooks/checkHooks.js @@ -185,23 +185,30 @@ const useResolveIncident = () => { return [resolveIncident, isLoading]; }; -const useAckAllChecks = () => { +const useAcknowledgeChecks = () => { const [isLoading, setIsLoading] = useState(false); const { t } = useTranslation(); - const ackAllChecks = async (setUpdateTrigger) => { + const acknowledge = async (setUpdateTrigger, monitorId = null) => { + setIsLoading(true); try { - setIsLoading(true); - await networkService.updateAllChecksStatus({ ack: true }); + if (monitorId) { + await networkService.updateMonitorChecksStatus({ monitorId, ack: true }); + } else { + await networkService.updateAllChecksStatus({ ack: true }); + } setUpdateTrigger((prev) => !prev); } catch (error) { - createToast({ body: t("checkHooks.failureResolveAll") }); + const toastMessage = monitorId + ? t("checkHooks.failureResolveMonitor") + : t("checkHooks.failureResolveAll"); + createToast({ body: toastMessage }); } finally { setIsLoading(false); } }; - return [ackAllChecks, isLoading]; + return { acknowledge, isLoading }; }; export { @@ -209,5 +216,5 @@ export { useFetchChecksTeam, useFetchChecksSummaryByTeamId, useResolveIncident, - useAckAllChecks, + useAcknowledgeChecks, }; diff --git a/client/src/Hooks/logHooks.js b/client/src/Hooks/logHooks.js index 000e4d0f0..b954efb4b 100644 --- a/client/src/Hooks/logHooks.js +++ b/client/src/Hooks/logHooks.js @@ -32,4 +32,83 @@ const useFetchLogs = () => { return [logs, isLoading, error]; }; -export { useFetchLogs }; +const useFetchQueueData = (trigger) => { + const [jobs, setJobs] = useState(undefined); + const [metrics, setMetrics] = useState(undefined); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(undefined); + + useEffect(() => { + const fetchJobs = async () => { + try { + setIsLoading(true); + const response = await networkService.getQueueData(); + if (response.status === 200) { + setJobs(response.data.data.jobs); + setMetrics(response.data.data.metrics); + } + } catch (error) { + setError(error); + } finally { + setIsLoading(false); + } + }; + + fetchJobs(); + }, [trigger]); + + return [jobs, metrics, isLoading, error]; +}; + +const useFlushQueue = () => { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(undefined); + + const flushQueue = async (trigger, setTrigger) => { + try { + setIsLoading(true); + await networkService.flushQueue(); + createToast({ + body: "Queue flushed", + }); + } catch (error) { + setError(error); + createToast({ + body: error.message, + }); + } finally { + setIsLoading(false); + setTrigger(!trigger); + } + }; + return [flushQueue, isLoading, error]; +}; + +const useFetchDiagnostics = () => { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(undefined); + const [diagnostics, setDiagnostics] = useState(undefined); + + const fetchDiagnostics = async () => { + try { + setIsLoading(true); + const response = await networkService.getDiagnostics(); + setDiagnostics(response.data.data); + } catch (error) { + createToast({ + body: error.message, + }); + setError(error); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchDiagnostics(); + }, []); + + return [diagnostics, fetchDiagnostics, isLoading, error]; +}; + +export { useFetchLogs, useFetchQueueData, useFlushQueue, useFetchDiagnostics }; diff --git a/client/src/Hooks/queueHooks.js b/client/src/Hooks/queueHooks.js deleted file mode 100644 index f7a25b3d3..000000000 --- a/client/src/Hooks/queueHooks.js +++ /dev/null @@ -1,57 +0,0 @@ -import { useState, useEffect } from "react"; -import { networkService } from "../main"; -import { createToast } from "../Utils/toastUtils"; - -const useFetchQueueData = (trigger) => { - const [jobs, setJobs] = useState(undefined); - const [metrics, setMetrics] = useState(undefined); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(undefined); - - useEffect(() => { - const fetchJobs = async () => { - try { - setIsLoading(true); - const response = await networkService.getQueueData(); - if (response.status === 200) { - setJobs(response.data.data.jobs); - setMetrics(response.data.data.metrics); - } - } catch (error) { - setError(error); - } finally { - setIsLoading(false); - } - }; - - fetchJobs(); - }, [trigger]); - - return [jobs, metrics, isLoading, error]; -}; - -const useFlushQueue = () => { - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(undefined); - - const flushQueue = async (trigger, setTrigger) => { - try { - setIsLoading(true); - await networkService.flushQueue(); - createToast({ - body: "Queue flushed", - }); - } catch (error) { - setError(error); - createToast({ - body: error.message, - }); - } finally { - setIsLoading(false); - setTrigger(!trigger); - } - }; - return [flushQueue, isLoading, error]; -}; - -export { useFetchQueueData, useFlushQueue }; diff --git a/client/src/Hooks/useIsAdmin.js b/client/src/Hooks/useIsAdmin.js index 7d593b874..8abf78ac9 100644 --- a/client/src/Hooks/useIsAdmin.js +++ b/client/src/Hooks/useIsAdmin.js @@ -8,4 +8,10 @@ const useIsAdmin = () => { return isAdmin; }; -export { useIsAdmin }; +const useIsSuperAdmin = () => { + const { user } = useSelector((state) => state.auth); + const isSuperAdmin = user?.role?.includes("superadmin"); + return isSuperAdmin; +}; + +export { useIsAdmin, useIsSuperAdmin }; diff --git a/client/src/Hooks/useMonitorUtils.js b/client/src/Hooks/useMonitorUtils.js index 417c6eadd..b57c11a12 100644 --- a/client/src/Hooks/useMonitorUtils.js +++ b/client/src/Hooks/useMonitorUtils.js @@ -6,18 +6,18 @@ const useMonitorUtils = () => { let uptimePercentage = ""; let percentageColor = ""; - if (monitor.uptimePercentage !== undefined) { + if (monitor?.uptimePercentage !== undefined) { uptimePercentage = - monitor.uptimePercentage === 0 + monitor?.uptimePercentage === 0 ? "0" - : (monitor.uptimePercentage * 100).toFixed(2); + : (monitor?.uptimePercentage * 100).toFixed(2); percentageColor = - monitor.uptimePercentage < 0.25 + monitor?.uptimePercentage < 0.25 ? theme.palette.error.main - : monitor.uptimePercentage < 0.5 + : monitor?.uptimePercentage < 0.5 ? theme.palette.warning.main - : monitor.uptimePercentage < 0.75 + : monitor?.uptimePercentage < 0.75 ? theme.palette.success.main : theme.palette.success.main; } @@ -32,7 +32,7 @@ const useMonitorUtils = () => { const determineState = useCallback((monitor) => { if (typeof monitor === "undefined") return "pending"; - if (monitor.isActive === false) return "paused"; + if (monitor?.isActive === false) return "paused"; if (monitor?.status === undefined) return "pending"; return monitor?.status == true ? "up" : "down"; }, []); diff --git a/client/src/Hooks/userHooks.js b/client/src/Hooks/userHooks.js new file mode 100644 index 000000000..8be32a082 --- /dev/null +++ b/client/src/Hooks/userHooks.js @@ -0,0 +1,61 @@ +import { useEffect, useState, useCallback } from "react"; +import { networkService } from "../main"; +import { createToast } from "../Utils/toastUtils"; +import { useTranslation } from "react-i18next"; + +export const useGetUser = (userId) => { + const [user, setUser] = useState(undefined); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchUser = useCallback(async () => { + try { + setIsLoading(true); + const response = await networkService.getUserById({ userId }); + setUser(response?.data?.data); + } catch (error) { + createToast({ + body: error.message, + }); + setError(error); + } finally { + setIsLoading(false); + } + }, [userId]); + + useEffect(() => { + if (userId) { + fetchUser(); + } + }, [userId, fetchUser]); + + return [user, isLoading, error]; +}; + +export const useEditUser = (userId) => { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const { t } = useTranslation(); + + const editUser = useCallback( + async (user) => { + try { + setIsLoading(true); + await networkService.editUser({ userId, user }); + createToast({ + body: t("editUserPage.toast.successUserUpdate"), + }); + } catch (error) { + createToast({ + body: error.message, + }); + setError(error); + } finally { + setIsLoading(false); + } + }, + [userId] + ); + + return [editUser, isLoading, error]; +}; diff --git a/client/src/Pages/Account/EditUser/hooks/editUser.js b/client/src/Pages/Account/EditUser/hooks/editUser.js new file mode 100644 index 000000000..2ffa914a0 --- /dev/null +++ b/client/src/Pages/Account/EditUser/hooks/editUser.js @@ -0,0 +1,102 @@ +import { useState, useEffect } from "react"; +import { ROLES } from "../../../../Utils/roleUtils"; +import { editUserValidation } from "../../../../Validation/validation"; +import Joi from "joi"; +import { createToast } from "../../../../Utils/toastUtils"; +import { useTranslation } from "react-i18next"; +export const useEditUserForm = (user) => { + const [searchInput, setSearchInput] = useState(""); + const [form, setForm] = useState({ + firstName: "", + lastName: "", + email: "", + role: [], + }); + + // Effect to set user + useEffect(() => { + if (user) { + setForm({ + firstName: user?.firstName, + lastName: user?.lastName, + email: user?.email, + role: user?.role, + }); + } + }, [user]); + + const handleRoleChange = (value) => { + const hasSuperAdmin = form.role.includes(ROLES.SUPERADMIN); + const newRoles = value.map((item) => item.role); + if (hasSuperAdmin && !newRoles.includes(ROLES.SUPERADMIN)) { + newRoles.push(ROLES.SUPERADMIN); + } + + setForm({ + ...form, + role: newRoles, + }); + }; + + const handleDeleteRole = (role) => { + if (role === ROLES.SUPERADMIN) return; + setForm({ ...form, role: form?.role?.filter((r) => r !== role) }); + }; + + const handleSearchInput = (value) => { + setSearchInput(value); + }; + + return [ + form, + setForm, + handleRoleChange, + handleDeleteRole, + searchInput, + handleSearchInput, + ]; +}; + +export const useValidateEditUserForm = () => { + const [errors, setErrors] = useState({}); + const { t } = useTranslation(); + const validateForm = (form) => { + const { error } = editUserValidation.validate(form, { + abortEarly: false, + }); + const errs = error?.details; + const errsObj = errs?.reduce((acc, curr) => { + acc[curr.path[0]] = curr.message; + return acc; + }, {}); + setErrors(errsObj); + + if (errs?.length > 0) { + createToast({ + variant: "warning", + hasIcon: true, + title: t("editUserPage.toast.validationErrors"), + body: errs.map((err) => t(err.message)), + }); + return false; + } + return true; + }; + + const validateField = (name, value) => { + const fieldSchema = Joi.object({ + [name]: editUserValidation.extract(name), + }); + + const { error } = fieldSchema.validate({ [name]: value }); + + setErrors((prev) => { + const prevErrors = { ...prev }; + if (error) prevErrors[name] = error.details[0].message; + else delete prevErrors[name]; + return prevErrors; + }); + }; + + return [errors, validateForm, validateField]; +}; diff --git a/client/src/Pages/Account/EditUser/index.jsx b/client/src/Pages/Account/EditUser/index.jsx new file mode 100644 index 000000000..e1eeead9d --- /dev/null +++ b/client/src/Pages/Account/EditUser/index.jsx @@ -0,0 +1,122 @@ +// Components +import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import Breadcrumbs from "../../../Components/Breadcrumbs"; +import TextInput from "../../../Components/Inputs/TextInput"; +import Search from "../../../Components/Inputs/Search"; +import Button from "@mui/material/Button"; +import RoleTable from "../components/RoleTable"; + +// Utils +import { useParams } from "react-router-dom"; +import { useTheme } from "@emotion/react"; +import { useTranslation } from "react-i18next"; +import { useGetUser, useEditUser } from "../../../Hooks/userHooks"; +import { EDITABLE_ROLES, ROLES } from "../../../Utils/roleUtils"; +import { useEditUserForm, useValidateEditUserForm } from "./hooks/editUser"; + +const EditUser = () => { + const { userId } = useParams(); + const theme = useTheme(); + const { t } = useTranslation(); + const BREADCRUMBS = [ + { name: t("menu.team"), path: "/account/team" }, + { name: t("editUserPage.title"), path: "" }, + ]; + + const [user, isLoading, error] = useGetUser(userId); + const [editUser, isSaving, saveError] = useEditUser(userId); + const [ + form, + setForm, + handleRoleChange, + handleDeleteRole, + searchInput, + handleSearchInput, + ] = useEditUserForm(user); + const [errors, validateForm, validateField] = useValidateEditUserForm(); + + const onChange = (e) => { + const name = e.target.name; + const value = e.target.value; + validateField(name, value); + setForm({ ...form, [name]: value }); + }; + + const handleSubmit = (e) => { + e.preventDefault(); + const valid = validateForm(form); + if (valid) { + editUser(form); + } + }; + + return ( + + + {t("editUserPage.title")} + + + + + role !== ROLES.SUPERADMIN) + .map((role) => ({ role, _id: role })) || [] + } + options={EDITABLE_ROLES} + multiple={true} + handleChange={handleRoleChange} + /> + + + + + + + ); +}; + +export default EditUser; diff --git a/client/src/Components/TabPanels/Account/PasswordPanel.jsx b/client/src/Pages/Account/components/PasswordPanel.jsx similarity index 97% rename from client/src/Components/TabPanels/Account/PasswordPanel.jsx rename to client/src/Pages/Account/components/PasswordPanel.jsx index 81ce93946..dabd67706 100644 --- a/client/src/Components/TabPanels/Account/PasswordPanel.jsx +++ b/client/src/Pages/Account/components/PasswordPanel.jsx @@ -2,10 +2,10 @@ import TabPanel from "@mui/lab/TabPanel"; import { useState } from "react"; import { useTheme } from "@emotion/react"; import { Box, Stack, Typography, Button } from "@mui/material"; -import { PasswordEndAdornment } from "../../Inputs/TextInput/Adornments"; -import TextInput from "../../Inputs/TextInput"; +import { PasswordEndAdornment } from "../../../Components/Inputs/TextInput/Adornments"; +import TextInput from "../../../Components/Inputs/TextInput"; import { newOrChangedCredentials } from "../../../Validation/validation"; -import Alert from "../../Alert"; +import Alert from "../../../Components/Alert"; import { update } from "../../../Features/Auth/authSlice"; import { useDispatch, useSelector } from "react-redux"; import { createToast } from "../../../Utils/toastUtils"; diff --git a/client/src/Components/TabPanels/Account/ProfilePanel.jsx b/client/src/Pages/Account/components/ProfilePanel.jsx similarity index 97% rename from client/src/Components/TabPanels/Account/ProfilePanel.jsx rename to client/src/Pages/Account/components/ProfilePanel.jsx index 9b209bf3e..7c73e7aa3 100644 --- a/client/src/Components/TabPanels/Account/ProfilePanel.jsx +++ b/client/src/Pages/Account/components/ProfilePanel.jsx @@ -2,16 +2,16 @@ import { useTheme } from "@emotion/react"; import { useState } from "react"; import TabPanel from "@mui/lab/TabPanel"; import { Box, Button, Divider, Stack, Typography } from "@mui/material"; -import Avatar from "../../Avatar"; -import TextInput from "../../Inputs/TextInput"; -import ImageUpload from "../../Inputs/ImageUpload"; +import Avatar from "../../../Components/Avatar"; +import TextInput from "../../../Components/Inputs/TextInput"; +import ImageUpload from "../../../Components/Inputs/ImageUpload"; import { newOrChangedCredentials } from "../../../Validation/validation"; import { useDispatch, useSelector } from "react-redux"; import { clearAuthState, deleteUser, update } from "../../../Features/Auth/authSlice"; import { createToast } from "../../../Utils/toastUtils"; import { logger } from "../../../Utils/Logger"; -import { GenericDialog } from "../../Dialog/genericDialog"; -import Dialog from "../../Dialog"; +import { GenericDialog } from "../../../Components/Dialog/genericDialog"; +import Dialog from "../../../Components/Dialog"; import { useTranslation } from "react-i18next"; /** @@ -191,7 +191,6 @@ const ProfilePanel = () => { > { + const { t } = useTranslation(); + const HEADERS = [ + { + id: "name", + content: {t("editUserPage.table.roleHeader")}, + render: (row) => { + return row; + }, + }, + { + id: "delete", + content: {t("editUserPage.table.actionHeader")}, + render: (row) => { + if (row === ROLES.SUPERADMIN) return null; + return ( + { + handleDeleteRole(row); + }} + sx={{ cursor: "pointer" }} + /> + ); + }, + }, + ]; + return ( + + ); +}; + +export default RoleTable; diff --git a/client/src/Components/TabPanels/Account/TeamPanel.jsx b/client/src/Pages/Account/components/TeamPanel.jsx similarity index 92% rename from client/src/Components/TabPanels/Account/TeamPanel.jsx rename to client/src/Pages/Account/components/TeamPanel.jsx index 45578cc6c..95ece219f 100644 --- a/client/src/Components/TabPanels/Account/TeamPanel.jsx +++ b/client/src/Pages/Account/components/TeamPanel.jsx @@ -3,14 +3,16 @@ import TabPanel from "@mui/lab/TabPanel"; import { Button, ButtonGroup, Stack, Typography } from "@mui/material"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import TextInput from "../../Inputs/TextInput"; +import TextInput from "../../../Components/Inputs/TextInput"; import { newOrChangedCredentials } from "../../../Validation/validation"; import { networkService } from "../../../main"; import { createToast } from "../../../Utils/toastUtils"; -import Select from "../../Inputs/Select"; -import { GenericDialog } from "../../Dialog/genericDialog"; -import DataTable from "../../Table/"; +import Select from "../../../Components/Inputs/Select"; +import { GenericDialog } from "../../../Components/Dialog/genericDialog"; +import DataTable from "../../../Components/Table"; import { useGetInviteToken } from "../../../Hooks/inviteHooks"; +import { useNavigate } from "react-router-dom"; +import { useIsSuperAdmin } from "../../../Hooks/useIsAdmin"; /** * TeamPanel component manages the organization and team members, * providing functionalities like renaming the organization, managing team members, @@ -34,8 +36,9 @@ const TeamPanel = () => { const [isDisabled, setIsDisabled] = useState(true); const [errors, setErrors] = useState({}); const [isSendingInvite, setIsSendingInvite] = useState(false); - + const navigate = useNavigate(); const [getInviteToken, clearToken, isLoading, error, token] = useGetInviteToken(); + const isSuperAdmin = useIsSuperAdmin(); const headers = [ { @@ -184,7 +187,6 @@ const TeamPanel = () => { return ( { { + if (isSuperAdmin) { + navigate(`/account/team/${row.id}`); + } + }, + }} /> diff --git a/client/src/Pages/Account/index.css b/client/src/Pages/Account/index.css deleted file mode 100644 index 41e25fffa..000000000 --- a/client/src/Pages/Account/index.css +++ /dev/null @@ -1,38 +0,0 @@ -.account h1.MuiTypography-root, -.account p.MuiTypography-root, -.account input, -.account button, -.account td, -.account .MuiSelect-select { - font-size: var(--env-var-font-size-medium); -} -.account h1.MuiTypography-root { - font-weight: 600; -} -.account .MuiTabPanel-root { - padding: 0; - margin-top: 50px; -} -.account button:not(.MuiIconButton-root) { - min-height: 34px; -} -.account .field { - flex: 1; -} -#modal-delete-account, -#modal-edit-org-name, -#modal-invite-member { - font-size: var(--env-var-font-size-large); -} - -.account .MuiStack-root:has(span.MuiTypography-root.input-error) { - position: relative; -} -.account:not(:has(#modal-invite-member)) span.MuiTypography-root.input-error { - position: absolute; - top: 100%; -} - -.account .MuiTableBody-root .MuiTableCell-root { - padding: var(--env-var-spacing-1-plus) var(--env-var-spacing-2); -} diff --git a/client/src/Pages/Account/index.jsx b/client/src/Pages/Account/index.jsx index d8672f51c..37c64aca6 100644 --- a/client/src/Pages/Account/index.jsx +++ b/client/src/Pages/Account/index.jsx @@ -5,11 +5,10 @@ import { useSelector } from "react-redux"; import { Box, Tab, useTheme } from "@mui/material"; import CustomTabList from "../../Components/Tab"; import TabContext from "@mui/lab/TabContext"; -import ProfilePanel from "../../Components/TabPanels/Account/ProfilePanel"; -import PasswordPanel from "../../Components/TabPanels/Account/PasswordPanel"; -import TeamPanel from "../../Components/TabPanels/Account/TeamPanel"; +import ProfilePanel from "./components/ProfilePanel"; +import PasswordPanel from "./components/PasswordPanel"; +import TeamPanel from "./components/TeamPanel"; import { useTranslation } from "react-i18next"; -import "./index.css"; /** * Account component renders a settings page with tabs for Profile, Password, and Team settings. @@ -33,7 +32,9 @@ const Account = ({ open = "profile" }) => { { name: t("menu.password"), value: "password" }, { name: t("menu.team"), value: "team" }, ]; + const hideTeams = !requiredRoles.some((role) => user.role.includes(role)); + if (hideTeams) { tabList = [ { name: t("menu.profile"), value: "profile" }, @@ -63,7 +64,6 @@ const Account = ({ open = "profile" }) => { return ( diff --git a/client/src/Pages/Incidents/index.jsx b/client/src/Pages/Incidents/index.jsx index 7b4c22e41..7696c453b 100644 --- a/client/src/Pages/Incidents/index.jsx +++ b/client/src/Pages/Incidents/index.jsx @@ -11,7 +11,7 @@ import { Box, Button } from "@mui/material"; import { useTheme } from "@emotion/react"; import { useFetchMonitorsByTeamId } from "../../Hooks/monitorHooks"; import { useFetchChecksSummaryByTeamId } from "../../Hooks/checkHooks"; -import { useAckAllChecks } from "../../Hooks/checkHooks"; +import { useAcknowledgeChecks } from "../../Hooks/checkHooks"; import { useState, useEffect } from "react"; import NetworkError from "../../Components/GenericFallback/NetworkError"; import { useTranslation } from "react-i18next"; @@ -34,7 +34,7 @@ const Incidents = () => { const [updateTrigger, setUpdateTrigger] = useState(false); //Hooks - const [ackAllChecks, ackAllLoading] = useAckAllChecks(); + const { acknowledge, isLoadingAcknowledge } = useAcknowledgeChecks(); //Utils const theme = useTheme(); @@ -62,8 +62,9 @@ const Incidents = () => { setMonitorLookup(monitorLookup); }, [monitors]); - const handleAckAllChecks = () => { - ackAllChecks(setUpdateTrigger); + const handleAcknowledge = () => { + const monitorId = selectedMonitor === "0" ? null : selectedMonitor; + acknowledge(setUpdateTrigger, monitorId); }; if (networkError || networkErrorSummary) { @@ -81,10 +82,12 @@ const Incidents = () => { - + { /> - + { disabled={!isCreate} /> {isCreate && ( - - {t("infrastructureProtocol")} + - + )} { +const Gauges = ({ isLoading = false, monitor }) => { const { decimalToPercentage, formatBytes } = useHardwareUtils(); const theme = useTheme(); const { t } = useTranslation(); - if (!shouldRender) { + if (isLoading) { return ; } @@ -60,6 +61,7 @@ const Gauges = ({ shouldRender, monitor }) => { return ( {gauges.map((gauge) => { @@ -79,4 +81,9 @@ const Gauges = ({ shouldRender, monitor }) => { ); }; +Gauges.propTypes = { + isLoading: PropTypes.bool, + monitor: PropTypes.object, +}; + export default Gauges; diff --git a/client/src/Pages/Infrastructure/Details/index.jsx b/client/src/Pages/Infrastructure/Details/index.jsx index b57e0b91e..30840851f 100644 --- a/client/src/Pages/Infrastructure/Details/index.jsx +++ b/client/src/Pages/Infrastructure/Details/index.jsx @@ -92,11 +92,11 @@ const InfrastructureDetails = () => { monitor={monitor} /> diff --git a/client/src/Pages/Infrastructure/Monitors/index.jsx b/client/src/Pages/Infrastructure/Monitors/index.jsx index e0f5c642b..f7ca892c2 100644 --- a/client/src/Pages/Infrastructure/Monitors/index.jsx +++ b/client/src/Pages/Infrastructure/Monitors/index.jsx @@ -11,7 +11,7 @@ import Filter from "./Components/Filters"; import SearchComponent from "../../Uptime/Monitors/Components/SearchComponent"; // Utils import { useTheme } from "@emotion/react"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useIsAdmin } from "../../../Hooks/useIsAdmin"; import { useTranslation } from "react-i18next"; import { useFetchMonitorsByTeamId } from "../../../Hooks/monitorHooks"; @@ -58,6 +58,12 @@ const InfrastructureMonitors = () => { setPage(0); }; + useEffect(() => { + if (isSearching) { + setPage(0); + } + }, [isSearching]); + const handleReset = () => { setSelectedStatus(undefined); setToFilterStatus(undefined); @@ -93,13 +99,9 @@ const InfrastructureMonitors = () => { if (!isLoading && typeof summary?.totalMonitors === "undefined") { return ( diff --git a/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx b/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx new file mode 100644 index 000000000..e9f235112 --- /dev/null +++ b/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx @@ -0,0 +1,109 @@ +import Stack from "@mui/material/Stack"; +import Gauge from "../../../../../Components/Charts/CustomGauge"; +import Typography from "@mui/material/Typography"; + +// Utils +import { useTheme } from "@emotion/react"; +import PropTypes from "prop-types"; +import { getPercentage } from "../../utils/utils"; +import { useTranslation } from "react-i18next"; + +const GaugeBox = ({ title, subtitle, children }) => { + const theme = useTheme(); + return ( + + {children} + + {title} + {subtitle} + + ); +}; + +GaugeBox.propTypes = { + title: PropTypes.string.isRequired, + subtitle: PropTypes.string.isRequired, + children: PropTypes.node.isRequired, +}; + +const Gauges = ({ diagnostics, isLoading }) => { + const heapTotalSize = getPercentage( + diagnostics?.v8HeapStats?.totalHeapSizeBytes, + diagnostics?.v8HeapStats?.heapSizeLimitBytes + ); + + const heapUsedSize = getPercentage( + diagnostics?.v8HeapStats?.usedHeapSizeBytes, + diagnostics?.v8HeapStats?.heapSizeLimitBytes + ); + + const actualHeapUsed = getPercentage( + diagnostics?.v8HeapStats?.usedHeapSizeBytes, + diagnostics?.v8HeapStats?.totalHeapSizeBytes + ); + + const theme = useTheme(); + const { t } = useTranslation(); + + return ( + + + + + + + + + + + + + + + ); +}; + +Gauges.propTypes = { + diagnostics: PropTypes.object, + isLoading: PropTypes.bool, +}; + +export default Gauges; diff --git a/client/src/Pages/Logs/Diagnostics/components/stats/index.jsx b/client/src/Pages/Logs/Diagnostics/components/stats/index.jsx new file mode 100644 index 000000000..103d1c698 --- /dev/null +++ b/client/src/Pages/Logs/Diagnostics/components/stats/index.jsx @@ -0,0 +1,97 @@ +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import CircularProgress from "@mui/material/CircularProgress"; + +import { useTheme } from "@emotion/react"; +import PropTypes from "prop-types"; + +import { getHumanReadableDuration } from "../../../../../Utils/timeUtils"; +import { formatBytes } from "../../utils/utils"; +import { useTranslation } from "react-i18next"; + +const StatsCard = ({ title, value, unit = "", isLoading }) => { + const theme = useTheme(); + return ( + + {isLoading ? ( + + + + ) : ( + + + {title} + + + {value} {unit} + + + )} + + ); +}; + +StatsCard.propTypes = { + title: PropTypes.string, + value: PropTypes.string, + unit: PropTypes.string, + isLoading: PropTypes.bool, +}; + +const Stats = ({ diagnostics, isLoading }) => { + const theme = useTheme(); + const { t } = useTranslation(); + return ( + + + + + + + + + + + ); +}; + +Stats.propTypes = { + diagnostics: PropTypes.object, + isLoading: PropTypes.bool, +}; + +export default Stats; diff --git a/client/src/Pages/Logs/Diagnostics/index.jsx b/client/src/Pages/Logs/Diagnostics/index.jsx new file mode 100644 index 000000000..7c5d1f5a9 --- /dev/null +++ b/client/src/Pages/Logs/Diagnostics/index.jsx @@ -0,0 +1,54 @@ +import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import Gauges from "./components/gauges"; +import Stats from "./components/stats"; +import Divider from "@mui/material/Divider"; +import Button from "@mui/material/Button"; + +import { useTheme } from "@emotion/react"; +import { useTranslation } from "react-i18next"; +import { useFetchDiagnostics } from "../../../Hooks/logHooks"; + +const Diagnostics = () => { + // Local state + + // Hooks + const theme = useTheme(); + const { t } = useTranslation(); + const [diagnostics, fetchDiagnostics, isLoading, error] = useFetchDiagnostics(); + // Setup + return ( + + + {t("diagnosticsPage.diagnosticDescription")} + + + + + + + + + + + ); +}; + +export default Diagnostics; diff --git a/client/src/Pages/Logs/Diagnostics/utils/utils.js b/client/src/Pages/Logs/Diagnostics/utils/utils.js new file mode 100644 index 000000000..a3b75631f --- /dev/null +++ b/client/src/Pages/Logs/Diagnostics/utils/utils.js @@ -0,0 +1,16 @@ +export const getPercentage = (value, total) => { + if (!value || !total) return 0; + return (value / total) * 100; +}; + +export const formatBytes = (bytes) => { + if (!bytes) return "N/A"; + + if (bytes === 0) return "0 Bytes"; + + if (bytes >= 1024 * 1024 * 1024) { + return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`; + } + + return `${(bytes / (1024 * 1024)).toFixed(2)} MB`; +}; diff --git a/client/src/Pages/Logs/Logs/index.jsx b/client/src/Pages/Logs/Logs/index.jsx index 6c5e1a301..b65f9c0fd 100644 --- a/client/src/Pages/Logs/Logs/index.jsx +++ b/client/src/Pages/Logs/Logs/index.jsx @@ -2,6 +2,7 @@ import Stack from "@mui/material/Stack"; import Box from "@mui/material/Box"; import Select from "../../../Components/Inputs/Select"; import Typography from "@mui/material/Typography"; +import Divider from "@mui/material/Divider"; import { useFetchLogs } from "../../../Hooks/logHooks"; import { useTheme } from "@emotion/react"; @@ -51,12 +52,15 @@ const Logs = () => { return ( - {t("logsPage.description")} + {t("logsPage.description")} + + {t("logsPage.logLevelSelect.title")} setRawInput(val)} + handleChange={handleTimezoneChange} + isAdorned={false} + unit="timezone" /> diff --git a/client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx b/client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx index a1ba117b0..4c2842b71 100644 --- a/client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx +++ b/client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx @@ -4,7 +4,7 @@ import { TabPanel } from "@mui/lab"; import ConfigBox from "../../../../../Components/ConfigBox"; import Checkbox from "../../../../../Components/Inputs/Checkbox"; import TextInput from "../../../../../Components/Inputs/TextInput"; -import Select from "../../../../../Components/Inputs/Select"; +import Search from "../../../../../Components/Inputs/Search"; import ImageUpload from "../../../../../Components/Inputs/ImageUpload"; import ColorPicker from "../../../../../Components/Inputs/ColorPicker"; import Progress from "../Progress"; @@ -14,6 +14,7 @@ import { useTheme } from "@emotion/react"; import timezones from "../../../../../Utils/timezones.json"; import PropTypes from "prop-types"; import { useTranslation } from "react-i18next"; +import { useMemo, useState, useCallback } from "react"; const TabSettings = ({ isCreate, @@ -28,6 +29,25 @@ const TabSettings = ({ // Utils const theme = useTheme(); const { t } = useTranslation(); + const [rawInput, setRawInput] = useState(""); + + const selectedTimezone = useMemo( + () => timezones.find((tz) => tz._id === form.timezone) ?? null, + [form.timezone] + ); + + const handleTimezoneChange = useCallback( + (newValue) => { + setRawInput(""); + handleFormChange({ + target: { + name: "timezone", + value: newValue?._id ?? "", + }, + }); + }, + [handleFormChange] + ); return ( @@ -101,13 +121,17 @@ const TabSettings = ({ - @@ -445,8 +445,8 @@ const Configure = () => { name="expectedValue" label={t("expectedValue")} isOptional={true} - placeholder={expectedValuePlaceholders[form.matchMethod || "equal"]} - value={form.expectedValue} + placeholder={expectedValuePlaceholders[form?.matchMethod || "equal"]} + value={form?.expectedValue} onChange={onChange} error={errors["expectedValue"] ? true : false} helperText={errors["expectedValue"]} @@ -466,7 +466,7 @@ const Configure = () => { label="JSON Path" isOptional={true} placeholder="data.status" - value={form.jsonPath} + value={form?.jsonPath} onChange={onChange} error={errors["jsonPath"] ? true : false} helperText={errors["jsonPath"]} diff --git a/client/src/Pages/Uptime/Create/index.jsx b/client/src/Pages/Uptime/Create/index.jsx index 3db73481a..761fd6346 100644 --- a/client/src/Pages/Uptime/Create/index.jsx +++ b/client/src/Pages/Uptime/Create/index.jsx @@ -17,12 +17,13 @@ import Checkbox from "../../../Components/Inputs/Checkbox"; // Utils import { useTheme } from "@emotion/react"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { monitorValidation } from "../../../Validation/validation"; import { createToast } from "../../../Utils/toastUtils"; import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications"; -import { useCreateMonitor } from "../../../Hooks/monitorHooks"; +import { useCreateMonitor, useFetchMonitorById } from "../../../Hooks/monitorHooks"; +import { useParams } from "react-router-dom"; const CreateMonitor = () => { // Local state @@ -42,12 +43,23 @@ const CreateMonitor = () => { }); // Setup + const MS_PER_MINUTE = 60000; const theme = useTheme(); const { t } = useTranslation(); const [notifications, notificationsAreLoading, error] = useGetNotificationsByTeamId(); const [createMonitor, isCreating] = useCreateMonitor(); + const { monitorId } = useParams(); + + const formatAndSet = (monitor) => { + monitor.interval = monitor.interval / MS_PER_MINUTE; + setMonitor(monitor); + }; + const [isLoading] = useFetchMonitorById({ + monitorId, + setMonitor: formatAndSet, + updateTrigger: true, + }); - const MS_PER_MINUTE = 60000; const SELECT_VALUES = [ { _id: 1, name: "1 minute" }, { _id: 2, name: "2 minutes" }, diff --git a/client/src/Pages/Uptime/Monitors/index.jsx b/client/src/Pages/Uptime/Monitors/index.jsx index 6e6f6caba..4ebac94d3 100644 --- a/client/src/Pages/Uptime/Monitors/index.jsx +++ b/client/src/Pages/Uptime/Monitors/index.jsx @@ -174,14 +174,9 @@ const UptimeMonitors = () => { ) { return ( diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index 3db8b772d..6de07a2de 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -42,6 +42,7 @@ import CreateNotifications from "../Pages/Notifications/create"; // Settings import Account from "../Pages/Account"; +import EditUser from "../Pages/Account/EditUser"; import Settings from "../Pages/Settings"; import Maintenance from "../Pages/Maintenance"; @@ -187,6 +188,14 @@ const Routes = () => { path="account/team" element={} /> + + + + } + /> } The response from the axios POST request. - */ - async checkEndpointResolution(config) { - const { monitorURL } = config; - const params = new URLSearchParams(); - - if (monitorURL) params.append("monitorURL", monitorURL); - - return this.axiosInstance.get(`/monitors/resolution/url?${params.toString()}`, { - headers: { - "Content-Type": "application/json", - }, - }); - } - /** * ************************************ * Get all uptime monitors for a Team @@ -393,7 +369,7 @@ class NetworkService { * */ async updateUser(config) { - return this.axiosInstance.put(`/auth/user/${config.userId}`, config.form); + return this.axiosInstance.put(`/auth/user`, config.form); } /** @@ -406,8 +382,8 @@ class NetworkService { * @param {string} config.userId - The ID of the user to be deleted. * **/ - async deleteUser(config) { - return this.axiosInstance.delete(`/auth/user/${config.userId}`); + async deleteUser() { + return this.axiosInstance.delete(`/auth/user`); } /** @@ -631,6 +607,35 @@ class NetworkService { }); } + /** + * ************************************ + * Update the status of all checks for a given monitor + * ************************************ + * + * @async + * @param {Object} config - The configuration object. + * @param {string} config.monitorId - The ID of the monitor. + * @param {boolean} config.ack - The acknowledgment to update the checks to. + * @returns {Promise} The response from the axios PUT request. + * + */ + async updateMonitorChecksStatus(config) { + return this.axiosInstance.put(`/checks/monitor/${config.monitorId}`, { + ack: config.ack, + }); + } + + /** + * ************************************ + * Update the status of all checks for a given team + * ************************************ + * + * @async + * @param {Object} config - The configuration object. + * @param {boolean} config.ack - The acknowledgment to update the checks to. + * @returns {Promise} The response from the axios PUT request. + * + */ async updateAllChecksStatus(config) { return this.axiosInstance.put(`/checks/team/`, { ack: config.ack, @@ -977,7 +982,7 @@ class NetworkService { }); } - return this.axiosInstance.get(`/monitors/summary/team?${params.toString()}`, { + return this.axiosInstance.get(`/monitors/team/summary?${params.toString()}`, { headers: { "Content-Type": "application/json", }, @@ -1091,6 +1096,32 @@ class NetworkService { }, }); } + + async getDiagnostics() { + return this.axiosInstance.get(`/diagnostic/system`, { + headers: { + "Content-Type": "application/json", + }, + }); + } + + async getUserById(config) { + const userId = config?.userId; + return this.axiosInstance.get(`auth/users/${userId}`, { + headers: { + "Content-Type": "application/json", + }, + }); + } + + async editUser(config) { + const { userId, user } = config; + return this.axiosInstance.put(`auth/users/${userId}`, user, { + headers: { + "Content-Type": "application/json", + }, + }); + } } export default NetworkService; diff --git a/client/src/Utils/Theme/globalTheme.js b/client/src/Utils/Theme/globalTheme.js index eb827a907..5866e517f 100644 --- a/client/src/Utils/Theme/globalTheme.js +++ b/client/src/Utils/Theme/globalTheme.js @@ -553,6 +553,11 @@ const baseTheme = (palette) => ({ "&:hover .MuiOutlinedInput-notchedOutline": { borderColor: theme.palette.primary.lowContrast, }, + padding: "0 13px", + minHeight: "34px", + display: "flex", + alignItems: "center", + lineHeight: 1, }), }, }, diff --git a/client/src/Utils/roleUtils.js b/client/src/Utils/roleUtils.js new file mode 100644 index 000000000..f359246b2 --- /dev/null +++ b/client/src/Utils/roleUtils.js @@ -0,0 +1,13 @@ +export const ROLES = { + SUPERADMIN: "superadmin", + ADMIN: "admin", + USER: "user", + DEMO: "demo", +}; + +export const VALID_ROLES = [ROLES.ADMIN, ROLES.USER, ROLES.DEMO]; + +export const EDITABLE_ROLES = [ + { role: ROLES.ADMIN, _id: ROLES.ADMIN }, + { role: ROLES.USER, _id: ROLES.USER }, +]; diff --git a/client/src/Utils/timeUtils.js b/client/src/Utils/timeUtils.js index 568b77b14..f6e2dcec0 100644 --- a/client/src/Utils/timeUtils.js +++ b/client/src/Utils/timeUtils.js @@ -85,6 +85,7 @@ export const getHumanReadableDuration = (ms) => { hours: durationObj.hours(), minutes: durationObj.minutes(), seconds: durationObj.seconds(), + milliseconds: durationObj.milliseconds(), }; const result = []; @@ -101,6 +102,9 @@ export const getHumanReadableDuration = (ms) => { if (result.length < 2 && parts.seconds > 0) { result.push(`${parts.seconds}s`); } + if (result.length < 2 && parts.milliseconds > 0 && parts.seconds < 1) { + result.push(`${parts.milliseconds.toFixed(2)}ms`); + } if (result.length === 0) { // fallback for durations < 1s diff --git a/client/src/Utils/toastUtils.jsx b/client/src/Utils/toastUtils.jsx index dbec9117e..21cf13e2b 100644 --- a/client/src/Utils/toastUtils.jsx +++ b/client/src/Utils/toastUtils.jsx @@ -1,7 +1,6 @@ import PropTypes from "prop-types"; import { toast, Slide } from "react-toastify"; -import Alert from "../Components/Alert"; - +import Toast from "../Components/Toast"; /** * @param {object} props * @param {'info' | 'error' | 'warning'} - The variant of the alert (e.g., "info", "error"). @@ -15,6 +14,7 @@ export const createToast = ({ variant = "info", title, body, + hasDismiss = false, hasIcon = false, config = {}, }) => { @@ -29,13 +29,14 @@ export const createToast = ({ toast( ({ closeToast }) => ( - ), toastConfig @@ -47,5 +48,6 @@ createToast.propTypes = { title: PropTypes.string, body: PropTypes.string.isRequired, hasIcon: PropTypes.bool, + hasDismiss: PropTypes.bool, config: PropTypes.object, }; diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js index d86bbd556..7754ee475 100644 --- a/client/src/Validation/validation.js +++ b/client/src/Validation/validation.js @@ -1,5 +1,6 @@ import joi from "joi"; import dayjs from "dayjs"; +import { ROLES } from "../Utils/roleUtils"; const THRESHOLD_COMMON_BASE_MSG = "Threshold must be a number."; @@ -410,17 +411,54 @@ const notificationValidation = joi.object({ "string.empty": "Notification name is required", "any.required": "Notification name is required", }), - address: joi.string().required().messages({ - "string.empty": "This field cannot be empty", - "string.base": "This field must be a string", - "any.required": "This field is required", - }), - type: joi.string().required().messages({ - "string.empty": "This field is required", - "any.required": "This field is required", + + type: joi + .string() + .valid("email", "webhook", "slack", "discord", "pager_duty") + .required() + .messages({ + "string.empty": "Notification type is required", + "any.required": "Notification type is required", + "any.only": "Notification type must be email, webhook, or pager_duty", + }), + + address: joi.when("type", { + is: "email", + then: joi + .string() + .email({ tlds: { allow: false } }) + .required() + .messages({ + "string.empty": "E-mail address cannot be empty", + "any.required": "E-mail address is required", + "string.email": "Please enter a valid e-mail address", + }), + otherwise: joi.string().uri().required().messages({ + "string.empty": "Webhook URL cannot be empty", + "any.required": "Webhook URL is required", + "string.uri": "Please enter a valid Webhook URL", + }), }), }); +const editUserValidation = joi.object({ + firstName: nameSchema, + lastName: lastnameSchema, + role: joi + .array() + .items(joi.string().valid(...Object.values(ROLES))) + .min(1) + .messages({ + "array.min": "auth.common.fields.role.errors.min", + }), + email: joi + .string() + .required() + .trim() + .email({ tlds: { allow: false } }) + .lowercase(), +}); + export { newOrChangedCredentials, loginCredentials, @@ -433,4 +471,5 @@ export { statusPageValidation, logoImageValidation, notificationValidation, + editUserValidation, }; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index ca4e716dd..b88b1d8a1 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -32,6 +32,11 @@ "errors": { "incorrect": "The password you provided does not match our records" } + }, + "role": { + "errors": { + "min": "At least one role is required" + } } }, "inputs": { @@ -212,8 +217,9 @@ "checkFormError": "Please check the form for errors.", "checkFrequency": "Check frequency", "checkHooks": { - "failureResolveOne": "Failed to resolve incident.", - "failureResolveAll": "Failed to resolve all incidents." + "failureResolveAll": "Failed to resolve all incidents.", + "failureResolveMonitor": "Failed to resolve monitor incidents.", + "failureResolveOne": "Failed to resolve incident." }, "checkingEvery": "Checking every", "city": "CITY", @@ -248,6 +254,8 @@ "createMonitor": "Create monitor", "createNew": "Create new", "createNotifications": { + "dialogDeleteConfirm": "Delete", + "dialogDeleteTitle": "Are you sure you want to delete this notification?", "discordSettings": { "description": "Configure your Discord webhook here", "title": "Discord", @@ -278,6 +286,7 @@ "webhookLabel": "Slack webhook URL", "webhookPlaceholder": "https://hooks.slack.com/services/..." }, + "testNotification": "Test notification", "title": "Create notification channel", "typeSettings": { "description": "Select the type of notification channel you want to create.", @@ -289,10 +298,7 @@ "title": "Webhook", "webhookLabel": "Webhook URL", "webhookPlaceholder": "https://your-server.com/webhook" - }, - "testNotification": "Test notification", - "dialogDeleteTitle": "Are you sure you want to delete this notification?", - "dialogDeleteConfirm": "Delete" + } }, "createYour": "Create your", "date&Time": "Date & Time", @@ -302,6 +308,27 @@ "deleteStatusPage": "Do you want to delete this status page?", "deleteStatusPageConfirm": "Yes, delete status page", "deleteStatusPageDescription": "Once deleted, your status page cannot be retrieved.", + "diagnosticsPage": { + "diagnosticDescription": "System diagnostics", + "gauges": { + "heapAllocationSubtitle": "% of available memory", + "heapAllocationTitle": "Heap allocation", + "heapUsageSubtitle": "% of available memory", + "heapUsageTitle": "Heap usage", + "heapUtilizationSubtitle": "% of allocated", + "heapUtilizationTitle": "Heap utilization", + "instantCpuUsageSubtitle": "% of 1s used by CPU", + "instantCpuUsageTitle": "Instant CPU usage" + }, + "stats": { + "eventLoopDelayTitle": "Event loop delay", + "osMemoryLimitTitle": "OS Memory Limit", + "totalHeapSizeTitle": "Total heap size", + "uptimeTitle": "Uptime", + "usedHeapSizeTitle": "Used heap size" + }, + "statsDescription": "System statistics" + }, "disk": "Disk", "diskUsage": "Disk Usage", "displayName": "Display name", @@ -352,6 +379,24 @@ "duration": "Duration", "edit": "Edit", "editMaintenance": "Edit maintenance", + "editUserPage": { + "form": { + "email": "Email", + "firstName": "First name", + "lastName": "Last name", + "role": "Roles", + "save": "Save" + }, + "table": { + "actionHeader": "Action", + "roleHeader": "Role" + }, + "title": "Edit user", + "toast": { + "successUserUpdate": "User updated successfully", + "validationErrors": "Validation errors" + } + }, "editing": "Editing...", "emailSent": "Email sent successfully", "errorInvalidFieldId": "Invalid field ID provided", @@ -382,6 +427,9 @@ "friendlyNameInput": "Friendly name", "friendlyNamePlaceholder": "Maintenance at __ : __ for ___ minutes", "gb": "GB", + "general": { + "noOptionsFound": "No {{unit}} found" + }, "greeting": { "append": "The afternoon is your playground—let's make it epic!", "overview": "Here's an overview of your {{type}} monitors.", @@ -396,24 +444,25 @@ "incidentsOptionsHeader": "Incidents for:", "incidentsOptionsHeaderFilterAll": "All", "incidentsOptionsHeaderFilterBy": "Filter by:", - "incidentsOptionsHeaderTotalIncidents": "Total Incidents", "incidentsOptionsHeaderFilterCannotResolve": "Cannot Resolve", - "incidentsTableResolvedAt": "Resolved at", "incidentsOptionsHeaderFilterDown": "Down", - "incidentsTableActionResolve": "Resolve", - "incidentsPageActionResolve": "Resolve all incidents", "incidentsOptionsHeaderFilterResolved": "Resolved", "incidentsOptionsHeaderLastDay": "Last day", "incidentsOptionsHeaderLastHour": "Last hour", "incidentsOptionsHeaderLastWeek": "Last week", "incidentsOptionsHeaderShow": "Show:", + "incidentsOptionsHeaderTotalIncidents": "Total Incidents", "incidentsOptionsPlaceholderAllServers": "All servers", + "incidentsPageActionResolveMonitor": "Resolve monitor incidents", + "incidentsPageActionResolveAll": "Resolve all incidents", "incidentsPageTitle": "Incidents", + "incidentsTableActionResolve": "Resolve", "incidentsTableDateTime": "Date & Time", "incidentsTableMessage": "Message", "incidentsTableMonitorName": "Monitor Name", "incidentsTableNoIncidents": "No incidents recorded", "incidentsTablePaginationLabel": "incidents", + "incidentsTableResolvedAt": "Resolved at", "incidentsTableStatus": "Status", "incidentsTableStatusCode": "Status Code", "infrastructureAlertNotificationDescription": "Send a notification to user(s) when thresholds exceed a specified percentage.", @@ -425,6 +474,17 @@ "infrastructureDisplayNameLabel": "Display name", "infrastructureEditMonitor": "Save Infrastructure Monitor", "infrastructureEditYour": "Edit your", + "infrastructureMonitor": { + "fallback": { + "checks": [ + "Track the performance of your servers", + "Identify bottlenecks and optimize usage", + "Ensure reliability with real-time monitoring" + ], + "title": "An infrastructure monitor is used to:", + "actionButton": "Let's create your first infrastructure monitor!" + } + }, "infrastructureMonitorCreated": "Infrastructure monitor created successfully!", "infrastructureMonitorUpdated": "Infrastructure monitor updated successfully!", "infrastructureProtocol": "Protocol", @@ -443,7 +503,7 @@ "inviteNoTokenFound": "No invite token found", "loginHere": "Login here", "logsPage": { - "description": "This page shows the latest 1000 lines of logs from the Checkmate server", + "description": "System logs - last 1000 lines", "logLevelSelect": { "title": "Log level", "values": { @@ -455,6 +515,7 @@ } }, "tabs": { + "diagnostics": "Diagnostics", "logs": "Server logs", "queue": "Job queue" }, @@ -467,6 +528,17 @@ "maintenance": "maintenance", "maintenanceRepeat": "Maintenance Repeat", "maintenanceTableActionMenuDialogTitle": "Do you really want to remove this maintenance window?", + "maintenanceWindow": { + "fallback": { + "checks": [ + "Mark your maintenance periods", + "Eliminate any misunderstandings", + "Stop sending alerts in maintenance windows" + ], + "title": "A maintenance window is used to:", + "actionButton": "Let's create your first maintenance window!" + } + }, "maintenanceWindowDescription": "Your pings won't be sent during this time frame", "maintenanceWindowName": "Maintenance Window Name", "maskedPageSpeedKeyPlaceholder": "*************************************", @@ -498,8 +570,8 @@ "message": "Message", "monitor": "monitor", "monitorActions": { - "deleteSuccess": "Monitor deleted successfully", "deleteFailed": "Failed to delete monitor", + "deleteSuccess": "Monitor deleted successfully", "details": "Details", "export": "Export Monitors", "import": "Import Monitors", @@ -564,7 +636,8 @@ "Let engineers know when incidents happen", "Keep administrators informed of system changes" ], - "title": "notification channel" + "title": "A notification channel is used to:", + "actionButton": "Let's create your first notification channel!" }, "fetch": { "failed": "Failed to fetch notifications", @@ -609,6 +682,17 @@ "notifySMS": "Notify via SMS (coming soon)", "now": "Now", "os": "OS", + "pageSpeed": { + "fallback": { + "checks": [ + "Report on the user experience of a page", + "Help analyze webpage speed", + "Give suggestions on how the page can be improved" + ], + "title": "A PageSpeed monitor is used to:", + "actionButton": "Let's create your first PageSpeed monitor!" + } + }, "pageSpeedAddApiKey": "to add your API key.", "pageSpeedConfigureSettingsDescription": "Here you can select the URL of the host, together with the type of monitor.", "pageSpeedDetailsPerformanceReport": "Values are estimated and may vary.", @@ -797,6 +881,14 @@ "pending": "Pending...", "up": "Your site is up." }, + "statusPage": { + "contents": "Contents", + "createSuccess": "Status page created successfully", + "deleteFailed": "Failed to delete status page", + "deleteSuccess": "Status page deleted successfully", + "generalSettings": "General settings", + "updateSuccess": "Status page updated successfully" + }, "statusPageCreate": { "buttonSave": "Save" }, @@ -822,7 +914,16 @@ "createSuccess": "Status page created successfully", "updateSuccess": "Status page updated successfully", "generalSettings": "General settings", - "contents": "Contents" + "contents": "Contents", + "fallback": { + "checks": [ + "Monitor and display the health of your services in real time", + "Track multiple services and share their status", + "Keep users informed about outages and performance" + ], + "title": "A status page is used to:", + "actionButton": "Let's create your first status page!" + } }, "submit": "Submit", "teamPanel": { @@ -851,13 +952,13 @@ "testLocale": "testLocale", "testNotificationsDisabled": "There are no notifications setup for this monitor. You need to add one by clicking 'Configure' button", "time": { - "threeMinutes": "3 minutes", "fiveMinutes": "5 minutes", - "tenMinutes": "10 minutes", - "twentyMinutes": "20 minutes", - "oneHour": "1 hour", "oneDay": "1 day", - "oneWeek": "1 week" + "oneHour": "1 hour", + "oneWeek": "1 week", + "tenMinutes": "10 minutes", + "threeMinutes": "3 minutes", + "twentyMinutes": "20 minutes" }, "timeZoneInfo": "All dates and times are in GMT+0 time zone.", "timezone": "Timezone", @@ -876,6 +977,18 @@ "ping": "Enter the IP address or hostname to ping (e.g., 192.168.1.100 or example.com) and add a clear display name that appears on the dashboard.", "port": "Enter the URL or IP of the server, the port number and a clear display name that appears on the dashboard." }, + "uptimeMonitor": { + "fallback": { + "checks": [ + "Check if websites or servers are online & responsive", + "Alert teams about downtime or performance issues", + "Monitor HTTP endpoints, pings, containers & ports", + "Track historical uptime and reliability trends" + ], + "title": "An uptime monitor is used to:", + "actionButton": "Let's create your first uptime monitor!" + } + }, "url": "URL", "urlMonitor": "URL to monitor", "used": "Used", diff --git a/client/src/locales/fi.json b/client/src/locales/fi.json deleted file mode 100644 index 90b30e925..000000000 --- a/client/src/locales/fi.json +++ /dev/null @@ -1,836 +0,0 @@ -{ - "submit": "Lähetä", - "title": "Otsikko", - "distributedStatusHeaderText": "Reaaliaikainen kattavuus oikeilla laitteilla", - "distributedStatusSubHeaderText": "Miljoonien laitteiden voimin ympäri maailman näet järjestelmän suorituskyvyn maailman alueittain, maittain tai kaupungeittain", - "settingsDisabled": "Poistettu käytöstä", - "settingsSuccessSaved": "", - "settingsFailedToSave": "", - "settingsStatsCleared": "", - "settingsFailedToClearStats": "", - "settingsFailedToAddDemoMonitors": "", - "settingsMonitorsDeleted": "", - "settingsFailedToDeleteMonitors": "", - "starPromptTitle": "", - "starPromptDescription": "", - "https": "HTTPS", - "http": "HTTP", - "monitor": "Seurain", - "aboutus": "Tietoja meistä", - "now": "Nyt", - "delete": "Poista", - "configure": "Määritä", - "responseTime": "Vasteaika", - "ms": "ms", - "bar": "Palkki", - "area": "Alue", - "country": "MAA", - "city": "KAUPUNKI", - "response": "Vastaus", - "monitorStatusUp": "Valvontakohde {name} ({url}) on nyt toiminnassa ja vastaa", - "monitorStatusDown": "Valvontakohde {name} ({url}) ei ole toiminnassa, eikä vastaa", - "webhookSendSuccess": "Webhook-ilmoitus lähetettiin onnistuneesti", - "webhookSendError": "Virhe lähetettäessä webhook-ilmoitusta palveluun {platform}", - "webhookUnsupportedPlatform": "Ei tuettu palvelu: {platform}", - "distributedRightCategoryTitle": "Valvonta", - "distributedStatusServerMonitors": "Palvelin valvonta", - "distributedStatusServerMonitorsDescription": "Valvo palvelimien tilaa", - "distributedUptimeCreateSelectURL": "Tässä voit valita isäntäkohteen URL-osoitteen ja valvontatyypin.", - "distributedUptimeCreateChecks": "Suoritettavat tarkistukset", - "distributedUptimeCreateChecksDescription": "Voit aina lisätä tai poistaa tarkistuksia sivuston lisäämisen jälkeen.", - "distributedUptimeCreateIncidentNotification": "Häiriöilmoitukset", - "distributedUptimeCreateIncidentDescription": "Jos ilmenee häiriö, ilmoita käyttäjille.", - "distributedUptimeCreateAdvancedSettings": "Lisäasetukset", - "distributedUptimeDetailsNoMonitorHistory": "Tälle valvontakohteelle ei ole vielä tarkistushistoriaa.", - "distributedUptimeDetailsStatusHeaderUptime": "", - "distributedUptimeDetailsStatusHeaderLastUpdate": "Viimeksi päivitetty", - "notifications": { - "enableNotifications": "", - "testNotification": "Testi-ilmoitus", - "addOrEditNotifications": "Lisää tai muokkaa ilmoituksia", - "slack": { - "label": "Slack", - "description": "", - "webhookLabel": "Webhookin URL", - "webhookPlaceholder": "", - "webhookRequired": "" - }, - "discord": { - "label": "Discord", - "description": "", - "webhookLabel": "Discord Webhookin URL-osoite", - "webhookPlaceholder": "https://discord.com/api/webhooks/...", - "webhookRequired": "" - }, - "telegram": { - "label": "Telegram", - "description": "", - "tokenLabel": "", - "tokenPlaceholder": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", - "chatIdLabel": "", - "chatIdPlaceholder": "-1001234567890", - "fieldsRequired": "" - }, - "webhook": { - "label": "Webhookit", - "description": "", - "urlLabel": "Webhookin URL", - "urlPlaceholder": "https://your-server.com/webhook", - "urlRequired": "" - }, - "testNotificationDevelop": "Testi-ilmoitus 2", - "integrationButton": "", - "testSuccess": "Testi ilmoituksen lähetys onnistui!", - "testFailed": "Testi ilmoituksen lähetys epäonnistui", - "unsupportedType": "", - "networkError": "", - "fallback": { - "title": "", - "checks": [""] - }, - "createButton": "", - "createTitle": "", - "create": { - "success": "", - "failed": "" - }, - "fetch": { - "success": "", - "failed": "" - }, - "delete": { - "success": "", - "failed": "" - }, - "edit": { - "success": "", - "failed": "" - }, - "test": { - "success": "", - "failed": "" - } - }, - "testLocale": "", - "add": "Lisää", - "monitors": "Seuraimet", - "distributedUptimeStatusCreateStatusPage": "", - "distributedUptimeStatusCreateStatusPageAccess": "", - "distributedUptimeStatusCreateStatusPageReady": "", - "distributedUptimeStatusBasicInfoHeader": "", - "distributedUptimeStatusBasicInfoDescription": "", - "distributedUptimeStatusLogoHeader": "Logo", - "distributedUptimeStatusLogoDescription": "", - "distributedUptimeStatusLogoUploadButton": "", - "distributedUptimeStatusStandardMonitorsHeader": "", - "distributedUptimeStatusStandardMonitorsDescription": "", - "distributedUptimeStatusCreateYour": "", - "distributedUptimeStatusEditYour": "", - "distributedUptimeStatusPublishedLabel": "", - "distributedUptimeStatusCompanyNameLabel": "Yrityksen nimi", - "distributedUptimeStatusPageAddressLabel": "", - "distributedUptimeStatus30Days": "30 päivää", - "distributedUptimeStatus60Days": "60 päivää", - "distributedUptimeStatus90Days": "90 päivää", - "distributedUptimeStatusPageNotSetUp": "", - "distributedUptimeStatusContactAdmin": "Ota yhteys järjestelmävalvojaasi", - "distributedUptimeStatusPageNotPublic": "", - "distributedUptimeStatusPageDeleteDialog": "", - "distributedUptimeStatusPageDeleteConfirm": "", - "distributedUptimeStatusPageDeleteDescription": "", - "distributedUptimeStatusDevices": "", - "distributedUptimeStatusUpt": "", - "distributedUptimeStatusUptBurned": "", - "distributedUptimeStatusUptLogo": "UPT logo", - "incidentsTableNoIncidents": "", - "incidentsTablePaginationLabel": "", - "incidentsTableMonitorName": "", - "incidentsTableStatus": "Tila", - "incidentsTableDateTime": "Päivämäärä ja aika", - "incidentsTableStatusCode": "", - "incidentsTableMessage": "Viesti", - "incidentsOptionsHeader": "", - "incidentsOptionsHeaderFilterBy": "", - "incidentsOptionsHeaderFilterAll": "Kaikki", - "incidentsOptionsHeaderFilterDown": "Alhaalla", - "incidentsOptionsHeaderFilterCannotResolve": "", - "incidentsOptionsHeaderShow": "Näytä:", - "incidentsOptionsHeaderLastHour": "Viime tunti", - "incidentsOptionsHeaderLastDay": "Viime päivä", - "incidentsOptionsHeaderLastWeek": "Viime viikko", - "incidentsOptionsPlaceholderAllServers": "Kaikki palvelimet", - "infrastructureCreateYour": "", - "infrastructureCreateGeneralSettingsDescription": "", - "infrastructureServerRequirement": "", - "infrastructureCustomizeAlerts": "", - "infrastructureAlertNotificationDescription": "", - "infrastructureCreateMonitor": "", - "infrastructureProtocol": "Protokolla", - "infrastructureServerUrlLabel": "Palvelimen URL-osoite", - "infrastructureDisplayNameLabel": "", - "infrastructureAuthorizationSecretLabel": "", - "gb": "", - "mb": "", - "mem": "", - "memoryUsage": "Muistinkäyttö", - "cpu": "", - "cpuUsage": "", - "cpuTemperature": "", - "diskUsage": "", - "used": "", - "total": "", - "cores": "", - "frequency": "", - "status": "Tila", - "cpuPhysical": "", - "cpuLogical": "", - "cpuFrequency": "", - "avgCpuTemperature": "", - "memory": "Muisti", - "disk": "", - "uptime": "", - "os": "", - "host": "", - "actions": "", - "integrations": "", - "integrationsPrism": "", - "integrationsSlack": "Slack", - "integrationsSlackInfo": "", - "integrationsDiscord": "", - "integrationsDiscordInfo": "", - "integrationsZapier": "Zapier", - "integrationsZapierInfo": "", - "commonSave": "Tallenna", - "createYour": "", - "createMonitor": "", - "pause": "", - "resume": "", - "editing": "", - "url": "", - "access": "", - "timezone": "Aikavyöhyke", - "features": "", - "administrator": "", - "loginHere": "", - "displayName": "", - "urlMonitor": "", - "portToMonitor": "", - "websiteMonitoring": "", - "websiteMonitoringDescription": "", - "pingMonitoring": "", - "pingMonitoringDescription": "", - "dockerContainerMonitoring": "", - "dockerContainerMonitoringDescription": "", - "portMonitoring": "", - "portMonitoringDescription": "", - "createMaintenanceWindow": "", - "createMaintenance": "", - "editMaintenance": "", - "maintenanceWindowName": "", - "friendlyNameInput": "", - "friendlyNamePlaceholder": "", - "maintenanceRepeat": "", - "maintenance": "", - "duration": "", - "addMonitors": "", - "window": "", - "cancel": "Peruuttaa", - "message": "Viesti", - "low": "", - "high": "", - "statusCode": "Tilakoodi", - "date&Time": "Päivämäärä ja aika", - "type": "", - "statusPageName": "", - "publicURL": "Julkinen URL-osoite", - "repeat": "", - "edit": "Muokkaa", - "createA": "", - "remove": "", - "maintenanceWindowDescription": "", - "startTime": "", - "timeZoneInfo": "", - "monitorsToApply": "", - "nextWindow": "", - "notFoundButton": "", - "pageSpeedConfigureSettingsDescription": "", - "monitorDisplayName": "", - "whenNewIncident": "", - "notifySMS": "Ilmoita tekstiviestillä (tulossa pian)", - "notifyEmails": "", - "seperateEmails": "", - "checkFrequency": "", - "matchMethod": "", - "expectedValue": "", - "deleteDialogTitle": "", - "deleteDialogDescription": "", - "pageSpeedMonitor": "", - "shown": "", - "ago": "sitten", - "companyName": "Yrityksen nimi", - "pageSpeedDetailsPerformanceReport": "", - "pageSpeedDetailsPerformanceReportCalculator": "", - "checkingEvery": "", - "statusPageCreateSettings": "", - "basicInformation": "", - "statusPageCreateBasicInfoDescription": "", - "statusPageCreateSelectTimeZoneDescription": "", - "statusPageCreateAppearanceDescription": "", - "statusPageCreateSettingsCheckboxLabel": "", - "statusPageCreateBasicInfoStatusPageAddress": "", - "statusPageCreateTabsContent": "", - "statusPageCreateTabsContentDescription": "", - "statusPageCreateTabsContentFeaturesDescription": "", - "showCharts": "Näytä kaaviot", - "showUptimePercentage": "", - "removeLogo": "Poista logo", - "statusPageStatus": "", - "statusPageStatusContactAdmin": "Ota yhteyttä järjestelmänvalvojaan", - "statusPageStatusNotPublic": "", - "statusPageStatusNoPage": "", - "statusPageStatusServiceStatus": "", - "deleteStatusPage": "", - "deleteStatusPageConfirm": "", - "deleteStatusPageDescription": "", - "uptimeCreate": "", - "uptimeCreateJsonPath": "", - "uptimeCreateJsonPathQuery": "", - "maintenanceTableActionMenuDialogTitle": "", - "infrastructureEditYour": "Muokkaa omaa", - "infrastructureEditMonitor": "", - "infrastructureMonitorCreated": "", - "infrastructureMonitorUpdated": "", - "errorInvalidTypeId": "", - "errorInvalidFieldId": "", - "inviteNoTokenFound": "", - "pageSpeedWarning": "", - "pageSpeedLearnMoreLink": "", - "pageSpeedAddApiKey": "", - "update": "Päivitä", - "invalidFileFormat": "", - "invalidFileSize": "Tiedoston koko on liian suuri!", - "ClickUpload": "", - "DragandDrop": "raahaa ja pudota", - "MaxSize": "Enimmäiskoko", - "SupportedFormats": "Tuetut muodot", - "FirstName": "Etunimi", - "LastName": "Sukunimi", - "EmailDescriptionText": "Tämä on nykyinen sähköpostiosoitteesi — sitä ei voi vaihtaa.", - "YourPhoto": "Profiilikuva", - "PhotoDescriptionText": "", - "save": "Tallenna", - "DeleteDescriptionText": "", - "DeleteAccountWarning": "", - "DeleteWarningTitle": "Haluatko todella poistaa tämän tilin?", - "bulkImport": { - "title": "", - "selectFileTips": "", - "selectFileDescription": "", - "selectFile": "Valitse tiedosto", - "parsingFailed": "", - "uploadSuccess": "", - "validationFailed": "", - "noFileSelected": "Ei valittua tiedostoa", - "fallbackPage": "" - }, - "DeleteAccountTitle": "Poista tili", - "DeleteAccountButton": "Poista tili", - "publicLink": "Julkinen linkki", - "maskedPageSpeedKeyPlaceholder": "*************************************", - "reset": "Palauta", - "ignoreTLSError": "", - "tlsErrorIgnored": "", - "ignoreTLSErrorDescription": "", - "createNew": "Luo uusi", - "greeting": { - "prepend": "Hei", - "append": "", - "overview": "" - }, - "roles": { - "superAdmin": "Pääylläpitäjä", - "admin": "Ylläpitäjä", - "teamMember": "Tiimin jäsen", - "demoUser": "Demokäyttäjä" - }, - "teamPanel": { - "teamMembers": "Tiimin jäsenet", - "filter": { - "all": "Kaikki", - "member": "Jäsen" - }, - "inviteTeamMember": "", - "inviteNewTeamMember": "", - "inviteDescription": "", - "email": "Sähköposti", - "selectRole": "Valitse rooli", - "inviteLink": "Kutsulinkki", - "cancel": "Peruuttaa", - "noMembers": "", - "getToken": "Hae tunniste", - "emailToken": "Sähköpostitunnus", - "table": { - "name": "Nimi", - "email": "Sähköposti", - "role": "Rooli", - "created": "Luotu" - } - }, - "monitorState": { - "paused": "Tauota", - "resumed": "Jatka", - "active": "" - }, - "menu": { - "uptime": "", - "pagespeed": "", - "infrastructure": "Infrastruktuuri", - "incidents": "", - "statusPages": "Tilasivut", - "maintenance": "Huolto", - "integrations": "", - "settings": "Asetukset", - "support": "Tuki", - "discussions": "Keskustelut", - "docs": "Dokumentit", - "changelog": "Muutosloki", - "profile": "Profiili", - "password": "Salasana", - "team": "Tiimi", - "logOut": "Kirjaudu ulos", - "notifications": "", - "logs": "" - }, - "settingsEmailUser": "", - "state": "Tila", - "statusBreadCrumbsStatusPages": "Tilasivut", - "statusBreadCrumbsDetails": "Tiedot", - "commonSaving": "Tallennetaan...", - "navControls": "Ohjaimet", - "incidentsPageTitle": "Häiriöt", - "passwordPanel": { - "passwordChangedSuccess": "Salasanasi vaihdettiin onnistuneesti.", - "passwordInputIncorrect": "", - "currentPassword": "Nykyinen salasana", - "enterCurrentPassword": "Kirjoita nykyinen salasanasi", - "newPassword": "Uusi salasana", - "enterNewPassword": "Kirjoita uusi salasana", - "confirmNewPassword": "Vahvista uusi salasana", - "passwordRequirements": "Uuden salasanan on oltava vähintään 8 merkkiä pitkä ja sen on sisältävä vähintään yksi iso kirjain, yksi pieni kirjain, yksi numero sekä yksi erikoismerkki.", - "saving": "Tallennetaan..." - }, - "emailSent": "Sähköpostin lähetys onnistui", - "failedToSendEmail": "Sähköpostin lähetys epäonnistui", - "settingsTestEmailSuccess": "Testisähköpostin lähetys onnistui", - "settingsTestEmailFailed": "Testisähköpostin lähetys epäonnistui", - "settingsTestEmailFailedWithReason": "Testisähköpostin lähetys epäonnistui: {{reason}}", - "settingsTestEmailUnknownError": "Tuntematon virhe", - "statusMsg": { - "paused": "Valvonta on keskeytetty.", - "up": "Sivusi on ylhäällä.", - "down": "Sivusi on alhaalla.", - "pending": "Odottaa..." - }, - "uptimeGeneralInstructions": { - "http": "", - "ping": "", - "docker": "", - "port": "" - }, - "common": { - "appName": "", - "monitoringAgentName": "", - "buttons": { - "toggleTheme": "" - }, - "toasts": { - "networkError": "", - "checkConnection": "", - "unknownError": "" - } - }, - "auth": { - "common": { - "navigation": { - "continue": "", - "back": "" - }, - "inputs": { - "email": { - "label": "", - "placeholder": "", - "errors": { - "empty": "", - "invalid": "" - } - }, - "password": { - "label": "", - "rules": { - "length": { - "beginning": "", - "highlighted": "" - }, - "special": { - "beginning": "", - "highlighted": "" - }, - "number": { - "beginning": "", - "highlighted": "" - }, - "uppercase": { - "beginning": "", - "highlighted": "" - }, - "lowercase": { - "beginning": "", - "highlighted": "" - }, - "match": { - "beginning": "", - "highlighted": "" - } - }, - "errors": { - "empty": "", - "length": "", - "uppercase": "", - "lowercase": "", - "number": "", - "special": "", - "incorrect": "" - } - }, - "passwordConfirm": { - "label": "", - "placeholder": "", - "errors": { - "empty": "", - "different": "" - } - }, - "firstName": { - "label": "", - "placeholder": "", - "errors": { - "empty": "", - "length": "", - "pattern": "" - } - }, - "lastName": { - "label": "", - "placeholder": "", - "errors": { - "empty": "", - "length": "", - "pattern": "" - } - } - }, - "errors": { - "validation": "" - }, - "fields": { - "password": { - "errors": { - "incorrect": "" - } - } - } - }, - "login": { - "heading": "", - "subheadings": { - "stepOne": "", - "stepTwo": "" - }, - "links": { - "forgotPassword": "", - "register": "", - "forgotPasswordLink": "", - "registerLink": "" - }, - "toasts": { - "success": "", - "incorrectPassword": "" - }, - "errors": { - "password": { - "incorrect": "" - } - } - }, - "registration": { - "heading": { - "superAdmin": "", - "user": "" - }, - "subheadings": { - "stepOne": "", - "stepTwo": "", - "stepThree": "" - }, - "description": { - "superAdmin": "", - "user": "" - }, - "gettingStartedButton": { - "superAdmin": "", - "user": "" - }, - "termsAndPolicies": "", - "links": { - "login": "" - }, - "toasts": { - "success": "" - } - }, - "forgotPassword": { - "heading": "", - "subheadings": { - "stepOne": "", - "stepTwo": "", - "stepThree": "", - "stepFour": "" - }, - "buttons": { - "openEmail": "", - "resetPassword": "" - }, - "imageAlts": { - "passwordKey": "", - "email": "", - "lock": "", - "passwordConfirm": "" - }, - "links": { - "login": "", - "resend": "" - }, - "toasts": { - "sent": "", - "emailNotFound": "", - "redirect": "", - "success": "", - "error": "" - } - } - }, - "errorPages": { - "serverUnreachable": { - "toasts": { - "reconnected": "", - "stillUnreachable": "" - }, - "alertBox": "", - "description": "", - "retryButton": { - "default": "", - "processing": "" - } - } - }, - "createNotifications": { - "title": "", - "nameSettings": { - "title": "", - "description": "", - "nameLabel": "", - "namePlaceholder": "" - }, - "typeSettings": { - "title": "", - "description": "", - "typeLabel": "" - }, - "emailSettings": { - "title": "", - "description": "", - "emailLabel": "", - "emailPlaceholder": "" - }, - "slackSettings": { - "title": "", - "description": "", - "webhookLabel": "", - "webhookPlaceholder": "" - }, - "pagerdutySettings": { - "title": "", - "description": "", - "integrationKeyLabel": "", - "integrationKeyPlaceholder": "" - }, - "discordSettings": { - "title": "", - "description": "", - "webhookLabel": "", - "webhookPlaceholder": "" - }, - "webhookSettings": { - "title": "", - "description": "", - "webhookLabel": "", - "webhookPlaceholder": "" - } - }, - "notificationConfig": { - "title": "", - "description": "" - }, - "monitorStatus": { - "checkingEvery": "", - "withCaptureAgent": "", - "up": "", - "down": "", - "paused": "" - }, - "advancedMatching": "", - "sendTestNotifications": "", - "testNotificationsDisabled": "", - "selectAll": "", - "showAdminLoginLink": "", - "logsPage": { - "title": "", - "description": "", - "tabs": { - "queue": "", - "logs": "" - }, - "toast": { - "fetchLogsSuccess": "" - }, - "logLevelSelect": { - "title": "", - "values": { - "all": "", - "info": "", - "warn": "", - "error": "", - "debug": "" - } - } - }, - "queuePage": { - "title": "", - "refreshButton": "", - "flushButton": "", - "jobTable": { - "title": "", - "idHeader": "", - "urlHeader": "", - "typeHeader": "", - "activeHeader": "", - "lockedAtHeader": "", - "runCountHeader": "", - "failCountHeader": "", - "lastRunHeader": "", - "lastFinishedAtHeader": "", - "lastRunTookHeader": "" - }, - "metricsTable": { - "title": "", - "metricHeader": "", - "valueHeader": "" - }, - "failedJobTable": { - "title": "", - "monitorIdHeader": "", - "monitorUrlHeader": "", - "failCountHeader": "", - "failedAtHeader": "", - "failReasonHeader": "" - } - }, - "export": { - "title": "", - "success": "", - "failed": "" - }, - "monitorActions": { - "title": "", - "import": "", - "export": "" - }, - "settingsPage": { - "aboutSettings": { - "labelDevelopedBy": "", - "labelVersion": "", - "title": "" - }, - "demoMonitorsSettings": { - "buttonAddMonitors": "", - "description": "", - "title": "" - }, - "emailSettings": { - "buttonSendTestEmail": "", - "description": "", - "descriptionTransport": "", - "labelAddress": "", - "labelConnectionHost": "", - "labelHost": "", - "labelIgnoreTLS": "", - "labelPassword": "", - "labelPasswordSet": "", - "labelPool": "", - "labelPort": "", - "labelRejectUnauthorized": "", - "labelRequireTLS": "", - "labelSecure": "", - "labelTLSServername": "", - "labelUser": "", - "linkTransport": "", - "placeholderUser": "", - "title": "" - }, - "pageSpeedSettings": { - "description": "", - "labelApiKeySet": "", - "labelApiKey": "", - "title": "" - }, - "saveButtonLabel": "", - "statsSettings": { - "clearAllStatsButton": "", - "clearAllStatsDescription": "", - "clearAllStatsDialogConfirm": "", - "clearAllStatsDialogDescription": "", - "clearAllStatsDialogTitle": "", - "description": "", - "labelTTL": "", - "labelTTLOptional": "", - "title": "" - }, - "systemResetSettings": { - "buttonRemoveAllMonitors": "", - "description": "", - "dialogConfirm": "", - "dialogDescription": "", - "dialogTitle": "", - "title": "" - }, - "timezoneSettings": { - "description": "", - "label": "", - "title": "" - }, - "title": "", - "uiSettings": { - "description": "", - "labelLanguage": "", - "labelTheme": "", - "title": "" - }, - "urlSettings": { - "description": "", - "label": "", - "selectDisabled": "", - "selectEnabled": "", - "title": "" - } - } -} diff --git a/client/src/locales/fr.json b/client/src/locales/fr.json deleted file mode 100644 index fd2173b7b..000000000 --- a/client/src/locales/fr.json +++ /dev/null @@ -1,836 +0,0 @@ -{ - "submit": "Envoyer", - "title": "Titre", - "distributedStatusHeaderText": "", - "distributedStatusSubHeaderText": "", - "settingsDisabled": "Désactivé", - "settingsSuccessSaved": "", - "settingsFailedToSave": "", - "settingsStatsCleared": "", - "settingsFailedToClearStats": "", - "settingsFailedToAddDemoMonitors": "", - "settingsMonitorsDeleted": "", - "settingsFailedToDeleteMonitors": "", - "starPromptTitle": "", - "starPromptDescription": "", - "https": "", - "http": "", - "monitor": "", - "aboutus": "A propos de nous", - "now": "", - "delete": "Supprimer", - "configure": "", - "responseTime": "", - "ms": "", - "bar": "", - "area": "Zone", - "country": "PAYS", - "city": "VILLE", - "response": "", - "monitorStatusUp": "", - "monitorStatusDown": "", - "webhookSendSuccess": "", - "webhookSendError": "", - "webhookUnsupportedPlatform": "", - "distributedRightCategoryTitle": "Moniteur", - "distributedStatusServerMonitors": "", - "distributedStatusServerMonitorsDescription": "", - "distributedUptimeCreateSelectURL": "", - "distributedUptimeCreateChecks": "", - "distributedUptimeCreateChecksDescription": "", - "distributedUptimeCreateIncidentNotification": "Notifications d'incidents", - "distributedUptimeCreateIncidentDescription": "", - "distributedUptimeCreateAdvancedSettings": "Paramètres avancés", - "distributedUptimeDetailsNoMonitorHistory": "", - "distributedUptimeDetailsStatusHeaderUptime": "", - "distributedUptimeDetailsStatusHeaderLastUpdate": "", - "notifications": { - "enableNotifications": "", - "testNotification": "Notification de test", - "addOrEditNotifications": "Ajouter ou éditer des notifications", - "slack": { - "label": "", - "description": "", - "webhookLabel": "URL du webhook", - "webhookPlaceholder": "", - "webhookRequired": "" - }, - "discord": { - "label": "", - "description": "", - "webhookLabel": "URL du webhook Discord", - "webhookPlaceholder": "", - "webhookRequired": "" - }, - "telegram": { - "label": "", - "description": "", - "tokenLabel": "", - "tokenPlaceholder": "", - "chatIdLabel": "Votre Chat ID", - "chatIdPlaceholder": "-1001234567890", - "fieldsRequired": "" - }, - "webhook": { - "label": "", - "description": "", - "urlLabel": "URL du webhook", - "urlPlaceholder": "https://votre-serveur.fr/webhook", - "urlRequired": "L'URL du webhook est nécessaire" - }, - "testNotificationDevelop": "Notification de test 2", - "integrationButton": "Intégration de notification", - "testSuccess": "La notification de test a été envoyée avec succès !", - "testFailed": "Échec de l'envoi de la notification de test", - "unsupportedType": "", - "networkError": "", - "fallback": { - "title": "", - "checks": [""] - }, - "createButton": "", - "createTitle": "", - "create": { - "success": "", - "failed": "" - }, - "fetch": { - "success": "", - "failed": "" - }, - "delete": { - "success": "", - "failed": "" - }, - "edit": { - "success": "", - "failed": "" - }, - "test": { - "success": "", - "failed": "" - } - }, - "testLocale": "", - "add": "Ajouter", - "monitors": "", - "distributedUptimeStatusCreateStatusPage": "page de statut", - "distributedUptimeStatusCreateStatusPageAccess": "Accès", - "distributedUptimeStatusCreateStatusPageReady": "", - "distributedUptimeStatusBasicInfoHeader": "", - "distributedUptimeStatusBasicInfoDescription": "", - "distributedUptimeStatusLogoHeader": "Logo", - "distributedUptimeStatusLogoDescription": "", - "distributedUptimeStatusLogoUploadButton": "Téléverser un logo", - "distributedUptimeStatusStandardMonitorsHeader": "", - "distributedUptimeStatusStandardMonitorsDescription": "", - "distributedUptimeStatusCreateYour": "", - "distributedUptimeStatusEditYour": "", - "distributedUptimeStatusPublishedLabel": "", - "distributedUptimeStatusCompanyNameLabel": "Nom de l'entreprise", - "distributedUptimeStatusPageAddressLabel": "Adresse de votre page de statut", - "distributedUptimeStatus30Days": "30 jours", - "distributedUptimeStatus60Days": "60 jours", - "distributedUptimeStatus90Days": "90 jours", - "distributedUptimeStatusPageNotSetUp": "", - "distributedUptimeStatusContactAdmin": "Veuillez contacter votre administrateur", - "distributedUptimeStatusPageNotPublic": "Cette page de statut n'est pas publique.", - "distributedUptimeStatusPageDeleteDialog": "Voulez-vous supprimer cette page de statut ?", - "distributedUptimeStatusPageDeleteConfirm": "Oui, supprimer la page de statut", - "distributedUptimeStatusPageDeleteDescription": "Supprimer votre page de statut est irréversible.", - "distributedUptimeStatusDevices": "", - "distributedUptimeStatusUpt": "", - "distributedUptimeStatusUptBurned": "", - "distributedUptimeStatusUptLogo": "", - "incidentsTableNoIncidents": "", - "incidentsTablePaginationLabel": "incidents", - "incidentsTableMonitorName": "", - "incidentsTableStatus": "", - "incidentsTableDateTime": "Date et heure", - "incidentsTableStatusCode": "", - "incidentsTableMessage": "Message", - "incidentsOptionsHeader": "", - "incidentsOptionsHeaderFilterBy": "", - "incidentsOptionsHeaderFilterAll": "", - "incidentsOptionsHeaderFilterDown": "", - "incidentsOptionsHeaderFilterCannotResolve": "", - "incidentsOptionsHeaderShow": "", - "incidentsOptionsHeaderLastHour": "Dernière heure", - "incidentsOptionsHeaderLastDay": "Dernier jour", - "incidentsOptionsHeaderLastWeek": "Dernière semaine", - "incidentsOptionsPlaceholderAllServers": "", - "infrastructureCreateYour": "", - "infrastructureCreateGeneralSettingsDescription": "", - "infrastructureServerRequirement": "", - "infrastructureCustomizeAlerts": "", - "infrastructureAlertNotificationDescription": "", - "infrastructureCreateMonitor": "", - "infrastructureProtocol": "Protocole", - "infrastructureServerUrlLabel": "URL du serveur", - "infrastructureDisplayNameLabel": "", - "infrastructureAuthorizationSecretLabel": "", - "gb": "", - "mb": "", - "mem": "", - "memoryUsage": "", - "cpu": "CPU", - "cpuUsage": "", - "cpuTemperature": "", - "diskUsage": "Utilisation du disque", - "used": "", - "total": "", - "cores": "", - "frequency": "", - "status": "", - "cpuPhysical": "", - "cpuLogical": "", - "cpuFrequency": "Fréquence CPU", - "avgCpuTemperature": "Température moyenne du CPU", - "memory": "", - "disk": "Disque", - "uptime": "", - "os": "", - "host": "", - "actions": "Actions", - "integrations": "Intégrations", - "integrationsPrism": "Connecter Prism à votre service préféré.", - "integrationsSlack": "Slack", - "integrationsSlackInfo": "Connecter à Slack et voir les incidents dans un canal", - "integrationsDiscord": "Discord", - "integrationsDiscordInfo": "Connecter à Discord et voir les incidents dans un canal", - "integrationsZapier": "Zapier", - "integrationsZapierInfo": "Envoyer tous les incidents à Zapier", - "commonSave": "Sauvegarder", - "createYour": "Créez votre", - "createMonitor": "", - "pause": "", - "resume": "", - "editing": "", - "url": "URL", - "access": "Accès", - "timezone": "Fuseau horaire", - "features": "", - "administrator": "Administrateur ?", - "loginHere": "", - "displayName": "Nom d'affichage", - "urlMonitor": "URL à suivre", - "portToMonitor": "", - "websiteMonitoring": "", - "websiteMonitoringDescription": "", - "pingMonitoring": "", - "pingMonitoringDescription": "", - "dockerContainerMonitoring": "", - "dockerContainerMonitoringDescription": "", - "portMonitoring": "", - "portMonitoringDescription": "", - "createMaintenanceWindow": "", - "createMaintenance": "Créer une maintenance", - "editMaintenance": "", - "maintenanceWindowName": "", - "friendlyNameInput": "", - "friendlyNamePlaceholder": "", - "maintenanceRepeat": "", - "maintenance": "", - "duration": "", - "addMonitors": "Ajouter des moniteurs", - "window": "", - "cancel": "Annuler", - "message": "", - "low": "", - "high": "", - "statusCode": "", - "date&Time": "Date et heure", - "type": "", - "statusPageName": "", - "publicURL": "", - "repeat": "", - "edit": "", - "createA": "Créer un", - "remove": "", - "maintenanceWindowDescription": "", - "startTime": "", - "timeZoneInfo": "", - "monitorsToApply": "", - "nextWindow": "", - "notFoundButton": "", - "pageSpeedConfigureSettingsDescription": "", - "monitorDisplayName": "", - "whenNewIncident": "Lors d'un nouvel incident,", - "notifySMS": "", - "notifyEmails": "", - "seperateEmails": "", - "checkFrequency": "Vérifier la fréquence", - "matchMethod": "", - "expectedValue": "", - "deleteDialogTitle": "", - "deleteDialogDescription": "", - "pageSpeedMonitor": "", - "shown": "", - "ago": "depuis", - "companyName": "", - "pageSpeedDetailsPerformanceReport": "", - "pageSpeedDetailsPerformanceReportCalculator": "", - "checkingEvery": "Vérification tous les", - "statusPageCreateSettings": "", - "basicInformation": "", - "statusPageCreateBasicInfoDescription": "", - "statusPageCreateSelectTimeZoneDescription": "", - "statusPageCreateAppearanceDescription": "", - "statusPageCreateSettingsCheckboxLabel": "", - "statusPageCreateBasicInfoStatusPageAddress": "", - "statusPageCreateTabsContent": "", - "statusPageCreateTabsContentDescription": "", - "statusPageCreateTabsContentFeaturesDescription": "", - "showCharts": "", - "showUptimePercentage": "", - "removeLogo": "", - "statusPageStatus": "", - "statusPageStatusContactAdmin": "", - "statusPageStatusNotPublic": "", - "statusPageStatusNoPage": "", - "statusPageStatusServiceStatus": "", - "deleteStatusPage": "", - "deleteStatusPageConfirm": "Oui, supprimer la page de statut", - "deleteStatusPageDescription": "", - "uptimeCreate": "", - "uptimeCreateJsonPath": "", - "uptimeCreateJsonPathQuery": "", - "maintenanceTableActionMenuDialogTitle": "", - "infrastructureEditYour": "", - "infrastructureEditMonitor": "", - "infrastructureMonitorCreated": "", - "infrastructureMonitorUpdated": "", - "errorInvalidTypeId": "", - "errorInvalidFieldId": "", - "inviteNoTokenFound": "", - "pageSpeedWarning": "", - "pageSpeedLearnMoreLink": "", - "pageSpeedAddApiKey": "", - "update": "", - "invalidFileFormat": "Format de fichier non supporté !", - "invalidFileSize": "Le fichier est trop volumineux !", - "ClickUpload": "Cliquez pour téléverser", - "DragandDrop": "", - "MaxSize": "", - "SupportedFormats": "", - "FirstName": "", - "LastName": "", - "EmailDescriptionText": "", - "YourPhoto": "Photo de profil", - "PhotoDescriptionText": "", - "save": "", - "DeleteDescriptionText": "", - "DeleteAccountWarning": "", - "DeleteWarningTitle": "Vous supprimez vraiment ce compte ?", - "bulkImport": { - "title": "", - "selectFileTips": "", - "selectFileDescription": "", - "selectFile": "", - "parsingFailed": "", - "uploadSuccess": "", - "validationFailed": "Echec de la validation", - "noFileSelected": "", - "fallbackPage": "" - }, - "DeleteAccountTitle": "Supprimer le compte", - "DeleteAccountButton": "Supprimer le compte", - "publicLink": "", - "maskedPageSpeedKeyPlaceholder": "", - "reset": "", - "ignoreTLSError": "", - "tlsErrorIgnored": "", - "ignoreTLSErrorDescription": "", - "createNew": "Créer un nouveau", - "greeting": { - "prepend": "", - "append": "L'après-midi est votre libre, rendons-le épique !", - "overview": "" - }, - "roles": { - "superAdmin": "", - "admin": "Admin", - "teamMember": "", - "demoUser": "" - }, - "teamPanel": { - "teamMembers": "Membres de l'équipe", - "filter": { - "all": "Tous", - "member": "" - }, - "inviteTeamMember": "", - "inviteNewTeamMember": "", - "inviteDescription": "", - "email": "Email", - "selectRole": "", - "inviteLink": "Lien d'invitation", - "cancel": "Annuler", - "noMembers": "", - "getToken": "", - "emailToken": "", - "table": { - "name": "", - "email": "Email", - "role": "", - "created": "Créée" - } - }, - "monitorState": { - "paused": "", - "resumed": "", - "active": "Actif" - }, - "menu": { - "uptime": "", - "pagespeed": "", - "infrastructure": "", - "incidents": "", - "statusPages": "", - "maintenance": "", - "integrations": "Intégrations", - "settings": "", - "support": "", - "discussions": "Discussions", - "docs": "", - "changelog": "Changelog", - "profile": "", - "password": "", - "team": "", - "logOut": "Déconnexion", - "notifications": "", - "logs": "" - }, - "settingsEmailUser": "", - "state": "", - "statusBreadCrumbsStatusPages": "", - "statusBreadCrumbsDetails": "", - "commonSaving": "Sauvegarde...", - "navControls": "", - "incidentsPageTitle": "Incidents", - "passwordPanel": { - "passwordChangedSuccess": "Votre mot de passe a été modifié avec succès.", - "passwordInputIncorrect": "La saisie de votre mot de passe est incorrecte.", - "currentPassword": "Mot de passe actuel", - "enterCurrentPassword": "Entrez votre mot de passe actuel", - "newPassword": "Nouveau mot de passe", - "enterNewPassword": "Entrez votre nouveau mot de passe", - "confirmNewPassword": "Confirmer le nouveau mot de passe", - "passwordRequirements": "Le nouveau mot de passe doit contenir au moins 8 caractères et au moins une lettre majuscule, une lettre minuscule, un chiffre et un caractère spécial.", - "saving": "Sauvegarde..." - }, - "emailSent": "", - "failedToSendEmail": "", - "settingsTestEmailSuccess": "", - "settingsTestEmailFailed": "", - "settingsTestEmailFailedWithReason": "", - "settingsTestEmailUnknownError": "Erreur inconnue", - "statusMsg": { - "paused": "", - "up": "", - "down": "", - "pending": "" - }, - "uptimeGeneralInstructions": { - "http": "", - "ping": "", - "docker": "", - "port": "" - }, - "common": { - "appName": "", - "monitoringAgentName": "", - "buttons": { - "toggleTheme": "" - }, - "toasts": { - "networkError": "", - "checkConnection": "", - "unknownError": "" - } - }, - "auth": { - "common": { - "navigation": { - "continue": "", - "back": "" - }, - "inputs": { - "email": { - "label": "", - "placeholder": "", - "errors": { - "empty": "", - "invalid": "" - } - }, - "password": { - "label": "", - "rules": { - "length": { - "beginning": "", - "highlighted": "" - }, - "special": { - "beginning": "", - "highlighted": "" - }, - "number": { - "beginning": "", - "highlighted": "" - }, - "uppercase": { - "beginning": "", - "highlighted": "" - }, - "lowercase": { - "beginning": "", - "highlighted": "" - }, - "match": { - "beginning": "", - "highlighted": "" - } - }, - "errors": { - "empty": "", - "length": "", - "uppercase": "", - "lowercase": "", - "number": "", - "special": "", - "incorrect": "" - } - }, - "passwordConfirm": { - "label": "", - "placeholder": "", - "errors": { - "empty": "", - "different": "" - } - }, - "firstName": { - "label": "", - "placeholder": "", - "errors": { - "empty": "", - "length": "", - "pattern": "" - } - }, - "lastName": { - "label": "", - "placeholder": "", - "errors": { - "empty": "", - "length": "", - "pattern": "" - } - } - }, - "errors": { - "validation": "" - }, - "fields": { - "password": { - "errors": { - "incorrect": "" - } - } - } - }, - "login": { - "heading": "", - "subheadings": { - "stepOne": "", - "stepTwo": "" - }, - "links": { - "forgotPassword": "", - "register": "", - "forgotPasswordLink": "", - "registerLink": "" - }, - "toasts": { - "success": "", - "incorrectPassword": "" - }, - "errors": { - "password": { - "incorrect": "" - } - } - }, - "registration": { - "heading": { - "superAdmin": "", - "user": "" - }, - "subheadings": { - "stepOne": "", - "stepTwo": "", - "stepThree": "" - }, - "description": { - "superAdmin": "", - "user": "" - }, - "gettingStartedButton": { - "superAdmin": "", - "user": "" - }, - "termsAndPolicies": "", - "links": { - "login": "" - }, - "toasts": { - "success": "" - } - }, - "forgotPassword": { - "heading": "", - "subheadings": { - "stepOne": "", - "stepTwo": "", - "stepThree": "", - "stepFour": "" - }, - "buttons": { - "openEmail": "", - "resetPassword": "" - }, - "imageAlts": { - "passwordKey": "", - "email": "", - "lock": "", - "passwordConfirm": "" - }, - "links": { - "login": "", - "resend": "" - }, - "toasts": { - "sent": "", - "emailNotFound": "", - "redirect": "", - "success": "", - "error": "" - } - } - }, - "errorPages": { - "serverUnreachable": { - "toasts": { - "reconnected": "", - "stillUnreachable": "" - }, - "alertBox": "", - "description": "", - "retryButton": { - "default": "", - "processing": "" - } - } - }, - "createNotifications": { - "title": "", - "nameSettings": { - "title": "", - "description": "", - "nameLabel": "", - "namePlaceholder": "" - }, - "typeSettings": { - "title": "", - "description": "", - "typeLabel": "" - }, - "emailSettings": { - "title": "", - "description": "", - "emailLabel": "", - "emailPlaceholder": "" - }, - "slackSettings": { - "title": "", - "description": "", - "webhookLabel": "", - "webhookPlaceholder": "" - }, - "pagerdutySettings": { - "title": "", - "description": "", - "integrationKeyLabel": "", - "integrationKeyPlaceholder": "" - }, - "discordSettings": { - "title": "", - "description": "", - "webhookLabel": "", - "webhookPlaceholder": "" - }, - "webhookSettings": { - "title": "", - "description": "", - "webhookLabel": "", - "webhookPlaceholder": "" - } - }, - "notificationConfig": { - "title": "", - "description": "" - }, - "monitorStatus": { - "checkingEvery": "", - "withCaptureAgent": "", - "up": "", - "down": "", - "paused": "" - }, - "advancedMatching": "", - "sendTestNotifications": "", - "testNotificationsDisabled": "", - "selectAll": "", - "showAdminLoginLink": "", - "logsPage": { - "title": "", - "description": "", - "tabs": { - "queue": "", - "logs": "" - }, - "toast": { - "fetchLogsSuccess": "" - }, - "logLevelSelect": { - "title": "", - "values": { - "all": "", - "info": "", - "warn": "", - "error": "", - "debug": "" - } - } - }, - "queuePage": { - "title": "", - "refreshButton": "", - "flushButton": "", - "jobTable": { - "title": "", - "idHeader": "", - "urlHeader": "", - "typeHeader": "", - "activeHeader": "", - "lockedAtHeader": "", - "runCountHeader": "", - "failCountHeader": "", - "lastRunHeader": "", - "lastFinishedAtHeader": "", - "lastRunTookHeader": "" - }, - "metricsTable": { - "title": "", - "metricHeader": "", - "valueHeader": "" - }, - "failedJobTable": { - "title": "", - "monitorIdHeader": "", - "monitorUrlHeader": "", - "failCountHeader": "", - "failedAtHeader": "", - "failReasonHeader": "" - } - }, - "export": { - "title": "", - "success": "", - "failed": "" - }, - "monitorActions": { - "title": "", - "import": "", - "export": "" - }, - "settingsPage": { - "aboutSettings": { - "labelDevelopedBy": "", - "labelVersion": "", - "title": "" - }, - "demoMonitorsSettings": { - "buttonAddMonitors": "", - "description": "", - "title": "" - }, - "emailSettings": { - "buttonSendTestEmail": "", - "description": "", - "descriptionTransport": "", - "labelAddress": "", - "labelConnectionHost": "", - "labelHost": "", - "labelIgnoreTLS": "", - "labelPassword": "", - "labelPasswordSet": "", - "labelPool": "", - "labelPort": "", - "labelRejectUnauthorized": "", - "labelRequireTLS": "", - "labelSecure": "", - "labelTLSServername": "", - "labelUser": "", - "linkTransport": "", - "placeholderUser": "", - "title": "" - }, - "pageSpeedSettings": { - "description": "", - "labelApiKeySet": "", - "labelApiKey": "", - "title": "" - }, - "saveButtonLabel": "", - "statsSettings": { - "clearAllStatsButton": "", - "clearAllStatsDescription": "", - "clearAllStatsDialogConfirm": "", - "clearAllStatsDialogDescription": "", - "clearAllStatsDialogTitle": "", - "description": "", - "labelTTL": "", - "labelTTLOptional": "", - "title": "" - }, - "systemResetSettings": { - "buttonRemoveAllMonitors": "", - "description": "", - "dialogConfirm": "", - "dialogDescription": "", - "dialogTitle": "", - "title": "" - }, - "timezoneSettings": { - "description": "", - "label": "", - "title": "" - }, - "title": "", - "uiSettings": { - "description": "", - "labelLanguage": "", - "labelTheme": "", - "title": "" - }, - "urlSettings": { - "description": "", - "label": "", - "selectDisabled": "", - "selectEnabled": "", - "title": "" - } - } -} diff --git a/client/src/locales/ar.json b/client/src/locales/ja.json similarity index 100% rename from client/src/locales/ar.json rename to client/src/locales/ja.json diff --git a/client/src/locales/es.json b/client/src/locales/uk.json similarity index 99% rename from client/src/locales/es.json rename to client/src/locales/uk.json index 0db51b102..b3b8fc4ce 100644 --- a/client/src/locales/es.json +++ b/client/src/locales/uk.json @@ -1,6 +1,6 @@ { - "submit": "Enviar", - "title": "Titulo", + "submit": "", + "title": "", "distributedStatusHeaderText": "", "distributedStatusSubHeaderText": "", "settingsDisabled": "", diff --git a/client/src/locales/cs.json b/client/src/locales/vi.json similarity index 74% rename from client/src/locales/cs.json rename to client/src/locales/vi.json index 2bb03f5ad..b3b8fc4ce 100644 --- a/client/src/locales/cs.json +++ b/client/src/locales/vi.json @@ -1,11 +1,11 @@ { - "submit": "Odeslat", + "submit": "", "title": "", "distributedStatusHeaderText": "", "distributedStatusSubHeaderText": "", - "settingsDisabled": "Vypnuto", - "settingsSuccessSaved": "Nastavení bylo úspěšně uloženo", - "settingsFailedToSave": "Nepodařilo se uložit nastavení", + "settingsDisabled": "", + "settingsSuccessSaved": "", + "settingsFailedToSave": "", "settingsStatsCleared": "", "settingsFailedToClearStats": "", "settingsFailedToAddDemoMonitors": "", @@ -438,99 +438,99 @@ "port": "" }, "common": { - "appName": "Checkmate", - "monitoringAgentName": "Capture", + "appName": "", + "monitoringAgentName": "", "buttons": { - "toggleTheme": "Přepnout mezi světlým a tmavým motivem" + "toggleTheme": "" }, "toasts": { - "networkError": "Chyba připojení k síti", - "checkConnection": "Zkontrolujte prosím své připojení k síti", - "unknownError": "Nastala neznámá chyba" + "networkError": "", + "checkConnection": "", + "unknownError": "" } }, "auth": { "common": { "navigation": { - "continue": "Pokračovat", - "back": "Zpět" + "continue": "", + "back": "" }, "inputs": { "email": { - "label": "E-mail", - "placeholder": "jan.novak@domena.cz", + "label": "", + "placeholder": "", "errors": { - "empty": "Zadejte prosím e-mailovou adresu", - "invalid": "Překontrolujte si prosím správnost zadané e-mailové adresy" + "empty": "", + "invalid": "" } }, "password": { - "label": "Heslo", + "label": "", "rules": { "length": { - "beginning": "Heslo musí být dlouhé alespoň", - "highlighted": "8 znaků" + "beginning": "", + "highlighted": "" }, "special": { - "beginning": "Heslo musí obsahovat alespoň", - "highlighted": "1 speciální znak" + "beginning": "", + "highlighted": "" }, "number": { - "beginning": "Heslo musí obsahovat alespoň", - "highlighted": "1 číslo" + "beginning": "", + "highlighted": "" }, "uppercase": { - "beginning": "Heslo musí obsahovat alespoň", - "highlighted": "1 velké písmeno" + "beginning": "", + "highlighted": "" }, "lowercase": { - "beginning": "Heslo musí obsahovat alespoň", - "highlighted": "1 malé písmeno" + "beginning": "", + "highlighted": "" }, "match": { - "beginning": "Obě hesla se", - "highlighted": "musí shodovat" + "beginning": "", + "highlighted": "" } }, "errors": { - "empty": "Zadejte prosím heslo", - "length": "Heslo musí být dlouhé alespoň 8 znaků", - "uppercase": "Heslo musí obsahovat alespoň 1 velké písmeno", - "lowercase": "Heslo musí obsahovat alespoň 1 malé písmeno", - "number": "Heslo musí obsahovat alespoň 1 číslo", - "special": "Heslo musí obsahovat alespoň 1 speciální znak", - "incorrect": "Zadané heslo neodpovídá tomu, co bylo nastaveno" + "empty": "", + "length": "", + "uppercase": "", + "lowercase": "", + "number": "", + "special": "", + "incorrect": "" } }, "passwordConfirm": { - "label": "Potvrzení hesla", - "placeholder": "Pro ověření správnosti zadejte heslo ještě jednou", + "label": "", + "placeholder": "", "errors": { - "empty": "Zadejte prosím své heslo ještě jednou, aby mohla být potvrzena jeho správnost (odhalí překlepy)", - "different": "Zadaná hesla se liší, takže je nejspíše jedno z nich špatně zapsané" + "empty": "", + "different": "" } }, "firstName": { - "label": "Jméno", - "placeholder": "Jan", + "label": "", + "placeholder": "", "errors": { - "empty": "Zadejte prosím své křestní jméno", - "length": "Jméno se musí vejít do 50 znaků", - "pattern": "Součástí jména mohou být pouze písmena, mezery apostrofy nebo spojovníky" + "empty": "", + "length": "", + "pattern": "" } }, "lastName": { - "label": "Příjmení", - "placeholder": "Novák", + "label": "", + "placeholder": "", "errors": { - "empty": "Zadejte prosím své příjmení", - "length": "Příjmení se musí vejít do 50 znaků", - "pattern": "Součástí příjmení mohou být pouze písmena, mezery apostrofy nebo spojovníky" + "empty": "", + "length": "", + "pattern": "" } } }, "errors": { - "validation": "Nastala chyba při ověřování dat." + "validation": "" }, "fields": { "password": { @@ -541,20 +541,20 @@ } }, "login": { - "heading": "Přihlášení", + "heading": "", "subheadings": { - "stepOne": "Zadejte svůj e-mail", - "stepTwo": "Zadejte své heslo" + "stepOne": "", + "stepTwo": "" }, "links": { - "forgotPassword": "Zapomněli jste heslo? Obnovte si ho", - "register": "Ještě nemáte účet? Zaregistrujte se", + "forgotPassword": "", + "register": "", "forgotPasswordLink": "", "registerLink": "" }, "toasts": { - "success": "Vítejte zpět", - "incorrectPassword": "Nesprávné heslo" + "success": "", + "incorrectPassword": "" }, "errors": { "password": { @@ -564,72 +564,72 @@ }, "registration": { "heading": { - "superAdmin": "Vytvoření superadministrátora", - "user": "Registrace" + "superAdmin": "", + "user": "" }, "subheadings": { - "stepOne": "Zadejte své osobní údaje", - "stepTwo": "Zadejte svůj e-mail", - "stepThree": "Nastavte si heslo" + "stepOne": "", + "stepTwo": "", + "stepThree": "" }, "description": { - "superAdmin": "Pro začátek vytvořte účet superadministrátora", - "user": "Zaregistrujte se jako uživatel a požádejte svého superadministrátora, aby vám přidělil oprávnění k hlídačům" + "superAdmin": "", + "user": "" }, "gettingStartedButton": { - "superAdmin": "Vytvořit účet superadministrátora", - "user": "Zaregistrovat se jako běžný uživatel" + "superAdmin": "", + "user": "" }, - "termsAndPolicies": "Vytvořením účtu souhlasíte s našimi Podmínkami použití (anglicky) a Podmínkami zpracování osobních údajů (anglicky).", + "termsAndPolicies": "", "links": { - "login": "Máte již vytvořený účet? Přihlašte se" + "login": "" }, "toasts": { - "success": "Vítejte! Váš účet byl úspěšně vytvořen." + "success": "" } }, "forgotPassword": { - "heading": "Zapomenuté heslo", + "heading": "", "subheadings": { - "stepOne": "Nebojte se, zašleme vám pokyny pro obnovení hesla.", - "stepTwo": "Odkaz pro obnovení hesla byl odeslán na ", - "stepThree": "Nové heslo se musí lišit od těch, které jste již použili.", - "stepFour": "Heslo bylo úspěšně obnoveno. Klepněte na tlačítko a dojde k čarovnému přihlášení." + "stepOne": "", + "stepTwo": "", + "stepThree": "", + "stepFour": "" }, "buttons": { - "openEmail": "Otevřít e-mailovou aplikaci", - "resetPassword": "Obnovit heslo" + "openEmail": "", + "resetPassword": "" }, "imageAlts": { - "passwordKey": "Ikonka přístupového klíče", - "email": "Ikonka e-mailu", - "lock": "Ikonka zámku", - "passwordConfirm": "Ikonka potvrzeného hesla" + "passwordKey": "", + "email": "", + "lock": "", + "passwordConfirm": "" }, "links": { - "login": "Vrátit se k Přihlášení", - "resend": "Nedorazil vám e-mail? Klepněte pro opětovné zaslání" + "login": "", + "resend": "" }, "toasts": { - "sent": "Instrukce vám dorazí na .", - "emailNotFound": "Nepodařilo se nalézt e-mailovou adresu.", - "redirect": "Přesměrování proběhne za ...", - "success": "Heslo bylo úspěšně obnoveno.", - "error": "Obnova hesla se nepodařila. Zkuste to prosím znovu nebo kontaktujte podporu." + "sent": "", + "emailNotFound": "", + "redirect": "", + "success": "", + "error": "" } } }, "errorPages": { "serverUnreachable": { "toasts": { - "reconnected": "Podařilo se úspěšně obnovit spojení se serverem", - "stillUnreachable": "Server je stále nedosažitelný. Zkuste to prosím později." + "reconnected": "", + "stillUnreachable": "" }, - "alertBox": "Chyba připojení k serveru", - "description": "Nepodařilo se připojit k serveru. Zkontrolujte si prosím internetové připojení a pokud problém přetrvává, ověřte konfiguraci aplikace Checkmate.", + "alertBox": "", + "description": "", "retryButton": { - "default": "Zkusit se znovu připojit", - "processing": "Probíhá připojování…" + "default": "", + "processing": "" } } }, diff --git a/server/.prettierrc b/server/.prettierrc index 5c345e37a..681669aa7 100755 --- a/server/.prettierrc +++ b/server/.prettierrc @@ -1,5 +1,5 @@ { - "printWidth": 90, + "printWidth": 150, "useTabs": true, "tabWidth": 2, "singleQuote": false, diff --git a/server/controllers/announcementsController.js b/server/controllers/announcementsController.js index 024ed95a5..aeb7a3b9d 100755 --- a/server/controllers/announcementsController.js +++ b/server/controllers/announcementsController.js @@ -1,5 +1,4 @@ import { createAnnouncementValidation } from "../validation/joi.js"; -import { handleError } from "./controllerUtils.js"; const SERVICE_NAME = "announcementController"; @@ -10,10 +9,9 @@ const SERVICE_NAME = "announcementController"; * @class AnnouncementController */ -class AnnouncementController { - constructor(db, stringService) { - this.db = db; - this.stringService = stringService; +class AnnouncementController extends BaseController { + constructor(commonDependencies) { + super(commonDependencies); this.createAnnouncement = this.createAnnouncement.bind(this); this.getAnnouncement = this.getAnnouncement.bind(this); } @@ -28,16 +26,10 @@ class AnnouncementController { * * @returns {Promise} A promise that resolves once the response is sent. */ - createAnnouncement = async (req, res, next) => { - try { + createAnnouncement = asyncHandler( + async (req, res, next) => { await createAnnouncementValidation.validateAsync(req.body); - } catch (error) { - return next(handleError(error, SERVICE_NAME)); // Handle Joi validation errors - } - - const { title, message } = req.body; - - try { + const { title, message } = req.body; const announcementData = { title: title.trim(), message: message.trim(), @@ -49,10 +41,10 @@ class AnnouncementController { msg: this.stringService.createAnnouncement, data: newAnnouncement, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "createAnnouncement")); - } - }; + }, + SERVICE_NAME, + "createAnnouncement" + ); /** * Handles retrieving announcements with pagination. @@ -63,17 +55,17 @@ class AnnouncementController { * - `msg`: A message about the success of the request. * @param {Function} next - The next middleware function in the stack for error handling. */ - getAnnouncement = async (req, res, next) => { - try { + getAnnouncement = asyncHandler( + async (req, res, next) => { const allAnnouncements = await this.db.getAnnouncements(); return res.success({ msg: this.stringService.getAnnouncement, data: allAnnouncements, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getAnnouncement")); - } - }; + }, + SERVICE_NAME, + "getAnnouncement" + ); } export default AnnouncementController; diff --git a/server/controllers/authController.js b/server/controllers/authController.js index 1d8c2af0e..00fc2caf5 100755 --- a/server/controllers/authController.js +++ b/server/controllers/authController.js @@ -1,452 +1,456 @@ +import BaseController from "./baseController.js"; import { registrationBodyValidation, loginValidation, - editUserParamValidation, editUserBodyValidation, recoveryValidation, - recoveryTokenValidation, + recoveryTokenBodyValidation, newPasswordValidation, + getUserByIdParamValidation, + editUserByIdParamValidation, + editUserByIdBodyValidation, + editSuperadminUserByIdBodyValidation, } from "../validation/joi.js"; -import jwt from "jsonwebtoken"; -import { getTokenFromHeaders } from "../utils/utils.js"; -import crypto from "crypto"; -import { handleValidationError, handleError } from "./controllerUtils.js"; + const SERVICE_NAME = "authController"; -class AuthController { - constructor({ db, settingsService, emailService, jobQueue, stringService, logger }) { - this.db = db; +/** + * Authentication Controller + * + * Handles all authentication-related HTTP requests including user registration, + * login, password recovery, and user management operations. + * + * @class AuthController + * @description Manages user authentication and authorization operations + */ +class AuthController extends BaseController { + /** + * Creates an instance of AuthController. + * + * @param {Object} commonDependencies - Common dependencies injected into the controller + * @param {Object} dependencies - The dependencies required by the controller + * @param {Object} dependencies.settingsService - Service for application settings + * @param {Object} dependencies.emailService - Service for email operations + * @param {Object} dependencies.jobQueue - Service for job queue operations + * @param {Object} dependencies.userService - User business logic service + */ + constructor(commonDependencies, { settingsService, emailService, jobQueue, userService }) { + super(commonDependencies); this.settingsService = settingsService; this.emailService = emailService; this.jobQueue = jobQueue; - this.stringService = stringService; - this.logger = logger; + this.userService = userService; } /** - * Creates and returns JWT token with an arbitrary payload - * @function - * @param {Object} payload - * @param {Object} appSettings - * @returns {String} - * @throws {Error} - */ - issueToken = (payload, appSettings) => { - try { - const tokenTTL = appSettings?.jwtTTL ?? "2h"; - const tokenSecret = appSettings?.jwtSecret; - const payloadData = payload; - - return jwt.sign(payloadData, tokenSecret, { expiresIn: tokenTTL }); - } catch (error) { - throw handleError(error, SERVICE_NAME, "issueToken"); - } - }; - - /** - * Registers a new user. If the user is the first account, a JWT secret is created. If not, an invite token is required. + * Registers a new user in the system. + * * @async - * @param {Object} req - The Express request object. - * @property {Object} req.body - The body of the request. - * @property {string} req.body.inviteToken - The invite token for registration. - * @property {Object} req.file - The file object for the user's profile image. - * @param {Object} res - The Express response object. - * @param {function} next - The next middleware function. - * @returns {Object} The response object with a success status, a message indicating the creation of the user, the created user data, and a JWT token. - * @throws {Error} If there is an error during the process, especially if there is a validation error (422). + * @function registerUser + * @param {Object} req - Express request object + * @param {Object} req.body - Request body containing user registration data + * @param {string} req.body.firstName - User's first name + * @param {string} req.body.lastName - User's last name + * @param {string} req.body.email - User's email address (will be converted to lowercase) + * @param {string} req.body.password - User's password + * @param {string} [req.body.inviteToken] - Invite token for registration (required if superadmin exists) + * @param {string} [req.body.teamId] - Team ID (auto-assigned if superadmin) + * @param {Array} [req.body.role] - User roles (auto-assigned if superadmin) + * @param {Object} [req.file] - Profile image file uploaded via multer + * @param {Object} res - Express response object + * @returns {Promise} Success response with user data and JWT token + * @throws {Error} 422 - Validation error if request body is invalid + * @throws {Error} 409 - Conflict if user already exists + * @example + * // Register first user (becomes superadmin) + * POST /auth/register + * { + * "firstName": "John", + * "lastName": "Doe", + * "email": "john@example.com", + * "password": "SecurePass123!" + * } + * + * // Register subsequent user (requires invite token) + * POST /auth/register + * { + * "firstName": "Jane", + * "lastName": "Smith", + * "email": "jane@example.com", + * "password": "SecurePass123!", + * "inviteToken": "abc123..." + * } */ - registerUser = async (req, res, next) => { - try { + registerUser = this.asyncHandler( + async (req, res) => { if (req.body?.email) { req.body.email = req.body.email?.toLowerCase(); } await registrationBodyValidation.validateAsync(req.body); - } catch (error) { - const validationError = handleValidationError(error, SERVICE_NAME); - next(validationError); - return; - } - // Create a new user - try { - const user = req.body; - // If superAdmin exists, a token should be attached to all further register requests - const superAdminExists = await this.db.checkSuperadmin(req, res); - if (superAdminExists) { - const invitedUser = await this.db.getInviteTokenAndDelete(user.inviteToken); - user.role = invitedUser.role; - user.teamId = invitedUser.teamId; - } else { - // This is the first account, create JWT secret to use if one is not supplied by env - const jwtSecret = crypto.randomBytes(64).toString("hex"); - await this.db.updateAppSettings({ jwtSecret }); - } - - const newUser = await this.db.insertUser({ ...req.body }, req.file); - this.logger.info({ - message: this.stringService.authCreateUser, - service: SERVICE_NAME, - details: newUser._id, - }); - - const userForToken = { ...newUser._doc }; - delete userForToken.profileImage; - delete userForToken.avatarImage; - - const appSettings = await this.settingsService.getSettings(); - - const token = this.issueToken(userForToken, appSettings); - - try { - const html = await this.emailService.buildEmail("welcomeEmailTemplate", { - name: newUser.firstName, - }); - this.emailService - .sendEmail(newUser.email, "Welcome to Uptime Monitor", html) - .catch((error) => { - this.logger.warn({ - message: error.message, - service: SERVICE_NAME, - method: "registerUser", - stack: error.stack, - }); - }); - } catch (error) { - this.logger.warn({ - message: error.message, - service: SERVICE_NAME, - method: "registerUser", - stack: error.stack, - }); - } - + const { user, token } = await this.userService.registerUser(req.body, req.file); res.success({ msg: this.stringService.authCreateUser, - data: { user: newUser, token: token }, + data: { user, token }, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "registerController")); - } - }; + }, + SERVICE_NAME, + "registerUser" + ); /** - * Logs in a user by validating the user's credentials and issuing a JWT token. + * Authenticates a user and returns a JWT token. + * * @async - * @param {Object} req - The Express request object. - * @property {Object} req.body - The body of the request. - * @property {string} req.body.email - The email of the user. - * @property {string} req.body.password - The password of the user. - * @param {Object} res - The Express response object. - * @param {function} next - The next middleware function. - * @returns {Object} The response object with a success status, a message indicating the login of the user, the user data (without password and avatar image), and a JWT token. - * @throws {Error} If there is an error during the process, especially if there is a validation error (422) or the password is incorrect. + * @function loginUser + * @param {Object} req - Express request object + * @param {Object} req.body - Request body containing login credentials + * @param {string} req.body.email - User's email address (will be converted to lowercase) + * @param {string} req.body.password - User's password + * @param {Object} res - Express response object + * @returns {Promise} Success response with user data and JWT token + * @throws {Error} 422 - Validation error if request body is invalid + * @throws {Error} 401 - Unauthorized if credentials are incorrect + * @example + * POST /auth/login + * { + * "email": "john@example.com", + * "password": "SecurePass123!" + * } */ - loginUser = async (req, res, next) => { - try { + loginUser = this.asyncHandler( + async (req, res) => { if (req.body?.email) { req.body.email = req.body.email?.toLowerCase(); } await loginValidation.validateAsync(req.body); - } catch (error) { - const validationError = handleValidationError(error, SERVICE_NAME); - next(validationError); - return; - } - try { - const { email, password } = req.body; - - // Check if user exists - const user = await this.db.getUserByEmail(email); - - // Compare password - const match = await user.comparePassword(password); - if (match !== true) { - const error = new Error(this.stringService.authIncorrectPassword); - error.status = 401; - next(error); - return; - } - - // Remove password from user object. Should this be abstracted to DB layer? - const userWithoutPassword = { ...user._doc }; - delete userWithoutPassword.password; - delete userWithoutPassword.avatarImage; - - // Happy path, return token - const appSettings = await this.settingsService.getSettings(); - const token = this.issueToken(userWithoutPassword, appSettings); - // reset avatar image - userWithoutPassword.avatarImage = user.avatarImage; + const { user, token } = await this.userService.loginUser(req.body.email, req.body.password); return res.success({ msg: this.stringService.authLoginUser, data: { - user: userWithoutPassword, - token: token, + user, + token, }, }); - } catch (error) { - error.status = 401; - next(handleError(error, SERVICE_NAME, "loginUser")); - } - }; + }, + SERVICE_NAME, + "loginUser" + ); /** - * Edits a user's information. If the user wants to change their password, the current password is checked before updating to the new password. + * Updates the current user's profile information. + * * @async - * @param {Object} req - The Express request object. - * @property {Object} req.params - The parameters of the request. - * @property {string} req.params.userId - The ID of the user to be edited. - * @property {Object} req.body - The body of the request. - * @property {string} req.body.password - The current password of the user. - * @property {string} req.body.newPassword - The new password of the user. - * @param {Object} res - The Express response object. - * @param {function} next - The next middleware function. - * @returns {Object} The response object with a success status, a message indicating the update of the user, and the updated user data. - * @throws {Error} If there is an error during the process, especially if there is a validation error (422), the user is unauthorized (401), or the password is incorrect (403). + * @function editUser + * @param {Object} req - Express request object + * @param {Object} req.body - Request body containing user update data + * @param {string} [req.body.firstName] - Updated first name + * @param {string} [req.body.lastName] - Updated last name + * @param {string} [req.body.password] - Current password (required for password change) + * @param {string} [req.body.newPassword] - New password (required for password change) + * @param {boolean} [req.body.deleteProfileImage] - Flag to delete profile image + * @param {Object} [req.file] - New profile image file + * @param {Object} req.user - Current authenticated user (from JWT) + * @param {Object} res - Express response object + * @returns {Promise} Success response with updated user data + * @throws {Error} 422 - Validation error if request body is invalid + * @throws {Error} 403 - Forbidden if current password is incorrect + * @example + * PUT /auth/user + * { + * "firstName": "John Updated", + * "lastName": "Doe Updated" + * } + * + * // Change password + * PUT /auth/user + * { + * "password": "OldPass123!", + * "newPassword": "NewPass123!" + * } */ - editUser = async (req, res, next) => { - try { - await editUserParamValidation.validateAsync(req.params); + editUser = this.asyncHandler( + async (req, res) => { await editUserBodyValidation.validateAsync(req.body); - } catch (error) { - const validationError = handleValidationError(error, SERVICE_NAME); - next(validationError); - return; - } - // TODO is this neccessary any longer? Verify ownership middleware should handle this - if (req.params.userId !== req.user._id.toString()) { - const error = new Error(this.stringService.unauthorized); - error.status = 401; - error.service = SERVICE_NAME; - next(error); - return; - } + const updatedUser = await this.userService.editUser(req.body, req.file, req.user); - try { - // Change Password check - if (req.body.password && req.body.newPassword) { - // Get token from headers - const token = getTokenFromHeaders(req.headers); - // Get email from token - const { jwtSecret } = this.settingsService.getSettings(); - const { email } = jwt.verify(token, jwtSecret); - // Add user email to body for DB operation - req.body.email = email; - // Get user - const user = await this.db.getUserByEmail(email); - // Compare passwords - const match = await user.comparePassword(req.body.password); - // If not a match, throw a 403 - // 403 instead of 401 to avoid triggering axios interceptor - if (!match) { - const error = new Error(this.stringService.authIncorrectPassword); - error.status = 403; - next(error); - return; - } - // If a match, update the password - req.body.password = req.body.newPassword; - } - - const updatedUser = await this.db.updateUser(req, res); res.success({ msg: this.stringService.authUpdateUser, data: updatedUser, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "userEditController")); - } - }; + }, + SERVICE_NAME, + "editUser" + ); /** - * Checks if a superadmin account exists in the database. + * Checks if a superadmin account exists in the system. + * * @async - * @param {Object} req - The Express request object. - * @param {Object} res - The Express response object. - * @param {function} next - The next middleware function. - * @returns {Object} The response object with a success status, a message indicating the existence of a superadmin, and a boolean indicating the existence of a superadmin. - * @throws {Error} If there is an error during the process. + * @function checkSuperadminExists + * @param {Object} req - Express request object + * @param {Object} res - Express response object + * @returns {Promise} Success response with boolean indicating superadmin existence + * @example + * GET /auth/users/superadmin + * // Response: { "data": true } or { "data": false } */ - checkSuperadminExists = async (req, res, next) => { - try { - const superAdminExists = await this.db.checkSuperadmin(req, res); - + checkSuperadminExists = this.asyncHandler( + async (req, res) => { + const superAdminExists = await this.userService.checkSuperadminExists(); return res.success({ msg: this.stringService.authAdminExists, data: superAdminExists, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "checkSuperadminController")); - } - }; + }, + SERVICE_NAME, + "checkSuperadminExists" + ); + /** - * Requests a recovery token for a user. The user's email is validated and a recovery token is created and sent via email. + * Initiates password recovery process by sending a recovery email. + * * @async - * @param {Object} req - The Express request object. - * @property {Object} req.body - The body of the request. - * @property {string} req.body.email - The email of the user requesting recovery. - * @param {Object} res - The Express response object. - * @param {function} next - The next middleware function. - * @returns {Object} The response object with a success status, a message indicating the creation of the recovery token, and the message ID of the sent email. - * @throws {Error} If there is an error during the process, especially if there is a validation error (422). + * @function requestRecovery + * @param {Object} req - Express request object + * @param {Object} req.body - Request body containing email + * @param {string} req.body.email - Email address for password recovery + * @param {Object} res - Express response object + * @returns {Promise} Success response with message ID + * @throws {Error} 422 - Validation error if email is invalid + * @throws {Error} 404 - Not found if user doesn't exist + * @example + * POST /auth/recovery/request + * { + * "email": "john@example.com" + * } */ - requestRecovery = async (req, res, next) => { - try { + requestRecovery = this.asyncHandler( + async (req, res) => { await recoveryValidation.validateAsync(req.body); - } catch (error) { - const validationError = handleValidationError(error, SERVICE_NAME); - next(validationError); - return; - } - - try { - const { email } = req.body; - const user = await this.db.getUserByEmail(email); - const recoveryToken = await this.db.requestRecoveryToken(req, res); - const name = user.firstName; - const { clientHost } = this.settingsService.getSettings(); - const url = `${clientHost}/set-new-password/${recoveryToken.token}`; - - const html = await this.emailService.buildEmail("passwordResetTemplate", { - name, - email, - url, - }); - const msgId = await this.emailService.sendEmail( - email, - "Checkmate Password Reset", - html - ); - + const email = req?.body?.email; + const msgId = await this.userService.requestRecovery(email); return res.success({ msg: this.stringService.authCreateRecoveryToken, data: msgId, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "recoveryRequestController")); - } - }; + }, + SERVICE_NAME, + "requestRecovery" + ); + /** - * Validates a recovery token. The recovery token is validated and if valid, a success message is returned. + * Validates a password recovery token. + * * @async - * @param {Object} req - The Express request object. - * @property {Object} req.body - The body of the request. - * @property {string} req.body.token - The recovery token to be validated. - * @param {Object} res - The Express response object. - * @param {function} next - The next middleware function. - * @returns {Object} The response object with a success status and a message indicating the validation of the recovery token. - * @throws {Error} If there is an error during the process, especially if there is a validation error (422). + * @function validateRecovery + * @param {Object} req - Express request object + * @param {Object} req.body - Request body containing recovery token + * @param {string} req.body.recoveryToken - Recovery token to validate + * @param {Object} res - Express response object + * @returns {Promise} Success response if token is valid + * @throws {Error} 422 - Validation error if token format is invalid + * @throws {Error} 400 - Bad request if token is invalid or expired + * @example + * POST /auth/recovery/validate + * { + * "recoveryToken": "abc123..." + * } */ - validateRecovery = async (req, res, next) => { - try { - await recoveryTokenValidation.validateAsync(req.body); - } catch (error) { - const validationError = handleValidationError(error, SERVICE_NAME); - next(validationError); - return; - } - - try { - await this.db.validateRecoveryToken(req, res); - + validateRecovery = this.asyncHandler( + async (req, res) => { + await recoveryTokenBodyValidation.validateAsync(req.body); + await this.userService.validateRecovery(req.body.recoveryToken); return res.success({ msg: this.stringService.authVerifyRecoveryToken, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "validateRecoveryTokenController")); - } - }; + }, + SERVICE_NAME, + "validateRecovery" + ); /** - * Resets a user's password. The new password is validated and if valid, the user's password is updated in the database and a new JWT token is issued. + * Resets user password using a valid recovery token. + * * @async - * @param {Object} req - The Express request object. - * @property {Object} req.body - The body of the request. - * @property {string} req.body.token - The recovery token. - * @property {string} req.body.password - The new password of the user. - * @param {Object} res - The Express response object. - * @param {function} next - The next middleware function. - * @returns {Object} The response object with a success status, a message indicating the reset of the password, the updated user data (without password and avatar image), and a new JWT token. - * @throws {Error} If there is an error during the process, especially if there is a validation error (422). + * @function resetPassword + * @param {Object} req - Express request object + * @param {Object} req.body - Request body containing new password and recovery token + * @param {string} req.body.password - New password + * @param {string} req.body.recoveryToken - Valid recovery token + * @param {Object} res - Express response object + * @returns {Promise} Success response with user data and JWT token + * @throws {Error} 422 - Validation error if password format is invalid + * @throws {Error} 400 - Bad request if token is invalid or expired + * @example + * POST /auth/recovery/reset + * { + * "password": "NewSecurePass123!", + * "recoveryToken": "abc123..." + * } */ - resetPassword = async (req, res, next) => { - try { + resetPassword = this.asyncHandler( + async (req, res) => { await newPasswordValidation.validateAsync(req.body); - } catch (error) { - const validationError = handleValidationError(error, SERVICE_NAME); - next(validationError); - return; - } - try { - const user = await this.db.resetPassword(req, res); - const appSettings = await this.settingsService.getSettings(); - const token = this.issueToken(user._doc, appSettings); - + const { user, token } = await this.userService.resetPassword(req.body.password, req.body.recoveryToken); return res.success({ msg: this.stringService.authResetPassword, data: { user, token }, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "resetPasswordController")); - } - }; + }, + SERVICE_NAME, + "resetPassword" + ); /** - * Deletes a user and all associated monitors, checks, and alerts. + * Deletes the current user's account and associated data. * - * @param {Object} req - The request object. - * @param {Object} res - The response object. - * @param {Function} next - The next middleware function. - * @returns {Object} The response object with success status and message. - * @throws {Error} If user validation fails or user is not found in the database. + * @async + * @function deleteUser + * @param {Object} req - Express request object + * @param {Object} req.user - Current authenticated user (from JWT) + * @param {string} req.user._id - User ID + * @param {string} req.user.email - User email + * @param {string} req.user.teamId - User's team ID + * @param {Array} req.user.role - User roles + * @param {Object} res - Express response object + * @returns {Promise} Success response confirming user deletion + * @throws {Error} 400 - Bad request if user is demo user + * @throws {Error} 404 - Not found if user doesn't exist + * @example + * DELETE /auth/user + * // Requires JWT authentication */ - deleteUser = async (req, res, next) => { - try { - const token = getTokenFromHeaders(req.headers); - const decodedToken = jwt.decode(token); - const { email } = decodedToken; - - // Check if the user exists - const user = await this.db.getUserByEmail(email); - // 1. Find all the monitors associated with the team ID if superadmin - - const result = await this.db.getMonitorsByTeamId({ - teamId: user.teamId.toString(), - }); - - if (user.role.includes("superadmin")) { - //2. Remove all jobs, delete checks and alerts - result?.monitors.length > 0 && - (await Promise.all( - result.monitors.map(async (monitor) => { - await this.jobQueue.deleteJob(monitor); - }) - )); - } - // 6. Delete the user by id - await this.db.deleteUser(user._id); - + deleteUser = this.asyncHandler( + async (req, res) => { + await this.userService.deleteUser(req.user); return res.success({ msg: this.stringService.authDeleteUser, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "deleteUserController")); - } - }; - - getAllUsers = async (req, res, next) => { - try { - const allUsers = await this.db.getAllUsers(req, res); + }, + SERVICE_NAME, + "deleteUser" + ); + /** + * Retrieves all users in the system (admin/superadmin only). + * + * @async + * @function getAllUsers + * @param {Object} req - Express request object + * @param {Object} res - Express response object + * @returns {Promise} Success response with array of users + * @throws {Error} 403 - Forbidden if user doesn't have admin/superadmin role + * @example + * GET /auth/users + * // Requires JWT authentication with admin/superadmin role + */ + getAllUsers = this.asyncHandler( + async (req, res) => { + const allUsers = await this.userService.getAllUsers(); return res.success({ msg: this.stringService.authGetAllUsers, data: allUsers, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getAllUsersController")); - } - }; + }, + SERVICE_NAME, + "getAllUsers" + ); + + /** + * Retrieves a specific user by ID (superadmin only). + * + * @async + * @function getUserById + * @param {Object} req - Express request object + * @param {Object} req.params - URL parameters + * @param {string} req.params.userId - ID of the user to retrieve + * @param {Object} req.user - Current authenticated user (from JWT) + * @param {Array} req.user.role - Current user's roles + * @param {Object} res - Express response object + * @returns {Promise} Success response with user data + * @throws {Error} 422 - Validation error if userId is invalid + * @throws {Error} 403 - Forbidden if user doesn't have superadmin role + * @throws {Error} 404 - Not found if user doesn't exist + * @example + * GET /auth/users/507f1f77bcf86cd799439011 + * // Requires JWT authentication with superadmin role + */ + getUserById = this.asyncHandler( + async (req, res) => { + await getUserByIdParamValidation.validateAsync(req.params); + const userId = req?.params?.userId; + const roles = req?.user?.role; + + if (!userId) { + throw new Error("No user ID in request"); + } + + if (!roles || roles.length === 0) { + throw new Error("No roles in request"); + } + + const user = await this.userService.getUserById(roles, userId); + + return res.success({ msg: "ok", data: user }); + }, + SERVICE_NAME, + "getUserById" + ); + + /** + * Updates a specific user by ID (superadmin only). + * + * @async + * @function editUserById + * @param {Object} req - Express request object + * @param {Object} req.params - URL parameters + * @param {string} req.params.userId - ID of the user to update + * @param {Object} req.body - Request body containing user update data + * @param {string} [req.body.firstName] - Updated first name + * @param {string} [req.body.lastName] - Updated last name + * @param {Array} [req.body.role] - Updated user roles + * @param {Object} req.user - Current authenticated user (from JWT) + * @param {string} req.user._id - Current user's ID + * @param {Array} req.user.role - Current user's roles + * @param {Object} res - Express response object + * @returns {Promise} Success response confirming user update + * @throws {Error} 422 - Validation error if parameters or body are invalid + * @throws {Error} 403 - Forbidden if user doesn't have superadmin role + * @throws {Error} 404 - Not found if user doesn't exist + * @example + * PUT /auth/users/507f1f77bcf86cd799439011 + * { + * "firstName": "Updated Name", + * "role": ["admin"] + * } + * // Requires JWT authentication with superadmin role + */ + editUserById = this.asyncHandler( + async (req, res) => { + const roles = req?.user?.role; + if (!roles.includes("superadmin")) { + throw createError("Unauthorized", 403); + } + + const userId = req.params.userId; + const user = { ...req.body }; + + await editUserByIdParamValidation.validateAsync(req.params); + // If this is superadmin self edit, allow "superadmin" role + if (userId === req.user._id) { + await editSuperadminUserByIdBodyValidation.validateAsync(req.body); + } else { + await editUserByIdBodyValidation.validateAsync(req.body); + } + + await this.userService.editUserById(userId, user); + return res.success({ msg: "ok" }); + }, + SERVICE_NAME, + "editUserById" + ); } export default AuthController; diff --git a/server/controllers/baseController.js b/server/controllers/baseController.js new file mode 100644 index 000000000..a84806a24 --- /dev/null +++ b/server/controllers/baseController.js @@ -0,0 +1,83 @@ +import { AppError } from "../service/infrastructure/errorService.js"; + +export const createCommonDependencies = (serviceRegistry, dbServiceName, loggerServiceName, errorServiceName, stringServiceName) => { + return { + db: serviceRegistry.get(dbServiceName), + errorService: serviceRegistry.get(errorServiceName), + logger: serviceRegistry.get(loggerServiceName), + stringService: serviceRegistry.get(stringServiceName), + }; +}; + +class BaseController { + constructor({ db, logger, errorService, ...additionalDependencies }) { + this.db = db; + this.logger = logger; + this.errorService = errorService; + Object.assign(this, additionalDependencies); + + this.asyncHandler = (fn, serviceName, methodName) => { + return async (req, res, next) => { + try { + await fn(req, res, next); + } catch (error) { + // Handle validation errors + if (error.isJoi) { + const validationError = this.errorService.createValidationError(error.message, error.details, serviceName, methodName); + return next(validationError); + } + + if (error.name === "ValidationError") { + const validationError = this.errorService.createValidationError("Database validation failed", error.errors, serviceName, methodName); + return next(validationError); + } + + if (error.name === "CastError") { + const notFoundError = this.errorService.createNotFoundError( + "Invalid resource identifier", + { field: error.path, value: error.value }, + serviceName, + methodName + ); + return next(notFoundError); + } + + if (error.code === "11000") { + const conflictError = this.errorService.createConflictError("Resource already exists", { + originalError: error.message, + code: error.code, + }); + conflictError.service = serviceName; + conflictError.method = methodName; + return next(conflictError); + } + + if (error instanceof AppError) { + error.service = error.service || serviceName; + error.method = error.method || methodName; + return next(error); + } + + if (error.status) { + const appError = this.errorService.createError(error.message, error.status, serviceName, methodName, { + originalError: error.message, + stack: error.stack, + }); + return next(appError); + } + + // For unknown errors, create a server error + const appError = this.errorService.createServerError(error.message || "An unexpected error occurred", { + originalError: error.message, + stack: error.stack, + }); + appError.service = serviceName; + appError.method = methodName; + appError.stack = error.stack; // Preserve original stack + return next(appError); + } + }; + }; + } +} +export default BaseController; diff --git a/server/controllers/checkController.js b/server/controllers/checkController.js index e50d5b7fc..08b6c2de9 100755 --- a/server/controllers/checkController.js +++ b/server/controllers/checkController.js @@ -1,9 +1,7 @@ +import BaseController from "./baseController.js"; import { - createCheckParamValidation, - createCheckBodyValidation, getChecksParamValidation, getChecksQueryValidation, - getTeamChecksParamValidation, getTeamChecksQueryValidation, deleteChecksParamValidation, deleteChecksByTeamIdParamValidation, @@ -12,230 +10,346 @@ import { ackAllChecksParamValidation, ackAllChecksBodyValidation, } from "../validation/joi.js"; -import jwt from "jsonwebtoken"; -import { getTokenFromHeaders } from "../utils/utils.js"; -import { handleValidationError, handleError } from "./controllerUtils.js"; const SERVICE_NAME = "checkController"; -class CheckController { - constructor(db, settingsService, stringService) { - this.db = db; +/** + * Check Controller + * + * Handles all check-related HTTP requests including retrieving checks, + * acknowledging checks, deleting checks, and managing check TTL settings. + * + * @class CheckController + * @description Manages check operations and monitoring data + */ +class CheckController extends BaseController { + /** + * Creates an instance of CheckController. + * + * @param {Object} commonDependencies - Common dependencies injected into the controller + * @param {Object} dependencies - The dependencies required by the controller + * @param {Object} dependencies.settingsService - Service for application settings + * @param {Object} dependencies.checkService - Check business logic service + */ + constructor(commonDependencies, { settingsService, checkService }) { + super(commonDependencies); this.settingsService = settingsService; - this.stringService = stringService; + this.checkService = checkService; } - createCheck = async (req, res, next) => { - try { - await createCheckParamValidation.validateAsync(req.params); - await createCheckBodyValidation.validateAsync(req.body); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - - try { - const checkData = { ...req.body }; - const check = await this.db.createCheck(checkData); - - return res.success({ - msg: this.stringService.checkCreate, - data: check, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "createCheck")); - } - }; - - getChecksByMonitor = async (req, res, next) => { - try { + /** + * Retrieves checks for a specific monitor with filtering and pagination. + * + * @async + * @function getChecksByMonitor + * @param {Object} req - Express request object + * @param {Object} req.params - URL parameters + * @param {string} req.params.monitorId - ID of the monitor to get checks for + * @param {Object} req.query - Query parameters for filtering and pagination + * @param {string} [req.query.type] - Type of checks to filter by + * @param {string} [req.query.sortOrder] - Sort order (asc/desc) + * @param {string} [req.query.dateRange] - Date range filter + * @param {string} [req.query.filter] - General filter string + * @param {boolean} [req.query.ack] - Filter by acknowledgment status + * @param {number} [req.query.page] - Page number for pagination + * @param {number} [req.query.rowsPerPage] - Number of rows per page + * @param {string} [req.query.status] - Filter by check status + * @param {Object} req.user - Current authenticated user (from JWT) + * @param {string} req.user.teamId - User's team ID + * @param {Object} res - Express response object + * @returns {Promise} Success response with checks data + * @throws {Error} 422 - Validation error if parameters are invalid + * @throws {Error} 404 - Not found if monitor doesn't exist + * @throws {Error} 403 - Forbidden if user doesn't have access to monitor + * @example + * GET /checks/monitor/507f1f77bcf86cd799439011?page=1&rowsPerPage=10&status=down + * // Requires JWT authentication + */ + getChecksByMonitor = this.asyncHandler( + async (req, res) => { await getChecksParamValidation.validateAsync(req.params); await getChecksQueryValidation.validateAsync(req.query); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { - const { monitorId } = req.params; - let { type, sortOrder, dateRange, filter, ack, page, rowsPerPage, status } = - req.query; - const result = await this.db.getChecksByMonitor({ - monitorId, - type, - sortOrder, - dateRange, - filter, - ack, - page, - rowsPerPage, - status, + const result = await this.checkService.getChecksByMonitor({ + monitorId: req?.params?.monitorId, + query: req?.query, + teamId: req?.user?.teamId, }); return res.success({ msg: this.stringService.checkGet, data: result, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getChecks")); - } - }; + }, + SERVICE_NAME, + "getChecksByMonitor" + ); - getChecksByTeam = async (req, res, next) => { - try { - await getTeamChecksParamValidation.validateAsync(req.params); + /** + * Retrieves all checks for the current user's team with filtering and pagination. + * + * @async + * @function getChecksByTeam + * @param {Object} req - Express request object + * @param {Object} req.query - Query parameters for filtering and pagination + * @param {string} [req.query.sortOrder] - Sort order (asc/desc) + * @param {string} [req.query.dateRange] - Date range filter + * @param {string} [req.query.filter] - General filter string + * @param {boolean} [req.query.ack] - Filter by acknowledgment status + * @param {number} [req.query.page] - Page number for pagination + * @param {number} [req.query.rowsPerPage] - Number of rows per page + * @param {Object} req.user - Current authenticated user (from JWT) + * @param {string} req.user.teamId - User's team ID + * @param {Object} res - Express response object + * @returns {Promise} Success response with checks data + * @throws {Error} 422 - Validation error if query parameters are invalid + * @example + * GET /checks/team?page=1&rowsPerPage=20&status=down&ack=false + * // Requires JWT authentication + */ + getChecksByTeam = this.asyncHandler( + async (req, res) => { await getTeamChecksQueryValidation.validateAsync(req.query); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { - let { sortOrder, dateRange, filter, ack, page, rowsPerPage } = req.query; - const { teamId } = req.user; - - const checkData = await this.db.getChecksByTeam({ - sortOrder, - dateRange, - filter, - ack, - page, - rowsPerPage, - teamId, + const checkData = await this.checkService.getChecksByTeam({ + teamId: req?.user?.teamId, + query: req?.query, }); return res.success({ msg: this.stringService.checkGet, data: checkData, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getTeamChecks")); - } - }; + }, + SERVICE_NAME, + "getChecksByTeam" + ); - getChecksSummaryByTeamId = async (req, res, next) => { - try { - const { teamId } = req.user; - const summary = await this.db.getChecksSummaryByTeamId({ teamId }); + /** + * Retrieves a summary of checks for the current user's team. + * + * @async + * @function getChecksSummaryByTeamId + * @param {Object} req - Express request object + * @param {Object} req.user - Current authenticated user (from JWT) + * @param {string} req.user.teamId - User's team ID + * @param {Object} res - Express response object + * @returns {Promise} Success response with checks summary + * @example + * GET /checks/summary + * // Requires JWT authentication + * // Response includes counts by status, time ranges, etc. + */ + getChecksSummaryByTeamId = this.asyncHandler( + async (req, res) => { + const summary = await this.checkService.getChecksSummaryByTeamId({ teamId: req?.user?.teamId }); return res.success({ msg: this.stringService.checkGetSummary, data: summary, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getChecksSummaryByTeamId")); - } - }; + }, + SERVICE_NAME, + "getChecksSummaryByTeamId" + ); - ackCheck = async (req, res, next) => { - try { + /** + * Acknowledges a specific check by ID. + * + * @async + * @function ackCheck + * @param {Object} req - Express request object + * @param {Object} req.params - URL parameters + * @param {string} req.params.checkId - ID of the check to acknowledge + * @param {Object} req.body - Request body containing acknowledgment data + * @param {boolean} req.body.ack - Acknowledgment status (true/false) + * @param {Object} req.user - Current authenticated user (from JWT) + * @param {string} req.user.teamId - User's team ID + * @param {Object} res - Express response object + * @returns {Promise} Success response with updated check data + * @throws {Error} 422 - Validation error if request body is invalid + * @throws {Error} 404 - Not found if check doesn't exist + * @throws {Error} 403 - Forbidden if user doesn't have access to check + * @example + * PUT /checks/507f1f77bcf86cd799439011/ack + * { + * "ack": true + * } + * // Requires JWT authentication + */ + ackCheck = this.asyncHandler( + async (req, res) => { await ackCheckBodyValidation.validateAsync(req.body); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { - const { checkId } = req.params; - const { ack } = req.body; - const { teamId } = req.user; - - const updatedCheck = await this.db.ackCheck(checkId, teamId, ack); + const updatedCheck = await this.checkService.ackCheck({ + checkId: req?.params?.checkId, + teamId: req?.user?.teamId, + ack: req?.body?.ack, + }); return res.success({ msg: this.stringService.checkUpdateStatus, data: updatedCheck, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "ackCheck")); - } - }; + }, + SERVICE_NAME, + "ackCheck" + ); - ackAllChecks = async (req, res, next) => { - try { + /** + * Acknowledges all checks for a specific monitor or path. + * + * @async + * @function ackAllChecks + * @param {Object} req - Express request object + * @param {Object} req.params - URL parameters + * @param {string} req.params.monitorId - ID of the monitor + * @param {string} req.params.path - Path for acknowledgment (e.g., "monitor") + * @param {Object} req.body - Request body containing acknowledgment data + * @param {boolean} req.body.ack - Acknowledgment status (true/false) + * @param {Object} req.user - Current authenticated user (from JWT) + * @param {string} req.user.teamId - User's team ID + * @param {Object} res - Express response object + * @returns {Promise} Success response with updated checks data + * @throws {Error} 422 - Validation error if parameters or body are invalid + * @throws {Error} 404 - Not found if monitor doesn't exist + * @throws {Error} 403 - Forbidden if user doesn't have access to monitor + * @example + * PUT /checks/monitor/507f1f77bcf86cd799439011/ack + * { + * "ack": true + * } + * // Requires JWT authentication + */ + ackAllChecks = this.asyncHandler( + async (req, res) => { await ackAllChecksParamValidation.validateAsync(req.params); await ackAllChecksBodyValidation.validateAsync(req.body); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { - const { monitorId, path } = req.params; - const { ack } = req.body; - const { teamId } = req.user; + const teamId = req?.user?.teamId; + if (!teamId) { + throw new Error("No team ID in request"); + } - const updatedChecks = await this.db.ackAllChecks(monitorId, teamId, ack, path); + const updatedChecks = await this.checkService.ackAllChecks({ + monitorId: req?.params?.monitorId, + path: req?.params?.path, + teamId: req?.user?.teamId, + ack: req?.body?.ack, + }); return res.success({ msg: this.stringService.checkUpdateStatus, data: updatedChecks, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "ackAllChecks")); - } - }; + }, + SERVICE_NAME, + "ackAllChecks" + ); - deleteChecks = async (req, res, next) => { - try { + /** + * Deletes all checks for a specific monitor. + * + * @async + * @function deleteChecks + * @param {Object} req - Express request object + * @param {Object} req.params - URL parameters + * @param {string} req.params.monitorId - ID of the monitor whose checks to delete + * @param {Object} req.user - Current authenticated user (from JWT) + * @param {string} req.user.teamId - User's team ID + * @param {Object} res - Express response object + * @returns {Promise} Success response with deletion count + * @throws {Error} 422 - Validation error if monitorId is invalid + * @throws {Error} 404 - Not found if monitor doesn't exist + * @throws {Error} 403 - Forbidden if user doesn't have access to monitor + * @example + * DELETE /checks/monitor/507f1f77bcf86cd799439011 + * // Requires JWT authentication + * // Response: { "data": { "deletedCount": 150 } } + */ + deleteChecks = this.asyncHandler( + async (req, res) => { await deleteChecksParamValidation.validateAsync(req.params); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { - const deletedCount = await this.db.deleteChecks(req.params.monitorId); + const deletedCount = await this.checkService.deleteChecks({ + monitorId: req.params.monitorId, + teamId: req?.user?.teamId, + }); return res.success({ msg: this.stringService.checkDelete, data: { deletedCount }, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "deleteChecks")); - } - }; + }, + SERVICE_NAME, + "deleteChecks" + ); - deleteChecksByTeamId = async (req, res, next) => { - try { + /** + * Deletes all checks for the current user's team. + * + * @async + * @function deleteChecksByTeamId + * @param {Object} req - Express request object + * @param {Object} req.user - Current authenticated user (from JWT) + * @param {string} req.user.teamId - User's team ID + * @param {Object} res - Express response object + * @returns {Promise} Success response with deletion count + * @throws {Error} 422 - Validation error if parameters are invalid + * @example + * DELETE /checks/team + * // Requires JWT authentication + * // Response: { "data": { "deletedCount": 1250 } } + */ + deleteChecksByTeamId = this.asyncHandler( + async (req, res) => { await deleteChecksByTeamIdParamValidation.validateAsync(req.params); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { - const { teamId } = req.user; - const deletedCount = await this.db.deleteChecksByTeamId(teamId); + const deletedCount = await this.checkService.deleteChecksByTeamId({ teamId: req?.user?.teamId }); return res.success({ msg: this.stringService.checkDelete, data: { deletedCount }, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "deleteChecksByTeamId")); - } - }; + }, + SERVICE_NAME, + "deleteChecksByTeamId" + ); - updateChecksTTL = async (req, res, next) => { - const SECONDS_PER_DAY = 86400; - - try { + /** + * Updates the TTL (Time To Live) setting for checks in the current user's team. + * + * @async + * @function updateChecksTTL + * @param {Object} req - Express request object + * @param {Object} req.body - Request body containing TTL data + * @param {number} req.body.ttl - TTL value in days + * @param {Object} req.user - Current authenticated user (from JWT) + * @param {string} req.user.teamId - User's team ID + * @param {Object} res - Express response object + * @returns {Promise} Success response confirming TTL update + * @throws {Error} 422 - Validation error if TTL value is invalid + * @example + * PUT /checks/ttl + * { + * "ttl": 30 + * } + * // Requires JWT authentication + * // Sets check TTL to 30 days + */ + updateChecksTTL = this.asyncHandler( + async (req, res) => { await updateChecksTTLBodyValidation.validateAsync(req.body); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { - // Get user's teamId - const { teamId } = req.user; - const ttl = parseInt(req.body.ttl, 10) * SECONDS_PER_DAY; - await this.db.updateChecksTTL(teamId, ttl); + await this.checkService.updateChecksTTL({ + teamId: req?.user?.teamId, + ttl: req?.body?.ttl, + }); return res.success({ msg: this.stringService.checkUpdateTTL, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "updateTTL")); - } - }; + }, + SERVICE_NAME, + "updateChecksTtl" + ); } + export default CheckController; diff --git a/server/controllers/controllerUtils.js b/server/controllers/controllerUtils.js index ec7f1122f..f21ba154b 100755 --- a/server/controllers/controllerUtils.js +++ b/server/controllers/controllerUtils.js @@ -1,17 +1,3 @@ -const handleValidationError = (error, serviceName) => { - error.status = 422; - error.service = serviceName; - error.message = error.details?.[0]?.message || error.message || "Validation Error"; - return error; -}; - -const handleError = (error, serviceName, method, status = 500) => { - error.status === undefined ? (error.status = status) : null; - error.service === undefined ? (error.service = serviceName) : null; - error.method === undefined ? (error.method = method) : null; - return error; -}; - const fetchMonitorCertificate = async (sslChecker, monitor) => { const monitorUrl = new URL(monitor.url); const hostname = monitorUrl.hostname; @@ -23,4 +9,4 @@ const fetchMonitorCertificate = async (sslChecker, monitor) => { return cert; }; -export { handleValidationError, handleError, fetchMonitorCertificate }; +export { fetchMonitorCertificate }; diff --git a/server/controllers/diagnosticController.js b/server/controllers/diagnosticController.js index 8fa5fa42b..91b90f96b 100755 --- a/server/controllers/diagnosticController.js +++ b/server/controllers/diagnosticController.js @@ -1,48 +1,57 @@ -import { handleError } from "./controllerUtils.js"; const SERVICE_NAME = "diagnosticController"; - -class DiagnosticController { - constructor(db) { - this.db = db; - this.getMonitorsByTeamIdExecutionStats = - this.getMonitorsByTeamIdExecutionStats.bind(this); - this.getDbStats = this.getDbStats.bind(this); +import BaseController from "./baseController.js"; +/** + * Diagnostic Controller + * + * Handles system diagnostic and monitoring requests including system statistics, + * performance metrics, and health checks. + * + * @class DiagnosticController + * @description Manages system diagnostics and performance monitoring + */ +class DiagnosticController extends BaseController { + /** + * Creates an instance of DiagnosticController. + * @param {Object} commonDependencies - Common dependencies injected into the controller + * @param {Object} dependencies - The dependencies required by the controller + * @param {Object} dependencies.diagnosticService - Service for system diagnostics and monitoring + */ + constructor(commonDependencies, { diagnosticService }) { + super(commonDependencies); + this.diagnosticService = diagnosticService; } - async getMonitorsByTeamIdExecutionStats(req, res, next) { - try { - const data = await this.db.getMonitorsByTeamIdExecutionStats(req); + /** + * Retrieves comprehensive system statistics and performance metrics. + * + * @async + * @function getSystemStats + * @param {Object} req - Express request object + * @param {Object} res - Express response object + * @returns {Promise} Success response with system diagnostics data + * @description Returns system performance metrics, memory usage, CPU statistics, + * and other diagnostic information useful for monitoring system health. + * @example + * GET /diagnostics/stats + * // Response includes: + * // - Memory usage (heap, external, arrayBuffers) + * // - CPU usage statistics + * // - System uptime + * // - Performance metrics + * // - Database connection status + * // - Active processes/connections + */ + getSystemStats = this.asyncHandler( + async (req, res) => { + const diagnostics = await this.diagnosticService.getSystemStats(); return res.success({ msg: "OK", - data, + data: diagnostics, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getMonitorsByTeamIdExecutionStats")); - } - } - - async getDbStats(req, res, next) { - try { - const { methodName, args = [] } = req.body; - if (!methodName || !this.db[methodName]) { - return res.error({ - msg: "Invalid method name or method doesn't exist", - status: 400, - }); - } - const explainMethod = await this.db[methodName].apply(this.db, args); - const stats = { - methodName, - timestamp: new Date(), - explain: explainMethod, - }; - return res.success({ - msg: "OK", - data: stats, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getDbStats")); - } - } + }, + SERVICE_NAME, + "getSystemStats" + ); } + export default DiagnosticController; diff --git a/server/controllers/inviteController.js b/server/controllers/inviteController.js index 3799c58b9..3f237c6ec 100755 --- a/server/controllers/inviteController.js +++ b/server/controllers/inviteController.js @@ -1,129 +1,96 @@ -import { - inviteRoleValidation, - inviteBodyValidation, - inviteVerificationBodyValidation, -} from "../validation/joi.js"; -import logger from "../utils/logger.js"; -import jwt from "jsonwebtoken"; -import { handleError, handleValidationError } from "./controllerUtils.js"; -import { getTokenFromHeaders } from "../utils/utils.js"; - +import { inviteBodyValidation, inviteVerificationBodyValidation } from "../validation/joi.js"; +import BaseController from "./baseController.js"; const SERVICE_NAME = "inviteController"; -class InviteController { - constructor(db, settingsService, emailService, stringService) { - this.db = db; - this.settingsService = settingsService; - this.emailService = emailService; - this.stringService = stringService; +/** + * Controller for handling user invitation operations + * Manages invite token generation, email sending, and token verification + */ +class InviteController extends BaseController { + /** + * Creates a new InviteController instance + * @param {Object} commonDependencies - Common dependencies injected into the controller + * @param {Object} dependencies.inviteService - Service for invite-related operations + */ + constructor(commonDependencies, { inviteService }) { + super(commonDependencies); + this.inviteService = inviteService; } /** - * Issues an invitation to a new user. Only admins can invite new users. An invitation token is created and sent via email. - * @async - * @param {Object} req - The Express request object. - * @property {Object} req.headers - The headers of the request. - * @property {string} req.headers.authorization - The authorization header containing the JWT token. - * @property {Object} req.body - The body of the request. - * @property {string} req.body.email - The email of the user to be invited. - * @param {Object} res - The Express response object. - * @param {function} next - The next middleware function. - * @returns {Object} The response object with a success status, a message indicating the sending of the invitation, and the invitation token. - * @throws {Error} If there is an error during the process, especially if there is a validation error (422). + * Generates an invite token for a user invitation + * @param {Object} req - Express request object + * @param {Object} req.body - Request body containing invite details + * @param {Object} req.user - Authenticated user object + * @param {string} req.user.teamId - Team ID of the authenticated user + * @param {Object} res - Express response object + * @returns {Promise} Response with invite token data */ - getInviteToken = async (req, res, next) => { - try { - // Only admins can invite - const token = getTokenFromHeaders(req.headers); - const { role, teamId } = jwt.decode(token); - req.body.teamId = teamId; - try { - await inviteRoleValidation.validateAsync({ roles: role }); - await inviteBodyValidation.validateAsync(req.body); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - - const inviteToken = await this.db.requestInviteToken({ ...req.body }); + getInviteToken = this.asyncHandler( + async (req, res) => { + const invite = req.body; + const teamId = req?.user?.teamId; + invite.teamId = teamId; + await inviteBodyValidation.validateAsync(invite); + const inviteToken = await this.inviteService.getInviteToken({ invite, teamId }); return res.success({ msg: this.stringService.inviteIssued, data: inviteToken, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "inviteController")); - } - }; + }, + SERVICE_NAME, + "getInviteToken" + ); - sendInviteEmail = async (req, res, next) => { - try { - // Only admins can invite - const token = getTokenFromHeaders(req.headers); - const { role, firstname, teamId } = jwt.decode(token); - req.body.teamId = teamId; - try { - await inviteRoleValidation.validateAsync({ roles: role }); - await inviteBodyValidation.validateAsync(req.body); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - - const inviteToken = await this.db.requestInviteToken({ ...req.body }); - const { clientHost } = this.settingsService.getSettings(); - - try { - const html = await this.emailService.buildEmail("employeeActivationTemplate", { - name: firstname, - link: `${clientHost}/register/${inviteToken.token}`, - }); - const result = await this.emailService.sendEmail( - req.body.email, - "Welcome to Uptime Monitor", - html - ); - if (!result) { - return res.error({ - msg: "Failed to send invite e-mail... Please verify your settings.", - }); - } - } catch (error) { - logger.warn({ - message: error.message, - service: SERVICE_NAME, - method: "sendInviteEmail", - stack: error.stack, - }); - } + /** + * Sends an invitation email to a user + * @param {Object} req - Express request object + * @param {Object} req.body - Request body containing invite details + * @param {Object} req.user - Authenticated user object + * @param {string} req.user.teamId - Team ID of the authenticated user + * @param {string} req.user.firstName - First name of the authenticated user + * @param {Object} res - Express response object + * @returns {Promise} Response with invite token data + */ + sendInviteEmail = this.asyncHandler( + async (req, res) => { + const inviteRequest = req.body; + inviteRequest.teamId = req?.user?.teamId; + await inviteBodyValidation.validateAsync(inviteRequest); + const inviteToken = await this.inviteService.sendInviteEmail({ + inviteRequest, + firstName: req?.user?.firstName, + }); return res.success({ msg: this.stringService.inviteIssued, data: inviteToken, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "inviteController")); - } - }; + }, + SERVICE_NAME, + "sendInviteEmail" + ); - inviteVerifyController = async (req, res, next) => { - try { + /** + * Verifies an invite token and returns invite details + * @param {Object} req - Express request object + * @param {Object} req.body - Request body containing the invite token + * @param {string} req.body.token - The invite token to verify + * @param {Object} res - Express response object + * @returns {Promise} Response with verified invite data + */ + verifyInviteToken = this.asyncHandler( + async (req, res) => { await inviteVerificationBodyValidation.validateAsync(req.body); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - - try { - const invite = await this.db.getInviteToken(req.body.token); - + const invite = await this.inviteService.verifyInviteToken({ inviteToken: req?.body?.token }); return res.success({ msg: this.stringService.inviteVerified, data: invite, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "inviteVerifyController")); - } - }; + }, + SERVICE_NAME, + "verifyInviteToken" + ); } export default InviteController; diff --git a/server/controllers/logController.js b/server/controllers/logController.js index 5381fd3fa..aa2780eb6 100644 --- a/server/controllers/logController.js +++ b/server/controllers/logController.js @@ -1,23 +1,21 @@ -import { handleError } from "./controllerUtils.js"; +import BaseController from "./baseController.js"; +const SERVICE_NAME = "LogController"; -const SERVICE_NAME = "JobQueueController"; - -class LogController { - constructor(logger) { - this.logger = logger; +class LogController extends BaseController { + constructor(commonDependencies) { + super(commonDependencies); } - getLogs = async (req, res, next) => { - try { + getLogs = this.asyncHandler( + async (req, res) => { const logs = await this.logger.getLogs(); res.success({ msg: "Logs fetched successfully", data: logs, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getLogs")); - return; - } - }; + }, + SERVICE_NAME, + "getLogs" + ); } export default LogController; diff --git a/server/controllers/maintenanceWindowController.js b/server/controllers/maintenanceWindowController.js index 20077cc65..e54726ea3 100755 --- a/server/controllers/maintenanceWindowController.js +++ b/server/controllers/maintenanceWindowController.js @@ -7,151 +7,135 @@ import { getMaintenanceWindowsByTeamIdQueryValidation, deleteMaintenanceWindowByIdParamValidation, } from "../validation/joi.js"; -import { handleValidationError, handleError } from "./controllerUtils.js"; +import BaseController from "./baseController.js"; const SERVICE_NAME = "maintenanceWindowController"; -class MaintenanceWindowController { - constructor(db, settingsService, stringService) { - this.db = db; +class MaintenanceWindowController extends BaseController { + constructor(commonDependencies, { settingsService, maintenanceWindowService }) { + super(commonDependencies); this.settingsService = settingsService; - this.stringService = stringService; + this.maintenanceWindowService = maintenanceWindowService; } - createMaintenanceWindows = async (req, res, next) => { - try { + createMaintenanceWindows = this.asyncHandler( + async (req, res) => { await createMaintenanceWindowBodyValidation.validateAsync(req.body); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { - const { teamId } = req.user; - const monitorIds = req.body.monitors; - const dbTransactions = monitorIds.map((monitorId) => { - return this.db.createMaintenanceWindow({ - teamId, - monitorId, - name: req.body.name, - active: req.body.active ? req.body.active : true, - repeat: req.body.repeat, - start: req.body.start, - end: req.body.end, - }); - }); - await Promise.all(dbTransactions); + + const teamId = req?.user?.teamId; + if (!teamId) { + throw this.errorService.createBadRequestError("Team ID is required"); + } + + await this.maintenanceWindowService.createMaintenanceWindow({ teamId, body: req.body }); return res.success({ msg: this.stringService.maintenanceWindowCreate, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "createMaintenanceWindow")); - } - }; + }, + SERVICE_NAME, + "createMaintenanceWindows" + ); - getMaintenanceWindowById = async (req, res, next) => { - try { + getMaintenanceWindowById = this.asyncHandler( + async (req, res) => { await getMaintenanceWindowByIdParamValidation.validateAsync(req.params); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { - const maintenanceWindow = await this.db.getMaintenanceWindowById(req.params.id); + + const teamId = req.user.teamId; + if (!teamId) { + throw this.errorService.createBadRequestError("Team ID is required"); + } + + const maintenanceWindow = await this.maintenanceWindowService.getMaintenanceWindowById({ id: req.params.id, teamId }); return res.success({ msg: this.stringService.maintenanceWindowGetById, data: maintenanceWindow, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getMaintenanceWindowById")); - } - }; + }, + SERVICE_NAME, + "getMaintenanceWindowById" + ); - getMaintenanceWindowsByTeamId = async (req, res, next) => { - try { + getMaintenanceWindowsByTeamId = this.asyncHandler( + async (req, res) => { await getMaintenanceWindowsByTeamIdQueryValidation.validateAsync(req.query); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { - const { teamId } = req.user; - const maintenanceWindows = await this.db.getMaintenanceWindowsByTeamId( - teamId, - req.query - ); + const teamId = req?.user?.teamId; + + if (!teamId) { + throw this.errorService.createBadRequestError("Team ID is required"); + } + + const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByTeamId({ teamId, query: req.query }); return res.success({ msg: this.stringService.maintenanceWindowGetByTeam, data: maintenanceWindows, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getMaintenanceWindowsByUserId")); - } - }; + }, + SERVICE_NAME, + "getMaintenanceWindowsByTeamId" + ); - getMaintenanceWindowsByMonitorId = async (req, res, next) => { - try { + getMaintenanceWindowsByMonitorId = this.asyncHandler( + async (req, res) => { await getMaintenanceWindowsByMonitorIdParamValidation.validateAsync(req.params); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { - const maintenanceWindows = await this.db.getMaintenanceWindowsByMonitorId( - req.params.monitorId - ); + const teamId = req?.user?.teamId; + if (!teamId) { + throw this.errorService.createBadRequestError("Team ID is required"); + } + + const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByMonitorId({ monitorId: req.params.monitorId, teamId }); return res.success({ msg: this.stringService.maintenanceWindowGetByUser, data: maintenanceWindows, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getMaintenanceWindowsByMonitorId")); - } - }; + }, + SERVICE_NAME, + "getMaintenanceWindowsByMonitorId" + ); - deleteMaintenanceWindow = async (req, res, next) => { - try { + deleteMaintenanceWindow = this.asyncHandler( + async (req, res) => { await deleteMaintenanceWindowByIdParamValidation.validateAsync(req.params); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { - await this.db.deleteMaintenanceWindowById(req.params.id); + + const teamId = req?.user?.teamId; + if (!teamId) { + throw this.errorService.createBadRequestError("Team ID is required"); + } + + await this.maintenanceWindowService.deleteMaintenanceWindow({ id: req.params.id, teamId }); + return res.success({ msg: this.stringService.maintenanceWindowDelete, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "deleteMaintenanceWindow")); - } - }; + }, + SERVICE_NAME, + "deleteMaintenanceWindow" + ); - editMaintenanceWindow = async (req, res, next) => { - try { + editMaintenanceWindow = this.asyncHandler( + async (req, res) => { await editMaintenanceWindowByIdParamValidation.validateAsync(req.params); await editMaintenanceByIdWindowBodyValidation.validateAsync(req.body); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { - const editedMaintenanceWindow = await this.db.editMaintenanceWindowById( - req.params.id, - req.body - ); + + const teamId = req.user.teamId; + if (!teamId) { + throw this.errorService.createBadRequestError("Team ID is required"); + } + + const editedMaintenanceWindow = await this.maintenanceWindowService.editMaintenanceWindow({ id: req.params.id, body: req.body, teamId }); return res.success({ msg: this.stringService.maintenanceWindowEdit, data: editedMaintenanceWindow, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "editMaintenanceWindow")); - } - }; + }, + SERVICE_NAME, + "editMaintenanceWindow" + ); } export default MaintenanceWindowController; diff --git a/server/controllers/monitorController.js b/server/controllers/monitorController.js index d32ef463e..2187df5d0 100755 --- a/server/controllers/monitorController.js +++ b/server/controllers/monitorController.js @@ -4,8 +4,6 @@ import { getMonitorsByTeamIdParamValidation, getMonitorsByTeamIdQueryValidation, createMonitorBodyValidation, - createMonitorsBodyValidation, - getMonitorURLByQueryValidation, editMonitorBodyValidation, pauseMonitorParamValidation, getMonitorStatsByIdParamValidation, @@ -15,106 +13,80 @@ import { getHardwareDetailsByIdQueryValidation, } from "../validation/joi.js"; import sslChecker from "ssl-checker"; -import logger from "../utils/logger.js"; -import { handleError, handleValidationError } from "./controllerUtils.js"; -import axios from "axios"; -import seedDb from "../db/mongo/utils/seedDb.js"; -const SERVICE_NAME = "monitorController"; -import pkg from "papaparse"; +import { fetchMonitorCertificate } from "./controllerUtils.js"; +import BaseController from "./baseController.js"; -class MonitorController { - constructor(db, settingsService, jobQueue, stringService, emailService) { - this.db = db; +const SERVICE_NAME = "monitorController"; +class MonitorController extends BaseController { + constructor(commonDependencies, { settingsService, jobQueue, emailService, monitorService }) { + super(commonDependencies); this.settingsService = settingsService; this.jobQueue = jobQueue; - this.stringService = stringService; this.emailService = emailService; + this.monitorService = monitorService; } - /** - * Returns all monitors - * @async - * @param {Express.Request} req - * @param {Express.Response} res - * @param {function} next - * @returns {Promise} - * @throws {Error} - */ - getAllMonitors = async (req, res, next) => { - try { - const monitors = await this.db.getAllMonitors(); + async verifyTeamAccess(teamId, monitorId) { + const monitor = await this.db.getMonitorById(monitorId); + if (!monitor.teamId.equals(teamId)) { + throw this.errorService.createAuthorizationError(); + } + } + + getAllMonitors = this.asyncHandler( + async (req, res) => { + const monitors = await this.monitorService.getAllMonitors(); return res.success({ msg: this.stringService.monitorGetAll, data: monitors, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getAllMonitors")); - } - }; + }, + SERVICE_NAME, + "getAllMonitors" + ); - /** - * Returns all monitors with uptime stats for 1,7,30, and 90 days - * @async - * @param {Express.Request} req - * @param {Express.Response} res - * @param {function} next - * @returns {Promise} - * @throws {Error} - */ - getAllMonitorsWithUptimeStats = async (req, res, next) => { - try { - const monitors = await this.db.getAllMonitorsWithUptimeStats(); - return res.success({ - msg: this.stringService.monitorGetAll, - data: monitors, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getAllMonitorsWithUptimeStats")); - } - }; + getUptimeDetailsById = this.asyncHandler( + async (req, res) => { + const monitorId = req?.params?.monitorId; + const dateRange = req?.query?.dateRange; + const normalize = req?.query?.normalize; - getUptimeDetailsById = async (req, res, next) => { - try { - const { monitorId } = req.params; - const { dateRange, normalize } = req.query; + const teamId = req?.user?.teamId; - const data = await this.db.getUptimeDetailsById({ + if (!teamId) { + throw this.errorService.createBadRequestError("Team ID is required"); + } + + const data = await this.monitorService.getUptimeDetailsById({ + teamId, monitorId, dateRange, normalize, }); return res.success({ msg: this.stringService.monitorGetByIdSuccess, - data, + data: data, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getMonitorDetailsById")); - } - }; + }, + SERVICE_NAME, + "getUptimeDetailsById" + ); - /** - * Returns monitor stats for monitor with matching ID - * @async - * @param {Express.Request} req - * @param {Express.Response} res - * @param {function} next - * @returns {Promise} - * @throws {Error} - */ - getMonitorStatsById = async (req, res, next) => { - try { + getMonitorStatsById = this.asyncHandler( + async (req, res) => { await getMonitorStatsByIdParamValidation.validateAsync(req.params); await getMonitorStatsByIdQueryValidation.validateAsync(req.query); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { let { limit, sortOrder, dateRange, numToDisplay, normalize } = req.query; - const { monitorId } = req.params; + const monitorId = req?.params?.monitorId; - const monitorStats = await this.db.getMonitorStatsById({ + const teamId = req?.user?.teamId; + if (!teamId) { + throw this.errorService.createBadRequestError("Team ID is required"); + } + + const monitorStats = await this.monitorService.getMonitorStatsById({ + teamId, monitorId, limit, sortOrder, @@ -122,14 +94,15 @@ class MonitorController { numToDisplay, normalize, }); + return res.success({ msg: this.stringService.monitorStatsById, data: monitorStats, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getMonitorStatsById")); - } - }; + }, + SERVICE_NAME, + "getMonitorStatsById" + ); /** * Get hardware details for a specific monitor by ID @@ -140,35 +113,37 @@ class MonitorController { * @returns {Promise} * @throws {Error} - Throws error if monitor not found or other database errors */ - getHardwareDetailsById = async (req, res, next) => { - try { + getHardwareDetailsById = this.asyncHandler( + async (req, res) => { await getHardwareDetailsByIdParamValidation.validateAsync(req.params); await getHardwareDetailsByIdQueryValidation.validateAsync(req.query); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { - const { monitorId } = req.params; - const { dateRange } = req.query; - const monitor = await this.db.getHardwareDetailsById({ monitorId, dateRange }); + + const monitorId = req?.params?.monitorId; + const dateRange = req?.query?.dateRange; + const teamId = req?.user?.teamId; + if (!teamId) { + throw this.errorService.createBadRequestError("Team ID is required"); + } + + const monitor = await this.monitorService.getHardwareDetailsById({ + teamId, + monitorId, + dateRange, + }); + return res.success({ msg: this.stringService.monitorGetByIdSuccess, data: monitor, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getHardwareDetailsById")); - } - }; + }, + SERVICE_NAME, + "getHardwareDetailsById" + ); - getMonitorCertificate = async (req, res, next, fetchMonitorCertificate) => { - try { + getMonitorCertificate = this.asyncHandler( + async (req, res) => { await getCertificateParamValidation.validateAsync(req.params); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - } - try { const { monitorId } = req.params; const monitor = await this.db.getMonitorById(monitorId); const certificate = await fetchMonitorCertificate(sslChecker, monitor); @@ -179,497 +154,249 @@ class MonitorController { certificateDate: new Date(certificate.validTo), }, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getMonitorCertificate")); - } - }; + }, + SERVICE_NAME, + "getMonitorCertificate" + ); - /** - * Retrieves a monitor by its ID. - * @async - * @param {Object} req - The Express request object. - * @property {Object} req.params - The parameters of the request. - * @property {string} req.params.monitorId - The ID of the monitor to be retrieved. - * @param {Object} res - The Express response object. - * @param {function} next - The next middleware function. - * @returns {Object} The response object with a success status, a message, and the retrieved monitor data. - * @throws {Error} If there is an error during the process, especially if the monitor is not found (404) or if there is a validation error (422). - */ - getMonitorById = async (req, res, next) => { - try { + getMonitorById = this.asyncHandler( + async (req, res) => { await getMonitorByIdParamValidation.validateAsync(req.params); await getMonitorByIdQueryValidation.validateAsync(req.query); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { - const monitor = await this.db.getMonitorById(req.params.monitorId); + const teamId = req?.user?.teamId; + if (!teamId) { + throw this.errorService.createBadRequestError("Team ID is required"); + } + + const monitor = await this.monitorService.getMonitorById({ teamId, monitorId: req?.params?.monitorId }); + return res.success({ msg: this.stringService.monitorGetByIdSuccess, data: monitor, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getMonitorById")); - } - }; + }, + SERVICE_NAME, + "getMonitorById" + ); - /** - * Creates a new monitor and adds it to the job queue. - * @async - * @param {Object} req - The Express request object. - * @property {Object} req.body - The body of the request. - * @property {Array} req.body.notifications - The notifications associated with the monitor. - * @param {Object} res - The Express response object. - * @param {function} next - The next middleware function. - * @returns {Object} The response object with a success status, a message indicating the creation of the monitor, and the created monitor data. - * @throws {Error} If there is an error during the process, especially if there is a validation error (422). - */ - createMonitor = async (req, res, next) => { - try { + createMonitor = this.asyncHandler( + async (req, res) => { await createMonitorBodyValidation.validateAsync(req.body); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { - const { _id, teamId } = req.user; - const monitor = await this.db.createMonitor({ - body: req.body, - teamId, - userId: _id, - }); + const userId = req?.user?._id; + const teamId = req?.user?.teamId; + + const monitor = await this.monitorService.createMonitor({ teamId, userId, body: req.body }); - // Add monitor to job queue - this.jobQueue.addJob(monitor._id, monitor); return res.success({ msg: this.stringService.monitorCreate, data: monitor, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "createMonitor")); - } - }; + }, + SERVICE_NAME, + "createMonitor" + ); - /** - * Creates bulk monitors and adds them to the job queue after parsing CSV. - * @async - * @param {Object} req - The Express request object. - * @property {Object} req.file - The uploaded CSV file. - * @param {Object} res - The Express response object. - * @param {function} next - The next middleware function. - * @returns {Object} The response object with a success status and message. - * @throws {Error} If there is an error during the process, especially if there is a validation error (422). - */ - createBulkMonitors = async (req, res, next) => { - try { - const { parse } = pkg; - - // validate the file + createBulkMonitors = this.asyncHandler( + async (req, res) => { if (!req.file) { - throw new Error("No file uploaded"); + throw this.errorService.createBadRequestError("No file uploaded"); } - // Check if the file is a CSV if (!req.file.mimetype.includes("csv")) { - throw new Error("File is not a CSV"); + throw this.errorService.createBadRequestError("File is not a CSV"); } - // Validate if the file is empty if (req.file.size === 0) { - throw new Error("File is empty"); + throw this.errorService.createBadRequestError("File is empty"); } - const { _id, teamId } = req.user; + const userId = req?.user?._id; + const teamId = req?.user?.teamId; - if (!_id || !teamId) { - throw new Error("Missing userId or teamId"); + if (!userId || !teamId) { + throw this.errorService.createBadRequestError("Missing userId or teamId"); } - // Get file buffer from memory and convert to string - const fileData = req.file.buffer.toString("utf-8"); + const fileData = req?.file?.buffer?.toString("utf-8"); + if (!fileData) { + throw this.errorService.createBadRequestError("Cannot get file from buffer"); + } - // Parse the CSV data - parse(fileData, { - header: true, - skipEmptyLines: true, - transform: (value, header) => { - if (value === "") return undefined; // Empty fields become undefined + const monitors = await this.monitorService.createBulkMonitors({ fileData, userId, teamId }); - // Handle 'port' and 'interval' fields, check if they're valid numbers - if (["port", "interval"].includes(header)) { - const num = parseInt(value, 10); - if (isNaN(num)) { - throw new Error(`${header} should be a valid number, got: ${value}`); - } - return num; - } - - return value; - }, - complete: async ({ data, errors }) => { - try { - if (errors.length > 0) { - throw new Error("Error parsing CSV"); - } - - if (!data || data.length === 0) { - throw new Error("CSV file contains no data rows"); - } - - const enrichedData = data.map((monitor) => ({ - userId: _id, - teamId, - ...monitor, - description: monitor.description || monitor.name || monitor.url, - name: monitor.name || monitor.url, - type: monitor.type || "http", - })); - - await createMonitorsBodyValidation.validateAsync(enrichedData); - - try { - const monitors = await this.db.createBulkMonitors(enrichedData); - - await Promise.all( - monitors.map(async (monitor, index) => { - this.jobQueue.addJob(monitor._id, monitor); - }) - ); - - return res.success({ - msg: this.stringService.bulkMonitorsCreate, - data: monitors, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "createBulkMonitors")); - } - } catch (error) { - next(handleError(error, SERVICE_NAME, "createBulkMonitors")); - } - }, - }); - } catch (error) { - return next(handleError(error, SERVICE_NAME, "createBulkMonitors")); - } - }; - /** - * Checks if the endpoint can be resolved - * @async - * @param {Object} req - The Express request object. - * @property {Object} req.query - The query parameters of the request. - * @param {Object} res - The Express response object. - * @param {function} next - The next middleware function. - * @returns {Object} The response object with a success status, a message, and the resolution result. - * @throws {Error} If there is an error during the process, especially if there is a validation error (422). - */ - checkEndpointResolution = async (req, res, next) => { - try { - await getMonitorURLByQueryValidation.validateAsync(req.query); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - - try { - const { monitorURL } = req.query; - const parsedUrl = new URL(monitorURL); - const response = await axios.get(parsedUrl, { - timeout: 5000, - validateStatus: () => true, - }); return res.success({ - status: response.status, - msg: response.statusText, + msg: this.stringService.bulkMonitorsCreate, + data: monitors, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "checkEndpointResolution")); - } - }; + }, + SERVICE_NAME, + "createBulkMonitors" + ); - /** - * Deletes a monitor by its ID and also deletes associated checks, alerts, and notifications. - * @async - * @param {Object} req - The Express request object. - * @property {Object} req.params - The parameters of the request. - * @property {string} req.params.monitorId - The ID of the monitor to be deleted. - * @param {Object} res - The Express response object. - * @param {function} next - The next middleware function. - * @returns {Object} The response object with a success status and a message indicating the deletion of the monitor. - * @throws {Error} If there is an error during the process, especially if there is a validation error (422) or an error in deleting associated records. - */ - deleteMonitor = async (req, res, next) => { - try { + deleteMonitor = this.asyncHandler( + async (req, res) => { await getMonitorByIdParamValidation.validateAsync(req.params); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - - try { const monitorId = req.params.monitorId; - const monitor = await this.db.deleteMonitor({ monitorId }); - await this.jobQueue.deleteJob(monitor); - await this.db.deleteStatusPagesByMonitorId(monitor._id); - return res.success({ msg: this.stringService.monitorDelete }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "deleteMonitor")); - } - }; + const teamId = req?.user?.teamId; + if (!teamId) { + throw this.errorService.createBadRequestError("Team ID is required"); + } + + const deletedMonitor = await this.monitorService.deleteMonitor({ teamId, monitorId }); + + return res.success({ msg: this.stringService.monitorDelete, data: deletedMonitor }); + }, + SERVICE_NAME, + "deleteMonitor" + ); + + deleteAllMonitors = this.asyncHandler( + async (req, res) => { + const teamId = req?.user?.teamId; + if (!teamId) { + throw this.errorService.createBadRequestError("Team ID is required"); + } + + const deletedCount = await this.monitorService.deleteAllMonitors({ teamId }); - /** - * Deletes all monitors associated with a team. - * @async - * @param {Object} req - The Express request object. - * @property {Object} req.headers - The headers of the request. - * @property {string} req.headers.authorization - The authorization header containing the JWT token. - * @param {Object} res - The Express response object. - * @param {function} next - * @returns {Object} The response object with a success status and a message indicating the number of deleted monitors. - * @throws {Error} If there is an error during the deletion process. - */ - deleteAllMonitors = async (req, res, next) => { - try { - const { teamId } = req.user; - const { monitors, deletedCount } = await this.db.deleteAllMonitors(teamId); - await Promise.all( - monitors.map(async (monitor) => { - try { - await this.jobQueue.deleteJob(monitor); - await this.db.deleteChecks(monitor._id); - await this.db.deletePageSpeedChecksByMonitorId(monitor._id); - await this.db.deleteNotificationsByMonitorId(monitor._id); - } catch (error) { - logger.error({ - message: `Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`, - service: SERVICE_NAME, - method: "deleteAllMonitors", - stack: error.stack, - }); - } - }) - ); return res.success({ msg: `Deleted ${deletedCount} monitors` }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "deleteAllMonitors")); - } - }; + }, + SERVICE_NAME, + "deleteAllMonitors" + ); - /** - * Edits a monitor by its ID, updates its notifications, and updates its job in the job queue. - * @async - * @param {Object} req - The Express request object. - * @property {Object} req.params - The parameters of the request. - * @property {string} req.params.monitorId - The ID of the monitor to be edited. - * @property {Object} req.body - The body of the request. - * @property {Array} req.body.notifications - The notifications to be associated with the monitor. - * @param {Object} res - The Express response object. - * @param {function} next - The next middleware function. - * @returns {Object} The response object with a success status, a message indicating the editing of the monitor, and the edited monitor data. - * @throws {Error} If there is an error during the process, especially if there is a validation error (422). - */ - editMonitor = async (req, res, next) => { - try { + editMonitor = this.asyncHandler( + async (req, res) => { await getMonitorByIdParamValidation.validateAsync(req.params); await editMonitorBodyValidation.validateAsync(req.body); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } + const monitorId = req?.params?.monitorId; - try { - const { monitorId } = req.params; + const teamId = req?.user?.teamId; + if (!teamId) { + throw this.errorService.createBadRequestError("Team ID is required"); + } - const editedMonitor = await this.db.editMonitor(monitorId, req.body); - - await this.jobQueue.updateJob(editedMonitor); + const editedMonitor = await this.monitorService.editMonitor({ teamId, monitorId, body: req.body }); return res.success({ msg: this.stringService.monitorEdit, data: editedMonitor, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "editMonitor")); - } - }; + }, + SERVICE_NAME, + "editMonitor" + ); - /** - * Pauses or resumes a monitor based on its current state. - * @async - * @param {Object} req - The Express request object. - * @property {Object} req.params - The parameters of the request. - * @property {string} req.params.monitorId - The ID of the monitor to be paused or resumed. - * @param {Object} res - The Express response object. - * @param {function} next - The next middleware function. - * @returns {Object} The response object with a success status, a message indicating the new state of the monitor, and the updated monitor data. - * @throws {Error} If there is an error during the process. - */ - pauseMonitor = async (req, res, next) => { - try { + pauseMonitor = this.asyncHandler( + async (req, res) => { await pauseMonitorParamValidation.validateAsync(req.params); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - } - try { const monitorId = req.params.monitorId; - const monitor = await this.db.pauseMonitor({ monitorId }); - monitor.isActive === true - ? await this.jobQueue.resumeJob(monitor._id, monitor) - : await this.jobQueue.pauseJob(monitor); + const teamId = req?.user?.teamId; + if (!teamId) { + throw this.errorService.createBadRequestError("Team ID is required"); + } + + const monitor = await this.monitorService.pauseMonitor({ teamId, monitorId }); return res.success({ - msg: monitor.isActive - ? this.stringService.monitorResume - : this.stringService.monitorPause, + msg: monitor.isActive ? this.stringService.monitorResume : this.stringService.monitorPause, data: monitor, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "pauseMonitor")); - } - }; + }, + SERVICE_NAME, + "pauseMonitor" + ); - /** - * Adds demo monitors for a team. - * @async - * @param {Object} req - The Express request object. - * @property {Object} req.headers - The headers of the request. - * @property {string} req.headers.authorization - The authorization header containing the JWT token. - * @param {Object} res - The Express response object. - * @param {function} next - The next middleware function. - * @returns {Object} The response object with a success status, a message indicating the addition of demo monitors, and the number of demo monitors added. - * @throws {Error} If there is an error during the process. - */ - addDemoMonitors = async (req, res, next) => { - try { + addDemoMonitors = this.asyncHandler( + async (req, res) => { const { _id, teamId } = req.user; - const demoMonitors = await this.db.addDemoMonitors(_id, teamId); - await Promise.all( - demoMonitors.map((monitor) => this.jobQueue.addJob(monitor._id, monitor)) - ); + const demoMonitors = await this.monitorService.addDemoMonitors({ userId: _id, teamId }); return res.success({ msg: this.stringService.monitorDemoAdded, - data: demoMonitors.length, + data: demoMonitors?.length ?? 0, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "addDemoMonitors")); - } - }; + }, + SERVICE_NAME, + "addDemoMonitors" + ); - /** - * Sends a test email to verify email delivery functionality. - * @async - * @param {Object} req - The Express request object. - * @property {Object} req.body - The body of the request. - * @property {string} req.body.to - The email address to send the test email to. - * @param {Object} res - The Express response object. - * @param {function} next - The next middleware function. - * @returns {Object} The response object with a success status and the email delivery message ID. - * @throws {Error} If there is an error while sending the test email. - */ - sendTestEmail = async (req, res, next) => { - try { + sendTestEmail = this.asyncHandler( + async (req, res) => { const { to } = req.body; if (!to || typeof to !== "string") { - throw new Error(this.stringService.errorForValidEmailAddress); - } - - const subject = this.stringService.testEmailSubject; - const context = { testName: "Monitoring System" }; - - const html = await this.emailService.buildEmail("testEmailTemplate", context); - const messageId = await this.emailService.sendEmail(to, subject, html); - - if (!messageId) { - return res.error({ - msg: "Failed to send test email.", - }); + throw this.errorService.createBadRequestError(this.stringService.errorForValidEmailAddress); } + const messageId = await this.monitorService.sendTestEmail({ to }); return res.success({ msg: this.stringService.sendTestEmail, data: { messageId }, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "sendTestEmail")); - } - }; + }, + SERVICE_NAME, + "sendTestEmail" + ); - getMonitorsByTeamId = async (req, res, next) => { - try { + getMonitorsByTeamId = this.asyncHandler( + async (req, res) => { await getMonitorsByTeamIdParamValidation.validateAsync(req.params); await getMonitorsByTeamIdQueryValidation.validateAsync(req.query); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - } - try { let { limit, type, page, rowsPerPage, filter, field, order } = req.query; - const teamId = req.user.teamId; + const teamId = req?.user?.teamId; + + const monitors = await this.monitorService.getMonitorsByTeamId({ teamId, limit, type, page, rowsPerPage, filter, field, order }); - const monitors = await this.db.getMonitorsByTeamId({ - limit, - type, - page, - rowsPerPage, - filter, - field, - order, - teamId, - }); return res.success({ msg: this.stringService.monitorGetByTeamId, data: monitors, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getMonitorsByTeamId")); - } - }; + }, + SERVICE_NAME, + "getMonitorsByTeamId" + ); - getMonitorsAndSummaryByTeamId = async (req, res, next) => { - try { + getMonitorsAndSummaryByTeamId = this.asyncHandler( + async (req, res) => { await getMonitorsByTeamIdParamValidation.validateAsync(req.params); await getMonitorsByTeamIdQueryValidation.validateAsync(req.query); - } catch (error) { - return next(handleValidationError(error, SERVICE_NAME)); - } - try { - const { explain } = req; - const { type } = req.query; - const { teamId } = req.user; + const explain = req?.query?.explain; + const type = req?.query?.type; + const teamId = req?.user?.teamId; + if (!teamId) { + throw this.errorService.createBadRequestError("Team ID is required"); + } + + const result = await this.monitorService.getMonitorsAndSummaryByTeamId({ teamId, type, explain }); - const result = await this.db.getMonitorsAndSummaryByTeamId({ - type, - explain, - teamId, - }); return res.success({ msg: "OK", // TODO data: result, }); - } catch (error) { - return next(handleError(error, SERVICE_NAME, "getMonitorsAndSummaryByTeamId")); - } - }; + }, + SERVICE_NAME, + "getMonitorsAndSummaryByTeamId" + ); - getMonitorsWithChecksByTeamId = async (req, res, next) => { - try { + getMonitorsWithChecksByTeamId = this.asyncHandler( + async (req, res) => { await getMonitorsByTeamIdParamValidation.validateAsync(req.params); await getMonitorsByTeamIdQueryValidation.validateAsync(req.query); - } catch (error) { - return next(handleValidationError(error, SERVICE_NAME)); - } - try { - const { explain } = req; + const explain = req?.query?.explain; let { limit, type, page, rowsPerPage, filter, field, order } = req.query; - const { teamId } = req.user; + const teamId = req?.user?.teamId; + if (!teamId) { + throw this.errorService.createBadRequestError("Team ID is required"); + } - const result = await this.db.getMonitorsWithChecksByTeamId({ + const monitors = await this.monitorService.getMonitorsWithChecksByTeamId({ + teamId, limit, type, page, @@ -677,51 +404,26 @@ class MonitorController { filter, field, order, - teamId, explain, }); + return res.success({ msg: "OK", - data: result, + data: monitors, }); - } catch (error) { - return next(handleError(error, SERVICE_NAME, "getMonitorsWithChecksByTeamId")); - } - }; + }, + SERVICE_NAME, + "getMonitorsWithChecksByTeamId" + ); - seedDb = async (req, res, next) => { - try { - const { _id, teamId } = req.user; - await seedDb(_id, teamId); - res.success({ msg: "Database seeded" }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "seedDb")); - } - }; - - exportMonitorsToCSV = async (req, res, next) => { - try { - const { teamId } = req.user; - - const monitors = await this.db.getMonitorsByTeamId({ teamId }); - if (!monitors || monitors.length === 0) { - return res.success({ - msg: this.stringService.noMonitorsFound, - data: null, - }); + exportMonitorsToCSV = this.asyncHandler( + async (req, res) => { + const teamId = req?.user?.teamId; + if (!teamId) { + throw this.errorService.createBadRequestError("Team ID is required"); } - const csvData = monitors?.filteredMonitors?.map((monitor) => ({ - name: monitor.name, - description: monitor.description, - type: monitor.type, - url: monitor.url, - interval: monitor.interval, - port: monitor.port, - ignoreTlsErrors: monitor.ignoreTlsErrors, - isActive: monitor.isActive, - })); - const csv = pkg.unparse(csvData); + const csv = await this.monitorService.exportMonitorsToCSV({ teamId }); return res.file({ data: csv, @@ -730,10 +432,10 @@ class MonitorController { "Content-Disposition": "attachment; filename=monitors.csv", }, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "exportMonitorsToCSV")); - } - }; + }, + SERVICE_NAME, + "exportMonitorsToCSV" + ); } export default MonitorController; diff --git a/server/controllers/notificationController.js b/server/controllers/notificationController.js index e7e2a1f2a..6e85b97b8 100755 --- a/server/controllers/notificationController.js +++ b/server/controllers/notificationController.js @@ -1,147 +1,175 @@ -import { - triggerNotificationBodyValidation, - createNotificationBodyValidation, -} from "../validation/joi.js"; -import { handleError, handleValidationError } from "./controllerUtils.js"; +import { createNotificationBodyValidation } from "../validation/joi.js"; +import BaseController from "./baseController.js"; const SERVICE_NAME = "NotificationController"; -const NOTIFICATION_TYPES = { - WEBHOOK: "webhook", - TELEGRAM: "telegram", -}; - -const PLATFORMS = { - SLACK: "slack", - DISCORD: "discord", - TELEGRAM: "telegram", -}; - -class NotificationController { - constructor({ notificationService, stringService, statusService, db }) { +class NotificationController extends BaseController { + constructor(commonDependencies, { notificationService, statusService }) { + super(commonDependencies); this.notificationService = notificationService; - this.stringService = stringService; this.statusService = statusService; - this.db = db; } - testNotification = async (req, res, next) => { - try { + testNotification = this.asyncHandler( + async (req, res) => { const notification = req.body; const success = await this.notificationService.sendTestNotification(notification); if (!success) { - return res.error({ - msg: "Sending notification failed", - status: 400, - }); + throw this.errorService.createServerError("Sending notification failed"); } return res.success({ msg: "Notification sent successfully", }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "testWebhook")); - } - }; + }, + SERVICE_NAME, + "testNotification" + ); - createNotification = async (req, res, next) => { - try { + createNotification = this.asyncHandler( + async (req, res) => { await createNotificationBodyValidation.validateAsync(req.body, { abortEarly: false, }); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { const body = req.body; - const { _id, teamId } = req.user; - body.userId = _id; + + const teamId = req?.user?.teamId; + if (!teamId) { + throw this.errorService.createBadRequestError("Team ID is required"); + } + + const userId = req?.user?._id; + if (!userId) { + throw this.errorService.createBadRequestError("User ID is required"); + } + body.userId = userId; body.teamId = teamId; + const notification = await this.db.createNotification(body); return res.success({ msg: "Notification created successfully", data: notification, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "createNotification")); - } - }; + }, + SERVICE_NAME, + "createNotification" + ); + + getNotificationsByTeamId = this.asyncHandler( + async (req, res) => { + const teamId = req?.user?.teamId; + if (!teamId) { + throw this.errorService.createBadRequestError("Team ID is required"); + } + + const notifications = await this.db.getNotificationsByTeamId(teamId); - getNotificationsByTeamId = async (req, res, next) => { - try { - const notifications = await this.db.getNotificationsByTeamId(req.user.teamId); return res.success({ msg: "Notifications fetched successfully", data: notifications, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getNotificationsByTeamId")); - } - }; + }, + SERVICE_NAME, + "getNotificationsByTeamId" + ); + + deleteNotification = this.asyncHandler( + async (req, res) => { + const teamId = req?.user?.teamId; + if (!teamId) { + throw this.errorService.createBadRequestError("Team ID is required"); + } + + const notification = await this.db.getNotificationById(req.params.id); + if (!notification.teamId.equals(teamId)) { + throw this.errorService.createAuthorizationError(); + } - deleteNotification = async (req, res, next) => { - try { await this.db.deleteNotificationById(req.params.id); return res.success({ msg: "Notification deleted successfully", }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "deleteNotification")); - } - }; + }, + SERVICE_NAME, + "deleteNotification" + ); - getNotificationById = async (req, res, next) => { - try { + getNotificationById = this.asyncHandler( + async (req, res) => { const notification = await this.db.getNotificationById(req.params.id); + + const teamId = req?.user?.teamId; + if (!teamId) { + throw this.errorService.createBadRequestError("Team ID is required"); + } + + if (!notification.teamId.equals(teamId)) { + throw this.errorService.createAuthorizationError(); + } return res.success({ msg: "Notification fetched successfully", data: notification, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getNotificationById")); - } - }; + }, + SERVICE_NAME, + "getNotificationById" + ); - editNotification = async (req, res, next) => { - try { + editNotification = this.asyncHandler( + async (req, res) => { await createNotificationBodyValidation.validateAsync(req.body, { abortEarly: false, }); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { - const notification = await this.db.editNotification(req.params.id, req.body); + const teamId = req?.user?.teamId; + if (!teamId) { + throw this.errorService.createBadRequestError("Team ID is required"); + } + + const notification = await this.db.getNotificationById(req.params.id); + + if (!notification.teamId.equals(teamId)) { + throw this.errorService.createAuthorizationError(); + } + + const editedNotification = await this.db.editNotification(req.params.id, req.body); return res.success({ msg: "Notification updated successfully", - data: notification, + data: editedNotification, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "editNotification")); - } - }; + }, + SERVICE_NAME, + "editNotification" + ); + + testAllNotifications = this.asyncHandler( + async (req, res) => { + const monitorId = req.body.monitorId; + const teamId = req?.user?.teamId; + if (!teamId) { + throw this.errorService.createBadRequestError("Team ID is required"); + } - testAllNotifications = async (req, res, next) => { - try { - const { monitorId } = req.body; const monitor = await this.db.getMonitorById(monitorId); + + if (!monitor.teamId.equals(teamId)) { + throw this.errorService.createAuthorizationError(); + } + const notifications = monitor.notifications; - if (notifications.length === 0) throw new Error("No notifications"); + if (notifications.length === 0) throw this.errorService.createBadRequestError("No notifications"); const result = await this.notificationService.testAllNotifications(notifications); - if (!result) throw new Error("Failed to send all notifications"); + if (!result) throw this.errorService.createServerError("Failed to send all notifications"); return res.success({ msg: "All notifications sent successfully", }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "testAllNotifications")); - } - }; + }, + SERVICE_NAME, + "testAllNotifications" + ); } export default NotificationController; diff --git a/server/controllers/queueController.js b/server/controllers/queueController.js index 968106a5a..757350652 100755 --- a/server/controllers/queueController.js +++ b/server/controllers/queueController.js @@ -1,89 +1,82 @@ -import { handleError } from "./controllerUtils.js"; - +import BaseController from "./baseController.js"; const SERVICE_NAME = "JobQueueController"; -class JobQueueController { - constructor(jobQueue, stringService) { +class JobQueueController extends BaseController { + constructor(commonDependencies, { jobQueue }) { + super(commonDependencies); this.jobQueue = jobQueue; - this.stringService = stringService; } - getMetrics = async (req, res, next) => { - try { + getMetrics = this.asyncHandler( + async (req, res) => { const metrics = await this.jobQueue.getMetrics(); res.success({ msg: this.stringService.queueGetMetrics, data: metrics, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getMetrics")); - return; - } - }; + }, + SERVICE_NAME, + "getMetrics" + ); - getJobs = async (req, res, next) => { - try { + getJobs = this.asyncHandler( + async (req, res) => { const jobs = await this.jobQueue.getJobs(); return res.success({ msg: this.stringService.queueGetJobs, data: jobs, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getJobs")); - return; - } - }; + }, + SERVICE_NAME, + "getJobs" + ); - getAllMetrics = async (req, res, next) => { - try { + getAllMetrics = this.asyncHandler( + async (req, res) => { const jobs = await this.jobQueue.getJobs(); const metrics = await this.jobQueue.getMetrics(); return res.success({ msg: this.stringService.queueGetAllMetrics, data: { jobs, metrics }, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getAllMetrics")); - return; - } - }; + }, + SERVICE_NAME, + "getAllMetrics" + ); - addJob = async (req, res, next) => { - try { + addJob = this.asyncHandler( + async (req, res) => { await this.jobQueue.addJob(Math.random().toString(36).substring(7)); return res.success({ msg: this.stringService.queueAddJob, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "addJob")); - return; - } - }; + }, + SERVICE_NAME, + "addJob" + ); - flushQueue = async (req, res, next) => { - try { + flushQueue = this.asyncHandler( + async (req, res) => { const result = await this.jobQueue.flushQueues(); return res.success({ msg: this.stringService.jobQueueFlush, data: result, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "flushQueue")); - return; - } - }; + }, + SERVICE_NAME, + "flushQueue" + ); - checkQueueHealth = async (req, res, next) => { - try { + checkQueueHealth = this.asyncHandler( + async (req, res) => { const stuckQueues = await this.jobQueue.checkQueueHealth(); return res.success({ msg: this.stringService.queueHealthCheck, data: stuckQueues, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "checkQueueHealth")); - return; - } - }; + }, + SERVICE_NAME, + "checkQueueHealth" + ); } export default JobQueueController; diff --git a/server/controllers/settingsController.js b/server/controllers/settingsController.js index ef1d689d6..d97c19e88 100755 --- a/server/controllers/settingsController.js +++ b/server/controllers/settingsController.js @@ -1,13 +1,13 @@ import { updateAppSettingsBodyValidation } from "../validation/joi.js"; -import { handleValidationError, handleError } from "./controllerUtils.js"; import { sendTestEmailBodyValidation } from "../validation/joi.js"; +import BaseController from "./baseController.js"; + const SERVICE_NAME = "SettingsController"; -class SettingsController { - constructor({ db, settingsService, stringService, emailService }) { - this.db = db; +class SettingsController extends BaseController { + constructor(commonDependencies, { settingsService, emailService }) { + super(commonDependencies); this.settingsService = settingsService; - this.stringService = stringService; this.emailService = emailService; } @@ -32,45 +32,39 @@ class SettingsController { return returnSettings; }; - getAppSettings = async (req, res, next) => { - const dbSettings = await this.settingsService.getDBSettings(); + getAppSettings = this.asyncHandler( + async (req, res) => { + const dbSettings = await this.settingsService.getDBSettings(); - const returnSettings = this.buildAppSettings(dbSettings); - return res.success({ - msg: this.stringService.getAppSettings, - data: returnSettings, - }); - }; + const returnSettings = this.buildAppSettings(dbSettings); + return res.success({ + msg: this.stringService.getAppSettings, + data: returnSettings, + }); + }, + SERVICE_NAME, + "getAppSettings" + ); - updateAppSettings = async (req, res, next) => { - try { + updateAppSettings = this.asyncHandler( + async (req, res) => { await updateAppSettingsBodyValidation.validateAsync(req.body); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { const updatedSettings = await this.db.updateAppSettings(req.body); const returnSettings = this.buildAppSettings(updatedSettings); return res.success({ msg: this.stringService.updateAppSettings, data: returnSettings, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "updateAppSettings")); - } - }; + }, + SERVICE_NAME, + "updateAppSettings" + ); - sendTestEmail = async (req, res, next) => { - try { + sendTestEmail = this.asyncHandler( + async (req, res) => { await sendTestEmailBodyValidation.validateAsync(req.body); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { const { to, systemEmailHost, @@ -107,20 +101,17 @@ class SettingsController { }); if (!messageId) { - return res.error({ - msg: "Failed to send test email.", - }); + throw this.errorService.createServerError("Failed to send test email."); } return res.success({ msg: this.stringService.sendTestEmail, data: { messageId }, }); - } catch (error) { - next(handleError(error, SERVICE_NAME)); - return; - } - }; + }, + SERVICE_NAME, + "sendTestEmail" + ); } export default SettingsController; diff --git a/server/controllers/statusPageController.js b/server/controllers/statusPageController.js index 577cf4997..09d1d8b5f 100755 --- a/server/controllers/statusPageController.js +++ b/server/controllers/statusPageController.js @@ -1,29 +1,18 @@ -import { handleError, handleValidationError } from "./controllerUtils.js"; -import { - createStatusPageBodyValidation, - getStatusPageParamValidation, - getStatusPageQueryValidation, - imageValidation, -} from "../validation/joi.js"; +import { createStatusPageBodyValidation, getStatusPageParamValidation, getStatusPageQueryValidation, imageValidation } from "../validation/joi.js"; +import BaseController from "./baseController.js"; const SERVICE_NAME = "statusPageController"; -class StatusPageController { - constructor(db, stringService) { - this.db = db; - this.stringService = stringService; +class StatusPageController extends BaseController { + constructor(commonDependencies) { + super(commonDependencies); } - createStatusPage = async (req, res, next) => { - try { + createStatusPage = this.asyncHandler( + async (req, res) => { await createStatusPageBodyValidation.validateAsync(req.body); await imageValidation.validateAsync(req.file); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { const { _id, teamId } = req.user; const statusPage = await this.db.createStatusPage({ statusPageData: req.body, @@ -35,70 +24,58 @@ class StatusPageController { msg: this.stringService.statusPageCreate, data: statusPage, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "createStatusPage")); - } - }; + }, + SERVICE_NAME, + "createStatusPage" + ); - updateStatusPage = async (req, res, next) => { - try { + updateStatusPage = this.asyncHandler( + async (req, res) => { await createStatusPageBodyValidation.validateAsync(req.body); await imageValidation.validateAsync(req.file); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { const statusPage = await this.db.updateStatusPage(req.body, req.file); if (statusPage === null) { - const error = new Error(this.stringService.statusPageNotFound); - error.status = 404; - throw error; + throw this.errorService.createNotFoundError(this.stringService.statusPageNotFound); } return res.success({ msg: this.stringService.statusPageUpdate, data: statusPage, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "updateStatusPage")); - } - }; + }, + SERVICE_NAME, + "updateStatusPage" + ); - getStatusPage = async (req, res, next) => { - try { + getStatusPage = this.asyncHandler( + async (req, res) => { const statusPage = await this.db.getStatusPage(); return res.success({ msg: this.stringService.statusPageByUrl, data: statusPage, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getStatusPage")); - } - }; + }, + SERVICE_NAME, + "getStatusPage" + ); - getStatusPageByUrl = async (req, res, next) => { - try { + getStatusPageByUrl = this.asyncHandler( + async (req, res) => { await getStatusPageParamValidation.validateAsync(req.params); await getStatusPageQueryValidation.validateAsync(req.query); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { const statusPage = await this.db.getStatusPageByUrl(req.params.url, req.query.type); return res.success({ msg: this.stringService.statusPageByUrl, data: statusPage, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getStatusPageByUrl")); - } - }; + }, + SERVICE_NAME, + "getStatusPageByUrl" + ); - getStatusPagesByTeamId = async (req, res, next) => { - try { + getStatusPagesByTeamId = this.asyncHandler( + async (req, res) => { const teamId = req.user.teamId; const statusPages = await this.db.getStatusPagesByTeamId(teamId); @@ -106,21 +83,21 @@ class StatusPageController { msg: this.stringService.statusPageByTeamId, data: statusPages, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getStatusPageByTeamId")); - } - }; + }, + SERVICE_NAME, + "getStatusPagesByTeamId" + ); - deleteStatusPage = async (req, res, next) => { - try { + deleteStatusPage = this.asyncHandler( + async (req, res) => { await this.db.deleteStatusPage(req.params.url); return res.success({ msg: this.stringService.statusPageDelete, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "deleteStatusPage")); - } - }; + }, + SERVICE_NAME, + "deleteStatusPage" + ); } export default StatusPageController; diff --git a/server/db/models/Monitor.js b/server/db/models/Monitor.js index c71ea5f18..266cebeef 100755 --- a/server/db/models/Monitor.js +++ b/server/db/models/Monitor.js @@ -126,18 +126,22 @@ MonitorSchema.pre("findOneAndDelete", async function (next) { try { const doc = await this.model.findOne(this.getFilter()); - if (doc.type === "pagespeed") { + if (!doc) { + throw new Error("Monitor not found"); + } + + if (doc?.type === "pagespeed") { await PageSpeedCheck.deleteMany({ monitorId: doc._id }); - } else if (doc.type === "hardware") { + } else if (doc?.type === "hardware") { await HardwareCheck.deleteMany({ monitorId: doc._id }); } else { await Check.deleteMany({ monitorId: doc._id }); } // Deal with status pages - await StatusPage.updateMany({ monitors: doc._id }, { $pull: { monitors: doc._id } }); + await StatusPage.updateMany({ monitors: doc?._id }, { $pull: { monitors: doc?._id } }); - await MonitorStats.deleteMany({ monitorId: doc._id.toString() }); + await MonitorStats.deleteMany({ monitorId: doc?._id.toString() }); next(); } catch (error) { next(error); @@ -156,10 +160,7 @@ MonitorSchema.pre("deleteMany", async function (next) { } else { await Check.deleteMany({ monitorId: monitor._id }); } - await StatusPage.updateMany( - { monitors: monitor._id }, - { $pull: { monitors: monitor._id } } - ); + await StatusPage.updateMany({ monitors: monitor._id }, { $pull: { monitors: monitor._id } }); await MonitorStats.deleteMany({ monitorId: monitor._id.toString() }); } next(); diff --git a/server/db/mongo/MongoDB.js b/server/db/mongo/MongoDB.js index 5e5e7aec7..6eb82384f 100755 --- a/server/db/mongo/MongoDB.js +++ b/server/db/mongo/MongoDB.js @@ -89,8 +89,7 @@ class MongoDB { connect = async () => { try { - const connectionString = - this.appSettings.dbConnectionString || "mongodb://localhost:27017/uptime_db"; + const connectionString = this.appSettings.dbConnectionString || "mongodb://localhost:27017/uptime_db"; await mongoose.connect(connectionString); // If there are no AppSettings, create one await AppSettings.findOneAndUpdate( @@ -139,13 +138,6 @@ class MongoDB { }); } }; - checkSuperadmin = async (req, res) => { - const superAdmin = await UserModel.findOne({ role: "superadmin" }); - if (superAdmin !== null) { - return true; - } - return false; - }; } export default MongoDB; diff --git a/server/db/mongo/modules/checkModule.js b/server/db/mongo/modules/checkModule.js index fb0cc539e..731d168bf 100755 --- a/server/db/mongo/modules/checkModule.js +++ b/server/db/mongo/modules/checkModule.js @@ -58,26 +58,13 @@ const createChecks = async (checks) => { * @returns {Promise>} * @throws {Error} */ -const getChecksByMonitor = async ({ - monitorId, - type, - sortOrder, - dateRange, - filter, - ack, - page, - rowsPerPage, - status, -}) => { +const getChecksByMonitor = async ({ monitorId, type, sortOrder, dateRange, filter, ack, page, rowsPerPage, status }) => { try { status = status === "true" ? true : status === "false" ? false : undefined; page = parseInt(page); rowsPerPage = parseInt(rowsPerPage); - const ackStage = - ack === "true" - ? { ack: true } - : { $or: [{ ack: false }, { ack: { $exists: false } }] }; + const ackStage = ack === "true" ? { ack: true } : { $or: [{ ack: false }, { ack: { $exists: false } }] }; // Match const matchStage = { @@ -158,23 +145,12 @@ const getChecksByMonitor = async ({ } }; -const getChecksByTeam = async ({ - sortOrder, - dateRange, - filter, - ack, - page, - rowsPerPage, - teamId, -}) => { +const getChecksByTeam = async ({ sortOrder, dateRange, filter, ack, page, rowsPerPage, teamId }) => { try { page = parseInt(page); rowsPerPage = parseInt(rowsPerPage); - const ackStage = - ack === "true" - ? { ack: true } - : { $or: [{ ack: false }, { ack: { $exists: false } }] }; + const ackStage = ack === "true" ? { ack: true } : { $or: [{ ack: false }, { ack: { $exists: false } }] }; const matchStage = { teamId: new ObjectId(teamId), @@ -264,11 +240,7 @@ const getChecksByTeam = async ({ */ const ackCheck = async (checkId, teamId, ack) => { try { - const updatedCheck = await Check.findOneAndUpdate( - { _id: checkId, teamId: teamId }, - { $set: { ack, ackAt: new Date() } }, - { new: true } - ); + const updatedCheck = await Check.findOneAndUpdate({ _id: checkId, teamId: teamId }, { $set: { ack, ackAt: new Date() } }, { new: true }); if (!updatedCheck) { throw new Error("Check not found"); @@ -293,10 +265,7 @@ const ackCheck = async (checkId, teamId, ack) => { */ const ackAllChecks = async (monitorId, teamId, ack, path) => { try { - const updatedChecks = await Check.updateMany( - path === "monitor" ? { monitorId } : { teamId }, - { $set: { ack, ackAt: new Date() } } - ); + const updatedChecks = await Check.updateMany(path === "monitor" ? { monitorId } : { teamId }, { $set: { ack, ackAt: new Date() } }); return updatedChecks.modifiedCount; } catch (error) { error.service = SERVICE_NAME; @@ -317,9 +286,7 @@ const getChecksSummaryByTeamId = async ({ teamId }) => { const matchStage = { teamId: new ObjectId(teamId), }; - const checks = await Check.aggregate( - buildChecksSummaryByTeamIdPipeline({ matchStage }) - ); + const checks = await Check.aggregate(buildChecksSummaryByTeamIdPipeline({ matchStage })); return checks[0].summary; } catch (error) { error.service = SERVICE_NAME; diff --git a/server/db/mongo/modules/checkModuleQueries.js b/server/db/mongo/modules/checkModuleQueries.js index ffea43ba5..f2ad2a922 100644 --- a/server/db/mongo/modules/checkModuleQueries.js +++ b/server/db/mongo/modules/checkModuleQueries.js @@ -10,20 +10,12 @@ const buildChecksSummaryByTeamIdPipeline = ({ matchStage }) => { totalChecks: { $sum: { $cond: [{ $eq: ["$status", false] }, 1, 0] } }, resolvedChecks: { $sum: { - $cond: [ - { $and: [{ $eq: ["$ack", true] }, { $eq: ["$status", false] }] }, - 1, - 0, - ], + $cond: [{ $and: [{ $eq: ["$ack", true] }, { $eq: ["$status", false] }] }, 1, 0], }, }, downChecks: { $sum: { - $cond: [ - { $and: [{ $eq: ["$ack", false] }, { $eq: ["$status", false] }] }, - 1, - 0, - ], + $cond: [{ $and: [{ $eq: ["$ack", false] }, { $eq: ["$status", false] }] }, 1, 0], }, }, cannotResolveChecks: { diff --git a/server/db/mongo/modules/diagnosticModule.js b/server/db/mongo/modules/diagnosticModule.js index e59830518..8717720a3 100755 --- a/server/db/mongo/modules/diagnosticModule.js +++ b/server/db/mongo/modules/diagnosticModule.js @@ -2,11 +2,7 @@ import Monitor from "../../models/Monitor.js"; import { ObjectId } from "mongodb"; const SERVICE_NAME = "diagnosticModule"; -import { - buildMonitorSummaryByTeamIdPipeline, - buildMonitorsByTeamIdPipeline, - buildFilteredMonitorsByTeamIdPipeline, -} from "./monitorModuleQueries.js"; +import { buildMonitorSummaryByTeamIdPipeline, buildMonitorsByTeamIdPipeline, buildFilteredMonitorsByTeamIdPipeline } from "./monitorModuleQueries.js"; const getMonitorsByTeamIdExecutionStats = async (req) => { try { @@ -24,13 +20,9 @@ const getMonitorsByTeamIdExecutionStats = async (req) => { matchStage.type = Array.isArray(type) ? { $in: type } : type; } - const summary = await Monitor.aggregate( - buildMonitorSummaryByTeamIdPipeline({ matchStage }) - ).explain("executionStats"); + const summary = await Monitor.aggregate(buildMonitorSummaryByTeamIdPipeline({ matchStage })).explain("executionStats"); - const monitors = await Monitor.aggregate( - buildMonitorsByTeamIdPipeline({ matchStage, field, order }) - ).explain("executionStats"); + const monitors = await Monitor.aggregate(buildMonitorsByTeamIdPipeline({ matchStage, field, order })).explain("executionStats"); const filteredMonitors = await Monitor.aggregate( buildFilteredMonitorsByTeamIdPipeline({ diff --git a/server/db/mongo/modules/hardwareCheckModule.js b/server/db/mongo/modules/hardwareCheckModule.js index 868c83958..bfb9b0730 100755 --- a/server/db/mongo/modules/hardwareCheckModule.js +++ b/server/db/mongo/modules/hardwareCheckModule.js @@ -23,14 +23,10 @@ const createHardwareCheck = async (hardwareCheckData) => { if (monitor.uptimePercentage === undefined) { newUptimePercentage = status === true ? 1 : 0; } else { - newUptimePercentage = - (monitor.uptimePercentage * (n - 1) + (status === true ? 1 : 0)) / n; + newUptimePercentage = (monitor.uptimePercentage * (n - 1) + (status === true ? 1 : 0)) / n; } - await Monitor.findOneAndUpdate( - { _id: monitorId }, - { uptimePercentage: newUptimePercentage } - ); + await Monitor.findOneAndUpdate({ _id: monitorId }, { uptimePercentage: newUptimePercentage }); const hardwareCheck = await new HardwareCheck({ ...hardwareCheckData, diff --git a/server/db/mongo/modules/inviteModule.js b/server/db/mongo/modules/inviteModule.js index f5c960697..89d8ad8d7 100755 --- a/server/db/mongo/modules/inviteModule.js +++ b/server/db/mongo/modules/inviteModule.js @@ -1,7 +1,7 @@ import InviteToken from "../../models/InviteToken.js"; import crypto from "crypto"; -import ServiceRegistry from "../../../service/serviceRegistry.js"; -import StringService from "../../../service/stringService.js"; +import ServiceRegistry from "../../../service/system/serviceRegistry.js"; +import StringService from "../../../service/system/stringService.js"; const SERVICE_NAME = "inviteModule"; /** diff --git a/server/db/mongo/modules/maintenanceWindowModule.js b/server/db/mongo/modules/maintenanceWindowModule.js index 959f3f961..f7fea4636 100755 --- a/server/db/mongo/modules/maintenanceWindowModule.js +++ b/server/db/mongo/modules/maintenanceWindowModule.js @@ -34,10 +34,11 @@ const createMaintenanceWindow = async (maintenanceWindowData) => { } }; -const getMaintenanceWindowById = async (maintenanceWindowId) => { +const getMaintenanceWindowById = async ({ id, teamId }) => { try { - const maintenanceWindow = await MaintenanceWindow.findById({ - _id: maintenanceWindowId, + const maintenanceWindow = await MaintenanceWindow.findOne({ + _id: id, + teamId: teamId, }); return maintenanceWindow; } catch (error) { @@ -63,8 +64,7 @@ const getMaintenanceWindowsByTeamId = async (teamId, query) => { if (active !== undefined) maintenanceQuery.active = active; - const maintenanceWindowCount = - await MaintenanceWindow.countDocuments(maintenanceQuery); + const maintenanceWindowCount = await MaintenanceWindow.countDocuments(maintenanceQuery); // Pagination let skip = 0; @@ -78,10 +78,7 @@ const getMaintenanceWindowsByTeamId = async (teamId, query) => { sort[field] = order === "asc" ? 1 : -1; } - const maintenanceWindows = await MaintenanceWindow.find(maintenanceQuery) - .skip(skip) - .limit(rowsPerPage) - .sort(sort); + const maintenanceWindows = await MaintenanceWindow.find(maintenanceQuery).skip(skip).limit(rowsPerPage).sort(sort); return { maintenanceWindows, maintenanceWindowCount }; } catch (error) { @@ -99,10 +96,11 @@ const getMaintenanceWindowsByTeamId = async (teamId, query) => { * @returns {Promise>} An array of MaintenanceWindow documents. * @throws {Error} If there is an error retrieving the documents. */ -const getMaintenanceWindowsByMonitorId = async (monitorId) => { +const getMaintenanceWindowsByMonitorId = async ({ monitorId, teamId }) => { try { const maintenanceWindows = await MaintenanceWindow.find({ monitorId: monitorId, + teamId: teamId, }); return maintenanceWindows; } catch (error) { @@ -120,10 +118,9 @@ const getMaintenanceWindowsByMonitorId = async (monitorId) => { * @returns {Promise} The deleted MaintenanceWindow document. * @throws {Error} If there is an error deleting the document. */ -const deleteMaintenanceWindowById = async (maintenanceWindowId) => { +const deleteMaintenanceWindowById = async ({ id, teamId }) => { try { - const maintenanceWindow = - await MaintenanceWindow.findByIdAndDelete(maintenanceWindowId); + const maintenanceWindow = await MaintenanceWindow.findOneAndDelete({ _id: id, teamId }); return maintenanceWindow; } catch (error) { error.service = SERVICE_NAME; @@ -172,13 +169,9 @@ const deleteMaintenanceWindowByUserId = async (userId) => { } }; -const editMaintenanceWindowById = async (maintenanceWindowId, maintenanceWindowData) => { +const editMaintenanceWindowById = async ({ id, body, teamId }) => { try { - const editedMaintenanceWindow = await MaintenanceWindow.findByIdAndUpdate( - maintenanceWindowId, - maintenanceWindowData, - { new: true } - ); + const editedMaintenanceWindow = await MaintenanceWindow.findByIdAndUpdate(id, body, { new: true }); return editedMaintenanceWindow; } catch (error) { error.service = SERVICE_NAME; diff --git a/server/db/mongo/modules/monitorModule.js b/server/db/mongo/modules/monitorModule.js index bc31915c2..6e2e80f49 100755 --- a/server/db/mongo/modules/monitorModule.js +++ b/server/db/mongo/modules/monitorModule.js @@ -4,8 +4,8 @@ import Check from "../../models/Check.js"; import PageSpeedCheck from "../../models/PageSpeedCheck.js"; import HardwareCheck from "../../models/HardwareCheck.js"; import { NormalizeData, NormalizeDataUptimeDetails } from "../../../utils/dataUtils.js"; -import ServiceRegistry from "../../../service/serviceRegistry.js"; -import StringService from "../../../service/stringService.js"; +import ServiceRegistry from "../../../service/system/serviceRegistry.js"; +import StringService from "../../../service/system/stringService.js"; import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; @@ -56,53 +56,6 @@ const getAllMonitors = async (req, res) => { } }; -/** - * Get all monitors with uptime stats for 1,7,30, and 90 days - * @async - * @param {Express.Request} req - * @param {Express.Response} res - * @returns {Promise>} - * @throws {Error} - */ -const getAllMonitorsWithUptimeStats = async () => { - const timeRanges = { - 1: new Date(Date.now() - 24 * 60 * 60 * 1000), - 7: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), - 30: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000), - 90: new Date(Date.now() - 90 * 24 * 60 * 60 * 1000), - }; - - try { - const monitors = await Monitor.find(); - const monitorsWithStats = await Promise.all( - monitors.map(async (monitor) => { - const model = CHECK_MODEL_LOOKUP[monitor.type]; - - const uptimeStats = await Promise.all( - Object.entries(timeRanges).map(async ([days, startDate]) => { - const checks = await model.find({ - monitorId: monitor._id, - createdAt: { $gte: startDate }, - }); - return [days, getUptimePercentage(checks)]; - }) - ); - - return { - ...monitor.toObject(), - ...Object.fromEntries(uptimeStats), - }; - }) - ); - - return monitorsWithStats; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getAllMonitorsWithUptimeStats"; - throw error; - } -}; - /** * Function to calculate uptime duration based on the most recent check. * @param {Array} checks Array of check objects. @@ -285,10 +238,7 @@ const groupChecksByTime = (checks, dateRange) => { return acc; } - const time = - dateRange === "day" - ? checkDate.setMinutes(0, 0, 0) - : checkDate.toISOString().split("T")[0]; + const time = dateRange === "day" ? checkDate.setMinutes(0, 0, 0) : checkDate.toISOString().split("T")[0]; if (!acc[time]) { acc[time] = { time, checks: [] }; @@ -306,9 +256,7 @@ const groupChecksByTime = (checks, dateRange) => { const calculateGroupStats = (group) => { const totalChecks = group.checks.length; - const checksWithResponseTime = group.checks.filter( - (check) => typeof check.responseTime === "number" && !Number.isNaN(check.responseTime) - ); + const checksWithResponseTime = group.checks.filter((check) => typeof check.responseTime === "number" && !Number.isNaN(check.responseTime)); return { time: group.time, @@ -317,8 +265,7 @@ const calculateGroupStats = (group) => { totalIncidents: group.checks.filter((check) => !check.status).length, avgResponseTime: checksWithResponseTime.length > 0 - ? checksWithResponseTime.reduce((sum, check) => sum + check.responseTime, 0) / - checksWithResponseTime.length + ? checksWithResponseTime.reduce((sum, check) => sum + check.responseTime, 0) / checksWithResponseTime.length : 0, }; }; @@ -332,7 +279,6 @@ const calculateGroupStats = (group) => { * @throws {Error} */ const getUptimeDetailsById = async ({ monitorId, dateRange, normalize }) => { - const stringService = ServiceRegistry.get(StringService.SERVICE_NAME); try { const dates = getDateRange(dateRange); const formatLookup = { @@ -344,29 +290,15 @@ const getUptimeDetailsById = async ({ monitorId, dateRange, normalize }) => { const dateString = formatLookup[dateRange]; - const results = await Check.aggregate( - buildUptimeDetailsPipeline(monitorId, dates, dateString) - ); + const results = await Check.aggregate(buildUptimeDetailsPipeline(monitorId, dates, dateString)); const monitorData = results[0]; - monitorData.groupedUpChecks = NormalizeDataUptimeDetails( - monitorData.groupedUpChecks, - 10, - 100 - ); + monitorData.groupedUpChecks = NormalizeDataUptimeDetails(monitorData.groupedUpChecks, 10, 100); - monitorData.groupedDownChecks = NormalizeDataUptimeDetails( - monitorData.groupedDownChecks, - 10, - 100 - ); + monitorData.groupedDownChecks = NormalizeDataUptimeDetails(monitorData.groupedDownChecks, 10, 100); - const normalizedGroupChecks = NormalizeDataUptimeDetails( - monitorData.groupedChecks, - 10, - 100 - ); + const normalizedGroupChecks = NormalizeDataUptimeDetails(monitorData.groupedChecks, 10, 100); monitorData.groupedChecks = normalizedGroupChecks; const monitorStats = await MonitorStats.findOne({ monitorId }); @@ -386,14 +318,7 @@ const getUptimeDetailsById = async ({ monitorId, dateRange, normalize }) => { * @returns {Promise} * @throws {Error} */ -const getMonitorStatsById = async ({ - monitorId, - limit, - sortOrder, - dateRange, - numToDisplay, - normalize, -}) => { +const getMonitorStatsById = async ({ monitorId, limit, sortOrder, dateRange, numToDisplay, normalize }) => { const stringService = ServiceRegistry.get(StringService.SERVICE_NAME); try { // Get monitor, if we can't find it, abort with error @@ -408,12 +333,7 @@ const getMonitorStatsById = async ({ // Get Checks for monitor in date range requested const model = CHECK_MODEL_LOOKUP[monitor.type]; const dates = getDateRange(dateRange); - const { checksAll, checksForDateRange } = await getMonitorChecks( - monitorId, - model, - dates, - sort - ); + const { checksAll, checksForDateRange } = await getMonitorChecks(monitorId, model, dates, sort); // Build monitor stats const monitorStats = { @@ -423,20 +343,10 @@ const getMonitorStatsById = async ({ latestResponseTime: getLatestResponseTime(checksAll), periodIncidents: getIncidents(checksForDateRange), periodTotalChecks: checksForDateRange.length, - checks: processChecksForDisplay( - NormalizeData, - checksForDateRange, - numToDisplay, - normalize - ), + checks: processChecksForDisplay(NormalizeData, checksForDateRange, numToDisplay, normalize), }; - if ( - monitor.type === "http" || - monitor.type === "ping" || - monitor.type === "docker" || - monitor.type === "port" - ) { + if (monitor.type === "http" || monitor.type === "ping" || monitor.type === "docker" || monitor.type === "port") { // HTTP/PING Specific stats monitorStats.periodAvgResponseTime = getAverageResponseTime(checksForDateRange); monitorStats.periodUptime = getUptimePercentage(checksForDateRange); @@ -463,9 +373,7 @@ const getHardwareDetailsById = async ({ monitorId, dateRange }) => { month: "%Y-%m-%dT00:00:00Z", }; const dateString = formatLookup[dateRange]; - const hardwareStats = await HardwareCheck.aggregate( - buildHardwareDetailsPipeline(monitor, dates, dateString) - ); + const hardwareStats = await HardwareCheck.aggregate(buildHardwareDetailsPipeline(monitor, dates, dateString)); const monitorStats = { ...monitor.toObject(), @@ -505,16 +413,18 @@ const getMonitorById = async (monitorId) => { } }; -const getMonitorsByTeamId = async ({ - limit, - type, - page, - rowsPerPage, - filter, - field, - order, - teamId, -}) => { +const getMonitorsByIds = async (monitorIds) => { + try { + const objectIds = monitorIds.map((id) => new ObjectId(id)); + return await Monitor.find({ _id: { $in: objectIds } }, { _id: 1, teamId: 1 }).lean(); + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getMonitorsByIds"; + throw error; + } +}; + +const getMonitorsByTeamId = async ({ limit, type, page, rowsPerPage, filter, field, order, teamId }) => { limit = parseInt(limit); page = parseInt(page); rowsPerPage = parseInt(rowsPerPage); @@ -528,14 +438,10 @@ const getMonitorsByTeamId = async ({ matchStage.type = Array.isArray(type) ? { $in: type } : type; } - const summaryResult = await Monitor.aggregate( - buildMonitorSummaryByTeamIdPipeline({ matchStage }) - ); + const summaryResult = await Monitor.aggregate(buildMonitorSummaryByTeamIdPipeline({ matchStage })); const summary = summaryResult[0]; - const monitors = await Monitor.aggregate( - buildMonitorsByTeamIdPipeline({ matchStage, field, order }) - ); + const monitors = await Monitor.aggregate(buildMonitorsByTeamIdPipeline({ matchStage, field, order })); const filteredMonitors = await Monitor.aggregate( buildFilteredMonitorsByTeamIdPipeline({ @@ -569,14 +475,10 @@ const getMonitorsAndSummaryByTeamId = async ({ type, explain, teamId }) => { } if (explain === true) { - return Monitor.aggregate( - buildMonitorsAndSummaryByTeamIdPipeline({ matchStage }) - ).explain("executionStats"); + return Monitor.aggregate(buildMonitorsAndSummaryByTeamIdPipeline({ matchStage })).explain("executionStats"); } - const queryResult = await Monitor.aggregate( - buildMonitorsAndSummaryByTeamIdPipeline({ matchStage }) - ); + const queryResult = await Monitor.aggregate(buildMonitorsAndSummaryByTeamIdPipeline({ matchStage })); const { monitors, summary } = queryResult?.[0] ?? {}; return { monitors, summary }; } catch (error) { @@ -586,17 +488,7 @@ const getMonitorsAndSummaryByTeamId = async ({ type, explain, teamId }) => { } }; -const getMonitorsWithChecksByTeamId = async ({ - limit, - type, - page, - rowsPerPage, - filter, - field, - order, - teamId, - explain, -}) => { +const getMonitorsWithChecksByTeamId = async ({ limit, type, page, rowsPerPage, filter, field, order, teamId, explain }) => { try { limit = parseInt(limit); page = parseInt(page); @@ -684,9 +576,7 @@ const createMonitor = async ({ body, teamId, userId }) => { */ const createBulkMonitors = async (req) => { try { - const monitors = req.map( - (item) => new Monitor({ ...item, notifications: undefined }) - ); + const monitors = req.map((item) => new Monitor({ ...item, notifications: undefined })); await Monitor.bulkSave(monitors); return monitors; } catch (error) { @@ -704,16 +594,16 @@ const createBulkMonitors = async (req) => { * @returns {Promise} * @throws {Error} */ -const deleteMonitor = async ({ monitorId }) => { +const deleteMonitor = async ({ teamId, monitorId }) => { const stringService = ServiceRegistry.get(StringService.SERVICE_NAME); try { - const monitor = await Monitor.findByIdAndDelete(monitorId); + const deletedMonitor = await Monitor.findOneAndDelete({ _id: monitorId, teamId }); - if (!monitor) { + if (!deletedMonitor) { throw new Error(stringService.getDbFindMonitorById(monitorId)); } - return monitor; + return deletedMonitor; } catch (error) { error.service = SERVICE_NAME; error.method = "deleteMonitor"; @@ -763,9 +653,9 @@ const deleteMonitorsByUserId = async (userId) => { * @returns {Promise} * @throws {Error} */ -const editMonitor = async (candidateId, candidateMonitor) => { +const editMonitor = async ({ monitorId, body }) => { try { - const editedMonitor = await Monitor.findByIdAndUpdate(candidateId, candidateMonitor, { + const editedMonitor = await Monitor.findByIdAndUpdate(monitorId, body, { new: true, }); return editedMonitor; @@ -823,9 +713,9 @@ const pauseMonitor = async ({ monitorId }) => { export { getAllMonitors, - getAllMonitorsWithUptimeStats, getMonitorStatsById, getMonitorById, + getMonitorsByIds, getMonitorsByTeamId, getMonitorsAndSummaryByTeamId, getMonitorsWithChecksByTeamId, diff --git a/server/db/mongo/modules/monitorModuleQueries.js b/server/db/mongo/modules/monitorModuleQueries.js index ab45eebb6..d96e128fa 100755 --- a/server/db/mongo/modules/monitorModuleQueries.js +++ b/server/db/mongo/modules/monitorModuleQueries.js @@ -31,11 +31,7 @@ const buildUptimeDetailsPipeline = (monitorId, dates, dateString) => { $project: { _id: 0, percentage: { - $cond: [ - { $eq: ["$totalChecks", 0] }, - 0, - { $divide: ["$upChecks", "$totalChecks"] }, - ], + $cond: [{ $eq: ["$totalChecks", 0] }, 0, { $divide: ["$upChecks", "$totalChecks"] }], }, }, }, @@ -142,6 +138,7 @@ const buildUptimeDetailsPipeline = (monitorId, dates, dateString) => { { $project: { _id: 1, + teamId: 1, name: 1, status: 1, interval: 1, @@ -235,11 +232,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { { $match: { $expr: { - $and: [ - { $eq: ["$monitorId", monitor._id] }, - { $gte: ["$createdAt", dates.start] }, - { $lte: ["$createdAt", dates.end] }, - ], + $and: [{ $eq: ["$monitorId", monitor._id] }, { $gte: ["$createdAt", dates.start] }, { $lte: ["$createdAt", dates.end] }], }, }, }, @@ -326,10 +319,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { input: "$disks", as: "diskArray", in: { - $arrayElemAt: [ - "$$diskArray.read_speed_bytes", - "$$diskIndex", - ], + $arrayElemAt: ["$$diskArray.read_speed_bytes", "$$diskIndex"], }, }, }, @@ -340,10 +330,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { input: "$disks", as: "diskArray", in: { - $arrayElemAt: [ - "$$diskArray.write_speed_bytes", - "$$diskIndex", - ], + $arrayElemAt: ["$$diskArray.write_speed_bytes", "$$diskIndex"], }, }, }, @@ -354,10 +341,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { input: "$disks", as: "diskArray", in: { - $arrayElemAt: [ - "$$diskArray.total_bytes", - "$$diskIndex", - ], + $arrayElemAt: ["$$diskArray.total_bytes", "$$diskIndex"], }, }, }, @@ -379,10 +363,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { input: "$disks", as: "diskArray", in: { - $arrayElemAt: [ - "$$diskArray.usage_percent", - "$$diskIndex", - ], + $arrayElemAt: ["$$diskArray.usage_percent", "$$diskIndex"], }, }, }, @@ -556,26 +537,14 @@ const buildMonitorsAndSummaryByTeamIdPipeline = ({ matchStage }) => { ]; }; -const buildMonitorsWithChecksByTeamIdPipeline = ({ - matchStage, - filter, - page, - rowsPerPage, - field, - order, - limit, - type, -}) => { +const buildMonitorsWithChecksByTeamIdPipeline = ({ matchStage, filter, page, rowsPerPage, field, order, limit, type }) => { const skip = page && rowsPerPage ? page * rowsPerPage : 0; const sort = { [field]: order === "asc" ? 1 : -1 }; const limitStage = rowsPerPage ? [{ $limit: rowsPerPage }] : []; // Match name if (typeof filter !== "undefined" && field === "name") { - matchStage.$or = [ - { name: { $regex: filter, $options: "i" } }, - { url: { $regex: filter, $options: "i" } }, - ]; + matchStage.$or = [{ name: { $regex: filter, $options: "i" } }, { url: { $regex: filter, $options: "i" } }]; } // Match isActive @@ -667,37 +636,20 @@ const buildMonitorsWithChecksByTeamIdPipeline = ({ return pipeline; }; -const buildFilteredMonitorsByTeamIdPipeline = ({ - matchStage, - filter, - page, - rowsPerPage, - field, - order, - limit, - type, -}) => { +const buildFilteredMonitorsByTeamIdPipeline = ({ matchStage, filter, page, rowsPerPage, field, order, limit, type }) => { const skip = page && rowsPerPage ? page * rowsPerPage : 0; const sort = { [field]: order === "asc" ? 1 : -1 }; const limitStage = rowsPerPage ? [{ $limit: rowsPerPage }] : []; if (typeof filter !== "undefined" && field === "name") { - matchStage.$or = [ - { name: { $regex: filter, $options: "i" } }, - { url: { $regex: filter, $options: "i" } }, - ]; + matchStage.$or = [{ name: { $regex: filter, $options: "i" } }, { url: { $regex: filter, $options: "i" } }]; } if (typeof filter !== "undefined" && field === "status") { matchStage.status = filter === "true"; } - const pipeline = [ - { $match: matchStage }, - { $sort: sort }, - { $skip: skip }, - ...limitStage, - ]; + const pipeline = [{ $match: matchStage }, { $sort: sort }, { $skip: skip }, ...limitStage]; // Add checks if (limit) { @@ -792,10 +744,7 @@ const buildGetMonitorsByTeamIdPipeline = (req) => { ? [ { $match: { - $or: [ - { name: { $regex: filter, $options: "i" } }, - { url: { $regex: filter, $options: "i" } }, - ], + $or: [{ name: { $regex: filter, $options: "i" } }, { url: { $regex: filter, $options: "i" } }], }, }, ] diff --git a/server/db/mongo/modules/networkCheckModule.js b/server/db/mongo/modules/networkCheckModule.js index 53c55c7e3..12a86e73b 100644 --- a/server/db/mongo/modules/networkCheckModule.js +++ b/server/db/mongo/modules/networkCheckModule.js @@ -32,9 +32,7 @@ const createNetworkCheck = async (networkCheckData) => { */ const getNetworkChecksByMonitorId = async (monitorId, limit = 100) => { try { - const networkChecks = await NetworkCheck.find({ monitorId }) - .sort({ createdAt: -1 }) - .limit(limit); + const networkChecks = await NetworkCheck.find({ monitorId }).sort({ createdAt: -1 }).limit(limit); return networkChecks; } catch (error) { error.service = SERVICE_NAME; diff --git a/server/db/mongo/modules/notificationModule.js b/server/db/mongo/modules/notificationModule.js index 08d57b1a9..2348a6321 100755 --- a/server/db/mongo/modules/notificationModule.js +++ b/server/db/mongo/modules/notificationModule.js @@ -74,6 +74,11 @@ const deleteNotificationsByMonitorId = async (monitorId) => { const deleteNotificationById = async (id) => { try { + const notification = await Notification.findById(id); + if (!notification) { + throw new Error("Notification not found"); + } + const result = await Notification.findByIdAndDelete(id); await Monitor.updateMany({ notifications: id }, { $pull: { notifications: id } }); return result; diff --git a/server/db/mongo/modules/recoveryModule.js b/server/db/mongo/modules/recoveryModule.js index 3b39e847c..2678f49c4 100755 --- a/server/db/mongo/modules/recoveryModule.js +++ b/server/db/mongo/modules/recoveryModule.js @@ -1,25 +1,24 @@ import UserModel from "../../models/User.js"; import RecoveryToken from "../../models/RecoveryToken.js"; import crypto from "crypto"; -import serviceRegistry from "../../../service/serviceRegistry.js"; -import StringService from "../../../service/stringService.js"; +import serviceRegistry from "../../../service/system/serviceRegistry.js"; +import StringService from "../../../service/system/stringService.js"; const SERVICE_NAME = "recoveryModule"; /** * Request a recovery token * @async - * @param {Express.Request} req - * @param {Express.Response} res + * @param {string} email * @returns {Promise} * @throws {Error} */ -const requestRecoveryToken = async (req, res) => { +const requestRecoveryToken = async (email) => { try { // Delete any existing tokens - await RecoveryToken.deleteMany({ email: req.body.email }); + await RecoveryToken.deleteMany({ email }); let recoveryToken = new RecoveryToken({ - email: req.body.email, + email, token: crypto.randomBytes(32).toString("hex"), }); await recoveryToken.save(); @@ -31,10 +30,9 @@ const requestRecoveryToken = async (req, res) => { } }; -const validateRecoveryToken = async (req, res) => { +const validateRecoveryToken = async (candidateToken) => { const stringService = serviceRegistry.get(StringService.SERVICE_NAME); try { - const candidateToken = req.body.recoveryToken; const recoveryToken = await RecoveryToken.findOne({ token: candidateToken, }); @@ -50,13 +48,13 @@ const validateRecoveryToken = async (req, res) => { } }; -const resetPassword = async (req, res) => { +const resetPassword = async (password, candidateToken) => { const stringService = serviceRegistry.get(StringService.SERVICE_NAME); try { - const newPassword = req.body.password; + const newPassword = password; // Validate token again - const recoveryToken = await validateRecoveryToken(req, res); + const recoveryToken = await validateRecoveryToken(candidateToken); const user = await UserModel.findOne({ email: recoveryToken.email }); if (user === null) { diff --git a/server/db/mongo/modules/settingsModule.js b/server/db/mongo/modules/settingsModule.js index 4fd0d1634..df325d2fa 100755 --- a/server/db/mongo/modules/settingsModule.js +++ b/server/db/mongo/modules/settingsModule.js @@ -29,9 +29,7 @@ const updateAppSettings = async (newSettings) => { await AppSettings.findOneAndUpdate({}, update, { upsert: true, }); - const settings = await AppSettings.findOne() - .select("-__v -_id -createdAt -updatedAt -singleton") - .lean(); + const settings = await AppSettings.findOne().select("-__v -_id -createdAt -updatedAt -singleton").lean(); return settings; } catch (error) { error.service = SERVICE_NAME; diff --git a/server/db/mongo/modules/statusPageModule.js b/server/db/mongo/modules/statusPageModule.js index 8a682d567..757fdd266 100755 --- a/server/db/mongo/modules/statusPageModule.js +++ b/server/db/mongo/modules/statusPageModule.js @@ -1,7 +1,7 @@ import StatusPage from "../../models/StatusPage.js"; import { NormalizeData } from "../../../utils/dataUtils.js"; -import ServiceRegistry from "../../../service/serviceRegistry.js"; -import StringService from "../../../service/stringService.js"; +import ServiceRegistry from "../../../service/system/serviceRegistry.js"; +import StringService from "../../../service/system/stringService.js"; const SERVICE_NAME = "statusPageModule"; @@ -48,13 +48,9 @@ const updateStatusPage = async (statusPageData, image) => { if (statusPageData.deleteSubmonitors === "true") { statusPageData.subMonitors = []; } - const statusPage = await StatusPage.findOneAndUpdate( - { url: statusPageData.url }, - statusPageData, - { - new: true, - } - ); + const statusPage = await StatusPage.findOneAndUpdate({ url: statusPageData.url }, statusPageData, { + new: true, + }); return statusPage; } catch (error) { @@ -98,19 +94,8 @@ const getStatusPage = async (url) => { } if (!preliminaryStatusPage.monitors || preliminaryStatusPage.monitors.length === 0) { - const { - _id, - color, - companyName, - isPublished, - logo, - originalMonitors, - showCharts, - showUptimePercentage, - timezone, - showAdminLoginLink, - url, - } = preliminaryStatusPage; + const { _id, color, companyName, isPublished, logo, originalMonitors, showCharts, showUptimePercentage, timezone, showAdminLoginLink, url } = + preliminaryStatusPage; return { statusPage: { _id, diff --git a/server/db/mongo/modules/userModule.js b/server/db/mongo/modules/userModule.js index 4069be8c6..974f2d2ed 100755 --- a/server/db/mongo/modules/userModule.js +++ b/server/db/mongo/modules/userModule.js @@ -4,10 +4,18 @@ import { GenerateAvatarImage } from "../../../utils/imageProcessing.js"; const DUPLICATE_KEY_CODE = 11000; // MongoDB error code for duplicate key import { ParseBoolean } from "../../../utils/utils.js"; -import ServiceRegistry from "../../../service/serviceRegistry.js"; -import StringService from "../../../service/stringService.js"; +import ServiceRegistry from "../../../service/system/serviceRegistry.js"; +import StringService from "../../../service/system/stringService.js"; const SERVICE_NAME = "userModule"; +const checkSuperadmin = async () => { + const superAdmin = await UserModel.findOne({ role: "superadmin" }); + if (superAdmin !== null) { + return true; + } + return false; +}; + /** * Insert a User * @async @@ -16,11 +24,7 @@ const SERVICE_NAME = "userModule"; * @returns {Promise} * @throws {Error} */ -const insertUser = async ( - userData, - imageFile, - generateAvatarImage = GenerateAvatarImage -) => { +const insertUser = async (userData, imageFile, generateAvatarImage = GenerateAvatarImage) => { const stringService = ServiceRegistry.get(StringService.SERVICE_NAME); try { if (imageFile) { @@ -47,9 +51,7 @@ const insertUser = async ( const newUser = new UserModel(userData); await newUser.save(); - return await UserModel.findOne({ _id: newUser._id }) - .select("-password") - .select("-profileImage"); // .select() doesn't work with create, need to save then find + return await UserModel.findOne({ _id: newUser._id }).select("-password").select("-profileImage"); // .select() doesn't work with create, need to save then find } catch (error) { if (error.code === DUPLICATE_KEY_CODE) { error.message = stringService.dbUserExists; @@ -98,31 +100,26 @@ const getUserByEmail = async (email) => { * @throws {Error} */ -const updateUser = async ( - req, - res, - parseBoolean = ParseBoolean, - generateAvatarImage = GenerateAvatarImage -) => { - const candidateUserId = req.params.userId; - try { - const candidateUser = { ...req.body }; - // ****************************************** - // Handle profile image - // ****************************************** +const updateUser = async ({ userId, user, file }) => { + if (!userId) { + throw new Error("No user in request"); + } - if (parseBoolean(candidateUser.deleteProfileImage) === true) { + try { + const candidateUser = { ...user }; + + if (ParseBoolean(candidateUser.deleteProfileImage) === true) { candidateUser.profileImage = null; candidateUser.avatarImage = null; - } else if (req.file) { + } else if (file) { // 1. Save the full size image candidateUser.profileImage = { - data: req.file.buffer, - contentType: req.file.mimetype, + data: file.buffer, + contentType: file.mimetype, }; // 2. Get the avatar sized image - const avatar = await generateAvatarImage(req.file); + const avatar = await GenerateAvatarImage(file); candidateUser.avatarImage = avatar; } @@ -131,7 +128,7 @@ const updateUser = async ( // ****************************************** const updatedUser = await UserModel.findByIdAndUpdate( - candidateUserId, + userId, candidateUser, { new: true } // Returns updated user instead of pre-update user ) @@ -198,7 +195,7 @@ const deleteAllOtherUsers = async () => { } }; -const getAllUsers = async (req, res) => { +const getAllUsers = async () => { try { const users = await UserModel.find().select("-password").select("-profileImage"); return users; @@ -220,7 +217,37 @@ const logoutUser = async (userId) => { } }; +const getUserById = async (roles, userId) => { + try { + if (!roles.includes("superadmin")) { + throw new Error("User is not a superadmin"); + } + + const user = await UserModel.findById(userId).select("-password").select("-profileImage"); + if (!user) { + throw new Error("User not found"); + } + + return user; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getUserById"; + throw error; + } +}; + +const editUserById = async (userId, user) => { + try { + await UserModel.findByIdAndUpdate(userId, user, { new: true }).select("-password").select("-profileImage"); + } catch (error) { + error.service = SERVICE_NAME; + error.method = "editUserById"; + throw error; + } +}; + export { + checkSuperadmin, insertUser, getUserByEmail, updateUser, @@ -229,4 +256,6 @@ export { deleteAllOtherUsers, getAllUsers, logoutUser, + getUserById, + editUserById, }; diff --git a/server/index.js b/server/index.js index 4b74ef46c..b6f1dd3fe 100755 --- a/server/index.js +++ b/server/index.js @@ -1,16 +1,19 @@ import path from "path"; import fs from "fs"; import swaggerUi from "swagger-ui-express"; - +import jwt from "jsonwebtoken"; +import papaparse from "papaparse"; import express from "express"; import helmet from "helmet"; import cors from "cors"; import compression from "compression"; +import { Logger } from "./utils/logger.js"; import logger from "./utils/logger.js"; import { verifyJWT } from "./middleware/verifyJWT.js"; import { handleErrors } from "./middleware/handleErrors.js"; import { responseHandler } from "./middleware/responseHandler.js"; import { fileURLToPath } from "url"; +import { createCommonDependencies } from "./controllers/baseController.js"; import AuthRoutes from "./routes/authRoute.js"; import AuthController from "./controllers/authController.js"; @@ -46,62 +49,73 @@ import DiagnosticRoutes from "./routes/diagnosticRoute.js"; import DiagnosticController from "./controllers/diagnosticController.js"; //JobQueue service and dependencies -import JobQueue from "./service/JobQueue/JobQueue.js"; -import JobQueueHelper from "./service/JobQueue/JobQueueHelper.js"; +import JobQueue from "./service/infrastructure/JobQueue/JobQueue.js"; +import JobQueueHelper from "./service/infrastructure/JobQueue/JobQueueHelper.js"; import { Queue, Worker } from "bullmq"; -import PulseQueue from "./service/PulseQueue/PulseQueue.js"; -import PulseQueueHelper from "./service/PulseQueue/PulseQueueHelper.js"; +import PulseQueue from "./service/infrastructure/PulseQueue/PulseQueue.js"; +import PulseQueueHelper from "./service/infrastructure/PulseQueue/PulseQueueHelper.js"; + +import SuperSimpleQueue from "./service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; +import SuperSimpleQueueHelper from "./service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; + +import ErrorService from "./service/infrastructure/errorService.js"; + +// Business services +import UserService from "./service/business/userService.js"; +import CheckService from "./service/business/checkService.js"; +import DiagnosticService from "./service/business/diagnosticService.js"; +import InviteService from "./service/business/inviteService.js"; +import MaintenanceWindowService from "./service/business/maintenanceWindowService.js"; +import MonitorService from "./service/business/monitorService.js"; //Network service and dependencies -import NetworkService from "./service/networkService.js"; +import NetworkService from "./service/infrastructure/networkService.js"; import axios from "axios"; import ping from "ping"; import http from "http"; import Docker from "dockerode"; import net from "net"; // Email service and dependencies -import EmailService from "./service/emailService.js"; +import EmailService from "./service/infrastructure/emailService.js"; import nodemailer from "nodemailer"; import pkg from "handlebars"; const { compile } = pkg; import mjml2html from "mjml"; // Settings Service and dependencies -import SettingsService from "./service/settingsService.js"; +import SettingsService from "./service/system/settingsService.js"; import AppSettings from "./db/models/AppSettings.js"; // Status Service and dependencies -import StatusService from "./service/statusService.js"; +import StatusService from "./service/infrastructure/statusService.js"; // Notification Service and dependencies -import NotificationService from "./service/notificationService.js"; -import NotificationUtils from "./service/notificationUtils.js"; +import NotificationService from "./service/infrastructure/notificationService.js"; +import NotificationUtils from "./service/infrastructure/notificationUtils.js"; // Buffer Service and dependencies -import BufferService from "./service/bufferService.js"; +import BufferService from "./service/infrastructure/bufferService.js"; // Service Registry -import ServiceRegistry from "./service/serviceRegistry.js"; +import ServiceRegistry from "./service/system/serviceRegistry.js"; import MongoDB from "./db/mongo/MongoDB.js"; // Redis Service and dependencies import IORedis from "ioredis"; -import RedisService from "./service/redisService.js"; +import RedisService from "./service/data/redisService.js"; -import TranslationService from "./service/translationService.js"; +import TranslationService from "./service/system/translationService.js"; import languageMiddleware from "./middleware/languageMiddleware.js"; -import StringService from "./service/stringService.js"; +import StringService from "./service/system/stringService.js"; const SERVICE_NAME = "Server"; const SHUTDOWN_TIMEOUT = 1000; let isShuttingDown = false; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const openApiSpec = JSON.parse( - fs.readFileSync(path.join(__dirname, "openapi.json"), "utf8") -); +const openApiSpec = JSON.parse(fs.readFileSync(path.join(__dirname, "openapi.json"), "utf8")); const frontendPath = path.join(__dirname, "public"); @@ -156,25 +170,8 @@ const startApp = async () => { // Set allowed origin const allowedOrigin = appSettings.clientHost; - const networkService = new NetworkService( - axios, - ping, - logger, - http, - Docker, - net, - stringService, - settingsService - ); - const emailService = new EmailService( - settingsService, - fs, - path, - compile, - mjml2html, - nodemailer, - logger - ); + const networkService = new NetworkService(axios, ping, logger, http, Docker, net, stringService, settingsService); + const emailService = new EmailService(settingsService, fs, path, compile, mjml2html, nodemailer, logger); const bufferService = new BufferService({ db, logger }); const statusService = new StatusService({ db, logger, buffer: bufferService }); const notificationUtils = new NotificationUtils({ @@ -192,6 +189,7 @@ const startApp = async () => { }); const redisService = new RedisService({ Redis: IORedis, logger }); + const errorService = new ErrorService(); // const jobQueueHelper = new JobQueueHelper({ // redisService, @@ -210,23 +208,80 @@ const startApp = async () => { // stringService, // }); - const pulseQueueHelper = new PulseQueueHelper({ + // const pulseQueueHelper = new PulseQueueHelper({ + // db, + // logger, + // networkService, + // statusService, + // notificationService, + // }); + // const pulseQueue = await PulseQueue.create({ + // appSettings, + // db, + // pulseQueueHelper, + // logger, + // }); + const superSimpleQueueHelper = new SuperSimpleQueueHelper({ db, logger, networkService, statusService, notificationService, }); - const pulseQueue = await PulseQueue.create({ + + const superSimpleQueue = await SuperSimpleQueue.create({ appSettings, db, - pulseQueueHelper, logger, + helper: superSimpleQueueHelper, }); + // Business services + const userService = new UserService({ + db, + emailService, + settingsService, + logger, + stringService, + jwt, + errorService, + }); + const checkService = new CheckService({ + db, + settingsService, + stringService, + errorService, + }); + const diagnosticService = new DiagnosticService(); + const inviteService = new InviteService({ + db, + settingsService, + emailService, + stringService, + errorService, + }); + const maintenanceWindowService = new MaintenanceWindowService({ + db, + settingsService, + stringService, + errorService, + }); + const monitorService = new MonitorService({ + db, + settingsService, + jobQueue: superSimpleQueue, + stringService, + emailService, + papaparse, + logger, + errorService, + }); // Register services // ServiceRegistry.register(JobQueue.SERVICE_NAME, jobQueue); - ServiceRegistry.register(JobQueue.SERVICE_NAME, pulseQueue); + // ServiceRegistry.register(JobQueue.SERVICE_NAME, pulseQueue); + ServiceRegistry.register(Logger.SERVICE_NAME, logger); + ServiceRegistry.register(JobQueue.SERVICE_NAME, superSimpleQueue); + ServiceRegistry.register(ErrorService.SERVICE_NAME, errorService); ServiceRegistry.register(MongoDB.SERVICE_NAME, db); ServiceRegistry.register(SettingsService.SERVICE_NAME, settingsService); ServiceRegistry.register(EmailService.SERVICE_NAME, emailService); @@ -236,6 +291,12 @@ const startApp = async () => { ServiceRegistry.register(NotificationService.SERVICE_NAME, notificationService); ServiceRegistry.register(TranslationService.SERVICE_NAME, translationService); ServiceRegistry.register(RedisService.SERVICE_NAME, redisService); + ServiceRegistry.register(UserService.SERVICE_NAME, userService); + ServiceRegistry.register(CheckService.SERVICE_NAME, checkService); + ServiceRegistry.register(InviteService.SERVICE_NAME, inviteService); + ServiceRegistry.register(MaintenanceWindowService.SERVICE_NAME, maintenanceWindowService); + ServiceRegistry.register(MonitorService.SERVICE_NAME, monitorService); + ServiceRegistry.register(DiagnosticService.SERVICE_NAME, diagnosticService); await translationService.initialize(); @@ -249,71 +310,64 @@ const startApp = async () => { process.on("SIGTERM", shutdown); //Create controllers - const authController = new AuthController({ - db: ServiceRegistry.get(MongoDB.SERVICE_NAME), + + const commonDependencies = createCommonDependencies( + ServiceRegistry, + MongoDB.SERVICE_NAME, + Logger.SERVICE_NAME, + ErrorService.SERVICE_NAME, + StringService.SERVICE_NAME + ); + + const authController = new AuthController(commonDependencies, { settingsService: ServiceRegistry.get(SettingsService.SERVICE_NAME), emailService: ServiceRegistry.get(EmailService.SERVICE_NAME), jobQueue: ServiceRegistry.get(JobQueue.SERVICE_NAME), - stringService: ServiceRegistry.get(StringService.SERVICE_NAME), - logger: logger, + userService: ServiceRegistry.get(UserService.SERVICE_NAME), }); - const monitorController = new MonitorController( - ServiceRegistry.get(MongoDB.SERVICE_NAME), - ServiceRegistry.get(SettingsService.SERVICE_NAME), - ServiceRegistry.get(JobQueue.SERVICE_NAME), - ServiceRegistry.get(StringService.SERVICE_NAME), - ServiceRegistry.get(EmailService.SERVICE_NAME) - ); - - const settingsController = new SettingsController({ - db: ServiceRegistry.get(MongoDB.SERVICE_NAME), + const monitorController = new MonitorController(commonDependencies, { + settingsService: ServiceRegistry.get(SettingsService.SERVICE_NAME), + jobQueue: ServiceRegistry.get(JobQueue.SERVICE_NAME), + emailService: ServiceRegistry.get(EmailService.SERVICE_NAME), + monitorService: ServiceRegistry.get(MonitorService.SERVICE_NAME), + }); + + const settingsController = new SettingsController(commonDependencies, { settingsService: ServiceRegistry.get(SettingsService.SERVICE_NAME), - stringService: ServiceRegistry.get(StringService.SERVICE_NAME), emailService: ServiceRegistry.get(EmailService.SERVICE_NAME), }); - const checkController = new CheckController( - ServiceRegistry.get(MongoDB.SERVICE_NAME), - ServiceRegistry.get(SettingsService.SERVICE_NAME), - ServiceRegistry.get(StringService.SERVICE_NAME) - ); - - const inviteController = new InviteController( - ServiceRegistry.get(MongoDB.SERVICE_NAME), - ServiceRegistry.get(SettingsService.SERVICE_NAME), - ServiceRegistry.get(EmailService.SERVICE_NAME), - ServiceRegistry.get(StringService.SERVICE_NAME) - ); - - const maintenanceWindowController = new MaintenanceWindowController( - ServiceRegistry.get(MongoDB.SERVICE_NAME), - ServiceRegistry.get(SettingsService.SERVICE_NAME), - ServiceRegistry.get(StringService.SERVICE_NAME) - ); - - const queueController = new QueueController( - ServiceRegistry.get(JobQueue.SERVICE_NAME), - ServiceRegistry.get(StringService.SERVICE_NAME) - ); - - const logController = new LogController(logger); - - const statusPageController = new StatusPageController( - ServiceRegistry.get(MongoDB.SERVICE_NAME), - ServiceRegistry.get(StringService.SERVICE_NAME) - ); - - const notificationController = new NotificationController({ - notificationService: ServiceRegistry.get(NotificationService.SERVICE_NAME), - stringService: ServiceRegistry.get(StringService.SERVICE_NAME), - statusService: ServiceRegistry.get(StatusService.SERVICE_NAME), - db: ServiceRegistry.get(MongoDB.SERVICE_NAME), + const checkController = new CheckController(commonDependencies, { + settingsService: ServiceRegistry.get(SettingsService.SERVICE_NAME), + checkService: ServiceRegistry.get(CheckService.SERVICE_NAME), }); - const diagnosticController = new DiagnosticController( - ServiceRegistry.get(MongoDB.SERVICE_NAME) - ); + const inviteController = new InviteController(commonDependencies, { + inviteService: ServiceRegistry.get(InviteService.SERVICE_NAME), + }); + + const maintenanceWindowController = new MaintenanceWindowController(commonDependencies, { + settingsService: ServiceRegistry.get(SettingsService.SERVICE_NAME), + maintenanceWindowService: ServiceRegistry.get(MaintenanceWindowService.SERVICE_NAME), + }); + + const queueController = new QueueController(commonDependencies, { + jobQueue: ServiceRegistry.get(JobQueue.SERVICE_NAME), + }); + + const logController = new LogController(commonDependencies); + + const statusPageController = new StatusPageController(commonDependencies); + + const notificationController = new NotificationController(commonDependencies, { + notificationService: ServiceRegistry.get(NotificationService.SERVICE_NAME), + statusService: ServiceRegistry.get(StatusService.SERVICE_NAME), + }); + + const diagnosticController = new DiagnosticController(commonDependencies, { + diagnosticService: ServiceRegistry.get(DiagnosticService.SERVICE_NAME), + }); //Create routes const authRoutes = new AuthRoutes(authController); @@ -321,15 +375,13 @@ const startApp = async () => { const settingsRoutes = new SettingsRoutes(settingsController); const checkRoutes = new CheckRoutes(checkController); const inviteRoutes = new InviteRoutes(inviteController); - const maintenanceWindowRoutes = new MaintenanceWindowRoutes( - maintenanceWindowController - ); + const maintenanceWindowRoutes = new MaintenanceWindowRoutes(maintenanceWindowController); const queueRoutes = new QueueRoutes(queueController); const logRoutes = new LogRoutes(logController); const statusPageRoutes = new StatusPageRoutes(statusPageController); - const notificationRoutes = new NotificationRoutes(notificationController); const diagnosticRoutes = new DiagnosticRoutes(diagnosticController); + // Middleware app.use(express.static(frontendPath)); app.use(responseHandler); @@ -372,16 +424,17 @@ const startApp = async () => { //routes app.use("/api/v1/auth", authRoutes.getRouter()); - app.use("/api/v1/settings", verifyJWT, settingsRoutes.getRouter()); - app.use("/api/v1/invite", inviteRoutes.getRouter()); app.use("/api/v1/monitors", verifyJWT, monitorRoutes.getRouter()); + app.use("/api/v1/settings", verifyJWT, settingsRoutes.getRouter()); app.use("/api/v1/checks", verifyJWT, checkRoutes.getRouter()); + app.use("/api/v1/invite", inviteRoutes.getRouter()); app.use("/api/v1/maintenance-window", verifyJWT, maintenanceWindowRoutes.getRouter()); app.use("/api/v1/queue", verifyJWT, queueRoutes.getRouter()); app.use("/api/v1/logs", verifyJWT, logRoutes.getRouter()); app.use("/api/v1/status-page", statusPageRoutes.getRouter()); app.use("/api/v1/notifications", verifyJWT, notificationRoutes.getRouter()); app.use("/api/v1/diagnostic", verifyJWT, diagnosticRoutes.getRouter()); + app.use("/api/v1/health", (req, res) => { res.json({ status: "OK", diff --git a/server/middleware/handleErrors.js b/server/middleware/handleErrors.js index 87895fcf2..782d37ad9 100755 --- a/server/middleware/handleErrors.js +++ b/server/middleware/handleErrors.js @@ -1,6 +1,6 @@ import logger from "../utils/logger.js"; -import ServiceRegistry from "../service/serviceRegistry.js"; -import StringService from "../service/stringService.js"; +import ServiceRegistry from "../service/system/serviceRegistry.js"; +import StringService from "../service/system/stringService.js"; const handleErrors = (error, req, res, next) => { const status = error.status || 500; diff --git a/server/middleware/isAllowed.js b/server/middleware/isAllowed.js index 4c6f89aa3..5d61974be 100755 --- a/server/middleware/isAllowed.js +++ b/server/middleware/isAllowed.js @@ -1,9 +1,9 @@ import jwt from "jsonwebtoken"; const TOKEN_PREFIX = "Bearer "; const SERVICE_NAME = "allowedRoles"; -import ServiceRegistry from "../service/serviceRegistry.js"; -import StringService from "../service/stringService.js"; -import SettingsService from "../service/settingsService.js"; +import ServiceRegistry from "../service/system/serviceRegistry.js"; +import StringService from "../service/system/stringService.js"; +import SettingsService from "../service/system/settingsService.js"; const isAllowed = (allowedRoles) => { return (req, res, next) => { @@ -29,9 +29,7 @@ const isAllowed = (allowedRoles) => { // Parse the token try { const parsedToken = token.slice(TOKEN_PREFIX.length, token.length); - const { jwtSecret } = ServiceRegistry.get( - SettingsService.SERVICE_NAME - ).getSettings(); + const { jwtSecret } = ServiceRegistry.get(SettingsService.SERVICE_NAME).getSettings(); var decoded = jwt.verify(parsedToken, jwtSecret); const userRoles = decoded.role; diff --git a/server/middleware/languageMiddleware.js b/server/middleware/languageMiddleware.js index 1449e203f..effd49cc7 100755 --- a/server/middleware/languageMiddleware.js +++ b/server/middleware/languageMiddleware.js @@ -1,28 +1,27 @@ import logger from "../utils/logger.js"; -const languageMiddleware = - (stringService, translationService) => async (req, res, next) => { - try { - const acceptLanguage = req.headers["accept-language"] || "en"; - const language = acceptLanguage.split(",")[0].slice(0, 2).toLowerCase(); +const languageMiddleware = (stringService, translationService) => async (req, res, next) => { + try { + const acceptLanguage = req.headers["accept-language"] || "en"; + const language = acceptLanguage.split(",")[0].slice(0, 2).toLowerCase(); - translationService.setLanguage(language); - stringService.setLanguage(language); + translationService.setLanguage(language); + stringService.setLanguage(language); - next(); - } catch (error) { - logger.error({ - message: error.message, - service: "languageMiddleware", - }); - const acceptLanguage = req.headers["accept-language"] || "en"; - const language = acceptLanguage.split(",")[0].slice(0, 2).toLowerCase(); + next(); + } catch (error) { + logger.error({ + message: error.message, + service: "languageMiddleware", + }); + const acceptLanguage = req.headers["accept-language"] || "en"; + const language = acceptLanguage.split(",")[0].slice(0, 2).toLowerCase(); - translationService.setLanguage(language); - stringService.setLanguage(language); + translationService.setLanguage(language); + stringService.setLanguage(language); - next(); - } - }; + next(); + } +}; export default languageMiddleware; diff --git a/server/middleware/verifyJWT.js b/server/middleware/verifyJWT.js index bd17edcfd..d01db0a97 100755 --- a/server/middleware/verifyJWT.js +++ b/server/middleware/verifyJWT.js @@ -1,7 +1,7 @@ import jwt from "jsonwebtoken"; -import ServiceRegistry from "../service/serviceRegistry.js"; -import SettingsService from "../service/settingsService.js"; -import StringService from "../service/stringService.js"; +import ServiceRegistry from "../service/system/serviceRegistry.js"; +import SettingsService from "../service/system/settingsService.js"; +import StringService from "../service/system/stringService.js"; const SERVICE_NAME = "verifyJWT"; const TOKEN_PREFIX = "Bearer "; @@ -39,13 +39,13 @@ const verifyJWT = (req, res, next) => { const { jwtSecret } = ServiceRegistry.get(SettingsService.SERVICE_NAME).getSettings(); jwt.verify(parsedToken, jwtSecret, (err, decoded) => { if (err) { - if (err) { - const errorMessage = - err.name === "TokenExpiredError" - ? stringService.expiredAuthToken - : stringService.invalidAuthToken; - return res.status(401).json({ success: false, msg: errorMessage }); - } + const errorMessage = err.name === "TokenExpiredError" ? stringService.expiredAuthToken : stringService.invalidAuthToken; + err.details = { msg: errorMessage }; + err.status = 401; + err.service = SERVICE_NAME; + err.method = "verifyJWT"; + next(err); + return; } else { // Token is valid, carry on req.user = decoded; diff --git a/server/middleware/verifyOwnership.js b/server/middleware/verifyOwnership.js index 2040d54ee..890c40b24 100755 --- a/server/middleware/verifyOwnership.js +++ b/server/middleware/verifyOwnership.js @@ -1,6 +1,6 @@ import logger from "../utils/logger.js"; -import ServiceRegistry from "../service/serviceRegistry.js"; -import StringService from "../service/stringService.js"; +import ServiceRegistry from "../service/system/serviceRegistry.js"; +import StringService from "../service/system/stringService.js"; import { ObjectId } from "mongodb"; const SERVICE_NAME = "verifyOwnership"; diff --git a/server/middleware/verifySuperAdmin.js b/server/middleware/verifySuperAdmin.js index 98c336fad..a5a93eede 100755 --- a/server/middleware/verifySuperAdmin.js +++ b/server/middleware/verifySuperAdmin.js @@ -47,9 +47,7 @@ const verifySuperAdmin = (req, res, next) => { stack: err.stack, details: stringService.invalidAuthToken, }); - return res - .status(401) - .json({ success: false, msg: stringService.invalidAuthToken }); + return res.status(401).json({ success: false, msg: stringService.invalidAuthToken }); } if (decoded.role.includes("superadmin") === false) { diff --git a/server/openapi.json b/server/openapi.json index 7b7bb3cb3..465bf948a 100755 --- a/server/openapi.json +++ b/server/openapi.json @@ -100,14 +100,7 @@ "application/json": { "schema": { "type": "object", - "required": [ - "firstName", - "lastName", - "email", - "password", - "role", - "teamId" - ], + "required": ["firstName", "lastName", "email", "password", "role", "teamId"], "properties": { "firstName": { "type": "string" diff --git a/server/package-lock.json b/server/package-lock.json old mode 100755 new mode 100644 index 3fd675af3..d2ba13f13 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -33,6 +33,7 @@ "ping": "0.4.4", "sharp": "0.33.5", "ssl-checker": "2.0.10", + "super-simple-scheduler": "1.3.0", "swagger-ui-express": "5.0.1", "winston": "^3.13.0" }, @@ -51,36 +52,33 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", + "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/runtime": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", - "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", + "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", "license": "MIT", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, "engines": { "node": ">=6.9.0" } @@ -92,9 +90,9 @@ "license": "Apache-2.0" }, "node_modules/@bcoe/v8-coverage": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.1.tgz", - "integrity": "sha512-W+a0/JpU28AqH4IKtwUPcEUnUyXMDLALcn5/JLczGGT9fHE2sIby/xP/oQnx3nxkForzgzPy201RAKcB4xPAFQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", "dev": true, "license": "MIT", "engines": { @@ -122,9 +120,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", - "integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz", + "integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==", "license": "MIT", "optional": true, "dependencies": { @@ -132,10 +130,11 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", - "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", "dev": true, + "license": "MIT", "dependencies": { "eslint-visitor-keys": "^3.4.3" }, @@ -154,6 +153,7 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -166,17 +166,19 @@ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "dev": true, + "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, "node_modules/@eslint/config-array": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz", - "integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.5", + "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -184,34 +186,20 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "node_modules/@eslint/config-helpers": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, + "license": "Apache-2.0", "engines": { - "node": "*" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", - "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -222,10 +210,11 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", - "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", "dev": true, + "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -244,22 +233,12 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -267,46 +246,37 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/@eslint/js": { - "version": "9.20.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.20.0.tgz", - "integrity": "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ==", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", + "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz", - "integrity": "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz", - "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", + "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.10.0", + "@eslint/core": "^0.15.1", "levn": "^0.4.1" }, "engines": { @@ -314,9 +284,9 @@ } }, "node_modules/@grpc/grpc-js": { - "version": "1.12.5", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.12.5.tgz", - "integrity": "sha512-d3iiHxdpg5+ZcJ6jnDSOT8Z0O0VMVGy34jAnYLUX8yd36b1qn8f1TwOA/Lc7TsOh03IkPJ38eGI5qD2EjNkoEA==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.4.tgz", + "integrity": "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==", "license": "Apache-2.0", "dependencies": { "@grpc/proto-loader": "^0.7.13", @@ -327,9 +297,9 @@ } }, "node_modules/@grpc/proto-loader": { - "version": "0.7.13", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", - "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", "license": "Apache-2.0", "dependencies": { "lodash.camelcase": "^4.3.0", @@ -364,6 +334,7 @@ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=18.18.0" } @@ -373,6 +344,7 @@ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" @@ -386,6 +358,7 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=18.18" }, @@ -399,6 +372,7 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=12.22" }, @@ -408,10 +382,11 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", - "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=18.18" }, @@ -825,16 +800,16 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", "dev": true, "license": "MIT", "dependencies": { @@ -853,9 +828,9 @@ } }, "node_modules/@mongodb-js/saslprep": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz", - "integrity": "sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.0.tgz", + "integrity": "sha512-zlayKCsIjYb7/IdfqxorK5+xUMyi4vOKcFy10wKJYc63NSdKI8mNME+uJqfatkPmOSMMUiojrL58IePKBm3gvQ==", "license": "MIT", "dependencies": { "sparse-bitfield": "^3.0.3" @@ -1027,23 +1002,6 @@ "mongodb": "^6.5.0" } }, - "node_modules/@pulsecron/pulse/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/@scarf/scarf": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", @@ -1126,15 +1084,18 @@ "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", "license": "ISC", + "optional": true, + "peer": true, "engines": { "node": ">=10.13.0" } }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", - "dev": true + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", @@ -1147,15 +1108,16 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/node": { - "version": "22.10.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.4.tgz", - "integrity": "sha512-99l6wv4HEzBQhvaU/UGoeBoCK61SCROQaCCGyQSgX2tEQ3rKkNZ2S7CEWnS/4s1LV+8ODdK21UeyR1fHP2mXug==", + "version": "24.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz", + "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==", "license": "MIT", "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~7.8.0" } }, "node_modules/@types/triple-beam": { @@ -1192,11 +1154,21 @@ "node": ">= 0.6" } }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -1209,20 +1181,18 @@ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, + "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "license": "MIT", - "dependencies": { - "debug": "4" - }, "engines": { - "node": ">= 6.0.0" + "node": ">= 14" } }, "node_modules/ajv": { @@ -1230,6 +1200,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -1340,13 +1311,13 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz", - "integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, @@ -1463,12 +1434,14 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, "node_modules/braces": { @@ -1491,9 +1464,9 @@ "license": "ISC" }, "node_modules/browserslist": { - "version": "4.24.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz", - "integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==", + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", "funding": [ { "type": "opencollective", @@ -1510,10 +1483,10 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", + "caniuse-lite": "^1.0.30001726", + "electron-to-chromium": "^1.5.173", "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" + "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" @@ -1523,9 +1496,9 @@ } }, "node_modules/bson": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.1.tgz", - "integrity": "sha512-P92xmHDQjSKPLHqFxefqMxASNq/aWJMEZugpCjf+AF/pgcUpMMQCg7t7+ewko0/u8AapvF3luf/FoehddEK+sA==", + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", + "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", "license": "Apache-2.0", "engines": { "node": ">=16.20.1" @@ -1646,9 +1619,9 @@ } }, "node_modules/call-bind-apply-helpers": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", - "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -1659,13 +1632,13 @@ } }, "node_modules/call-bound": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", - "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "get-intrinsic": "^1.2.6" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -1709,9 +1682,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001690", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz", - "integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==", + "version": "1.0.30001727", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", + "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", "funding": [ { "type": "opencollective", @@ -1834,6 +1807,24 @@ "fsevents": "~2.3.2" } }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -2063,15 +2054,6 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, - "node_modules/compression/node_modules/negotiator": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", - "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2265,9 +2247,9 @@ } }, "node_modules/css-select": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", - "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", @@ -2285,6 +2267,8 @@ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "mdn-data": "2.0.30", "source-map-js": "^1.0.1" @@ -2294,9 +2278,9 @@ } }, "node_modules/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", "license": "BSD-2-Clause", "engines": { "node": ">= 6" @@ -2318,13 +2302,13 @@ } }, "node_modules/cssnano": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.0.6.tgz", - "integrity": "sha512-54woqx8SCbp8HwvNZYn68ZFAepuouZW4lTwiMVnBErM3VkO7/Sd4oTOt3Zz3bPx3kxQ36aISppyXj2Md4lg8bw==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.1.0.tgz", + "integrity": "sha512-Pu3rlKkd0ZtlCUzBrKL1Z4YmhKppjC1H9jo7u1o4qaKqyhvixFgu5qLyNIAOjSTg9DjVPtUqdROq2EfpVMEe+w==", "license": "MIT", "dependencies": { - "cssnano-preset-default": "^7.0.6", - "lilconfig": "^3.1.2" + "cssnano-preset-default": "^7.0.8", + "lilconfig": "^3.1.3" }, "engines": { "node": "^18.12.0 || ^20.9.0 || >=22.0" @@ -2334,63 +2318,63 @@ "url": "https://opencollective.com/cssnano" }, "peerDependencies": { - "postcss": "^8.4.31" + "postcss": "^8.4.32" } }, "node_modules/cssnano-preset-default": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-7.0.6.tgz", - "integrity": "sha512-ZzrgYupYxEvdGGuqL+JKOY70s7+saoNlHSCK/OGn1vB2pQK8KSET8jvenzItcY+kA7NoWvfbb/YhlzuzNKjOhQ==", + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-7.0.8.tgz", + "integrity": "sha512-d+3R2qwrUV3g4LEMOjnndognKirBZISylDZAF/TPeCWVjEwlXS2e4eN4ICkoobRe7pD3H6lltinKVyS1AJhdjQ==", "license": "MIT", "dependencies": { - "browserslist": "^4.23.3", + "browserslist": "^4.25.1", "css-declaration-sorter": "^7.2.0", - "cssnano-utils": "^5.0.0", - "postcss-calc": "^10.0.2", - "postcss-colormin": "^7.0.2", - "postcss-convert-values": "^7.0.4", - "postcss-discard-comments": "^7.0.3", - "postcss-discard-duplicates": "^7.0.1", - "postcss-discard-empty": "^7.0.0", - "postcss-discard-overridden": "^7.0.0", - "postcss-merge-longhand": "^7.0.4", - "postcss-merge-rules": "^7.0.4", - "postcss-minify-font-values": "^7.0.0", - "postcss-minify-gradients": "^7.0.0", - "postcss-minify-params": "^7.0.2", - "postcss-minify-selectors": "^7.0.4", - "postcss-normalize-charset": "^7.0.0", - "postcss-normalize-display-values": "^7.0.0", - "postcss-normalize-positions": "^7.0.0", - "postcss-normalize-repeat-style": "^7.0.0", - "postcss-normalize-string": "^7.0.0", - "postcss-normalize-timing-functions": "^7.0.0", - "postcss-normalize-unicode": "^7.0.2", - "postcss-normalize-url": "^7.0.0", - "postcss-normalize-whitespace": "^7.0.0", - "postcss-ordered-values": "^7.0.1", - "postcss-reduce-initial": "^7.0.2", - "postcss-reduce-transforms": "^7.0.0", - "postcss-svgo": "^7.0.1", - "postcss-unique-selectors": "^7.0.3" + "cssnano-utils": "^5.0.1", + "postcss-calc": "^10.1.1", + "postcss-colormin": "^7.0.4", + "postcss-convert-values": "^7.0.6", + "postcss-discard-comments": "^7.0.4", + "postcss-discard-duplicates": "^7.0.2", + "postcss-discard-empty": "^7.0.1", + "postcss-discard-overridden": "^7.0.1", + "postcss-merge-longhand": "^7.0.5", + "postcss-merge-rules": "^7.0.6", + "postcss-minify-font-values": "^7.0.1", + "postcss-minify-gradients": "^7.0.1", + "postcss-minify-params": "^7.0.4", + "postcss-minify-selectors": "^7.0.5", + "postcss-normalize-charset": "^7.0.1", + "postcss-normalize-display-values": "^7.0.1", + "postcss-normalize-positions": "^7.0.1", + "postcss-normalize-repeat-style": "^7.0.1", + "postcss-normalize-string": "^7.0.1", + "postcss-normalize-timing-functions": "^7.0.1", + "postcss-normalize-unicode": "^7.0.4", + "postcss-normalize-url": "^7.0.1", + "postcss-normalize-whitespace": "^7.0.1", + "postcss-ordered-values": "^7.0.2", + "postcss-reduce-initial": "^7.0.4", + "postcss-reduce-transforms": "^7.0.1", + "postcss-svgo": "^7.1.0", + "postcss-unique-selectors": "^7.0.4" }, "engines": { "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.4.31" + "postcss": "^8.4.32" } }, "node_modules/cssnano-utils": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-5.0.0.tgz", - "integrity": "sha512-Uij0Xdxc24L6SirFr25MlwC2rCFX6scyUmuKpzI+JQ7cyqDEwD42fJ0xfB3yLfOnRDU5LKGgjQ9FA6LYh76GWQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-5.0.1.tgz", + "integrity": "sha512-ZIP71eQgG9JwjVZsTPSqhc6GHgEr53uJ7tK5///VfyWj6Xp2DBmixWHqJgPno+PqATzn48pL42ww9x5SSGmhZg==", "license": "MIT", "engines": { "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.4.31" + "postcss": "^8.4.32" } }, "node_modules/csso": { @@ -2426,6 +2410,15 @@ "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", "license": "CC0-1.0" }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/date.js": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/date.js/-/date.js-0.3.3.tgz", @@ -2451,9 +2444,9 @@ "license": "MIT" }, "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2494,7 +2487,8 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/delayed-stream": { "version": "1.0.0", @@ -2534,9 +2528,9 @@ } }, "node_modules/detect-libc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", "license": "Apache-2.0", "engines": { "node": ">=8" @@ -2646,9 +2640,9 @@ } }, "node_modules/domutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", - "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^2.0.0", @@ -2660,9 +2654,9 @@ } }, "node_modules/dotenv": { - "version": "16.4.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -2707,9 +2701,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.74", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.74.tgz", - "integrity": "sha512-ck3//9RC+6oss/1Bh9tiAVFy5vfSKbRHAFh7Z3/eTRkEqJeWgymloShB17Vg3Z4nmDNp35vAd1BZ6CMW4Wt6Iw==", + "version": "1.5.190", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.190.tgz", + "integrity": "sha512-k4McmnB2091YIsdCgkS0fMVMPOJgxl93ltFzaryXqwip1AaxeDqKCGLxkXODDA5Ab/D+tV5EL5+aTx76RvLRxw==", "license": "ISC" }, "node_modules/emoji-regex": { @@ -2734,9 +2728,9 @@ } }, "node_modules/encoding-sniffer": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz", - "integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", "license": "MIT", "dependencies": { "iconv-lite": "^0.6.3", @@ -2759,9 +2753,9 @@ } }, "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", "license": "MIT", "dependencies": { "once": "^1.4.0" @@ -2816,9 +2810,9 @@ } }, "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -2827,6 +2821,21 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -2868,22 +2877,23 @@ } }, "node_modules/eslint": { - "version": "9.20.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.20.1.tgz", - "integrity": "sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g==", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", + "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.0", - "@eslint/core": "^0.11.0", - "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.20.0", - "@eslint/plugin-kit": "^0.2.5", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.0", + "@eslint/core": "^0.15.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.31.0", + "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.1", + "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", @@ -2891,9 +2901,9 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.2.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -2932,6 +2942,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-10.5.0.tgz", "integrity": "sha512-F2ALmQVPT1GoP27O1JTZGrV9Pqg8k79OeIuvw63UxMtQKREZtmkK1NFgkZQ2TW7L2JSSFKHFPTtHu5z8R9QNRw==", "dev": true, + "license": "MIT", "dependencies": { "eslint-utils": "^3.0.0", "globals": "^13.24.0", @@ -2949,6 +2960,7 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, + "license": "MIT", "dependencies": { "type-fest": "^0.20.2" }, @@ -2960,10 +2972,11 @@ } }, "node_modules/eslint-scope": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", - "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -2980,6 +2993,7 @@ "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", "dev": true, + "license": "MIT", "dependencies": { "eslint-visitor-keys": "^2.0.0" }, @@ -2998,15 +3012,17 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=10" } }, "node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -3014,56 +3030,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/@eslint/core": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.11.0.tgz", - "integrity": "sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/esm": { "version": "3.2.25", "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", @@ -3075,14 +3041,15 @@ } }, "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.14.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3096,6 +3063,7 @@ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" }, @@ -3108,6 +3076,7 @@ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -3120,6 +3089,7 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } @@ -3129,6 +3099,7 @@ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" } @@ -3213,19 +3184,22 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fecha": { "version": "4.2.3", @@ -3233,11 +3207,35 @@ "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", "license": "MIT" }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, + "license": "MIT", "dependencies": { "flat-cache": "^4.0.0" }, @@ -3322,6 +3320,7 @@ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, + "license": "MIT", "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" @@ -3331,10 +3330,11 @@ } }, "node_modules/flatted": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", - "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", - "dev": true + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" }, "node_modules/fn.name": { "version": "1.1.0", @@ -3363,12 +3363,12 @@ } }, "node_modules/foreground-child": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", - "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "license": "ISC", "dependencies": { - "cross-spawn": "^7.0.0", + "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" }, "engines": { @@ -3379,19 +3379,33 @@ } }, "node_modules/form-data": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", - "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { "node": ">= 6" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -3440,18 +3454,19 @@ } }, "node_modules/gaxios": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz", - "integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==", + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", "license": "Apache-2.0", "dependencies": { "extend": "^3.0.2", - "https-proxy-agent": "^5.0.0", + "https-proxy-agent": "^7.0.1", "is-stream": "^2.0.0", - "node-fetch": "^2.6.9" + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" }, "engines": { - "node": ">=12" + "node": ">=14" } }, "node_modules/get-caller-file": { @@ -3464,21 +3479,21 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz", - "integrity": "sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "dunder-proto": "^1.0.0", + "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", + "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", - "math-intrinsics": "^1.0.0" + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -3487,6 +3502,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -3508,15 +3536,40 @@ } }, "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, "license": "ISC", "dependencies": { - "is-glob": "^4.0.1" + "is-glob": "^4.0.3" }, "engines": { - "node": ">= 6" + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/globals": { @@ -3587,6 +3640,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -3610,9 +3678,9 @@ } }, "node_modules/helmet": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.0.0.tgz", - "integrity": "sha512-VyusHLEIIO5mjQPUI1wpOAEu+wl6Q0998jzTxqUYGE45xCIcAxy3MsbEK/yyJUJ3ADeMoB6MornPH6GMWAf+Pw==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", + "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -3626,19 +3694,18 @@ "license": "MIT" }, "node_modules/htmlnano": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/htmlnano/-/htmlnano-2.1.1.tgz", - "integrity": "sha512-kAERyg/LuNZYmdqgCdYvugyLWNFAm8MWXpQMz1pLpetmCbFwoMxvkSoaAMlFrOC4OKTWI4KlZGT/RsNxg4ghOw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/htmlnano/-/htmlnano-2.1.2.tgz", + "integrity": "sha512-8Fst+0bhAfU362S6oHVb4wtJj/UYEFr0qiCLAEi8zioqmp1JYBQx5crZAADlFVX0Ly/6s/IQz6G7PL9/hgoJaQ==", "license": "MIT", "dependencies": { "cosmiconfig": "^9.0.0", - "posthtml": "^0.16.5", - "timsort": "^0.3.0" + "posthtml": "^0.16.5" }, "peerDependencies": { "cssnano": "^7.0.0", "postcss": "^8.3.11", - "purgecss": "^6.0.0", + "purgecss": "^7.0.2", "relateurl": "^0.2.7", "srcset": "5.0.1", "svgo": "^3.0.2", @@ -3708,16 +3775,16 @@ } }, "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "license": "MIT", "dependencies": { - "agent-base": "6", + "agent-base": "^7.1.2", "debug": "4" }, "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/human-interval": { @@ -3766,6 +3833,7 @@ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } @@ -3778,9 +3846,9 @@ "license": "ISC" }, "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -3798,6 +3866,7 @@ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.19" } @@ -3809,9 +3878,9 @@ "license": "ISC" }, "node_modules/ioredis": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.5.0.tgz", - "integrity": "sha512-7CutT89g23FfSa8MDoIFs2GYYa0PaNiW/OrT+nRyjRXHDZd17HmIgy+reOQ/yhh72NznNjGuS8kbCAcA4Ro4mw==", + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.6.1.tgz", + "integrity": "sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==", "license": "MIT", "dependencies": { "@ioredis/commands": "^1.1.1", @@ -3952,13 +4021,31 @@ "license": "ISC" }, "node_modules/isomorphic-unfetch": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/isomorphic-unfetch/-/isomorphic-unfetch-3.1.0.tgz", - "integrity": "sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/isomorphic-unfetch/-/isomorphic-unfetch-4.0.2.tgz", + "integrity": "sha512-1Yd+CF/7al18/N2BDbsLBcp6RO3tucSW+jcLq24dqdX5MNbCNTw1z4BsGsp4zNmjr/Izm2cs/cEqZPp4kvWSCA==", "license": "MIT", "dependencies": { - "node-fetch": "^2.6.1", - "unfetch": "^4.2.0" + "node-fetch": "^3.2.0", + "unfetch": "^5.0.0" + } + }, + "node_modules/isomorphic-unfetch/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" } }, "node_modules/istanbul-lib-coverage": { @@ -3986,22 +4073,6 @@ "node": ">=10" } }, - "node_modules/istanbul-lib-report/node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/istanbul-reports": { "version": "3.1.7", "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", @@ -4075,7 +4146,8 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", @@ -4087,13 +4159,15 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/jsonwebtoken": { "version": "9.0.2", @@ -4118,13 +4192,14 @@ } }, "node_modules/juice": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/juice/-/juice-11.0.0.tgz", - "integrity": "sha512-sGF8hPz9/Wg+YXbaNDqc1Iuoaw+J/P9lBHNQKXAGc9pPNjCd4fyPai0Zxj7MRtdjMr0lcgk5PjEIkP2b8R9F3w==", + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/juice/-/juice-11.0.1.tgz", + "integrity": "sha512-R3KLud4l/sN9AMmFZs0QY7cugGSiKvPhGyIsufCV5nJ0MjSlngUE7k80TmFeK9I62wOXrjWBtYA1knVs2OkF8w==", "license": "MIT", "dependencies": { "cheerio": "^1.0.0", "commander": "^12.1.0", + "entities": "^4.5.0", "mensch": "^0.3.4", "slick": "^1.12.2", "web-resource-inliner": "^7.0.0" @@ -4137,34 +4212,34 @@ } }, "node_modules/juice/node_modules/cheerio": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", - "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.2.tgz", + "integrity": "sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg==", "license": "MIT", "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", - "domutils": "^3.1.0", - "encoding-sniffer": "^0.2.0", - "htmlparser2": "^9.1.0", - "parse5": "^7.1.2", - "parse5-htmlparser2-tree-adapter": "^7.0.0", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.0.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", "parse5-parser-stream": "^7.1.2", - "undici": "^6.19.5", + "undici": "^7.12.0", "whatwg-mimetype": "^4.0.0" }, "engines": { - "node": ">=18.17" + "node": ">=20.18.1" }, "funding": { "url": "https://github.com/cheeriojs/cheerio?sponsor=1" } }, "node_modules/juice/node_modules/htmlparser2": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", - "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", + "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==", "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", { @@ -4176,8 +4251,20 @@ "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", - "domutils": "^3.1.0", - "entities": "^4.5.0" + "domutils": "^3.2.1", + "entities": "^6.0.0" + } + }, + "node_modules/juice/node_modules/htmlparser2/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, "node_modules/just-extend": { @@ -4188,12 +4275,12 @@ "license": "MIT" }, "node_modules/jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", "license": "MIT", "dependencies": { - "buffer-equal-constant-time": "1.0.1", + "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } @@ -4222,6 +4309,7 @@ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, + "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } @@ -4237,6 +4325,7 @@ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -4301,6 +4390,7 @@ "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", "dev": true, "license": "MIT" }, @@ -4356,7 +4446,8 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.once": { "version": "4.1.1", @@ -4405,15 +4496,15 @@ } }, "node_modules/long": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", - "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", "license": "Apache-2.0" }, "node_modules/loupe": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz", - "integrity": "sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.4.tgz", + "integrity": "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==", "dev": true, "license": "MIT" }, @@ -4424,25 +4515,41 @@ "license": "ISC" }, "node_modules/luxon": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", - "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.1.tgz", + "integrity": "sha512-RkRWjA926cTvz5rAb1BqyWkKbbjzCGchDUIKMCUvNi17j6f6j8uHGDV82Aqcqtzd+icoYpELmG3ksgGiFNNcNg==", "license": "MIT", "engines": { "node": ">=12" } }, "node_modules/mailersend": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/mailersend/-/mailersend-2.3.0.tgz", - "integrity": "sha512-pe498Ry7VaAb+oqcYqmPw1V7FlECG/mcqahQ3SiK54en4ZkyRwjyxoQwA9VU4s3npB+I44LlQGUudObZQe4/jA==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mailersend/-/mailersend-2.6.0.tgz", + "integrity": "sha512-YX2Gyc6Wyw4Q4IsJ4np2Reof8nFWQ2OP/yXZTZcGCz4B7B1BOAYs71Kjb1uNRTTfDChP7rzzWBoOaX6iNlvPAg==", "license": "MIT", "dependencies": { - "gaxios": "^5.0.1", - "isomorphic-unfetch": "^3.1.0", + "gaxios": "^6.0.0", + "isomorphic-unfetch": "^4.0.0", "qs": "^6.11.0" } }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4456,7 +4563,9 @@ "version": "2.0.30", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", - "license": "CC0-1.0" + "license": "CC0-1.0", + "optional": true, + "peer": true }, "node_modules/media-typer": { "version": "0.3.0", @@ -4510,9 +4619,9 @@ } }, "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -4530,19 +4639,26 @@ "node": ">= 0.6" } }, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": "*" } }, "node_modules/minimist": { @@ -4643,6 +4759,30 @@ "mjml-cli": "bin/mjml" } }, + "node_modules/mjml-cli/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/mjml-cli/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/mjml-column": { "version": "5.0.0-alpha.6", "resolved": "https://registry.npmjs.org/mjml-column/-/mjml-column-5.0.0-alpha.6.tgz", @@ -5022,6 +5162,16 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/mocha/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/mocha/node_modules/minimatch": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", @@ -5073,13 +5223,13 @@ } }, "node_modules/mongodb": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.13.0.tgz", - "integrity": "sha512-KeESYR5TEaFxOuwRqkOm3XOsMqCSkdeDMjaW5u2nuKfX7rqaofp7JQGoi7sVqQcNJTKuveNbzZtWMstb8ABP6Q==", + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.18.0.tgz", + "integrity": "sha512-fO5ttN9VC8P0F5fqtQmclAkgXZxbIkYRTUi1j8JO6IYwvamkhtYDilJr35jOPELR49zqCJgXZWwCtW7B+TM8vQ==", "license": "Apache-2.0", "dependencies": { "@mongodb-js/saslprep": "^1.1.9", - "bson": "^6.10.1", + "bson": "^6.10.4", "mongodb-connection-string-url": "^3.0.0" }, "engines": { @@ -5119,24 +5269,24 @@ } }, "node_modules/mongodb-connection-string-url": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz", - "integrity": "sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", "license": "Apache-2.0", "dependencies": { "@types/whatwg-url": "^11.0.2", - "whatwg-url": "^13.0.0" + "whatwg-url": "^14.1.0 || ^13.0.0" } }, "node_modules/mongoose": { - "version": "8.10.1", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.10.1.tgz", - "integrity": "sha512-5beTeBZnJNndRXU9rxPol0JmTWZMAtgkPbooROkGilswvrZALDERY4cJrGZmgGwDS9dl0mxiB7si+Mv9Yms2fg==", + "version": "8.16.4", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.16.4.tgz", + "integrity": "sha512-jslgdQ8pY2vcNSKPv3Dbi5ogo/NT8zcvf6kPDyD8Sdsjsa1at3AFAF0F5PT+jySPGSPbvlNaQ49nT9h+Kx2UDA==", "license": "MIT", "dependencies": { - "bson": "^6.10.1", + "bson": "^6.10.4", "kareem": "2.6.3", - "mongodb": "~6.13.0", + "mongodb": "~6.17.0", "mpath": "0.9.0", "mquery": "5.0.0", "ms": "2.1.3", @@ -5150,6 +5300,52 @@ "url": "https://opencollective.com/mongoose" } }, + "node_modules/mongoose/node_modules/mongodb": { + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.17.0.tgz", + "integrity": "sha512-neerUzg/8U26cgruLysKEjJvoNSXhyID3RvzvdcpsIi2COYM3FS3o9nlH7fxFtefTb942dX3W9i37oPfCVj4wA==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.1.9", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.0" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, "node_modules/mpath": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", @@ -5178,9 +5374,9 @@ "license": "MIT" }, "node_modules/msgpackr": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.2.tgz", - "integrity": "sha512-F9UngXRlPyWCDEASDpTf6c9uNhGPTqnTeLVt7bN+bU1eajoR/8V9ys2BRaV5C/e5ihE6sJ9uPIKaYt6bFuO32g==", + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.5.tgz", + "integrity": "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==", "license": "MIT", "optionalDependencies": { "msgpackr-extract": "^3.0.2" @@ -5209,9 +5405,10 @@ } }, "node_modules/multer": { - "version": "1.4.5-lts.1", - "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", - "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", + "version": "1.4.5-lts.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", + "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", + "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.", "license": "MIT", "dependencies": { "append-field": "^1.0.0", @@ -5227,16 +5424,16 @@ } }, "node_modules/nan": { - "version": "2.22.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", - "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz", + "integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==", "license": "MIT", "optional": true }, "node_modules/nanoid": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", - "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "funding": [ { "type": "github", @@ -5255,12 +5452,13 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -5302,6 +5500,26 @@ "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", "license": "MIT" }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -5366,9 +5584,9 @@ "license": "MIT" }, "node_modules/nodemailer": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.0.tgz", - "integrity": "sha512-SQ3wZCExjeSatLE/HBaXS5vqUOQk6GtBdIIKxiFdmm01mOQZX/POJkO3SUX1wDiYcwUOJwT23scFSC9fY2H8IA==", + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz", + "integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==", "license": "MIT-0", "engines": { "node": ">=6.0.0" @@ -5403,17 +5621,6 @@ "url": "https://opencollective.com/nodemon" } }, - "node_modules/nodemon/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/nodemon/node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -5424,19 +5631,6 @@ "node": ">=4" } }, - "node_modules/nodemon/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/nodemon/node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -5487,9 +5681,9 @@ } }, "node_modules/object-inspect": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", - "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -5542,6 +5736,7 @@ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, + "license": "MIT", "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", @@ -5593,9 +5788,9 @@ "license": "BlueOak-1.0.0" }, "node_modules/papaparse": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.2.tgz", - "integrity": "sha512-PZXg8UuAc4PcVwLosEEDYjPyfWnTEhOrUfdv+3Bx+NuAb+5NhDmXzg5fHWmdCh1mP5p7JAZfFr3IMQfcntNAdA==", + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz", + "integrity": "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==", "license": "MIT" }, "node_modules/parent-module": { @@ -5629,12 +5824,12 @@ } }, "node_modules/parse5": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", - "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", "license": "MIT", "dependencies": { - "entities": "^4.5.0" + "entities": "^6.0.0" }, "funding": { "url": "https://github.com/inikulin/parse5?sponsor=1" @@ -5665,6 +5860,18 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -5716,9 +5923,9 @@ "license": "MIT" }, "node_modules/pathval": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", - "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", "dev": true, "license": "MIT", "engines": { @@ -5753,9 +5960,9 @@ } }, "node_modules/postcss": { - "version": "8.4.49", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", - "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "funding": [ { "type": "opencollective", @@ -5772,7 +5979,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -5781,12 +5988,12 @@ } }, "node_modules/postcss-calc": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-10.0.2.tgz", - "integrity": "sha512-DT/Wwm6fCKgpYVI7ZEWuPJ4az8hiEHtCUeYjZXqU7Ou4QqYh1Df2yCQ7Ca6N7xqKPFkxN3fhf+u9KSoOCJNAjg==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-10.1.1.tgz", + "integrity": "sha512-NYEsLHh8DgG/PRH2+G9BTuUdtf9ViS+vdoQ0YA5OQdGsfN4ztiwtDWNtBl9EKeqNMFnIu8IKZ0cLxEQ5r5KVMw==", "license": "MIT", "dependencies": { - "postcss-selector-parser": "^6.1.2", + "postcss-selector-parser": "^7.0.0", "postcss-value-parser": "^4.2.0" }, "engines": { @@ -5797,12 +6004,12 @@ } }, "node_modules/postcss-colormin": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-7.0.2.tgz", - "integrity": "sha512-YntRXNngcvEvDbEjTdRWGU606eZvB5prmHG4BF0yLmVpamXbpsRJzevyy6MZVyuecgzI2AWAlvFi8DAeCqwpvA==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-7.0.4.tgz", + "integrity": "sha512-ziQuVzQZBROpKpfeDwmrG+Vvlr0YWmY/ZAk99XD+mGEBuEojoFekL41NCsdhyNUtZI7DPOoIWIR7vQQK9xwluw==", "license": "MIT", "dependencies": { - "browserslist": "^4.23.3", + "browserslist": "^4.25.1", "caniuse-api": "^3.0.0", "colord": "^2.9.3", "postcss-value-parser": "^4.2.0" @@ -5811,114 +6018,114 @@ "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.4.31" + "postcss": "^8.4.32" } }, "node_modules/postcss-convert-values": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-7.0.4.tgz", - "integrity": "sha512-e2LSXPqEHVW6aoGbjV9RsSSNDO3A0rZLCBxN24zvxF25WknMPpX8Dm9UxxThyEbaytzggRuZxaGXqaOhxQ514Q==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-7.0.6.tgz", + "integrity": "sha512-MD/eb39Mr60hvgrqpXsgbiqluawYg/8K4nKsqRsuDX9f+xN1j6awZCUv/5tLH8ak3vYp/EMXwdcnXvfZYiejCQ==", "license": "MIT", "dependencies": { - "browserslist": "^4.23.3", + "browserslist": "^4.25.1", "postcss-value-parser": "^4.2.0" }, "engines": { "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.4.31" + "postcss": "^8.4.32" } }, "node_modules/postcss-discard-comments": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-7.0.3.tgz", - "integrity": "sha512-q6fjd4WU4afNhWOA2WltHgCbkRhZPgQe7cXF74fuVB/ge4QbM9HEaOIzGSiMvM+g/cOsNAUGdf2JDzqA2F8iLA==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-7.0.4.tgz", + "integrity": "sha512-6tCUoql/ipWwKtVP/xYiFf1U9QgJ0PUvxN7pTcsQ8Ns3Fnwq1pU5D5s1MhT/XySeLq6GXNvn37U46Ded0TckWg==", "license": "MIT", "dependencies": { - "postcss-selector-parser": "^6.1.2" + "postcss-selector-parser": "^7.1.0" }, "engines": { "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.4.31" + "postcss": "^8.4.32" } }, "node_modules/postcss-discard-duplicates": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-7.0.1.tgz", - "integrity": "sha512-oZA+v8Jkpu1ct/xbbrntHRsfLGuzoP+cpt0nJe5ED2FQF8n8bJtn7Bo28jSmBYwqgqnqkuSXJfSUEE7if4nClQ==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-7.0.2.tgz", + "integrity": "sha512-eTonaQvPZ/3i1ASDHOKkYwAybiM45zFIc7KXils4mQmHLqIswXD9XNOKEVxtTFnsmwYzF66u4LMgSr0abDlh5w==", "license": "MIT", "engines": { "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.4.31" + "postcss": "^8.4.32" } }, "node_modules/postcss-discard-empty": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-7.0.0.tgz", - "integrity": "sha512-e+QzoReTZ8IAwhnSdp/++7gBZ/F+nBq9y6PomfwORfP7q9nBpK5AMP64kOt0bA+lShBFbBDcgpJ3X4etHg4lzA==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-7.0.1.tgz", + "integrity": "sha512-cFrJKZvcg/uxB6Ijr4l6qmn3pXQBna9zyrPC+sK0zjbkDUZew+6xDltSF7OeB7rAtzaaMVYSdbod+sZOCWnMOg==", "license": "MIT", "engines": { "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.4.31" + "postcss": "^8.4.32" } }, "node_modules/postcss-discard-overridden": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-7.0.0.tgz", - "integrity": "sha512-GmNAzx88u3k2+sBTZrJSDauR0ccpE24omTQCVmaTTZFz1du6AasspjaUPMJ2ud4RslZpoFKyf+6MSPETLojc6w==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-7.0.1.tgz", + "integrity": "sha512-7c3MMjjSZ/qYrx3uc1940GSOzN1Iqjtlqe8uoSg+qdVPYyRb0TILSqqmtlSFuE4mTDECwsm397Ya7iXGzfF7lg==", "license": "MIT", "engines": { "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.4.31" + "postcss": "^8.4.32" } }, "node_modules/postcss-merge-longhand": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-7.0.4.tgz", - "integrity": "sha512-zer1KoZA54Q8RVHKOY5vMke0cCdNxMP3KBfDerjH/BYHh4nCIh+1Yy0t1pAEQF18ac/4z3OFclO+ZVH8azjR4A==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-7.0.5.tgz", + "integrity": "sha512-Kpu5v4Ys6QI59FxmxtNB/iHUVDn9Y9sYw66D6+SZoIk4QTz1prC4aYkhIESu+ieG1iylod1f8MILMs1Em3mmIw==", "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0", - "stylehacks": "^7.0.4" + "stylehacks": "^7.0.5" }, "engines": { "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.4.31" + "postcss": "^8.4.32" } }, "node_modules/postcss-merge-rules": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-7.0.4.tgz", - "integrity": "sha512-ZsaamiMVu7uBYsIdGtKJ64PkcQt6Pcpep/uO90EpLS3dxJi6OXamIobTYcImyXGoW0Wpugh7DSD3XzxZS9JCPg==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-7.0.6.tgz", + "integrity": "sha512-2jIPT4Tzs8K87tvgCpSukRQ2jjd+hH6Bb8rEEOUDmmhOeTcqDg5fEFK8uKIu+Pvc3//sm3Uu6FRqfyv7YF7+BQ==", "license": "MIT", "dependencies": { - "browserslist": "^4.23.3", + "browserslist": "^4.25.1", "caniuse-api": "^3.0.0", - "cssnano-utils": "^5.0.0", - "postcss-selector-parser": "^6.1.2" + "cssnano-utils": "^5.0.1", + "postcss-selector-parser": "^7.1.0" }, "engines": { "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.4.31" + "postcss": "^8.4.32" } }, "node_modules/postcss-minify-font-values": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-7.0.0.tgz", - "integrity": "sha512-2ckkZtgT0zG8SMc5aoNwtm5234eUx1GGFJKf2b1bSp8UflqaeFzR50lid4PfqVI9NtGqJ2J4Y7fwvnP/u1cQog==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-7.0.1.tgz", + "integrity": "sha512-2m1uiuJeTplll+tq4ENOQSzB8LRnSUChBv7oSyFLsJRtUgAAJGP6LLz0/8lkinTgxrmJSPOEhgY1bMXOQ4ZXhQ==", "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0" @@ -5927,75 +6134,75 @@ "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.4.31" + "postcss": "^8.4.32" } }, "node_modules/postcss-minify-gradients": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-7.0.0.tgz", - "integrity": "sha512-pdUIIdj/C93ryCHew0UgBnL2DtUS3hfFa5XtERrs4x+hmpMYGhbzo6l/Ir5de41O0GaKVpK1ZbDNXSY6GkXvtg==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-7.0.1.tgz", + "integrity": "sha512-X9JjaysZJwlqNkJbUDgOclyG3jZEpAMOfof6PUZjPnPrePnPG62pS17CjdM32uT1Uq1jFvNSff9l7kNbmMSL2A==", "license": "MIT", "dependencies": { "colord": "^2.9.3", - "cssnano-utils": "^5.0.0", + "cssnano-utils": "^5.0.1", "postcss-value-parser": "^4.2.0" }, "engines": { "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.4.31" + "postcss": "^8.4.32" } }, "node_modules/postcss-minify-params": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-7.0.2.tgz", - "integrity": "sha512-nyqVLu4MFl9df32zTsdcLqCFfE/z2+f8GE1KHPxWOAmegSo6lpV2GNy5XQvrzwbLmiU7d+fYay4cwto1oNdAaQ==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-7.0.4.tgz", + "integrity": "sha512-3OqqUddfH8c2e7M35W6zIwv7jssM/3miF9cbCSb1iJiWvtguQjlxZGIHK9JRmc8XAKmE2PFGtHSM7g/VcW97sw==", "license": "MIT", "dependencies": { - "browserslist": "^4.23.3", - "cssnano-utils": "^5.0.0", + "browserslist": "^4.25.1", + "cssnano-utils": "^5.0.1", "postcss-value-parser": "^4.2.0" }, "engines": { "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.4.31" + "postcss": "^8.4.32" } }, "node_modules/postcss-minify-selectors": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-7.0.4.tgz", - "integrity": "sha512-JG55VADcNb4xFCf75hXkzc1rNeURhlo7ugf6JjiiKRfMsKlDzN9CXHZDyiG6x/zGchpjQS+UAgb1d4nqXqOpmA==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-7.0.5.tgz", + "integrity": "sha512-x2/IvofHcdIrAm9Q+p06ZD1h6FPcQ32WtCRVodJLDR+WMn8EVHI1kvLxZuGKz/9EY5nAmI6lIQIrpo4tBy5+ug==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", - "postcss-selector-parser": "^6.1.2" + "postcss-selector-parser": "^7.1.0" }, "engines": { "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.4.31" + "postcss": "^8.4.32" } }, "node_modules/postcss-normalize-charset": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-7.0.0.tgz", - "integrity": "sha512-ABisNUXMeZeDNzCQxPxBCkXexvBrUHV+p7/BXOY+ulxkcjUZO0cp8ekGBwvIh2LbCwnWbyMPNJVtBSdyhM2zYQ==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-7.0.1.tgz", + "integrity": "sha512-sn413ofhSQHlZFae//m9FTOfkmiZ+YQXsbosqOWRiVQncU2BA3daX3n0VF3cG6rGLSFVc5Di/yns0dFfh8NFgQ==", "license": "MIT", "engines": { "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.4.31" + "postcss": "^8.4.32" } }, "node_modules/postcss-normalize-display-values": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-7.0.0.tgz", - "integrity": "sha512-lnFZzNPeDf5uGMPYgGOw7v0BfB45+irSRz9gHQStdkkhiM0gTfvWkWB5BMxpn0OqgOQuZG/mRlZyJxp0EImr2Q==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-7.0.1.tgz", + "integrity": "sha512-E5nnB26XjSYz/mGITm6JgiDpAbVuAkzXwLzRZtts19jHDUBFxZ0BkXAehy0uimrOjYJbocby4FVswA/5noOxrQ==", "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0" @@ -6004,13 +6211,13 @@ "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.4.31" + "postcss": "^8.4.32" } }, "node_modules/postcss-normalize-positions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-7.0.0.tgz", - "integrity": "sha512-I0yt8wX529UKIGs2y/9Ybs2CelSvItfmvg/DBIjTnoUSrPxSV7Z0yZ8ShSVtKNaV/wAY+m7bgtyVQLhB00A1NQ==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-7.0.1.tgz", + "integrity": "sha512-pB/SzrIP2l50ZIYu+yQZyMNmnAcwyYb9R1fVWPRxm4zcUFCY2ign7rcntGFuMXDdd9L2pPNUgoODDk91PzRZuQ==", "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0" @@ -6019,13 +6226,13 @@ "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.4.31" + "postcss": "^8.4.32" } }, "node_modules/postcss-normalize-repeat-style": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-7.0.0.tgz", - "integrity": "sha512-o3uSGYH+2q30ieM3ppu9GTjSXIzOrRdCUn8UOMGNw7Af61bmurHTWI87hRybrP6xDHvOe5WlAj3XzN6vEO8jLw==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-7.0.1.tgz", + "integrity": "sha512-NsSQJ8zj8TIDiF0ig44Byo3Jk9e4gNt9x2VIlJudnQQ5DhWAHJPF4Tr1ITwyHio2BUi/I6Iv0HRO7beHYOloYQ==", "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0" @@ -6034,13 +6241,13 @@ "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.4.31" + "postcss": "^8.4.32" } }, "node_modules/postcss-normalize-string": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-7.0.0.tgz", - "integrity": "sha512-w/qzL212DFVOpMy3UGyxrND+Kb0fvCiBBujiaONIihq7VvtC7bswjWgKQU/w4VcRyDD8gpfqUiBQ4DUOwEJ6Qg==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-7.0.1.tgz", + "integrity": "sha512-QByrI7hAhsoze992kpbMlJSbZ8FuCEc1OT9EFbZ6HldXNpsdpZr+YXC5di3UEv0+jeZlHbZcoCADgb7a+lPmmQ==", "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0" @@ -6049,13 +6256,13 @@ "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.4.31" + "postcss": "^8.4.32" } }, "node_modules/postcss-normalize-timing-functions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-7.0.0.tgz", - "integrity": "sha512-tNgw3YV0LYoRwg43N3lTe3AEWZ66W7Dh7lVEpJbHoKOuHc1sLrzMLMFjP8SNULHaykzsonUEDbKedv8C+7ej6g==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-7.0.1.tgz", + "integrity": "sha512-bHifyuuSNdKKsnNJ0s8fmfLMlvsQwYVxIoUBnowIVl2ZAdrkYQNGVB4RxjfpvkMjipqvbz0u7feBZybkl/6NJg==", "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0" @@ -6064,29 +6271,29 @@ "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.4.31" + "postcss": "^8.4.32" } }, "node_modules/postcss-normalize-unicode": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-7.0.2.tgz", - "integrity": "sha512-ztisabK5C/+ZWBdYC+Y9JCkp3M9qBv/XFvDtSw0d/XwfT3UaKeW/YTm/MD/QrPNxuecia46vkfEhewjwcYFjkg==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-7.0.4.tgz", + "integrity": "sha512-LvIURTi1sQoZqj8mEIE8R15yvM+OhbR1avynMtI9bUzj5gGKR/gfZFd8O7VMj0QgJaIFzxDwxGl/ASMYAkqO8g==", "license": "MIT", "dependencies": { - "browserslist": "^4.23.3", + "browserslist": "^4.25.1", "postcss-value-parser": "^4.2.0" }, "engines": { "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.4.31" + "postcss": "^8.4.32" } }, "node_modules/postcss-normalize-url": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-7.0.0.tgz", - "integrity": "sha512-+d7+PpE+jyPX1hDQZYG+NaFD+Nd2ris6r8fPTBAjE8z/U41n/bib3vze8x7rKs5H1uEw5ppe9IojewouHk0klQ==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-7.0.1.tgz", + "integrity": "sha512-sUcD2cWtyK1AOL/82Fwy1aIVm/wwj5SdZkgZ3QiUzSzQQofrbq15jWJ3BA7Z+yVRwamCjJgZJN0I9IS7c6tgeQ==", "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0" @@ -6095,13 +6302,13 @@ "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.4.31" + "postcss": "^8.4.32" } }, "node_modules/postcss-normalize-whitespace": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-7.0.0.tgz", - "integrity": "sha512-37/toN4wwZErqohedXYqWgvcHUGlT8O/m2jVkAfAe9Bd4MzRqlBmXrJRePH0e9Wgnz2X7KymTgTOaaFizQe3AQ==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-7.0.1.tgz", + "integrity": "sha512-vsbgFHMFQrJBJKrUFJNZ2pgBeBkC2IvvoHjz1to0/0Xk7sII24T0qFOiJzG6Fu3zJoq/0yI4rKWi7WhApW+EFA==", "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0" @@ -6110,45 +6317,45 @@ "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.4.31" + "postcss": "^8.4.32" } }, "node_modules/postcss-ordered-values": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-7.0.1.tgz", - "integrity": "sha512-irWScWRL6nRzYmBOXReIKch75RRhNS86UPUAxXdmW/l0FcAsg0lvAXQCby/1lymxn/o0gVa6Rv/0f03eJOwHxw==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-7.0.2.tgz", + "integrity": "sha512-AMJjt1ECBffF7CEON/Y0rekRLS6KsePU6PRP08UqYW4UGFRnTXNrByUzYK1h8AC7UWTZdQ9O3Oq9kFIhm0SFEw==", "license": "MIT", "dependencies": { - "cssnano-utils": "^5.0.0", + "cssnano-utils": "^5.0.1", "postcss-value-parser": "^4.2.0" }, "engines": { "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.4.31" + "postcss": "^8.4.32" } }, "node_modules/postcss-reduce-initial": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-7.0.2.tgz", - "integrity": "sha512-pOnu9zqQww7dEKf62Nuju6JgsW2V0KRNBHxeKohU+JkHd/GAH5uvoObqFLqkeB2n20mr6yrlWDvo5UBU5GnkfA==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-7.0.4.tgz", + "integrity": "sha512-rdIC9IlMBn7zJo6puim58Xd++0HdbvHeHaPgXsimMfG1ijC5A9ULvNLSE0rUKVJOvNMcwewW4Ga21ngyJjY/+Q==", "license": "MIT", "dependencies": { - "browserslist": "^4.23.3", + "browserslist": "^4.25.1", "caniuse-api": "^3.0.0" }, "engines": { "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.4.31" + "postcss": "^8.4.32" } }, "node_modules/postcss-reduce-transforms": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-7.0.0.tgz", - "integrity": "sha512-pnt1HKKZ07/idH8cpATX/ujMbtOGhUfE+m8gbqwJE05aTaNw8gbo34a2e3if0xc0dlu75sUOiqvwCGY3fzOHew==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-7.0.1.tgz", + "integrity": "sha512-MhyEbfrm+Mlp/36hvZ9mT9DaO7dbncU0CvWI8V93LRkY6IYlu38OPg3FObnuKTUxJ4qA8HpurdQOo5CyqqO76g==", "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0" @@ -6157,13 +6364,13 @@ "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.4.31" + "postcss": "^8.4.32" } }, "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -6174,34 +6381,87 @@ } }, "node_modules/postcss-svgo": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-7.0.1.tgz", - "integrity": "sha512-0WBUlSL4lhD9rA5k1e5D8EN5wCEyZD6HJk0jIvRxl+FDVOMlJ7DePHYWGGVc5QRqrJ3/06FTXM0bxjmJpmTPSA==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-7.1.0.tgz", + "integrity": "sha512-KnAlfmhtoLz6IuU3Sij2ycusNs4jPW+QoFE5kuuUOK8awR6tMxZQrs5Ey3BUz7nFCzT3eqyFgqkyrHiaU2xx3w==", "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0", - "svgo": "^3.3.2" + "svgo": "^4.0.0" }, "engines": { "node": "^18.12.0 || ^20.9.0 || >= 18" }, "peerDependencies": { - "postcss": "^8.4.31" + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-svgo/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/postcss-svgo/node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/postcss-svgo/node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "license": "CC0-1.0" + }, + "node_modules/postcss-svgo/node_modules/svgo": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.0.tgz", + "integrity": "sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw==", + "license": "MIT", + "dependencies": { + "commander": "^11.1.0", + "css-select": "^5.1.0", + "css-tree": "^3.0.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.1.1", + "sax": "^1.4.1" + }, + "bin": { + "svgo": "bin/svgo.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" } }, "node_modules/postcss-unique-selectors": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-7.0.3.tgz", - "integrity": "sha512-J+58u5Ic5T1QjP/LDV9g3Cx4CNOgB5vz+kM6+OxHHhFACdcDeKhBXjQmB7fnIZM12YSTvsL0Opwco83DmacW2g==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-7.0.4.tgz", + "integrity": "sha512-pmlZjsmEAG7cHd7uK3ZiNSW6otSZ13RHuZ/4cDN/bVglS5EpF2r2oxY99SuOHa8m7AWoBCelTS3JPpzsIs8skQ==", "license": "MIT", "dependencies": { - "postcss-selector-parser": "^6.1.2" + "postcss-selector-parser": "^7.1.0" }, "engines": { "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.4.31" + "postcss": "^8.4.32" } }, "node_modules/postcss-value-parser": { @@ -6335,14 +6595,15 @@ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8.0" } }, "node_modules/prettier": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.1.tgz", - "integrity": "sha512-hPpFQvHwL3Qv5AdRvBFMhnKo4tYxp0ReXiPn2bxkiohEX6mBeBwEpBSQTkD458RaaDKQMYSp4hX4UtfUTA5wDw==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" @@ -6361,9 +6622,9 @@ "license": "MIT" }, "node_modules/protobufjs": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", - "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.3.tgz", + "integrity": "sha512-sildjKwVqOI2kmFDiXQ6aEB0fjYTafpEvIBs8tOR8qI4spuL9OPROLVu2qZqi/xgCfsHIwVqlaF8JBjWFHnKbw==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { @@ -6411,9 +6672,9 @@ "license": "MIT" }, "node_modules/pump": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", - "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", @@ -6448,7 +6709,8 @@ "version": "7.5.0", "resolved": "https://registry.npmjs.org/rambda/-/rambda-7.5.0.tgz", "integrity": "sha512-y/M9weqWAH4iopRd7EHDEQQvpFPHj1AA3oHozE9tfITHUtTR7Z9PSlIRRG2l1GuW7sefC1cXFfIcF+cgnShdBA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/randombytes": { "version": "2.1.0", @@ -6531,12 +6793,6 @@ "node": ">=4" } }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "license": "MIT" - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -6590,10 +6846,16 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "license": "ISC" + }, "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -7104,19 +7366,113 @@ } }, "node_modules/stylehacks": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-7.0.4.tgz", - "integrity": "sha512-i4zfNrGMt9SB4xRK9L83rlsFCgdGANfeDAYacO1pkqcE7cRHPdWHwnKZVz7WY17Veq/FvyYsRAU++Ga+qDFIww==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-7.0.6.tgz", + "integrity": "sha512-iitguKivmsueOmTO0wmxURXBP8uqOO+zikLGZ7Mm9e/94R4w5T999Js2taS/KBOnQ/wdC3jN3vNSrkGDrlnqQg==", "license": "MIT", "dependencies": { - "browserslist": "^4.23.3", - "postcss-selector-parser": "^6.1.2" + "browserslist": "^4.25.1", + "postcss-selector-parser": "^7.1.0" }, "engines": { "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.4.31" + "postcss": "^8.4.32" + } + }, + "node_modules/super-simple-scheduler": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/super-simple-scheduler/-/super-simple-scheduler-1.3.0.tgz", + "integrity": "sha512-iEGZ+a9Xv7pIaA+XhVuHSGaasQg2T6/afTRlQhosQiiU/7ykpZBsiZB/L2Hqfem5YUrbr3Q8tzrjjXCXKMg81A==", + "license": "MIT", + "dependencies": { + "human-interval": "2.0.1", + "ioredis": "5.6.1", + "mongoose": "8.16.1", + "uuid": "11.1.0", + "winston": "3.17.0" + } + }, + "node_modules/super-simple-scheduler/node_modules/mongodb": { + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.17.0.tgz", + "integrity": "sha512-neerUzg/8U26cgruLysKEjJvoNSXhyID3RvzvdcpsIi2COYM3FS3o9nlH7fxFtefTb942dX3W9i37oPfCVj4wA==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.1.9", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.0" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/super-simple-scheduler/node_modules/mongoose": { + "version": "8.16.1", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.16.1.tgz", + "integrity": "sha512-Q+0TC+KLdY4SYE+u9gk9pdW1tWu/pl0jusyEkMGTgBoAbvwQdfy4f9IM8dmvCwb/blSfp7IfLkob7v76x6ZGpQ==", + "license": "MIT", + "dependencies": { + "bson": "^6.10.4", + "kareem": "2.6.3", + "mongodb": "~6.17.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=16.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/super-simple-scheduler/node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" } }, "node_modules/supports-color": { @@ -7137,6 +7493,8 @@ "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@trysound/sax": "0.2.0", "commander": "^7.2.0", @@ -7162,14 +7520,16 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">= 10" } }, "node_modules/swagger-ui-dist": { - "version": "5.18.2", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.18.2.tgz", - "integrity": "sha512-J+y4mCw/zXh1FOj5wGJvnAajq6XgHOyywsa9yITmwxIlJbMqITq3gYRZHaeqLVH/eV/HOPphE6NjF+nbSNC5Zw==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.27.0.tgz", + "integrity": "sha512-tS6LRyBhY6yAqxrfsA9IYpGWPUJOri6sclySa7TdC7XQfGLvTwDY531KLgfQwHEtQsn+sT4JlUspbeQDBVGWig==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "=1.4.0" @@ -7202,12 +7562,6 @@ "tar-stream": "^2.1.4" } }, - "node_modules/tar-fs/node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC" - }, "node_modules/tar-stream": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", @@ -7239,18 +7593,38 @@ "node": ">=18" } }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", "license": "MIT" }, - "node_modules/timsort": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", - "integrity": "sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==", - "license": "MIT" - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -7283,15 +7657,15 @@ } }, "node_modules/tr46": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", - "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", "license": "MIT", "dependencies": { - "punycode": "^2.3.0" + "punycode": "^2.3.1" }, "engines": { - "node": ">=14" + "node": ">=18" } }, "node_modules/triple-beam": { @@ -7320,6 +7694,7 @@ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1" }, @@ -7342,6 +7717,7 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -7389,25 +7765,28 @@ "license": "MIT" }, "node_modules/undici": { - "version": "6.21.3", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", - "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.12.0.tgz", + "integrity": "sha512-GrKEsc3ughskmGA9jevVlIOPMiiAHJ4OFUtaAH+NhfTUSiZ1wMPIQqQvAJUrJspFXJt3EBWgpAeoHEDVT1IBug==", "license": "MIT", "engines": { - "node": ">=18.17" + "node": ">=20.18.1" } }, "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", "license": "MIT" }, "node_modules/unfetch": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz", - "integrity": "sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==", - "license": "MIT" + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-5.0.0.tgz", + "integrity": "sha512-3xM2c89siXg0nHvlmYsQ2zkLASvVMBisZm5lF3gFDqfF2xonNStDJyMpvaOBe0a1Edxmqrf2E0HBdmy9QyZaeg==", + "license": "MIT", + "workspaces": [ + "./packages/isomorphic-unfetch" + ] }, "node_modules/unpipe": { "version": "1.0.0", @@ -7419,9 +7798,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", - "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", "funding": [ { "type": "opencollective", @@ -7439,7 +7818,7 @@ "license": "MIT", "dependencies": { "escalade": "^3.2.0", - "picocolors": "^1.1.0" + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -7453,6 +7832,7 @@ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } @@ -7643,6 +8023,15 @@ "node": ">=4.0.0" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -7686,16 +8075,16 @@ } }, "node_modules/whatwg-url": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", - "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", "license": "MIT", "dependencies": { - "tr46": "^4.1.1", + "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/which": { @@ -7754,6 +8143,7 @@ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } diff --git a/server/package.json b/server/package.json index 51e7cd218..62a91921b 100755 --- a/server/package.json +++ b/server/package.json @@ -40,6 +40,7 @@ "ping": "0.4.4", "sharp": "0.33.5", "ssl-checker": "2.0.10", + "super-simple-scheduler": "1.3.0", "swagger-ui-express": "5.0.1", "winston": "^3.13.0" }, diff --git a/server/routes/announcementsRoute.js b/server/routes/announcementsRoute.js index 0b234cb96..011dfac4a 100755 --- a/server/routes/announcementsRoute.js +++ b/server/routes/announcementsRoute.js @@ -15,12 +15,7 @@ class AnnouncementRoutes { * @desc Create a new announcement * @access Private (Requires JWT verification) */ - this.router.post( - "/", - verifyJWT, - isAllowed(["admin", "superadmin"]), - this.announcementController.createAnnouncement - ); + this.router.post("/", verifyJWT, isAllowed(["admin", "superadmin"]), this.announcementController.createAnnouncement); /** * @route GET / diff --git a/server/routes/authRoute.js b/server/routes/authRoute.js index aabd8de89..e790c60b0 100755 --- a/server/routes/authRoute.js +++ b/server/routes/authRoute.js @@ -15,40 +15,21 @@ class AuthRoutes { } initRoutes() { - this.router.post( - "/register", - upload.single("profileImage"), - this.authController.registerUser - ); + this.router.post("/register", upload.single("profileImage"), this.authController.registerUser); this.router.post("/login", this.authController.loginUser); - this.router.put( - "/user/:userId", - upload.single("profileImage"), - verifyJWT, - this.authController.editUser - ); - - this.router.get("/users/superadmin", this.authController.checkSuperadminExists); - - this.router.get( - "/users", - verifyJWT, - isAllowed(["admin", "superadmin"]), - this.authController.getAllUsers - ); - - this.router.delete( - "/user/:userId", - verifyJWT, - verifyOwnership(User, "userId"), - this.authController.deleteUser - ); - - // Recovery routes this.router.post("/recovery/request", this.authController.requestRecovery); this.router.post("/recovery/validate", this.authController.validateRecovery); this.router.post("/recovery/reset/", this.authController.resetPassword); + + this.router.get("/users/superadmin", this.authController.checkSuperadminExists); + + this.router.get("/users", verifyJWT, isAllowed(["admin", "superadmin"]), this.authController.getAllUsers); + this.router.get("/users/:userId", verifyJWT, isAllowed(["superadmin"]), this.authController.getUserById); + this.router.put("/users/:userId", verifyJWT, isAllowed(["superadmin"]), this.authController.editUserById); + + this.router.put("/user", verifyJWT, upload.single("profileImage"), this.authController.editUser); + this.router.delete("/user", verifyJWT, this.authController.deleteUser); } getRouter() { diff --git a/server/routes/checkRoute.js b/server/routes/checkRoute.js index f215cbe37..8bdb2d4fb 100755 --- a/server/routes/checkRoute.js +++ b/server/routes/checkRoute.js @@ -13,35 +13,16 @@ class CheckRoutes { } initRoutes() { - this.router.get("/team", this.checkController.getChecksByTeam); this.router.get("/team/summary", this.checkController.getChecksSummaryByTeamId); - this.router.delete( - "/team", - isAllowed(["admin", "superadmin"]), - this.checkController.deleteChecksByTeamId - ); - this.router.put( - "/check/:checkId", - verifyTeamAccess(Check, "checkId"), - this.checkController.ackCheck - ); - this.router.put( - "/team/ttl", - isAllowed(["admin", "superadmin"]), - this.checkController.updateChecksTTL - ); + this.router.get("/team", this.checkController.getChecksByTeam); + this.router.put("/team/ttl", isAllowed(["admin", "superadmin"]), this.checkController.updateChecksTTL); + this.router.delete("/team", isAllowed(["admin", "superadmin"]), this.checkController.deleteChecksByTeamId); + + this.router.put("/check/:checkId", this.checkController.ackCheck); this.router.get("/:monitorId", this.checkController.getChecksByMonitor); - this.router.post( - "/:monitorId", - verifyOwnership(Monitor, "monitorId"), - this.checkController.createCheck - ); - this.router.delete( - "/:monitorId", - verifyOwnership(Monitor, "monitorId"), - this.checkController.deleteChecks - ); + this.router.delete("/:monitorId", this.checkController.deleteChecks); + this.router.put("/:path/:monitorId?", this.checkController.ackAllChecks); } diff --git a/server/routes/diagnosticRoute.js b/server/routes/diagnosticRoute.js index 616d01ccf..a15f5a038 100755 --- a/server/routes/diagnosticRoute.js +++ b/server/routes/diagnosticRoute.js @@ -1,4 +1,6 @@ import { Router } from "express"; +import { verifyJWT } from "../middleware/verifyJWT.js"; +import { isAllowed } from "../middleware/isAllowed.js"; class DiagnosticRoutes { constructor(diagnosticController) { @@ -7,12 +9,7 @@ class DiagnosticRoutes { this.initRoutes(); } initRoutes() { - this.router.get( - "/db/get-monitors-by-team-id/:teamId", - this.diagnosticController.getMonitorsByTeamIdExecutionStats - ); - - this.router.post("/db/stats", this.diagnosticController.getDbStats); + this.router.get("/system", verifyJWT, isAllowed(["admin", "superadmin"]), this.diagnosticController.getSystemStats); } getRouter() { diff --git a/server/routes/inviteRoute.js b/server/routes/inviteRoute.js index 301959389..d495c5476 100755 --- a/server/routes/inviteRoute.js +++ b/server/routes/inviteRoute.js @@ -10,15 +10,9 @@ class InviteRoutes { } initRoutes() { - this.router.post( - "/", - isAllowed(["admin", "superadmin"]), - verifyJWT, - this.inviteController.getInviteToken - ); - - this.router.post("/send", this.inviteController.sendInviteEmail); - this.router.post("/verify", this.inviteController.inviteVerifyController); + this.router.post("/send", verifyJWT, isAllowed(["admin", "superadmin"]), this.inviteController.sendInviteEmail); + this.router.post("/verify", this.inviteController.verifyInviteToken); + this.router.post("/", verifyJWT, isAllowed(["admin", "superadmin"]), this.inviteController.getInviteToken); } getRouter() { diff --git a/server/routes/maintenanceWindowRoute.js b/server/routes/maintenanceWindowRoute.js index 36f982e25..774434ed6 100755 --- a/server/routes/maintenanceWindowRoute.js +++ b/server/routes/maintenanceWindowRoute.js @@ -1,41 +1,20 @@ import { Router } from "express"; -import { verifyOwnership } from "../middleware/verifyOwnership.js"; -import { verifyTeamAccess } from "../middleware/verifyTeamAccess.js"; -import Monitor from "../db/models/Monitor.js"; import MaintenanceWindow from "../db/models/MaintenanceWindow.js"; class MaintenanceWindowRoutes { constructor(maintenanceWindowController) { this.router = Router(); - this.maintenanceWindowController = maintenanceWindowController; + this.mwController = maintenanceWindowController; this.initRoutes(); } initRoutes() { - this.router.post("/", this.maintenanceWindowController.createMaintenanceWindows); + this.router.post("/", this.mwController.createMaintenanceWindows); + this.router.get("/team/", this.mwController.getMaintenanceWindowsByTeamId); - this.router.get( - "/monitor/:monitorId", - verifyOwnership(Monitor, "monitorId"), - this.maintenanceWindowController.getMaintenanceWindowsByMonitorId - ); + this.router.get("/monitor/:monitorId", this.mwController.getMaintenanceWindowsByMonitorId); - this.router.get( - "/team/", - this.maintenanceWindowController.getMaintenanceWindowsByTeamId - ); - - this.router.get("/:id", this.maintenanceWindowController.getMaintenanceWindowById); - - this.router.put( - "/:id", - verifyTeamAccess(MaintenanceWindow, "id"), - this.maintenanceWindowController.editMaintenanceWindow - ); - - this.router.delete( - "/:id", - verifyTeamAccess(MaintenanceWindow, "id"), - this.maintenanceWindowController.deleteMaintenanceWindow - ); + this.router.get("/:id", this.mwController.getMaintenanceWindowById); + this.router.put("/:id", this.mwController.editMaintenanceWindow); + this.router.delete("/:id", this.mwController.deleteMaintenanceWindow); } getRouter() { diff --git a/server/routes/monitorRoute.js b/server/routes/monitorRoute.js index 168f8b88d..0124c209d 100755 --- a/server/routes/monitorRoute.js +++ b/server/routes/monitorRoute.js @@ -2,9 +2,6 @@ import { Router } from "express"; import { isAllowed } from "../middleware/isAllowed.js"; import multer from "multer"; import { fetchMonitorCertificate } from "../controllers/controllerUtils.js"; -import Monitor from "../db/models/Monitor.js"; -import { verifyOwnership } from "../middleware/verifyOwnership.js"; -import { verifyTeamAccess } from "../middleware/verifyTeamAccess.js"; const upload = multer({ storage: multer.memoryStorage(), // Store file in memory as Buffer }); @@ -17,103 +14,41 @@ class MonitorRoutes { } initRoutes() { - this.router.get("/", this.monitorController.getAllMonitors); - this.router.get("/uptime", this.monitorController.getAllMonitorsWithUptimeStats); - this.router.get( - "/export", - isAllowed(["admin", "superadmin"]), - this.monitorController.exportMonitorsToCSV - ); - this.router.get("/stats/:monitorId", this.monitorController.getMonitorStatsById); - this.router.get( - "/hardware/details/:monitorId", - this.monitorController.getHardwareDetailsById - ); - - this.router.get( - "/uptime/details/:monitorId", - this.monitorController.getUptimeDetailsById - ); - this.router.get("/certificate/:monitorId", (req, res, next) => { - this.monitorController.getMonitorCertificate( - req, - res, - next, - fetchMonitorCertificate - ); - }); + // Team routes this.router.get("/team", this.monitorController.getMonitorsByTeamId); + this.router.get("/team/with-checks", this.monitorController.getMonitorsWithChecksByTeamId); + this.router.get("/team/summary", this.monitorController.getMonitorsAndSummaryByTeamId); + // Uptime routes + this.router.get("/uptime/details/:monitorId", this.monitorController.getUptimeDetailsById); + + // Hardware routes + this.router.get("/hardware/details/:monitorId", this.monitorController.getHardwareDetailsById); + + // General monitor routes + this.router.post("/pause/:monitorId", isAllowed(["admin", "superadmin"]), this.monitorController.pauseMonitor); + this.router.get("/stats/:monitorId", this.monitorController.getMonitorStatsById); + + // Util routes + this.router.get("/certificate/:monitorId", (req, res, next) => { + this.monitorController.getMonitorCertificate(req, res, next, fetchMonitorCertificate); + }); + + // General monitor CRUD routes + this.router.get("/", this.monitorController.getAllMonitors); + this.router.post("/", isAllowed(["admin", "superadmin"]), this.monitorController.createMonitor); + this.router.delete("/", isAllowed(["superadmin"]), this.monitorController.deleteAllMonitors); + + // Other static routes + this.router.post("/demo", isAllowed(["admin", "superadmin"]), this.monitorController.addDemoMonitors); + this.router.get("/export", isAllowed(["admin", "superadmin"]), this.monitorController.exportMonitorsToCSV); + this.router.post("/bulk", isAllowed(["admin", "superadmin"]), upload.single("csvFile"), this.monitorController.createBulkMonitors); + this.router.post("/test-email", isAllowed(["admin", "superadmin"]), this.monitorController.sendTestEmail); + + // Individual monitor CRUD routes this.router.get("/:monitorId", this.monitorController.getMonitorById); - - this.router.get( - "/summary/team", - this.monitorController.getMonitorsAndSummaryByTeamId - ); - - this.router.get( - "/team/with-checks", - this.monitorController.getMonitorsWithChecksByTeamId - ); - - this.router.get( - "/resolution/url", - isAllowed(["admin", "superadmin"]), - this.monitorController.checkEndpointResolution - ); - - this.router.delete( - "/:monitorId", - verifyOwnership(Monitor, "monitorId"), - isAllowed(["admin", "superadmin"]), - this.monitorController.deleteMonitor - ); - - this.router.post( - "/", - isAllowed(["admin", "superadmin"]), - this.monitorController.createMonitor - ); - - this.router.put( - "/:monitorId", - verifyTeamAccess(Monitor, "monitorId"), - isAllowed(["admin", "superadmin"]), - this.monitorController.editMonitor - ); - - this.router.delete( - "/", - isAllowed(["superadmin"]), - this.monitorController.deleteAllMonitors - ); - - this.router.post( - "/pause/:monitorId", - isAllowed(["admin", "superadmin"]), - this.monitorController.pauseMonitor - ); - - this.router.post( - "/demo", - isAllowed(["admin", "superadmin"]), - this.monitorController.addDemoMonitors - ); - - this.router.post( - "/bulk", - isAllowed(["admin", "superadmin"]), - upload.single("csvFile"), - this.monitorController.createBulkMonitors - ); - - this.router.post("/seed", isAllowed(["superadmin"]), this.monitorController.seedDb); - - this.router.post( - "/test-email", - isAllowed(["admin", "superadmin"]), - this.monitorController.sendTestEmail - ); + this.router.put("/:monitorId", isAllowed(["admin", "superadmin"]), this.monitorController.editMonitor); + this.router.delete("/:monitorId", isAllowed(["admin", "superadmin"]), this.monitorController.deleteMonitor); } getRouter() { diff --git a/server/routes/notificationRoute.js b/server/routes/notificationRoute.js index 09becd16c..f88b0b96c 100755 --- a/server/routes/notificationRoute.js +++ b/server/routes/notificationRoute.js @@ -1,8 +1,4 @@ import { Router } from "express"; -import { verifyJWT } from "../middleware/verifyJWT.js"; -import { verifyOwnership } from "../middleware/verifyOwnership.js"; -import { verifyTeamAccess } from "../middleware/verifyTeamAccess.js"; -import Notification from "../db/models/Notification.js"; class NotificationRoutes { constructor(notificationController) { this.router = Router(); @@ -11,27 +7,16 @@ class NotificationRoutes { } initializeRoutes() { - this.router.use(verifyJWT); - - this.router.post("/test", this.notificationController.testNotification); - this.router.post("/test/all", this.notificationController.testAllNotifications); - this.router.post("/", this.notificationController.createNotification); + this.router.post("/test/all", this.notificationController.testAllNotifications); + this.router.post("/test", this.notificationController.testNotification); + this.router.get("/team", this.notificationController.getNotificationsByTeamId); - this.router.delete( - "/:id", - verifyOwnership(Notification, "id"), - this.notificationController.deleteNotification - ); - this.router.get("/:id", this.notificationController.getNotificationById); - this.router.put( - "/:id", - verifyTeamAccess(Notification, "id"), - this.notificationController.editNotification - ); + this.router.delete("/:id", this.notificationController.deleteNotification); + this.router.put("/:id", this.notificationController.editNotification); } getRouter() { diff --git a/server/routes/queueRoute.js b/server/routes/queueRoute.js index c40d21528..55e829b91 100755 --- a/server/routes/queueRoute.js +++ b/server/routes/queueRoute.js @@ -7,41 +7,13 @@ class QueueRoutes { this.initRoutes(); } initRoutes() { - this.router.get( - "/metrics", - isAllowed(["admin", "superadmin"]), - this.queueController.getMetrics - ); + this.router.get("/jobs", isAllowed(["admin", "superadmin"]), this.queueController.getJobs); + this.router.post("/jobs", isAllowed(["admin", "superadmin"]), this.queueController.addJob); - this.router.get( - "/jobs", - isAllowed(["admin", "superadmin"]), - this.queueController.getJobs - ); - - this.router.get( - "/all-metrics", - isAllowed(["admin", "superadmin"]), - this.queueController.getAllMetrics - ); - - this.router.post( - "/jobs", - isAllowed(["admin", "superadmin"]), - this.queueController.addJob - ); - - this.router.post( - "/flush", - isAllowed(["admin", "superadmin"]), - this.queueController.flushQueue - ); - - this.router.get( - "/health", - isAllowed(["admin", "superadmin"]), - this.queueController.checkQueueHealth - ); + this.router.get("/metrics", isAllowed(["admin", "superadmin"]), this.queueController.getMetrics); + this.router.get("/health", isAllowed(["admin", "superadmin"]), this.queueController.checkQueueHealth); + this.router.get("/all-metrics", isAllowed(["admin", "superadmin"]), this.queueController.getAllMetrics); + this.router.post("/flush", isAllowed(["admin", "superadmin"]), this.queueController.flushQueue); } getRouter() { diff --git a/server/routes/settingsRoute.js b/server/routes/settingsRoute.js index f293fd660..88eb67e65 100755 --- a/server/routes/settingsRoute.js +++ b/server/routes/settingsRoute.js @@ -10,16 +10,8 @@ class SettingsRoutes { initRoutes() { this.router.get("/", this.settingsController.getAppSettings); - this.router.put( - "/", - isAllowed(["admin", "superadmin"]), - this.settingsController.updateAppSettings - ); - this.router.post( - "/test-email", - isAllowed(["admin", "superadmin"]), - this.settingsController.sendTestEmail - ); + this.router.put("/", isAllowed(["admin", "superadmin"]), this.settingsController.updateAppSettings); + this.router.post("/test-email", isAllowed(["admin", "superadmin"]), this.settingsController.sendTestEmail); } getRouter() { diff --git a/server/routes/statusPageRoute.js b/server/routes/statusPageRoute.js index 98b3a3433..53eb21fba 100755 --- a/server/routes/statusPageRoute.js +++ b/server/routes/statusPageRoute.js @@ -13,20 +13,11 @@ class StatusPageRoutes { initRoutes() { this.router.get("/", this.statusPageController.getStatusPage); this.router.get("/team", verifyJWT, this.statusPageController.getStatusPagesByTeamId); - this.router.get("/:url", this.statusPageController.getStatusPageByUrl); - this.router.post( - "/", - upload.single("logo"), - verifyJWT, - this.statusPageController.createStatusPage - ); - this.router.put( - "/", - upload.single("logo"), - verifyJWT, - this.statusPageController.updateStatusPage - ); + this.router.post("/", upload.single("logo"), verifyJWT, this.statusPageController.createStatusPage); + this.router.put("/", upload.single("logo"), verifyJWT, this.statusPageController.updateStatusPage); + + this.router.get("/:url", this.statusPageController.getStatusPageByUrl); this.router.delete("/:url(*)", verifyJWT, this.statusPageController.deleteStatusPage); } diff --git a/server/service/business/checkService.js b/server/service/business/checkService.js new file mode 100644 index 000000000..3d8206644 --- /dev/null +++ b/server/service/business/checkService.js @@ -0,0 +1,146 @@ +const SERVICE_NAME = "checkService"; + +class CheckService { + static SERVICE_NAME = SERVICE_NAME; + + constructor({ db, settingsService, stringService, errorService }) { + this.db = db; + this.settingsService = settingsService; + this.stringService = stringService; + this.errorService = errorService; + } + + getChecksByMonitor = async ({ monitorId, query, teamId }) => { + if (!monitorId) { + throw this.errorService.createBadRequestError("No monitor ID in request"); + } + + if (!teamId) { + throw this.errorService.createBadRequestError("No team ID in request"); + } + + const monitor = await this.db.getMonitorById(monitorId); + + if (!monitor) { + throw this.errorService.createNotFoundError("Monitor not found"); + } + + if (!monitor.teamId.equals(teamId)) { + throw this.errorService.createAuthorizationError(); + } + + let { type, sortOrder, dateRange, filter, ack, page, rowsPerPage, status } = query; + const result = await this.db.getChecksByMonitor({ + monitorId, + type, + sortOrder, + dateRange, + filter, + ack, + page, + rowsPerPage, + status, + }); + return result; + }; + + getChecksByTeam = async ({ teamId, query }) => { + let { sortOrder, dateRange, filter, ack, page, rowsPerPage } = query; + + if (!teamId) { + throw this.errorService.createBadRequestError("No team ID in request"); + } + + const checkData = await this.db.getChecksByTeam({ + sortOrder, + dateRange, + filter, + ack, + page, + rowsPerPage, + teamId, + }); + return checkData; + }; + + getChecksSummaryByTeamId = async ({ teamId }) => { + if (!teamId) { + throw this.errorService.createBadRequestError("No team ID in request"); + } + + const summary = await this.db.getChecksSummaryByTeamId({ teamId }); + return summary; + }; + + ackCheck = async ({ checkId, teamId, ack }) => { + if (!checkId) { + throw this.errorService.createBadRequestError("No check ID in request"); + } + + if (!teamId) { + throw this.errorService.createBadRequestError("No team ID in request"); + } + + const updatedCheck = await this.db.ackCheck(checkId, teamId, ack); + return updatedCheck; + }; + + ackAllChecks = async ({ monitorId, path, teamId, ack }) => { + if (path === "monitor") { + if (!monitorId) { + throw this.errorService.createBadRequestError("No monitor ID in request"); + } + + const monitor = await this.db.getMonitorById(monitorId); + if (!monitor) { + throw this.errorService.createNotFoundError("Monitor not found"); + } + + if (!monitor.teamId.equals(teamId)) { + throw this.errorService.createAuthorizationError(); + } + } + + const updatedChecks = await this.db.ackAllChecks(monitorId, teamId, ack, path); + return updatedChecks; + }; + + deleteChecks = async ({ monitorId, teamId }) => { + if (!monitorId) { + throw this.errorService.createBadRequestError("No monitor ID in request"); + } + + if (!teamId) { + throw this.errorService.createBadRequestError("No team ID in request"); + } + + const monitor = await this.db.getMonitorById(monitorId); + + if (!monitor) { + throw this.errorService.createNotFoundError("Monitor not found"); + } + + if (!monitor.teamId.equals(teamId)) { + throw this.errorService.createAuthorizationError(); + } + + const deletedCount = await this.db.deleteChecks(monitorId); + return deletedCount; + }; + deleteChecksByTeamId = async ({ teamId }) => { + if (!teamId) { + throw this.errorService.createBadRequestError("No team ID in request"); + } + + const deletedCount = await this.db.deleteChecksByTeamId(teamId); + return deletedCount; + }; + + updateChecksTTL = async ({ teamId, ttl }) => { + const SECONDS_PER_DAY = 86400; + const newTTL = parseInt(ttl, 10) * SECONDS_PER_DAY; + await this.db.updateChecksTTL(teamId, newTTL); + }; +} + +export default CheckService; diff --git a/server/service/business/diagnosticService.js b/server/service/business/diagnosticService.js new file mode 100644 index 000000000..5c37cba14 --- /dev/null +++ b/server/service/business/diagnosticService.js @@ -0,0 +1,89 @@ +import v8 from "v8"; +import os from "os"; + +const SERVICE_NAME = "diagnosticService"; + +class DiagnosticService { + static SERVICE_NAME = SERVICE_NAME; + constructor() { + /** + * Performance Observer for monitoring system performance metrics. + * Clears performance marks after each measurement to prevent memory leaks. + */ + const obs = new PerformanceObserver((items) => { + // Get the first entry but we don't need to store it + items.getEntries()[0]; + performance.clearMarks(); + }); + obs.observe({ entryTypes: ["measure"] }); + } + + getCPUUsage = async () => { + const startUsage = process.cpuUsage(); + const timingPeriod = 1000; // measured in ms + await new Promise((resolve) => setTimeout(resolve, timingPeriod)); + const endUsage = process.cpuUsage(startUsage); + const cpuUsage = { + userUsageMs: endUsage.user / 1000, + systemUsageMs: endUsage.system / 1000, + usagePercentage: ((endUsage.user + endUsage.system) / 1000 / timingPeriod) * 100, + }; + return cpuUsage; + }; + + getSystemStats = async () => { + // Memory Usage + const totalMemory = os.totalmem(); + const freeMemory = os.freemem(); + + const osStats = { + freeMemoryBytes: freeMemory, // bytes + totalMemoryBytes: totalMemory, // bytes + }; + + const used = process.memoryUsage(); + const memoryUsage = {}; + for (let key in used) { + memoryUsage[`${key}Mb`] = Math.round((used[key] / 1024 / 1024) * 100) / 100; // MB + } + + // CPU Usage + const cpuMetrics = await this.getCPUUsage(); + + // V8 Heap Statistics + const heapStats = v8.getHeapStatistics(); + const v8Metrics = { + totalHeapSizeBytes: heapStats.total_heap_size, // bytes + usedHeapSizeBytes: heapStats.used_heap_size, // bytes + heapSizeLimitBytes: heapStats.heap_size_limit, // bytes + }; + + // Event Loop Delay + let eventLoopDelay = 0; + performance.mark("start"); + await new Promise((resolve) => setTimeout(resolve, 0)); + performance.mark("end"); + performance.measure("eventLoopDelay", "start", "end"); + const entries = performance.getEntriesByName("eventLoopDelay"); + if (entries.length > 0) { + eventLoopDelay = entries[0].duration; + } + + // Uptime + const uptimeMs = process.uptime() * 1000; // ms + + // Combine Metrics + const diagnostics = { + osStats, + memoryUsage, + cpuUsage: cpuMetrics, + v8HeapStats: v8Metrics, + eventLoopDelayMs: eventLoopDelay, + uptimeMs, + }; + + return diagnostics; + }; +} + +export default DiagnosticService; diff --git a/server/service/business/inviteService.js b/server/service/business/inviteService.js new file mode 100644 index 000000000..d23d725e4 --- /dev/null +++ b/server/service/business/inviteService.js @@ -0,0 +1,40 @@ +const SERVICE_NAME = "inviteService"; + +class InviteService { + static SERVICE_NAME = SERVICE_NAME; + + constructor({ db, settingsService, emailService, stringService, errorService }) { + this.db = db; + this.settingsService = settingsService; + this.emailService = emailService; + this.stringService = stringService; + this.errorService = errorService; + } + + getInviteToken = async ({ invite, teamId }) => { + invite.teamId = teamId; + const inviteToken = await this.db.requestInviteToken(invite); + return inviteToken; + }; + + sendInviteEmail = async ({ inviteRequest, firstName }) => { + const inviteToken = await this.db.requestInviteToken({ ...inviteRequest }); + const { clientHost } = this.settingsService.getSettings(); + + const html = await this.emailService.buildEmail("employeeActivationTemplate", { + name: firstName, + link: `${clientHost}/register/${inviteToken.token}`, + }); + const result = await this.emailService.sendEmail(inviteRequest.email, "Welcome to Uptime Monitor", html); + if (!result) { + throw this.errorService.createServerError("Failed to send invite e-mail... Please verify your settings."); + } + }; + + verifyInviteToken = async ({ inviteToken }) => { + const invite = await this.db.getInviteToken(inviteToken); + return invite; + }; +} + +export default InviteService; diff --git a/server/service/business/maintenanceWindowService.js b/server/service/business/maintenanceWindowService.js new file mode 100644 index 000000000..0f58aca85 --- /dev/null +++ b/server/service/business/maintenanceWindowService.js @@ -0,0 +1,61 @@ +const SERVICE_NAME = "maintenanceWindowService"; + +class MaintenanceWindowService { + static SERVICE_NAME = SERVICE_NAME; + constructor({ db, settingsService, stringService, errorService }) { + this.db = db; + this.settingsService = settingsService; + this.stringService = stringService; + this.errorService = errorService; + } + + createMaintenanceWindow = async ({ teamId, body }) => { + const monitorIds = body.monitors; + const monitors = await this.db.getMonitorsByIds(monitorIds); + + const unauthorizedMonitors = monitors.filter((monitor) => !monitor.teamId.equals(teamId)); + + if (unauthorizedMonitors.length > 0) { + throw this.errorService.createAuthorizationError(); + } + + const dbTransactions = monitorIds.map((monitorId) => { + return this.db.createMaintenanceWindow({ + teamId, + monitorId, + name: body.name, + active: body.active ? body.active : true, + repeat: body.repeat, + start: body.start, + end: body.end, + }); + }); + await Promise.all(dbTransactions); + }; + + getMaintenanceWindowById = async ({ id, teamId }) => { + const maintenanceWindow = await this.db.getMaintenanceWindowById({ id, teamId }); + return maintenanceWindow; + }; + + getMaintenanceWindowsByTeamId = async ({ teamId, query }) => { + const maintenanceWindows = await this.db.getMaintenanceWindowsByTeamId(teamId, query); + return maintenanceWindows; + }; + + getMaintenanceWindowsByMonitorId = async ({ monitorId, teamId }) => { + const maintenanceWindows = await this.db.getMaintenanceWindowsByMonitorId({ monitorId, teamId }); + return maintenanceWindows; + }; + + deleteMaintenanceWindow = async ({ id, teamId }) => { + await this.db.deleteMaintenanceWindowById({ id, teamId }); + }; + + editMaintenanceWindow = async ({ id, teamId, body }) => { + const editedMaintenanceWindow = await this.db.editMaintenanceWindowById({ id, body, teamId }); + return editedMaintenanceWindow; + }; +} + +export default MaintenanceWindowService; diff --git a/server/service/business/monitorService.js b/server/service/business/monitorService.js new file mode 100644 index 000000000..2a2b27aa3 --- /dev/null +++ b/server/service/business/monitorService.js @@ -0,0 +1,262 @@ +import { createMonitorsBodyValidation } from "../../validation/joi.js"; + +const SERVICE_NAME = "MonitorService"; +class MonitorService { + SERVICE_NAME = SERVICE_NAME; + + constructor({ db, settingsService, jobQueue, stringService, emailService, papaparse, logger, errorService }) { + this.db = db; + this.settingsService = settingsService; + this.jobQueue = jobQueue; + this.stringService = stringService; + this.emailService = emailService; + this.papaparse = papaparse; + this.logger = logger; + this.errorService = errorService; + } + + verifyTeamAccess = async ({ teamId, monitorId }) => { + const monitor = await this.db.getMonitorById(monitorId); + if (!monitor?.teamId?.equals(teamId)) { + throw this.errorService.createAuthorizationError(); + } + }; + + getAllMonitors = async () => { + const monitors = await this.db.getAllMonitors(); + return monitors; + }; + + getUptimeDetailsById = async ({ teamId, monitorId, dateRange, normalize }) => { + await this.verifyTeamAccess({ teamId, monitorId }); + const data = await this.db.getUptimeDetailsById({ + monitorId, + dateRange, + normalize, + }); + + return data; + }; + + getMonitorStatsById = async ({ teamId, monitorId, limit, sortOrder, dateRange, numToDisplay, normalize }) => { + await this.verifyTeamAccess({ teamId, monitorId }); + const monitorStats = await this.db.getMonitorStatsById({ + monitorId, + limit, + sortOrder, + dateRange, + numToDisplay, + normalize, + }); + + return monitorStats; + }; + + getHardwareDetailsById = async ({ teamId, monitorId, dateRange }) => { + await this.verifyTeamAccess({ teamId, monitorId }); + const monitor = await this.db.getHardwareDetailsById({ monitorId, dateRange }); + + return monitor; + }; + + getMonitorById = async ({ teamId, monitorId }) => { + await this.verifyTeamAccess({ teamId, monitorId }); + const monitor = await this.db.getMonitorById(monitorId); + + return monitor; + }; + + createMonitor = async ({ teamId, userId, body }) => { + const monitor = await this.db.createMonitor({ + body, + teamId, + userId, + }); + + this.jobQueue.addJob(monitor._id, monitor); + }; + + createBulkMonitors = async ({ fileData, userId, teamId }) => { + const { parse } = this.papaparse; + + return new Promise((resolve, reject) => { + parse(fileData, { + header: true, + skipEmptyLines: true, + transform: (value, header) => { + if (value === "") return undefined; // Empty fields become undefined + + // Handle 'port' and 'interval' fields, check if they're valid numbers + if (["port", "interval"].includes(header)) { + const num = parseInt(value, 10); + if (isNaN(num)) { + throw this.errorService.createBadRequestError(`${header} should be a valid number, got: ${value}`); + } + return num; + } + + return value; + }, + complete: async ({ data, errors }) => { + try { + if (errors.length > 0) { + throw this.errorService.createServerError("Error parsing CSV"); + } + + if (!data || data.length === 0) { + throw this.errorService.createServerError("CSV file contains no data rows"); + } + + const enrichedData = data.map((monitor) => ({ + userId, + teamId, + ...monitor, + description: monitor.description || monitor.name || monitor.url, + name: monitor.name || monitor.url, + type: monitor.type || "http", + })); + + await createMonitorsBodyValidation.validateAsync(enrichedData); + + const monitors = await this.db.createBulkMonitors(enrichedData); + + await Promise.all( + monitors.map(async (monitor) => { + this.jobQueue.addJob(monitor._id, monitor); + }) + ); + + resolve(monitors); + } catch (error) { + reject(error); + } + }, + }); + }); + }; + + deleteMonitor = async ({ teamId, monitorId }) => { + await this.verifyTeamAccess({ teamId, monitorId }); + const monitor = await this.db.deleteMonitor({ teamId, monitorId }); + await this.jobQueue.deleteJob(monitor); + await this.db.deleteStatusPagesByMonitorId(monitor._id); + return monitor; + }; + + deleteAllMonitors = async ({ teamId }) => { + const { monitors, deletedCount } = await this.db.deleteAllMonitors(teamId); + await Promise.all( + monitors.map(async (monitor) => { + try { + await this.jobQueue.deleteJob(monitor); + await this.db.deleteChecks(monitor._id); + await this.db.deletePageSpeedChecksByMonitorId(monitor._id); + await this.db.deleteNotificationsByMonitorId(monitor._id); + } catch (error) { + this.logger.warn({ + message: `Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`, + service: SERVICE_NAME, + method: "deleteAllMonitors", + stack: error.stack, + }); + } + }) + ); + return deletedCount; + }; + + editMonitor = async ({ teamId, monitorId, body }) => { + await this.verifyTeamAccess({ teamId, monitorId }); + const editedMonitor = await this.db.editMonitor({ monitorId, body }); + await this.jobQueue.updateJob(editedMonitor); + }; + + pauseMonitor = async ({ teamId, monitorId }) => { + await this.verifyTeamAccess({ teamId, monitorId }); + const monitor = await this.db.pauseMonitor({ monitorId }); + monitor.isActive === true ? await this.jobQueue.resumeJob(monitor._id, monitor) : await this.jobQueue.pauseJob(monitor); + return monitor; + }; + + addDemoMonitors = async ({ userId, teamId }) => { + const demoMonitors = await this.db.addDemoMonitors(userId, teamId); + await Promise.all(demoMonitors.map((monitor) => this.jobQueue.addJob(monitor._id, monitor))); + return demoMonitors; + }; + + sendTestEmail = async ({ to }) => { + const subject = this.stringService.testEmailSubject; + const context = { testName: "Monitoring System" }; + + const html = await this.emailService.buildEmail("testEmailTemplate", context); + const messageId = await this.emailService.sendEmail(to, subject, html); + + if (!messageId) { + throw this.errorService.createServerError("Failed to send test email."); + } + + return messageId; + }; + + getMonitorsByTeamId = async ({ teamId, limit, type, page, rowsPerPage, filter, field, order }) => { + const monitors = await this.db.getMonitorsByTeamId({ + limit, + type, + page, + rowsPerPage, + filter, + field, + order, + teamId, + }); + return monitors; + }; + + getMonitorsAndSummaryByTeamId = async ({ teamId, type, explain }) => { + const result = await this.db.getMonitorsAndSummaryByTeamId({ + type, + explain, + teamId, + }); + return result; + }; + + getMonitorsWithChecksByTeamId = async ({ teamId, limit, type, page, rowsPerPage, filter, field, order, explain }) => { + const result = await this.db.getMonitorsWithChecksByTeamId({ + limit, + type, + page, + rowsPerPage, + filter, + field, + order, + teamId, + explain, + }); + return result; + }; + + exportMonitorsToCSV = async ({ teamId }) => { + const monitors = await this.db.getMonitorsByTeamId({ teamId }); + + if (!monitors || monitors.length === 0) { + throw this.errorService.createNotFoundError("No monitors to export"); + } + + const csvData = monitors?.filteredMonitors?.map((monitor) => ({ + name: monitor.name, + description: monitor.description, + type: monitor.type, + url: monitor.url, + interval: monitor.interval, + port: monitor.port, + ignoreTlsErrors: monitor.ignoreTlsErrors, + isActive: monitor.isActive, + })); + + const csv = this.papaparse.unparse(csvData); + return csv; + }; +} + +export default MonitorService; diff --git a/server/service/business/userService.js b/server/service/business/userService.js new file mode 100644 index 000000000..12dd4207f --- /dev/null +++ b/server/service/business/userService.js @@ -0,0 +1,208 @@ +const SERVICE_NAME = "userService"; + +class UserService { + static SERVICE_NAME = SERVICE_NAME; + constructor({ db, emailService, settingsService, logger, stringService, jwt, errorService }) { + this.db = db; + this.emailService = emailService; + this.settingsService = settingsService; + this.logger = logger; + this.stringService = stringService; + this.jwt = jwt; + this.errorService = errorService; + } + + issueToken = (payload, appSettings) => { + const tokenTTL = appSettings?.jwtTTL ?? "2h"; + const tokenSecret = appSettings?.jwtSecret; + const payloadData = payload; + return this.jwt.sign(payloadData, tokenSecret, { expiresIn: tokenTTL }); + }; + + registerUser = async (user, file) => { + // Create a new user + // If superAdmin exists, a token should be attached to all further register requests + const superAdminExists = await this.db.checkSuperadmin(); + if (superAdminExists) { + const invitedUser = await this.db.getInviteTokenAndDelete(user.inviteToken); + user.role = invitedUser.role; + user.teamId = invitedUser.teamId; + } else { + // This is the first account, create JWT secret to use if one is not supplied by env + const jwtSecret = crypto.randomBytes(64).toString("hex"); + await this.db.updateAppSettings({ jwtSecret }); + } + + const newUser = await this.db.insertUser({ ...user }, file); + + this.logger.debug({ + message: "New user created", + service: SERVICE_NAME, + method: "registerUser", + details: newUser._id, + }); + + const userForToken = { ...newUser._doc }; + delete userForToken.profileImage; + delete userForToken.avatarImage; + + const appSettings = await this.settingsService.getSettings(); + + const token = this.issueToken(userForToken, appSettings); + + try { + const html = await this.emailService.buildEmail("welcomeEmailTemplate", { + name: newUser.firstName, + }); + this.emailService.sendEmail(newUser.email, "Welcome to Uptime Monitor", html).catch((error) => { + this.logger.warn({ + message: error.message, + service: SERVICE_NAME, + method: "registerUser", + stack: error.stack, + }); + }); + } catch (error) { + this.logger.warn({ + message: error.message, + service: SERVICE_NAME, + method: "registerUser", + stack: error.stack, + }); + } + + return { user: newUser, token }; + }; + + loginUser = async (email, password) => { + // Check if user exists + const user = await this.db.getUserByEmail(email); + // Compare password + const match = await user.comparePassword(password); + if (match !== true) { + throw this.errorService.createAuthenticationError(this.stringService.authIncorrectPassword); + } + + // Remove password from user object. Should this be abstracted to DB layer? + const userWithoutPassword = { ...user._doc }; + delete userWithoutPassword.password; + delete userWithoutPassword.avatarImage; + + // Happy path, return token + const appSettings = await this.settingsService.getSettings(); + const token = this.issueToken(userWithoutPassword, appSettings); + // reset avatar image + userWithoutPassword.avatarImage = user.avatarImage; + return { user: userWithoutPassword, token }; + }; + + editUser = async (updates, file, currentUser) => { + // Change Password check + if (updates?.password && updates?.newPassword) { + // Get user's email + // Add user email to body for DB operation + updates.email = currentUser.email; + // Get user + const user = await this.db.getUserByEmail(currentUser.email); + // Compare passwords + const match = await user.comparePassword(updates?.password); + // If not a match, throw a 403 + // 403 instead of 401 to avoid triggering axios interceptor + if (!match) { + throw this.errorService.createAuthorizationError(this.stringService.authIncorrectPassword); + } + // If a match, update the password + updates.password = updates.newPassword; + } + + const updatedUser = await this.db.updateUser({ userId: currentUser?._id, user: updates, file: file }); + return updatedUser; + }; + + checkSuperadminExists = async () => { + const superAdminExists = await this.db.checkSuperadmin(); + return superAdminExists; + }; + + requestRecovery = async (email) => { + const user = await this.db.getUserByEmail(email); + const recoveryToken = await this.db.requestRecoveryToken(email); + const name = user.firstName; + const { clientHost } = this.settingsService.getSettings(); + const url = `${clientHost}/set-new-password/${recoveryToken.token}`; + + const html = await this.emailService.buildEmail("passwordResetTemplate", { + name, + email, + url, + }); + const msgId = await this.emailService.sendEmail(email, "Checkmate Password Reset", html); + return msgId; + }; + + validateRecovery = async (recoveryToken) => { + await this.db.validateRecoveryToken(recoveryToken); + }; + + resetPassword = async (password, recoveryToken) => { + const user = await this.db.resetPassword(password, recoveryToken); + const appSettings = await this.settingsService.getSettings(); + const token = this.issueToken(user._doc, appSettings); + return { user, token }; + }; + + deleteUser = async (user) => { + const email = user?.email; + if (!email) { + throw this.errorService.createBadRequestError("No email in request"); + } + + const teamId = user?.teamId; + const userId = user?._id; + + if (!teamId) { + throw this.errorService.createBadRequestError("No team ID in request"); + } + + if (!userId) { + throw this.errorService.createBadRequestError("No user ID in request"); + } + + const roles = user?.role; + if (roles.includes("demo")) { + throw this.errorService.createBadRequestError("Demo user cannot be deleted"); + } + + // 1. Find all the monitors associated with the team ID if superadmin + const result = await this.db.getMonitorsByTeamId({ + teamId: teamId, + }); + + if (roles.includes("superadmin")) { + // 2. Remove all jobs, delete checks and alerts + result?.monitors.length > 0 && + (await Promise.all( + result.monitors.map(async (monitor) => { + await this.jobQueue.deleteJob(monitor); + }) + )); + } + // 6. Delete the user by id + await this.db.deleteUser(userId); + }; + + getAllUsers = async () => { + const users = await this.db.getAllUsers(); + return users; + }; + + getUserById = async (roles, userId) => { + const user = await this.db.getUserById(roles, userId); + return user; + }; + + editUserById = async (userId, user) => { + await this.db.editUserById(userId, user); + }; +} +export default UserService; diff --git a/server/service/redisService.js b/server/service/data/redisService.js similarity index 100% rename from server/service/redisService.js rename to server/service/data/redisService.js diff --git a/server/service/JobQueue/JobQueue.js b/server/service/infrastructure/JobQueue/JobQueue.js similarity index 98% rename from server/service/JobQueue/JobQueue.js rename to server/service/infrastructure/JobQueue/JobQueue.js index 38cca7d4d..6e316c616 100644 --- a/server/service/JobQueue/JobQueue.js +++ b/server/service/infrastructure/JobQueue/JobQueue.js @@ -150,11 +150,7 @@ class JobQueue { }; const schedulerId = getSchedulerId(monitor); - await queue.upsertJobScheduler( - schedulerId, - { every: monitor?.interval ?? 60000 }, - jobTemplate - ); + await queue.upsertJobScheduler(schedulerId, { every: monitor?.interval ?? 60000 }, jobTemplate); } async deleteJob(monitor) { diff --git a/server/service/JobQueue/JobQueueHelper.js b/server/service/infrastructure/JobQueue/JobQueueHelper.js similarity index 96% rename from server/service/JobQueue/JobQueueHelper.js rename to server/service/infrastructure/JobQueue/JobQueueHelper.js index d286b7b14..4107966e8 100644 --- a/server/service/JobQueue/JobQueueHelper.js +++ b/server/service/infrastructure/JobQueue/JobQueueHelper.js @@ -1,16 +1,7 @@ const SERVICE_NAME = "JobQueueHelper"; class JobQueueHelper { - constructor({ - redisService, - Queue, - Worker, - logger, - db, - networkService, - statusService, - notificationService, - }) { + constructor({ redisService, Queue, Worker, logger, db, networkService, statusService, notificationService }) { this.db = db; this.redisService = redisService; this.Queue = Queue; @@ -265,11 +256,7 @@ class JobQueueHelper { // Handle status change await job.updateProgress(60); - const { - monitor: updatedMonitor, - statusChanged, - prevStatus, - } = await this.statusService.updateStatus(networkResponse); + const { monitor: updatedMonitor, statusChanged, prevStatus } = await this.statusService.updateStatus(networkResponse); // Handle notifications await job.updateProgress(80); this.notificationService diff --git a/server/service/PulseQueue/PulseQueue.js b/server/service/infrastructure/PulseQueue/PulseQueue.js similarity index 95% rename from server/service/PulseQueue/PulseQueue.js rename to server/service/infrastructure/PulseQueue/PulseQueue.js index 50a959559..85d14ab07 100644 --- a/server/service/PulseQueue/PulseQueue.js +++ b/server/service/infrastructure/PulseQueue/PulseQueue.js @@ -22,8 +22,7 @@ class PulseQueue { // **************************************** init = async () => { try { - const mongoConnectionString = - this.appSettings.dbConnectionString || "mongodb://localhost:27017/uptime_db"; + const mongoConnectionString = this.appSettings.dbConnectionString || "mongodb://localhost:27017/uptime_db"; this.pulse = new Pulse({ db: { address: mongoConnectionString } }); await this.pulse.start(); this.pulse.define("monitor-job", this.pulseQueueHelper.getMonitorJob(), {}); @@ -186,9 +185,7 @@ class PulseQueue { failReason: job.attrs.failReason, lastRunAt: job.attrs.lastRunAt, lastFinishedAt: job.attrs.lastFinishedAt, - lastRunTook: job.attrs.lockedAt - ? null - : job.attrs.lastFinishedAt - job.attrs.lastRunAt, + lastRunTook: job.attrs.lockedAt ? null : job.attrs.lastFinishedAt - job.attrs.lastRunAt, lastFailedAt: job.attrs.failedAt, }; }); diff --git a/server/service/PulseQueue/PulseQueueHelper.js b/server/service/infrastructure/PulseQueue/PulseQueueHelper.js similarity index 94% rename from server/service/PulseQueue/PulseQueueHelper.js rename to server/service/infrastructure/PulseQueue/PulseQueueHelper.js index 959c12e3b..c3b8992d8 100644 --- a/server/service/PulseQueue/PulseQueueHelper.js +++ b/server/service/infrastructure/PulseQueue/PulseQueueHelper.js @@ -34,11 +34,7 @@ class PulseQueueHelper { throw new Error("No network response"); } - const { - monitor: updatedMonitor, - statusChanged, - prevStatus, - } = await this.statusService.updateStatus(networkResponse); + const { monitor: updatedMonitor, statusChanged, prevStatus } = await this.statusService.updateStatus(networkResponse); this.notificationService .handleNotifications({ diff --git a/server/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js b/server/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js new file mode 100644 index 000000000..f0b59640e --- /dev/null +++ b/server/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js @@ -0,0 +1,166 @@ +import Scheduler from "super-simple-scheduler"; +const SERVICE_NAME = "SuperSimpleQueue"; + +class SuperSimpleQueue { + static SERVICE_NAME = SERVICE_NAME; + + constructor({ appSettings, db, logger, helper }) { + this.appSettings = appSettings; + this.db = db; + this.logger = logger; + this.helper = helper; + } + + static async create({ appSettings, db, logger, helper }) { + const instance = new SuperSimpleQueue({ appSettings, db, logger, helper }); + await instance.init(); + return instance; + } + + init = async () => { + try { + this.scheduler = new Scheduler({ + // storeType: "mongo", + // storeType: "redis", + logLevel: "debug", + debug: true, + dbUri: this.appSettings.dbConnectionString, + }); + this.scheduler.start(); + + this.scheduler.addTemplate("monitor-job", this.helper.getMonitorJob()); + const monitors = await this.db.getAllMonitors(); + for (const monitor of monitors) { + await this.addJob(monitor._id, monitor); + } + return true; + } catch (error) { + this.logger.error({ + message: "Failed to initialize SuperSimpleQueue", + service: SERVICE_NAME, + method: "init", + details: error, + }); + return false; + } + }; + + addJob = async (monitorId, monitor) => { + this.scheduler.addJob({ + id: monitorId.toString(), + template: "monitor-job", + repeat: monitor.interval, + data: monitor.toObject(), + }); + }; + + deleteJob = async (monitor) => { + this.scheduler.removeJob(monitor._id.toString()); + }; + + pauseJob = async (monitor) => { + const result = this.scheduler.pauseJob(monitor._id.toString()); + if (result === false) { + throw new Error("Failed to resume monitor"); + } + this.logger.debug({ + message: `Paused monitor ${monitor._id}`, + service: SERVICE_NAME, + method: "pauseJob", + }); + }; + + resumeJob = async (monitor) => { + const result = this.scheduler.resumeJob(monitor._id.toString()); + if (result === false) { + throw new Error("Failed to resume monitor"); + } + this.logger.debug({ + message: `Resumed monitor ${monitor._id}`, + service: SERVICE_NAME, + method: "resumeJob", + }); + }; + + updateJob = async (monitor) => { + this.scheduler.updateJob(monitor._id.toString(), monitor.interval); + }; + + shutdown = async () => { + this.scheduler.stop(); + }; + + getMetrics = async () => { + const jobs = await this.scheduler.getJobs(); + const metrics = jobs.reduce( + (acc, job) => { + acc.totalRuns += job.runCount || 0; + acc.totalFailures += job.failCount || 0; + acc.jobs++; + if (job.failCount > 0 && job.lastFailedAt >= job.lsatRunAt) { + acc.failingJobs++; + } + + if (job.lockedAt) { + acc.activeJobs++; + } + + if (job.failCount > 0) { + acc.jobsWithFailures.push({ + monitorId: job.id, + monitorUrl: job?.data?.url || null, + monitorType: job?.data?.type || null, + failedAt: job.lastFailedAt, + failCount: job.failCount, + failReason: job.lastFailReason, + }); + } + return acc; + }, + { + jobs: 0, + activeJobs: 0, + failingJobs: 0, + jobsWithFailures: [], + totalRuns: 0, + totalFailures: 0, + } + ); + return metrics; + }; + + getJobs = async () => { + const jobs = await this.scheduler.getJobs(); + return jobs.map((job) => { + return { + monitorId: job.id, + monitorUrl: job?.data?.url || null, + monitorType: job?.data?.type || null, + active: job.active, + lockedAt: job.lockedAt, + runCount: job.runCount || 0, + failCount: job.failCount || 0, + failReason: job.lastFailReason, + lastRunAt: job.lastRunAt, + lastFinishedAt: job.lastFinishedAt, + lastRunTook: job.lockedAt ? null : job.lastFinishedAt - job.lastRunAt, + lastFailedAt: job.lastFailedAt, + }; + }); + }; + + flushQueues = async () => { + const stopRes = this.scheduler.stop(); + const flushRes = this.scheduler.flushJobs(); + const initRes = await this.init(); + return { + success: stopRes && flushRes && initRes, + }; + }; + + obliterate = async () => { + console.log("obliterate not implemented"); + }; +} + +export default SuperSimpleQueue; diff --git a/server/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js b/server/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js new file mode 100644 index 000000000..fd41a5179 --- /dev/null +++ b/server/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js @@ -0,0 +1,95 @@ +const SERVICE_NAME = "SuperSimpleQueueHelper"; + +class SuperSimpleQueueHelper { + constructor({ db, logger, networkService, statusService, notificationService }) { + this.db = db; + this.logger = logger; + this.networkService = networkService; + this.statusService = statusService; + this.notificationService = notificationService; + } + + getMonitorJob = () => { + return async (monitor) => { + try { + const monitorId = monitor._id; + if (!monitorId) { + throw new Error("No monitor id"); + } + + const maintenanceWindowActive = await this.isInMaintenanceWindow(monitorId); + if (maintenanceWindowActive) { + this.logger.info({ + message: `Monitor ${monitorId} is in maintenance window`, + service: SERVICE_NAME, + method: "getMonitorJob", + }); + return; + } + const networkResponse = await this.networkService.getStatus(monitor); + if (!networkResponse) { + throw new Error("No network response"); + } + + const { monitor: updatedMonitor, statusChanged, prevStatus } = await this.statusService.updateStatus(networkResponse); + + this.notificationService + .handleNotifications({ + ...networkResponse, + monitor: updatedMonitor, + prevStatus, + statusChanged, + }) + .catch((error) => { + this.logger.error({ + message: error.message, + service: SERVICE_NAME, + method: "getMonitorJob", + details: `Error sending notifications for job ${monitor._id}: ${error.message}`, + stack: error.stack, + }); + }); + } catch (error) { + this.logger.warn({ + message: error.message, + service: error.service || SERVICE_NAME, + method: error.method || "getMonitorJob", + stack: error.stack, + }); + throw error; + } + }; + }; + + async isInMaintenanceWindow(monitorId) { + const maintenanceWindows = await this.db.getMaintenanceWindowsByMonitorId(monitorId); + // Check for active maintenance window: + const maintenanceWindowIsActive = maintenanceWindows.reduce((acc, window) => { + if (window.active) { + const start = new Date(window.start); + const end = new Date(window.end); + const now = new Date(); + const repeatInterval = window.repeat || 0; + + // If start is < now and end > now, we're in maintenance + if (start <= now && end >= now) return true; + + // If maintenance window was set in the past with a repeat, + // we need to advance start and end to see if we are in range + + while (start < now && repeatInterval !== 0) { + start.setTime(start.getTime() + repeatInterval); + end.setTime(end.getTime() + repeatInterval); + if (start <= now && end >= now) { + return true; + } + } + return false; + } + return acc; + }, false); + return maintenanceWindowIsActive; + } +} + +export default SuperSimpleQueueHelper; diff --git a/server/service/bufferService.js b/server/service/infrastructure/bufferService.js similarity index 100% rename from server/service/bufferService.js rename to server/service/infrastructure/bufferService.js diff --git a/server/service/emailService.js b/server/service/infrastructure/emailService.js similarity index 97% rename from server/service/emailService.js rename to server/service/infrastructure/emailService.js index 433fd0871..b6b0eb4af 100755 --- a/server/service/emailService.js +++ b/server/service/infrastructure/emailService.js @@ -42,10 +42,7 @@ class EmailService { */ this.loadTemplate = (templateName) => { try { - const templatePath = this.path.join( - __dirname, - `../templates/${templateName}.mjml` - ); + const templatePath = this.path.join(__dirname, `../templates/${templateName}.mjml`); const templateContent = this.fs.readFileSync(templatePath, "utf8"); return this.compile(templateContent); } catch (error) { diff --git a/server/service/infrastructure/errorService.js b/server/service/infrastructure/errorService.js new file mode 100644 index 000000000..552816b73 --- /dev/null +++ b/server/service/infrastructure/errorService.js @@ -0,0 +1,98 @@ +export class AppError extends Error { + constructor(message, status = 500, service = null, method = null, details = null) { + super(message); + this.status = status; + this.service = service; + this.method = method; + this.details = details; + + Error.captureStackTrace(this, this.constructor); + } +} + +class ValidationError extends AppError { + constructor(message, details = null, service = null, method = null) { + super(message, 422, service, method, details); + } +} + +class AuthenticationError extends AppError { + constructor(message, details = null, service = null, method = null) { + super(message, 401, service, method, details); + } +} + +class AuthorizationError extends AppError { + constructor(message, details = null, service = null, method = null) { + super(message, 403, service, method, details); + } +} + +class NotFoundError extends AppError { + constructor(message, details = null, service = null, method = null) { + super(message, 404, service, method, details); + } +} + +class ConflictError extends AppError { + constructor(message, details = null, service = null, method = null) { + super(message, 409, service, method, details); + } +} + +class DatabaseError extends AppError { + constructor(message, details = null, service = null, method = null) { + super(message, 500, service, method, details); + } +} + +class BadRequestError extends AppError { + constructor(message, details = null, service = null, method = null) { + super(message, 400, service, method, details); + } +} + +const SERVICE_NAME = "ErrorService"; +class ErrorService { + static SERVICE_NAME = SERVICE_NAME; + + constructor() {} + + createError = (message, status = 500, service = null, method = null, details = null) => { + return new AppError(message, status, service, method, details); + }; + + createValidationError = (message, details = null, service = null, method = null) => { + return new ValidationError(message, details, service, method); + }; + + createAuthenticationError = (message = "Unauthorized", details = null, service = null, method = null) => { + return new AuthenticationError(message, details, service, method); + }; + + createAuthorizationError = (message, details = null, service = null, method = null) => { + return new AuthorizationError(message, details, service, method); + }; + + createNotFoundError = (message, details = null, service = null, method = null) => { + return new NotFoundError(message, details, service, method); + }; + + createConflictError = (message, details = null, service = null, method = null) => { + return new ConflictError(message, details, service, method); + }; + + createDatabaseError = (message, details = null, service = null, method = null) => { + return new DatabaseError(message, details, service, method); + }; + + createServerError = (message, details = null, service = null, method = null) => { + return this.createError(message, 500, service, method, details); + }; + + createBadRequestError = (message = "BadRequest", details = null, service = null, method = null) => { + return new BadRequestError(message, details, service, method); + }; +} + +export default ErrorService; diff --git a/server/service/networkService.js b/server/service/infrastructure/networkService.js similarity index 96% rename from server/service/networkService.js rename to server/service/infrastructure/networkService.js index 08b2f065e..70d7cc932 100755 --- a/server/service/networkService.js +++ b/server/service/infrastructure/networkService.js @@ -78,9 +78,7 @@ class NetworkService { async requestPing(monitor) { try { const url = monitor.url; - const { response, responseTime, error } = await this.timeRequest(() => - this.ping.promise.probe(url) - ); + const { response, responseTime, error } = await this.timeRequest(() => this.ping.promise.probe(url)); const pingResponse = { monitorId: monitor._id, @@ -133,18 +131,7 @@ class NetworkService { */ async requestHttp(monitor) { try { - const { - url, - secret, - _id, - name, - teamId, - type, - ignoreTlsErrors, - jsonPath, - matchMethod, - expectedValue, - } = monitor; + const { url, secret, _id, name, teamId, type, ignoreTlsErrors, jsonPath, matchMethod, expectedValue } = monitor; const config = {}; secret !== undefined && (config.headers = { Authorization: `Bearer ${secret}` }); @@ -155,9 +142,7 @@ class NetworkService { }); } - const { response, responseTime, error } = await this.timeRequest(() => - this.axios.get(url, config) - ); + const { response, responseTime, error } = await this.timeRequest(() => this.axios.get(url, config)); const httpResponse = { monitorId: _id, @@ -171,8 +156,7 @@ class NetworkService { const code = error.response?.status || this.NETWORK_ERROR; httpResponse.code = code; httpResponse.status = false; - httpResponse.message = - this.http.STATUS_CODES[code] || this.stringService.httpNetworkError; + httpResponse.message = this.http.STATUS_CODES[code] || this.stringService.httpNetworkError; return httpResponse; } @@ -227,9 +211,7 @@ class NetworkService { else match = result === expectedValue; httpResponse.status = match; - httpResponse.message = match - ? this.stringService.httpMatchSuccess - : this.stringService.httpMatchFail; + httpResponse.message = match ? this.stringService.httpMatchSuccess : this.stringService.httpMatchFail; return httpResponse; } catch (error) { error.service = this.SERVICE_NAME; @@ -320,9 +302,7 @@ class NetworkService { } const container = docker.getContainer(monitor.url); - const { response, responseTime, error } = await this.timeRequest(() => - container.inspect() - ); + const { response, responseTime, error } = await this.timeRequest(() => container.inspect()); const dockerResponse = { monitorId: monitor._id, @@ -333,8 +313,7 @@ class NetworkService { if (error) { dockerResponse.status = false; dockerResponse.code = error.statusCode || this.NETWORK_ERROR; - dockerResponse.message = - error.reason || "Failed to fetch Docker container information"; + dockerResponse.message = error.reason || "Failed to fetch Docker container information"; return dockerResponse; } dockerResponse.status = response?.State?.Status === "running" ? true : false; diff --git a/server/service/notificationService.js b/server/service/infrastructure/notificationService.js similarity index 91% rename from server/service/notificationService.js rename to server/service/infrastructure/notificationService.js index a6048b8cd..f253cf760 100644 --- a/server/service/notificationService.js +++ b/server/service/infrastructure/notificationService.js @@ -3,14 +3,7 @@ const SERVICE_NAME = "NotificationService"; class NotificationService { static SERVICE_NAME = SERVICE_NAME; - constructor({ - emailService, - db, - logger, - networkService, - stringService, - notificationUtils, - }) { + constructor({ emailService, db, logger, networkService, stringService, notificationUtils }) { this.emailService = emailService; this.db = db; this.logger = logger; @@ -69,20 +62,15 @@ class NotificationService { const alerts = await this.notificationUtils.buildHardwareAlerts(networkResponse); if (alerts.length === 0) return false; - const { subject, html } = await this.notificationUtils.buildHardwareEmail( - networkResponse, - alerts - ); - const content = - await this.notificationUtils.buildHardwareNotificationMessage(alerts); + const { subject, html } = await this.notificationUtils.buildHardwareEmail(networkResponse, alerts); + const content = await this.notificationUtils.buildHardwareNotificationMessage(alerts); const success = await this.notifyAll({ notificationIDs, subject, html, content }); return success; } // Status monitors - const { subject, html } = - await this.notificationUtils.buildStatusEmail(networkResponse); + const { subject, html } = await this.notificationUtils.buildStatusEmail(networkResponse); const content = await this.notificationUtils.buildWebhookMessage(networkResponse); const success = this.notifyAll({ notificationIDs, subject, html, content }); return success; diff --git a/server/service/notificationUtils.js b/server/service/infrastructure/notificationUtils.js similarity index 80% rename from server/service/notificationUtils.js rename to server/service/infrastructure/notificationUtils.js index 61c3ec36a..d070a9995 100644 --- a/server/service/notificationUtils.js +++ b/server/service/infrastructure/notificationUtils.js @@ -26,10 +26,7 @@ class NotificationUtils { const date = new Date(timestamp); // Get timezone abbreviation and format the date - const timeZoneAbbr = date - .toLocaleTimeString("en-US", { timeZoneName: "short" }) - .split(" ") - .pop(); + const timeZoneAbbr = date.toLocaleTimeString("en-US", { timeZoneName: "short" }).split(" ").pop(); // Format the date with readable format return ( @@ -50,9 +47,7 @@ class NotificationUtils { }; // Get formatted time - const formattedTime = timestamp - ? formatTime(timestamp) - : formatTime(new Date().getTime()); + const formattedTime = timestamp ? formatTime(timestamp) : formatTime(new Date().getTime()); // Create different messages based on status with extra spacing let messageText; @@ -73,29 +68,15 @@ class NotificationUtils { buildHardwareAlerts = async (networkResponse) => { const monitor = networkResponse?.monitor; const thresholds = networkResponse?.monitor?.thresholds; - const { - usage_cpu: cpuThreshold = -1, - usage_memory: memoryThreshold = -1, - usage_disk: diskThreshold = -1, - } = thresholds; + const { usage_cpu: cpuThreshold = -1, usage_memory: memoryThreshold = -1, usage_disk: diskThreshold = -1 } = thresholds; const metrics = networkResponse?.payload?.data; - const { - cpu: { usage_percent: cpuUsage = -1 } = {}, - memory: { usage_percent: memoryUsage = -1 } = {}, - disk = [], - } = metrics; + const { cpu: { usage_percent: cpuUsage = -1 } = {}, memory: { usage_percent: memoryUsage = -1 } = {}, disk = [] } = metrics; const alerts = { cpu: cpuThreshold !== -1 && cpuUsage > cpuThreshold ? true : false, memory: memoryThreshold !== -1 && memoryUsage > memoryThreshold ? true : false, - disk: - disk?.some( - (d) => - diskThreshold !== -1 && - typeof d?.usage_percent === "number" && - d?.usage_percent > diskThreshold - ) ?? false, + disk: disk?.some((d) => diskThreshold !== -1 && typeof d?.usage_percent === "number" && d?.usage_percent > diskThreshold) ?? false, }; const alertsToSend = []; @@ -110,16 +91,13 @@ class NotificationUtils { monitor[`${type}AlertThreshold`] = monitor.alertThreshold; const formatAlert = { - cpu: () => - `Your current CPU usage (${(cpuUsage * 100).toFixed(0)}%) is above your threshold (${(cpuThreshold * 100).toFixed(0)}%)`, + cpu: () => `Your current CPU usage (${(cpuUsage * 100).toFixed(0)}%) is above your threshold (${(cpuThreshold * 100).toFixed(0)}%)`, memory: () => `Your current memory usage (${(memoryUsage * 100).toFixed(0)}%) is above your threshold (${(memoryThreshold * 100).toFixed(0)}%)`, disk: () => `Your current disk usage: ${disk .map((d, idx) => `(Disk${idx}: ${(d.usage_percent * 100).toFixed(0)}%)`) - .join( - ", " - )} is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`, + .join(", ")} is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`, }; alertsToSend.push(formatAlert[type]()); } diff --git a/server/service/statusService.js b/server/service/infrastructure/statusService.js similarity index 94% rename from server/service/statusService.js rename to server/service/infrastructure/statusService.js index 3d7bb4543..1c91a407c 100755 --- a/server/service/statusService.js +++ b/server/service/infrastructure/statusService.js @@ -1,5 +1,5 @@ -import MonitorStats from "../db/models/MonitorStats.js"; -import { safelyParseFloat } from "../utils/dataUtils.js"; +import MonitorStats from "../../db/models/MonitorStats.js"; +import { safelyParseFloat } from "../../utils/dataUtils.js"; const SERVICE_NAME = "StatusService"; class StatusService { @@ -48,9 +48,7 @@ class StatusService { if (avgResponseTime === 0) { avgResponseTime = responseTime; } else { - avgResponseTime = - (avgResponseTime * (stats.totalChecks - 1) + responseTime) / - stats.totalChecks; + avgResponseTime = (avgResponseTime * (stats.totalChecks - 1) + responseTime) / stats.totalChecks; } } stats.avgResponseTime = avgResponseTime; @@ -138,9 +136,7 @@ class StatusService { // Monitor status changed, save prev status and update monitor this.logger.info({ service: this.SERVICE_NAME, - message: `${monitor.name} went from ${this.getStatusString( - monitor.status - )} to ${this.getStatusString(status)}`, + message: `${monitor.name} went from ${this.getStatusString(monitor.status)} to ${this.getStatusString(status)}`, prevStatus: monitor.status, newStatus: status, }); @@ -279,9 +275,7 @@ class StatusService { message: error.message, service: error.service || this.SERVICE_NAME, method: error.method || "insertCheck", - details: - error.details || - `Error inserting check for monitor: ${networkResponse?.monitorId}`, + details: error.details || `Error inserting check for monitor: ${networkResponse?.monitorId}`, stack: error.stack, }); } diff --git a/server/service/serviceRegistry.js b/server/service/system/serviceRegistry.js similarity index 93% rename from server/service/serviceRegistry.js rename to server/service/system/serviceRegistry.js index 69a3b8be5..5f8b3bfe6 100755 --- a/server/service/serviceRegistry.js +++ b/server/service/system/serviceRegistry.js @@ -1,5 +1,5 @@ const SERVICE_NAME = "ServiceRegistry"; -import logger from "../utils/logger.js"; +import logger from "../../utils/logger.js"; class ServiceRegistry { static SERVICE_NAME = SERVICE_NAME; constructor() { diff --git a/server/service/settingsService.js b/server/service/system/settingsService.js similarity index 93% rename from server/service/settingsService.js rename to server/service/system/settingsService.js index de035922c..d2c1a17ae 100755 --- a/server/service/settingsService.js +++ b/server/service/system/settingsService.js @@ -62,14 +62,10 @@ class SettingsService { // Remove any old settings await this.AppSettings.deleteMany({ version: { $exists: false } }); - let settings = await this.AppSettings.findOne({ singleton: true }) - .select("-__v -_id -createdAt -updatedAt -singleton") - .lean(); + let settings = await this.AppSettings.findOne({ singleton: true }).select("-__v -_id -createdAt -updatedAt -singleton").lean(); if (settings === null) { await this.AppSettings.create({}); - settings = await this.AppSettings.findOne({ singleton: true }) - .select("-__v -_id -createdAt -updatedAt -singleton") - .lean(); + settings = await this.AppSettings.findOne({ singleton: true }).select("-__v -_id -createdAt -updatedAt -singleton").lean(); } return settings; } diff --git a/server/service/stringService.js b/server/service/system/stringService.js similarity index 94% rename from server/service/stringService.js rename to server/service/system/stringService.js index 6f9ad8cd8..b88e73917 100755 --- a/server/service/stringService.js +++ b/server/service/system/stringService.js @@ -211,23 +211,16 @@ class StringService { } getWebhookUnsupportedPlatform(platform) { - return this.translationService - .getTranslation("webhookUnsupportedPlatform") - .replace("{platform}", platform); + return this.translationService.getTranslation("webhookUnsupportedPlatform").replace("{platform}", platform); } getWebhookSendError(platform) { - return this.translationService - .getTranslation("webhookSendError") - .replace("{platform}", platform); + return this.translationService.getTranslation("webhookSendError").replace("{platform}", platform); } getMonitorStatus(name, status, url) { const translationKey = status === true ? "monitorStatusUp" : "monitorStatusDown"; - return this.translationService - .getTranslation(translationKey) - .replace("{name}", name) - .replace("{url}", url); + return this.translationService.getTranslation(translationKey).replace("{name}", name).replace("{url}", url); } // Error Messages @@ -378,9 +371,7 @@ class StringService { } getDeletedCount(count) { - return this.translationService - .getTranslation("deletedCount") - .replace("{count}", count); + return this.translationService.getTranslation("deletedCount").replace("{count}", count); } get pingSuccess() { @@ -424,9 +415,7 @@ class StringService { } getDbFindMonitorById(monitorId) { - return this.translationService - .getTranslation("dbFindMonitorById") - .replace("${monitorId}", monitorId); + return this.translationService.getTranslation("dbFindMonitorById").replace("${monitorId}", monitorId); } get dbUserExists() { diff --git a/server/service/translationService.js b/server/service/system/translationService.js similarity index 94% rename from server/service/translationService.js rename to server/service/system/translationService.js index de29b5eb1..38346f35d 100755 --- a/server/service/translationService.js +++ b/server/service/system/translationService.js @@ -38,9 +38,7 @@ class TranslationService { return false; } - const files = fs - .readdirSync(this.localesDir) - .filter((file) => file.endsWith(".json")); + const files = fs.readdirSync(this.localesDir).filter((file) => file.endsWith(".json")); if (files.length === 0) { return false; diff --git a/server/service/userService.js b/server/service/userService.js new file mode 100644 index 000000000..3e1bf285f --- /dev/null +++ b/server/service/userService.js @@ -0,0 +1,208 @@ +const SERVICE_NAME = "userService"; +import { createAuthError, createError } from "../utils/errorUtils.js"; + +class UserService { + static SERVICE_NAME = SERVICE_NAME; + constructor({ db, emailService, settingsService, logger, stringService, jwt }) { + this.db = db; + this.emailService = emailService; + this.settingsService = settingsService; + this.logger = logger; + this.stringService = stringService; + this.jwt = jwt; + } + + issueToken = (payload, appSettings) => { + const tokenTTL = appSettings?.jwtTTL ?? "2h"; + const tokenSecret = appSettings?.jwtSecret; + const payloadData = payload; + return this.jwt.sign(payloadData, tokenSecret, { expiresIn: tokenTTL }); + }; + + registerUser = async (user, file) => { + // Create a new user + // If superAdmin exists, a token should be attached to all further register requests + const superAdminExists = await this.db.checkSuperadmin(); + if (superAdminExists) { + const invitedUser = await this.db.getInviteTokenAndDelete(user.inviteToken); + user.role = invitedUser.role; + user.teamId = invitedUser.teamId; + } else { + // This is the first account, create JWT secret to use if one is not supplied by env + const jwtSecret = crypto.randomBytes(64).toString("hex"); + await this.db.updateAppSettings({ jwtSecret }); + } + + const newUser = await this.db.insertUser({ ...user }, file); + + this.logger.debug({ + message: "New user created", + service: SERVICE_NAME, + method: "registerUser", + details: newUser._id, + }); + + const userForToken = { ...newUser._doc }; + delete userForToken.profileImage; + delete userForToken.avatarImage; + + const appSettings = await this.settingsService.getSettings(); + + const token = this.issueToken(userForToken, appSettings); + + try { + const html = await this.emailService.buildEmail("welcomeEmailTemplate", { + name: newUser.firstName, + }); + this.emailService.sendEmail(newUser.email, "Welcome to Uptime Monitor", html).catch((error) => { + this.logger.warn({ + message: error.message, + service: SERVICE_NAME, + method: "registerUser", + stack: error.stack, + }); + }); + } catch (error) { + this.logger.warn({ + message: error.message, + service: SERVICE_NAME, + method: "registerUser", + stack: error.stack, + }); + } + + return { user: newUser, token }; + }; + + loginUser = async (email, password) => { + // Check if user exists + const user = await this.db.getUserByEmail(email); + // Compare password + const match = await user.comparePassword(password); + if (match !== true) { + throw createAuthError(this.stringService.authIncorrectPassword); + } + + // Remove password from user object. Should this be abstracted to DB layer? + const userWithoutPassword = { ...user._doc }; + delete userWithoutPassword.password; + delete userWithoutPassword.avatarImage; + + // Happy path, return token + const appSettings = await this.settingsService.getSettings(); + const token = this.issueToken(userWithoutPassword, appSettings); + // reset avatar image + userWithoutPassword.avatarImage = user.avatarImage; + return { user: userWithoutPassword, token }; + }; + + editUser = async (updates, file, currentUser) => { + // Change Password check + if (updates?.password && updates?.newPassword) { + // Get user's email + // Add user email to body for DB operation + updates.email = currentUser.email; + // Get user + const user = await this.db.getUserByEmail(currentUser.email); + // Compare passwords + const match = await user.comparePassword(updates?.password); + // If not a match, throw a 403 + // 403 instead of 401 to avoid triggering axios interceptor + if (!match) { + throw createError(this.stringService.authIncorrectPassword, 403); + } + // If a match, update the password + updates.password = updates.newPassword; + } + + const updatedUser = await this.db.updateUser({ userId: currentUser?._id, user: updates, file: file }); + return updatedUser; + }; + + checkSuperadminExists = async () => { + const superAdminExists = await this.db.checkSuperadmin(); + return superAdminExists; + }; + + requestRecovery = async (email) => { + const user = await this.db.getUserByEmail(email); + const recoveryToken = await this.db.requestRecoveryToken(email); + const name = user.firstName; + const { clientHost } = this.settingsService.getSettings(); + const url = `${clientHost}/set-new-password/${recoveryToken.token}`; + + const html = await this.emailService.buildEmail("passwordResetTemplate", { + name, + email, + url, + }); + const msgId = await this.emailService.sendEmail(email, "Checkmate Password Reset", html); + return msgId; + }; + + validateRecovery = async (recoveryToken) => { + await this.db.validateRecoveryToken(recoveryToken); + }; + + resetPassword = async (password, recoveryToken) => { + const user = await this.db.resetPassword(password, recoveryToken); + const appSettings = await this.settingsService.getSettings(); + const token = this.issueToken(user._doc, appSettings); + return { user, token }; + }; + + deleteUser = async (user) => { + const email = user?.email; + if (!email) { + throw new Error("No email in request"); + } + + const teamId = user?.teamId; + const userId = user?._id; + + if (!teamId) { + throw new Error("No team ID in request"); + } + + if (!userId) { + throw new Error("No user ID in request"); + } + + const roles = user?.role; + if (roles.includes("demo")) { + throw new Error("Demo user cannot be deleted"); + } + + // 1. Find all the monitors associated with the team ID if superadmin + const result = await this.db.getMonitorsByTeamId({ + teamId: teamId, + }); + + if (roles.includes("superadmin")) { + // 2. Remove all jobs, delete checks and alerts + result?.monitors.length > 0 && + (await Promise.all( + result.monitors.map(async (monitor) => { + await this.jobQueue.deleteJob(monitor); + }) + )); + } + // 6. Delete the user by id + await this.db.deleteUser(userId); + }; + + getAllUsers = async () => { + const users = await this.db.getAllUsers(); + return users; + }; + + getUserById = async (roles, userId) => { + const user = await this.db.getUserById(roles, userId); + return user; + }; + + editUserById = async (userId, user) => { + await this.db.editUserById(userId, user); + }; +} +export default UserService; diff --git a/server/templates/addReview.mjml b/server/templates/addReview.mjml index 9d00be7d0..e193ac1ba 100755 --- a/server/templates/addReview.mjml +++ b/server/templates/addReview.mjml @@ -30,22 +30,17 @@ -

Hello {{name}}!

+

Hello {{ name }}!

- We hope you’re finding Checkmate helpful in monitoring your infrastructure. - Your support means a lot to us, and we truly appreciate having you as - part of our community. + We hope you’re finding Checkmate helpful in monitoring your infrastructure. Your support means a lot to us, and we + truly appreciate having you as part of our community.

- If you’re happy with Checkmate, we’d love to hear about your experience! - Leaving a review on G2 helps others discover Checkmate and supports our - ongoing improvements. + If you’re happy with Checkmate, we’d love to hear about your experience! Leaving a review on G2 helps others discover Checkmate and + supports our ongoing improvements.

G2 Link: TBD -

- Thank you for taking the time to share your thoughts - we greatly appreciate - it! -

+

Thank you for taking the time to share your thoughts - we greatly appreciate it!

Checkmate Team
diff --git a/server/templates/employeeActivation.mjml b/server/templates/employeeActivation.mjml index 3e35db6e8..317832c1f 100755 --- a/server/templates/employeeActivation.mjml +++ b/server/templates/employeeActivation.mjml @@ -29,10 +29,12 @@ -

Hello {{name}}!

+

Hello {{ name }}!

One of the admins created an account for you on the Checkmate server.

You can go ahead and create your account using this link.

-

{{link}}

+

+ {{ link }} +

Thank you.

diff --git a/server/templates/hardwareIncident.mjml b/server/templates/hardwareIncident.mjml index 19fb589fc..3f50a3d58 100755 --- a/server/templates/hardwareIncident.mjml +++ b/server/templates/hardwareIncident.mjml @@ -47,10 +47,10 @@ -

Hello {{name}}!

-

{{monitor}} at {{url}} has the following infrastructure alerts:

+

Hello {{ name }}!

+

{{ monitor }} at {{ url }} has the following infrastructure alerts:

{{#each alerts}} -

• {{this}}

+

• {{ this }}

{{/each}}
diff --git a/server/templates/noIncidentsThisWeek.mjml b/server/templates/noIncidentsThisWeek.mjml index d31d63d10..d160e44de 100755 --- a/server/templates/noIncidentsThisWeek.mjml +++ b/server/templates/noIncidentsThisWeek.mjml @@ -45,7 +45,7 @@ -

Hello {{name}}!

+

Hello {{ name }}!

There were no incidents this week. Good job!

Current monitors:

Google: 100% availability

diff --git a/server/templates/passwordReset.mjml b/server/templates/passwordReset.mjml index e0b85a4f4..568b363ee 100755 --- a/server/templates/passwordReset.mjml +++ b/server/templates/passwordReset.mjml @@ -31,10 +31,10 @@ -

Hello {{name}}!

+

Hello {{ name }}!

- You are receiving this email because a password reset request has been made - for {{email}}. Please use the link below on the site to reset your password. + You are receiving this email because a password reset request has been made for {{ email }}. Please use the link below on the site to + reset your password.

Reset Password

If you didn't request this, please ignore this email.

diff --git a/server/templates/serverIsDown.mjml b/server/templates/serverIsDown.mjml index b75d970e6..bf6424a6b 100755 --- a/server/templates/serverIsDown.mjml +++ b/server/templates/serverIsDown.mjml @@ -36,7 +36,7 @@ font-size="18px" color="red" > - {{monitor}} is down + {{ monitor }} is down
-

Hello {{name}}!

-

- We detected an incident on one of your monitors. Your service is currently - down. We'll send a message to you once it is up again. -

-

Monitor name: {{monitor}}

-

URL: {{url}}

+

Hello {{ name }}!

+

We detected an incident on one of your monitors. Your service is currently down. We'll send a message to you once it is up again.

+

Monitor name: {{ monitor }}

+

URL: {{ url }}

diff --git a/server/templates/serverIsUp.mjml b/server/templates/serverIsUp.mjml index beb34481d..074c5e3dd 100755 --- a/server/templates/serverIsUp.mjml +++ b/server/templates/serverIsUp.mjml @@ -36,7 +36,7 @@ font-size="18px" color="green" > - {{monitor}} is up + {{ monitor }} is up
-

Hello {{name}}!

+

Hello {{ name }}!

Your latest incident is resolved and your monitored service is up again.

-

Monitor name: {{monitor}}

-

URL: {{url}}

+

Monitor name: {{ monitor }}

+

URL: {{ url }}

diff --git a/server/templates/welcomeEmail.mjml b/server/templates/welcomeEmail.mjml index 663518ec2..e9f569e3f 100755 --- a/server/templates/welcomeEmail.mjml +++ b/server/templates/welcomeEmail.mjml @@ -30,15 +30,9 @@ -

Hello {{name}}!

-

- Thank you for trying out Checkmate! We developed it with great care to meet - our own needs, and we're excited to share it with you. -

-

- Checkmate is an automated way of checking whether a service such as a website - or an application is available or not. -

+

Hello {{ name }}!

+

Thank you for trying out Checkmate! We developed it with great care to meet our own needs, and we're excited to share it with you.

+

Checkmate is an automated way of checking whether a service such as a website or an application is available or not.

We hope you find our service as valuable as we do.

Thank you.

diff --git a/server/tests/controllers/authController.test.js b/server/tests/controllers/authController.test.js index 2990afe22..341c8eda6 100755 --- a/server/tests/controllers/authController.test.js +++ b/server/tests/controllers/authController.test.js @@ -28,9 +28,7 @@ describe("Auth Controller - issueToken", function () { stub = sinon.stub(jwt, "sign").throws(error); const payload = { id: "123" }; const appSettings = { jwtSecret: "my_secret" }; - expect(() => issueToken(payload, tokenType.ACCESS_TOKEN, appSettings)).to.throw( - error - ); + 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", function () { @@ -163,9 +161,7 @@ describe("Auth Controller - registerUser", function () { req.db.checkSuperadmin.resolves(false); req.db.updateAppSettings.resolves(); req.db.insertUser.resolves({ _id: "123" }); - req.settingsService.getSettings.rejects( - new Error("settingsService.getSettings error") - ); + req.settingsService.getSettings.rejects(new Error("settingsService.getSettings error")); await registerUser(req, res, next); expect(next.firstCall.args[0]).to.be.an("error"); expect(next.firstCall.args[0].message).to.equal("settingsService.getSettings error"); @@ -304,9 +300,7 @@ describe("Auth Controller - loginUser", function () { user.comparePassword.resolves(false); await loginUser(req, res, next); expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal( - errorMessages.AUTH_INCORRECT_PASSWORD - ); + expect(next.firstCall.args[0].message).to.equal(errorMessages.AUTH_INCORRECT_PASSWORD); }); }); @@ -370,9 +364,7 @@ describe("Auth Controller - refreshAuthToken", function () { }); it("should reject if settingsService.getSettings fails", async function () { - req.settingsService.getSettings.rejects( - new Error("settingsService.getSettings error") - ); + req.settingsService.getSettings.rejects(new Error("settingsService.getSettings error")); await refreshAuthToken(req, res, next); expect(next.firstCall.args[0]).to.be.an("error"); @@ -475,9 +467,7 @@ describe("Auth Controller - editUser", function () { await editUser(req, res, next); expect(next.firstCall.args[0]).to.be.an("error"); expect(next.firstCall.args[0].status).to.equal(401); - expect(next.firstCall.args[0].message).to.equal( - errorMessages.AUTH_INCORRECT_PASSWORD - ); + expect(next.firstCall.args[0].message).to.equal(errorMessages.AUTH_INCORRECT_PASSWORD); }); it("should edit a user if it receives a proper request", async function () { @@ -863,8 +853,7 @@ describe("Auth Controller - deleteUser", function () { ).to.be.true; expect(req.jobQueue.deleteJob.calledOnceWith(monitors[0])).to.be.true; expect(req.db.deleteChecks.calledOnceWith("monitor_id")).to.be.true; - expect(req.db.deletePageSpeedChecksByMonitorId.calledOnceWith("monitor_id")).to.be - .true; + expect(req.db.deletePageSpeedChecksByMonitorId.calledOnceWith("monitor_id")).to.be.true; expect(req.db.deleteNotificationsByMonitorId.calledOnceWith("monitor_id")).to.be.true; expect(req.db.deleteTeam.calledOnceWith("team_id")).to.be.true; expect(req.db.deleteAllOtherUsers.calledOnce).to.be.true; diff --git a/server/tests/controllers/checkController.test.js b/server/tests/controllers/checkController.test.js index 322e6c8ca..c5fb57d50 100755 --- a/server/tests/controllers/checkController.test.js +++ b/server/tests/controllers/checkController.test.js @@ -1,11 +1,4 @@ -import { - createCheck, - getChecks, - getTeamChecks, - deleteChecks, - deleteChecksByTeamId, - updateChecksTTL, -} from "../../controllers/checkController.js"; +import { createCheck, getChecks, getTeamChecks, deleteChecks, deleteChecksByTeamId, updateChecksTTL } from "../../controllers/checkController.js"; import jwt from "jsonwebtoken"; import { errorMessages, successMessages } from "../../utils/messages.js"; import sinon from "sinon"; diff --git a/server/tests/controllers/controllerUtils.test.js b/server/tests/controllers/controllerUtils.test.js index d594e932d..a55318857 100755 --- a/server/tests/controllers/controllerUtils.test.js +++ b/server/tests/controllers/controllerUtils.test.js @@ -1,10 +1,6 @@ import sinon from "sinon"; -import { - handleValidationError, - handleError, - fetchMonitorCertificate, -} from "../../controllers/controllerUtils.js"; +import { handleValidationError, handleError, fetchMonitorCertificate } from "../../controllers/controllerUtils.js"; import { expect } from "chai"; import sslChecker from "ssl-checker"; import { afterEach } from "node:test"; diff --git a/server/tests/controllers/inviteController.test.js b/server/tests/controllers/inviteController.test.js index 3311f715e..635bdf653 100755 --- a/server/tests/controllers/inviteController.test.js +++ b/server/tests/controllers/inviteController.test.js @@ -1,7 +1,4 @@ -import { - issueInvitation, - inviteVerifyController, -} from "../../controllers/inviteController.js"; +import { issueInvitation, inviteVerifyController } from "../../controllers/inviteController.js"; import jwt from "jsonwebtoken"; import sinon from "sinon"; import joi from "joi"; diff --git a/server/tests/controllers/monitorController.test.js b/server/tests/controllers/monitorController.test.js index 5aa68b514..963862579 100755 --- a/server/tests/controllers/monitorController.test.js +++ b/server/tests/controllers/monitorController.test.js @@ -623,9 +623,7 @@ describe("Monitor Controller - deleteMonitor", function () { req.jobQueue.deleteJob.rejects(error); await deleteMonitor(req, res, next); expect(logger.error.calledOnce).to.be.true; - expect(logger.error.firstCall.args[0].message).to.equal( - `Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}` - ); + expect(logger.error.firstCall.args[0].message).to.equal(`Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`); }); it("should log an error if deleteChecks throws an error", async function () { @@ -635,9 +633,7 @@ describe("Monitor Controller - deleteMonitor", function () { req.db.deleteChecks.rejects(error); await deleteMonitor(req, res, next); expect(logger.error.calledOnce).to.be.true; - expect(logger.error.firstCall.args[0].message).to.equal( - `Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}` - ); + expect(logger.error.firstCall.args[0].message).to.equal(`Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`); }); it("should log an error if deletePageSpeedChecksByMonitorId throws an error", async function () { @@ -647,9 +643,7 @@ describe("Monitor Controller - deleteMonitor", function () { req.db.deletePageSpeedChecksByMonitorId.rejects(error); await deleteMonitor(req, res, next); expect(logger.error.calledOnce).to.be.true; - expect(logger.error.firstCall.args[0].message).to.equal( - `Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}` - ); + expect(logger.error.firstCall.args[0].message).to.equal(`Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`); }); it("should log an error if deleteNotificationsByMonitorId throws an error", async function () { @@ -659,9 +653,7 @@ describe("Monitor Controller - deleteMonitor", function () { req.db.deleteNotificationsByMonitorId.rejects(error); await deleteMonitor(req, res, next); expect(logger.error.calledOnce).to.be.true; - expect(logger.error.firstCall.args[0].message).to.equal( - `Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}` - ); + expect(logger.error.firstCall.args[0].message).to.equal(`Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`); }); it("should return success message if all operations succeed", async function () { diff --git a/server/tests/controllers/queueController.test.js b/server/tests/controllers/queueController.test.js index 72450a0bd..6201bdb5a 100755 --- a/server/tests/controllers/queueController.test.js +++ b/server/tests/controllers/queueController.test.js @@ -1,10 +1,5 @@ import { afterEach } from "node:test"; -import { - getMetrics, - getJobs, - addJob, - obliterateQueue, -} from "../../controllers/queueController.js"; +import { getMetrics, getJobs, addJob, obliterateQueue } from "../../controllers/queueController.js"; import { successMessages } from "../../utils/messages.js"; import sinon from "sinon"; diff --git a/server/tests/controllers/settingsController.test.js b/server/tests/controllers/settingsController.test.js index 0f8e628af..721c41c02 100755 --- a/server/tests/controllers/settingsController.test.js +++ b/server/tests/controllers/settingsController.test.js @@ -1,8 +1,5 @@ import { afterEach } from "node:test"; -import { - getAppSettings, - updateAppSettings, -} from "../../controllers/settingsController.js"; +import { getAppSettings, updateAppSettings } from "../../controllers/settingsController.js"; import { successMessages } from "../../utils/messages.js"; import sinon from "sinon"; diff --git a/server/tests/controllers/statusPageController.test.js b/server/tests/controllers/statusPageController.test.js index 3247ca975..cada662e0 100755 --- a/server/tests/controllers/statusPageController.test.js +++ b/server/tests/controllers/statusPageController.test.js @@ -1,8 +1,5 @@ import sinon from "sinon"; -import { - createStatusPage, - getStatusPageByUrl, -} from "../../controllers/statusPageController.js"; +import { createStatusPage, getStatusPageByUrl } from "../../controllers/statusPageController.js"; describe("statusPageController", function () { let req, res, next; diff --git a/server/tests/db/checkModule.test.js b/server/tests/db/checkModule.test.js index 7ef2f5b77..6eef12445 100755 --- a/server/tests/db/checkModule.test.js +++ b/server/tests/db/checkModule.test.js @@ -216,11 +216,7 @@ describe("checkModule", function () { const result = await getChecksCount(req); expect(result).to.equal(4); - expect(checkCountDocumentStub.firstCall.args[0]).to.have.all.keys( - "monitorId", - "createdAt", - "status" - ); + expect(checkCountDocumentStub.firstCall.args[0]).to.have.all.keys("monitorId", "createdAt", "status"); }); }); diff --git a/server/tests/db/hardwareCheckModule.test.js b/server/tests/db/hardwareCheckModule.test.js index 566c45a8e..20ce197fb 100755 --- a/server/tests/db/hardwareCheckModule.test.js +++ b/server/tests/db/hardwareCheckModule.test.js @@ -52,10 +52,7 @@ const mockMonitor = { }; describe("HardwareCheckModule", function () { - let hardwareCheckSaveStub, - hardwareCheckCountDocumentsStub, - monitorFindByIdStub, - loggerStub; + let hardwareCheckSaveStub, hardwareCheckCountDocumentsStub, monitorFindByIdStub, loggerStub; beforeEach(function () { loggerStub = sinon.stub(logger, "error"); diff --git a/server/tests/db/inviteModule.test.js b/server/tests/db/inviteModule.test.js index 9b3aab9cc..e8913c983 100755 --- a/server/tests/db/inviteModule.test.js +++ b/server/tests/db/inviteModule.test.js @@ -1,10 +1,6 @@ import sinon from "sinon"; import InviteToken from "../../db/models/InviteToken.js"; -import { - requestInviteToken, - getInviteToken, - getInviteTokenAndDelete, -} from "../../db/mongo/modules/inviteModule.js"; +import { requestInviteToken, getInviteToken, getInviteTokenAndDelete } from "../../db/mongo/modules/inviteModule.js"; import { errorMessages } from "../../utils/messages.js"; describe("Invite Module", function () { @@ -15,10 +11,7 @@ describe("Invite Module", function () { token: "123", }; const mockInviteToken = { _id: 123, time: 123 }; - let inviteTokenDeleteManyStub, - inviteTokenSaveStub, - inviteTokenFindOneStub, - inviteTokenFindOneAndDeleteStub; + let inviteTokenDeleteManyStub, inviteTokenSaveStub, inviteTokenFindOneStub, inviteTokenFindOneAndDeleteStub; beforeEach(function () { inviteTokenDeleteManyStub = sinon.stub(InviteToken, "deleteMany"); diff --git a/server/tests/db/maintenanceWindowModule.test.js b/server/tests/db/maintenanceWindowModule.test.js index 842c283c8..576c97f84 100755 --- a/server/tests/db/maintenanceWindowModule.test.js +++ b/server/tests/db/maintenanceWindowModule.test.js @@ -40,15 +40,9 @@ describe("MaintenanceWindow Module", function () { }), }), }); - maintenanceWindowFindByIdAndDeleteStub = sinon.stub( - MaintenanceWindow, - "findByIdAndDelete" - ); + maintenanceWindowFindByIdAndDeleteStub = sinon.stub(MaintenanceWindow, "findByIdAndDelete"); maintenanceWindowDeleteManyStub = sinon.stub(MaintenanceWindow, "deleteMany"); - maintenanceWindowFindByIdAndUpdateStub = sinon.stub( - MaintenanceWindow, - "findByIdAndUpdate" - ); + maintenanceWindowFindByIdAndUpdateStub = sinon.stub(MaintenanceWindow, "findByIdAndUpdate"); }); afterEach(function () { @@ -110,10 +104,7 @@ describe("MaintenanceWindow Module", function () { it("should return a list of maintenance windows and count", async function () { maintenanceWindowCountDocumentsStub.resolves(1); - const result = await getMaintenanceWindowsByTeamId( - mockMaintenanceWindow.teamId, - query - ); + const result = await getMaintenanceWindowsByTeamId(mockMaintenanceWindow.teamId, query); expect(result).to.deep.equal({ maintenanceWindows: mockMaintenanceWindows, maintenanceWindowCount: 1, @@ -123,10 +114,7 @@ describe("MaintenanceWindow Module", function () { it("should return a list of maintenance windows and count with empty query", async function () { query = undefined; maintenanceWindowCountDocumentsStub.resolves(1); - const result = await getMaintenanceWindowsByTeamId( - mockMaintenanceWindow.teamId, - query - ); + const result = await getMaintenanceWindowsByTeamId(mockMaintenanceWindow.teamId, query); expect(result).to.deep.equal({ maintenanceWindows: mockMaintenanceWindows, maintenanceWindowCount: 1, @@ -137,10 +125,7 @@ describe("MaintenanceWindow Module", function () { query.page = undefined; query.rowsPerPage = undefined; maintenanceWindowCountDocumentsStub.resolves(1); - const result = await getMaintenanceWindowsByTeamId( - mockMaintenanceWindow.teamId, - query - ); + const result = await getMaintenanceWindowsByTeamId(mockMaintenanceWindow.teamId, query); expect(result).to.deep.equal({ maintenanceWindows: mockMaintenanceWindows, maintenanceWindowCount: 1, @@ -150,10 +135,7 @@ describe("MaintenanceWindow Module", function () { it("should return a list of maintenance windows and count with field and desc order", async function () { query.order = "desc"; maintenanceWindowCountDocumentsStub.resolves(1); - const result = await getMaintenanceWindowsByTeamId( - mockMaintenanceWindow.teamId, - query - ); + const result = await getMaintenanceWindowsByTeamId(mockMaintenanceWindow.teamId, query); expect(result).to.deep.equal({ maintenanceWindows: mockMaintenanceWindows, maintenanceWindowCount: 1, @@ -163,10 +145,7 @@ describe("MaintenanceWindow Module", function () { it("should return a list of maintenance windows and count no field", async function () { query.field = undefined; maintenanceWindowCountDocumentsStub.resolves(1); - const result = await getMaintenanceWindowsByTeamId( - mockMaintenanceWindow.teamId, - query - ); + const result = await getMaintenanceWindowsByTeamId(mockMaintenanceWindow.teamId, query); expect(result).to.deep.equal({ maintenanceWindows: mockMaintenanceWindows, maintenanceWindowCount: 1, @@ -187,9 +166,7 @@ describe("MaintenanceWindow Module", function () { describe("getMaintenanceWindowsByMonitorId", function () { it("should return a list of maintenance windows", async function () { maintenanceWindowFindStub.resolves(mockMaintenanceWindows); - const result = await getMaintenanceWindowsByMonitorId( - mockMaintenanceWindow.monitorId - ); + const result = await getMaintenanceWindowsByMonitorId(mockMaintenanceWindow.monitorId); expect(result).to.deep.equal(mockMaintenanceWindows); }); @@ -225,9 +202,7 @@ describe("MaintenanceWindow Module", function () { describe("deleteMaintenanceWindowByMonitorId", function () { it("should return the number of documents deleted", async function () { maintenanceWindowDeleteManyStub.resolves({ deletedCount: 1 }); - const result = await deleteMaintenanceWindowByMonitorId( - mockMaintenanceWindow.monitorId - ); + const result = await deleteMaintenanceWindowByMonitorId(mockMaintenanceWindow.monitorId); expect(result).to.deep.equal({ deletedCount: 1 }); }); @@ -263,10 +238,7 @@ describe("MaintenanceWindow Module", function () { describe("editMaintenanceWindowById", function () { it("should return the updated maintenance window", async function () { maintenanceWindowFindByIdAndUpdateStub.resolves(mockMaintenanceWindow); - const result = await editMaintenanceWindowById( - mockMaintenanceWindow.id, - mockMaintenanceWindow - ); + const result = await editMaintenanceWindowById(mockMaintenanceWindow.id, mockMaintenanceWindow); expect(result).to.deep.equal(mockMaintenanceWindow); }); diff --git a/server/tests/db/monitorModule.test.js b/server/tests/db/monitorModule.test.js index 3d9bb5324..e59c352cf 100755 --- a/server/tests/db/monitorModule.test.js +++ b/server/tests/db/monitorModule.test.js @@ -120,12 +120,7 @@ describe("monitorModule", function () { }, ]; - const mockChecks = [ - { status: true }, - { status: true }, - { status: false }, - { status: true }, - ]; + const mockChecks = [{ status: true }, { status: true }, { status: false }, { status: true }]; monitorFindStub.resolves(mockMonitors); checkFindStub.resolves(mockChecks); @@ -166,12 +161,7 @@ describe("monitorModule", function () { }, ]; - const mockChecks = [ - { status: true }, - { status: true }, - { status: false }, - { status: true }, - ]; + const mockChecks = [{ status: true }, { status: true }, { status: false }, { status: true }]; monitorFindStub.resolves(mockMonitors); pageSpeedCheckFindStub.resolves(mockChecks); @@ -212,12 +202,7 @@ describe("monitorModule", function () { }, ]; - const mockChecks = [ - { status: true }, - { status: true }, - { status: false }, - { status: true }, - ]; + const mockChecks = [{ status: true }, { status: true }, { status: false }, { status: true }]; monitorFindStub.resolves(mockMonitors); hardwareCheckFindStub.resolves(mockChecks); @@ -400,10 +385,7 @@ describe("monitorModule", function () { }); it("should handle missing responseTime in checks", function () { - const checks = [ - { createdAt: "2024-01-01T11:30:00Z" }, - { responseTime: 200, createdAt: "2024-01-01T11:00:00Z" }, - ]; + const checks = [{ createdAt: "2024-01-01T11:30:00Z" }, { responseTime: 200, createdAt: "2024-01-01T11:00:00Z" }]; expect(getLatestResponseTime(checks)).to.equal(0); }); @@ -441,10 +423,7 @@ describe("monitorModule", function () { }); it("should return 0 when no checks have responseTime", function () { - const checks = [ - { createdAt: "2024-01-01T11:30:00Z" }, - { createdAt: "2024-01-01T11:00:00Z" }, - ]; + const checks = [{ createdAt: "2024-01-01T11:30:00Z" }, { createdAt: "2024-01-01T11:00:00Z" }]; expect(getAverageResponseTime(checks)).to.equal(0); }); @@ -470,12 +449,7 @@ describe("monitorModule", function () { }); it("should calculate correct percentage for mixed status checks", function () { - const checks = [ - { status: true }, - { status: false }, - { status: true }, - { status: true }, - ]; + const checks = [{ status: true }, { status: false }, { status: true }, { status: true }]; // 3 up out of 4 total = 75% expect(getUptimePercentage(checks)).to.equal(75); }); @@ -507,23 +481,12 @@ describe("monitorModule", function () { }); it("should count correct number of incidents for mixed status checks", function () { - const checks = [ - { status: true }, - { status: false }, - { status: true }, - { status: false }, - { status: true }, - ]; + const checks = [{ status: true }, { status: false }, { status: true }, { status: false }, { status: true }]; expect(getIncidents(checks)).to.equal(2); }); it("should handle undefined status values", function () { - const checks = [ - { status: true }, - { status: undefined }, - { status: false }, - { status: false }, - ]; + const checks = [{ status: true }, { status: undefined }, { status: false }, { status: false }]; // Only counts explicit false values expect(getIncidents(checks)).to.equal(2); }); @@ -628,12 +591,8 @@ describe("monitorModule", function () { ); // Assert - expect(result.checksAll[0].createdAt).to.be.greaterThan( - result.checksAll[1].createdAt - ); - expect(result.checksForDateRange[0].createdAt).to.be.greaterThan( - result.checksForDateRange[1].createdAt - ); + expect(result.checksAll[0].createdAt).to.be.greaterThan(result.checksAll[1].createdAt); + expect(result.checksForDateRange[0].createdAt).to.be.greaterThan(result.checksForDateRange[1].createdAt); }); }); @@ -753,10 +712,7 @@ describe("monitorModule", function () { }); it("should handle checks in same time group", function () { - const checksInSameHour = [ - { createdAt: "2024-01-15T10:15:00Z" }, - { createdAt: "2024-01-15T10:45:00Z" }, - ]; + const checksInSameHour = [{ createdAt: "2024-01-15T10:15:00Z" }, { createdAt: "2024-01-15T10:45:00Z" }]; const result = groupChecksByTime(checksInSameHour, "day"); @@ -816,11 +772,7 @@ describe("monitorModule", function () { it("should handle missing responseTime values", function () { const mockGroup = { time: "2024-01-15", - checks: [ - { status: true }, - { status: false, responseTime: 200 }, - { status: true, responseTime: undefined }, - ], + checks: [{ status: true }, { status: false, responseTime: 200 }, { status: true, responseTime: undefined }], }; const result = calculateGroupStats(mockGroup, uptimePercentageStub); @@ -1413,10 +1365,7 @@ describe("monitorModule", function () { expect(Monitor.find.firstCall.args[0]).to.deep.equal({ teamId: "team123", - $or: [ - { name: { $regex: "search", $options: "i" } }, - { url: { $regex: "search", $options: "i" } }, - ], + $or: [{ name: { $regex: "search", $options: "i" } }, { url: { $regex: "search", $options: "i" } }], }); }); @@ -1816,14 +1765,9 @@ describe("monitorModule", function () { // Assert expect(result).to.deep.equal(mockUpdatedMonitor); - sinon.assert.calledWith( - monitorFindByIdAndUpdateStub, - candidateId, - expectedUpdateData, - { - new: true, - } - ); + sinon.assert.calledWith(monitorFindByIdAndUpdateStub, candidateId, expectedUpdateData, { + new: true, + }); }); it("should return null when monitor not found", async function () { @@ -1840,12 +1784,7 @@ describe("monitorModule", function () { // Assert expect(result).to.be.null; - sinon.assert.calledWith( - monitorFindByIdAndUpdateStub, - candidateId, - { name: "Updated Monitor", notifications: undefined }, - { new: true } - ); + sinon.assert.calledWith(monitorFindByIdAndUpdateStub, candidateId, { name: "Updated Monitor", notifications: undefined }, { new: true }); }); it("should remove notifications from update data", async function () { @@ -1870,14 +1809,9 @@ describe("monitorModule", function () { await editMonitor(candidateId, candidateMonitor); // Assert - sinon.assert.calledWith( - monitorFindByIdAndUpdateStub, - candidateId, - expectedUpdateData, - { - new: true, - } - ); + sinon.assert.calledWith(monitorFindByIdAndUpdateStub, candidateId, expectedUpdateData, { + new: true, + }); }); it("should handle database errors", async function () { diff --git a/server/tests/db/notificationModule.test.js b/server/tests/db/notificationModule.test.js index 3128130f8..fbb97f33a 100755 --- a/server/tests/db/notificationModule.test.js +++ b/server/tests/db/notificationModule.test.js @@ -1,10 +1,6 @@ import sinon from "sinon"; import Notification from "../../db/models/Notification.js"; -import { - createNotification, - getNotificationsByMonitorId, - deleteNotificationsByMonitorId, -} from "../../db/mongo/modules/notificationModule.js"; +import { createNotification, getNotificationsByMonitorId, deleteNotificationsByMonitorId } from "../../db/mongo/modules/notificationModule.js"; describe("notificationModule", function () { const mockNotification = { diff --git a/server/tests/db/pageSpeedCheckModule.test.js b/server/tests/db/pageSpeedCheckModule.test.js index 5ef0e1845..45135135d 100755 --- a/server/tests/db/pageSpeedCheckModule.test.js +++ b/server/tests/db/pageSpeedCheckModule.test.js @@ -1,9 +1,6 @@ import sinon from "sinon"; import PageSpeedCheck from "../../db/models/PageSpeedCheck.js"; -import { - createPageSpeedCheck, - deletePageSpeedChecksByMonitorId, -} from "../../db/mongo/modules/pageSpeedCheckModule.js"; +import { createPageSpeedCheck, deletePageSpeedChecksByMonitorId } from "../../db/mongo/modules/pageSpeedCheckModule.js"; const mockPageSpeedCheck = { monitorId: "monitorId", diff --git a/server/tests/db/recoveryModule.test.js b/server/tests/db/recoveryModule.test.js index 3f4e11c5c..88afa257a 100755 --- a/server/tests/db/recoveryModule.test.js +++ b/server/tests/db/recoveryModule.test.js @@ -1,11 +1,7 @@ import sinon from "sinon"; import RecoveryToken from "../../db/models/RecoveryToken.js"; import User from "../../db/models/User.js"; -import { - requestRecoveryToken, - validateRecoveryToken, - resetPassword, -} from "../../db/mongo/modules/recoveryModule.js"; +import { requestRecoveryToken, validateRecoveryToken, resetPassword } from "../../db/mongo/modules/recoveryModule.js"; import { errorMessages } from "../../utils/messages.js"; const mockRecoveryToken = { @@ -44,12 +40,7 @@ const createQueryChain = (finalResult, comparePasswordResult = false) => ({ }); describe("recoveryModule", function () { - let deleteManyStub, - saveStub, - findOneStub, - userCompareStub, - userSaveStub, - userFindOneStub; + let deleteManyStub, saveStub, findOneStub, userCompareStub, userSaveStub, userFindOneStub; let req, res; beforeEach(function () { @@ -153,9 +144,7 @@ describe("recoveryModule", function () { it("should throw an error if the passwords match", async function () { findOneStub.resolves(mockRecoveryToken); saveStub.resolves(); - userFindOneStub = sinon - .stub(User, "findOne") - .returns(createQueryChain(mockUser, true)); + userFindOneStub = sinon.stub(User, "findOne").returns(createQueryChain(mockUser, true)); try { await resetPassword(req, res); } catch (error) { diff --git a/server/tests/db/settingsModule.test.js b/server/tests/db/settingsModule.test.js index 7651adb4b..9d1a80c3e 100755 --- a/server/tests/db/settingsModule.test.js +++ b/server/tests/db/settingsModule.test.js @@ -1,8 +1,5 @@ import sinon from "sinon"; -import { - getAppSettings, - updateAppSettings, -} from "../../db/mongo/modules/settingsModule.js"; +import { getAppSettings, updateAppSettings } from "../../db/mongo/modules/settingsModule.js"; import AppSettings from "../../db/models/AppSettings.js"; const mockAppSettings = { diff --git a/server/tests/db/statusPageModule.test.js b/server/tests/db/statusPageModule.test.js index 60c356aa5..54e1a287f 100755 --- a/server/tests/db/statusPageModule.test.js +++ b/server/tests/db/statusPageModule.test.js @@ -1,8 +1,5 @@ import sinon from "sinon"; -import { - createStatusPage, - getStatusPageByUrl, -} from "../../db/mongo/modules/statusPageModule.js"; +import { createStatusPage, getStatusPageByUrl } from "../../db/mongo/modules/statusPageModule.js"; import StatusPage from "../../db/models/StatusPage.js"; import { errorMessages } from "../../utils/messages.js"; diff --git a/server/tests/db/userModule.test.js b/server/tests/db/userModule.test.js index 884956995..d0f5ecb3e 100755 --- a/server/tests/db/userModule.test.js +++ b/server/tests/db/userModule.test.js @@ -157,12 +157,7 @@ describe("userModule", function () { select: sinon.stub().resolves(mockUser), }), }); - const result = await updateUser( - req, - res, - parseBooleanStub, - generateAvatarImageStub - ); + const result = await updateUser(req, res, parseBooleanStub, generateAvatarImageStub); expect(result).to.deep.equal(mockUser); }); @@ -173,12 +168,7 @@ describe("userModule", function () { select: sinon.stub().resolves(mockUser), }), }); - const result = await updateUser( - req, - res, - parseBooleanStub, - generateAvatarImageStub - ); + const result = await updateUser(req, res, parseBooleanStub, generateAvatarImageStub); expect(result).to.deep.equal(mockUser); }); diff --git a/server/tests/services/emailService.test.js b/server/tests/services/emailService.test.js index 541789d99..cca86896a 100755 --- a/server/tests/services/emailService.test.js +++ b/server/tests/services/emailService.test.js @@ -48,15 +48,7 @@ describe("EmailService - Constructor", function () { }); it("should initialize template loaders and email transporter", function () { - const emailService = new EmailService( - settingsServiceMock, - fsMock, - pathMock, - compileMock, - mjml2htmlMock, - nodemailerMock, - loggerMock - ); + const emailService = new EmailService(settingsServiceMock, fsMock, pathMock, compileMock, mjml2htmlMock, nodemailerMock, loggerMock); // Verify that the settingsService is assigned correctly expect(emailService.settingsService).to.equal(settingsServiceMock); @@ -83,15 +75,7 @@ describe("EmailService - Constructor", function () { fsMock = { readFileSync: sinon.stub().throws(new Error("File read error")), }; - const emailService = new EmailService( - settingsServiceMock, - fsMock, - pathMock, - compileMock, - mjml2htmlMock, - nodemailerMock, - loggerMock - ); + const emailService = new EmailService(settingsServiceMock, fsMock, pathMock, compileMock, mjml2htmlMock, nodemailerMock, loggerMock); expect(loggerMock.error.called).to.be.true; expect(loggerMock.error.firstCall.args[0].message).to.equal("File read error"); }); @@ -139,15 +123,7 @@ describe("EmailService - buildAndSendEmail", function () { error: sinon.stub(), }; - emailService = new EmailService( - settingsServiceMock, - fsMock, - pathMock, - compileMock, - mjml2htmlMock, - nodemailerMock, - loggerMock - ); + emailService = new EmailService(settingsServiceMock, fsMock, pathMock, compileMock, mjml2htmlMock, nodemailerMock, loggerMock); }); afterEach(function () { @@ -155,12 +131,7 @@ describe("EmailService - buildAndSendEmail", function () { }); it("should build and send email successfully", async function () { - const messageId = await emailService.buildAndSendEmail( - "welcomeEmailTemplate", - {}, - "recipient@example.com", - "Welcome" - ); + const messageId = await emailService.buildAndSendEmail("welcomeEmailTemplate", {}, "recipient@example.com", "Welcome"); expect(messageId).to.equal("12345"); expect(nodemailerMock.createTransport().sendMail.calledOnce).to.be.true; @@ -169,24 +140,14 @@ describe("EmailService - buildAndSendEmail", function () { it("should log error if building HTML fails", async function () { mjml2htmlMock.throws(new Error("MJML error")); - const messageId = await emailService.buildAndSendEmail( - "welcomeEmailTemplate", - {}, - "recipient@example.com", - "Welcome" - ); + const messageId = await emailService.buildAndSendEmail("welcomeEmailTemplate", {}, "recipient@example.com", "Welcome"); expect(loggerMock.error.calledOnce).to.be.true; expect(loggerMock.error.getCall(0).args[0].message).to.equal("MJML error"); }); it("should log error if sending email fails", async function () { nodemailerMock.createTransport().sendMail.rejects(new Error("SMTP error")); - await emailService.buildAndSendEmail( - "welcomeEmailTemplate", - {}, - "recipient@example.com", - "Welcome" - ); + await emailService.buildAndSendEmail("welcomeEmailTemplate", {}, "recipient@example.com", "Welcome"); expect(loggerMock.error.calledOnce).to.be.true; expect(loggerMock.error.getCall(0).args[0].message).to.equal("SMTP error"); }); @@ -195,12 +156,7 @@ describe("EmailService - buildAndSendEmail", function () { mjml2htmlMock.throws(new Error("MJML error")); nodemailerMock.createTransport().sendMail.rejects(new Error("SMTP error")); - const messageId = await emailService.buildAndSendEmail( - "welcomeEmailTemplate", - {}, - "recipient@example.com", - "Welcome" - ); + const messageId = await emailService.buildAndSendEmail("welcomeEmailTemplate", {}, "recipient@example.com", "Welcome"); expect(messageId).to.be.undefined; expect(loggerMock.error.calledTwice).to.be.true; diff --git a/server/tests/services/jobQueue.test.js b/server/tests/services/jobQueue.test.js index 88d57f339..155f9119e 100755 --- a/server/tests/services/jobQueue.test.js +++ b/server/tests/services/jobQueue.test.js @@ -52,13 +52,7 @@ class WorkerStub { } describe("JobQueue", function () { - let settingsService, - logger, - db, - networkService, - statusService, - notificationService, - jobQueue; + let settingsService, logger, db, networkService, statusService, notificationService, jobQueue; beforeEach(async function () { settingsService = { getSettings: sinon.stub() }; @@ -71,16 +65,7 @@ describe("JobQueue", function () { getMaintenanceWindowsByMonitorId: sinon.stub().returns([]), }; networkService = { getStatus: sinon.stub() }; - jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); + jobQueue = await JobQueue.createJobQueue(db, networkService, statusService, notificationService, settingsService, logger, QueueStub, WorkerStub); }); afterEach(function () { @@ -327,9 +312,7 @@ describe("JobQueue", function () { const handler = jobQueue.createJobHandler(); await handler({ data: { _id: 1 } }); expect(logger.info.calledOnce).to.be.true; - expect(logger.info.firstCall.args[0].message).to.equal( - "Monitor 1 is in maintenance window" - ); + expect(logger.info.firstCall.args[0].message).to.equal("Monitor 1 is in maintenance window"); }); it("should return if status has not changed", async function () { @@ -362,9 +345,7 @@ describe("JobQueue", function () { WorkerStub ); jobQueue.isInMaintenanceWindow = sinon.stub().returns(false); - statusService.updateStatus = sinon - .stub() - .returns({ statusChanged: true, prevStatus: undefined }); + statusService.updateStatus = sinon.stub().returns({ statusChanged: true, prevStatus: undefined }); const handler = jobQueue.createJobHandler(); await handler({ data: { _id: 1 } }); expect(jobQueue.notificationService.handleNotifications.notCalled).to.be.true; @@ -382,9 +363,7 @@ describe("JobQueue", function () { WorkerStub ); jobQueue.isInMaintenanceWindow = sinon.stub().returns(false); - statusService.updateStatus = sinon - .stub() - .returns({ statusChanged: true, prevStatus: false }); + statusService.updateStatus = sinon.stub().returns({ statusChanged: true, prevStatus: false }); const handler = jobQueue.createJobHandler(); await handler({ data: { _id: 1 } }); expect(jobQueue.notificationService.handleNotifications.calledOnce).to.be.true; diff --git a/server/tests/services/networkService.test.js b/server/tests/services/networkService.test.js index 44ddf8576..cb48ed9b2 100755 --- a/server/tests/services/networkService.test.js +++ b/server/tests/services/networkService.test.js @@ -26,9 +26,7 @@ describe("Network Service", function () { }; ping = { promise: { - probe: sinon - .stub() - .resolves({ response: { alive: true }, responseTime: 100, alive: true }), + probe: sinon.stub().resolves({ response: { alive: true }, responseTime: 100, alive: true }), }, }; logger = { error: sinon.stub() }; @@ -73,9 +71,7 @@ describe("Network Service", function () { it("should return a response object if ping unsuccessful", async function () { const error = new Error("Test error"); - networkService.timeRequest = sinon - .stub() - .resolves({ response: null, responseTime: 1, error }); + networkService.timeRequest = sinon.stub().resolves({ response: null, responseTime: 1, error }); const pingResult = await networkService.requestPing({ data: { url: "http://test.com", _id: "123" }, }); @@ -113,9 +109,7 @@ describe("Network Service", function () { it("should return a response object if http unsuccessful", async function () { const error = new Error("Test error"); error.response = { status: 404 }; - networkService.timeRequest = sinon - .stub() - .resolves({ response: null, responseTime: 1, error }); + networkService.timeRequest = sinon.stub().resolves({ response: null, responseTime: 1, error }); const job = { data: { url: "http://test.com", _id: "123", type: "http" } }; const httpResult = await networkService.requestHttp(job); expect(httpResult.monitorId).to.equal("123"); @@ -128,9 +122,7 @@ describe("Network Service", function () { it("should return a response object if http unsuccessful with unknown code", async function () { const error = new Error("Test error"); error.response = {}; - networkService.timeRequest = sinon - .stub() - .resolves({ response: null, responseTime: 1, error }); + networkService.timeRequest = sinon.stub().resolves({ response: null, responseTime: 1, error }); const job = { data: { url: "http://test.com", _id: "123", type: "http" } }; const httpResult = await networkService.requestHttp(job); expect(httpResult.monitorId).to.equal("123"); @@ -167,9 +159,7 @@ describe("Network Service", function () { it("should return a response object if pagespeed unsuccessful", async function () { const error = new Error("Test error"); error.response = { status: 404 }; - networkService.timeRequest = sinon - .stub() - .resolves({ response: null, responseTime: 1, error }); + networkService.timeRequest = sinon.stub().resolves({ response: null, responseTime: 1, error }); const job = { data: { url: "http://test.com", _id: "123", type: "pagespeed" } }; const pagespeedResult = await networkService.requestPagespeed(job); expect(pagespeedResult.monitorId).to.equal("123"); @@ -182,9 +172,7 @@ describe("Network Service", function () { it("should return a response object if pagespeed unsuccessful with an unknown code", async function () { const error = new Error("Test error"); error.response = {}; - networkService.timeRequest = sinon - .stub() - .resolves({ response: null, responseTime: 1, error }); + networkService.timeRequest = sinon.stub().resolves({ response: null, responseTime: 1, error }); const job = { data: { url: "http://test.com", _id: "123", type: "pagespeed" } }; const pagespeedResult = await networkService.requestPagespeed(job); expect(pagespeedResult.monitorId).to.equal("123"); @@ -237,9 +225,7 @@ describe("Network Service", function () { it("should return a response object if hardware unsuccessful", async function () { const error = new Error("Test error"); error.response = { status: 404 }; - networkService.timeRequest = sinon - .stub() - .resolves({ response: null, responseTime: 1, error }); + networkService.timeRequest = sinon.stub().resolves({ response: null, responseTime: 1, error }); const job = { data: { url: "http://test.com", _id: "123", type: "hardware" } }; const httpResult = await networkService.requestHardware(job); expect(httpResult.monitorId).to.equal("123"); @@ -252,9 +238,7 @@ describe("Network Service", function () { it("should return a response object if hardware unsuccessful with unknown code", async function () { const error = new Error("Test error"); error.response = {}; - networkService.timeRequest = sinon - .stub() - .resolves({ response: null, responseTime: 1, error }); + networkService.timeRequest = sinon.stub().resolves({ response: null, responseTime: 1, error }); const job = { data: { url: "http://test.com", _id: "123", type: "hardware" } }; const httpResult = await networkService.requestHardware(job); expect(httpResult.monitorId).to.equal("123"); diff --git a/server/tests/services/notificationService.test.js b/server/tests/services/notificationService.test.js index 207b40db0..211cc8b46 100755 --- a/server/tests/services/notificationService.test.js +++ b/server/tests/services/notificationService.test.js @@ -43,10 +43,7 @@ describe("NotificationService", function () { await notificationService.sendEmail(networkResponse, address); expect(notificationService.emailService.buildAndSendEmail.calledOnce).to.be.true; expect( - notificationService.emailService.buildAndSendEmail.calledWith( - "serverIsUpTemplate", - { monitor: "Test Monitor", url: "http://test.com" } - ) + notificationService.emailService.buildAndSendEmail.calledWith("serverIsUpTemplate", { monitor: "Test Monitor", url: "http://test.com" }) ); }); @@ -132,22 +129,14 @@ describe("NotificationService", function () { it("should send an email notification with Hardware Template", async function () { emailService.buildAndSendEmail.resolves(true); - const res = await notificationService.sendHardwareEmail( - networkResponse, - address, - alerts - ); + const res = await notificationService.sendHardwareEmail(networkResponse, address, alerts); expect(res).to.be.true; }); it("should return false if no alerts are provided", async function () { alerts = []; emailService.buildAndSendEmail.resolves(true); - const res = await notificationService.sendHardwareEmail( - networkResponse, - address, - alerts - ); + const res = await notificationService.sendHardwareEmail(networkResponse, address, alerts); expect(res).to.be.false; }); }); @@ -172,9 +161,7 @@ describe("NotificationService", function () { }); it("should handle status notifications", async function () { - db.getNotificationsByMonitorId.resolves([ - { type: "email", address: "test@test.com" }, - ]); + db.getNotificationsByMonitorId.resolves([{ type: "email", address: "test@test.com" }]); const res = await notificationService.handleStatusNotifications(networkResponse); expect(res).to.be.true; }); @@ -247,15 +234,13 @@ describe("NotificationService", function () { describe("it should return false if no thresholds are set", function () { it("should return false if no thresholds are set", async function () { networkResponse.monitor.thresholds = undefined; - const res = - await notificationService.handleHardwareNotifications(networkResponse); + const res = await notificationService.handleHardwareNotifications(networkResponse); expect(res).to.be.false; }); it("should return false if metrics are null", async function () { networkResponse.payload.data = null; - const res = - await notificationService.handleHardwareNotifications(networkResponse); + const res = await notificationService.handleHardwareNotifications(networkResponse); expect(res).to.be.false; }); @@ -271,8 +256,7 @@ describe("NotificationService", function () { save: sinon.stub().resolves(), }, ]); - const res = - await notificationService.handleHardwareNotifications(networkResponse); + const res = await notificationService.handleHardwareNotifications(networkResponse); expect(res).to.be.true; }); @@ -293,8 +277,7 @@ describe("NotificationService", function () { usage_memory: 0.01, usage_disk: 0.01, }; - const res = - await notificationService.handleHardwareNotifications(networkResponse); + const res = await notificationService.handleHardwareNotifications(networkResponse); expect(res).to.be.true; }); }); diff --git a/server/tests/services/settingsService.test.js b/server/tests/services/settingsService.test.js index 3e04ced38..6c9960e5e 100755 --- a/server/tests/services/settingsService.test.js +++ b/server/tests/services/settingsService.test.js @@ -13,9 +13,7 @@ describe("SettingsService", function () { sandbox.stub(process.env, "JWT_SECRET").value("secret"); sandbox.stub(process.env, "REFRESH_TOKEN_SECRET").value("refreshSecret"); sandbox.stub(process.env, "DB_TYPE").value("postgres"); - sandbox - .stub(process.env, "DB_CONNECTION_STRING") - .value("postgres://user:pass@localhost/db"); + sandbox.stub(process.env, "DB_CONNECTION_STRING").value("postgres://user:pass@localhost/db"); sandbox.stub(process.env, "REDIS_HOST").value("localhost"); sandbox.stub(process.env, "REDIS_PORT").value("6379"); sandbox.stub(process.env, "TOKEN_TTL").value("3600"); diff --git a/server/tests/services/statusService.test.js b/server/tests/services/statusService.test.js index aa8865024..289a36884 100755 --- a/server/tests/services/statusService.test.js +++ b/server/tests/services/statusService.test.js @@ -73,9 +73,7 @@ describe("StatusService", () => { }); it("should return {statusChanged: true} if status has changed from down to up", async function () { - statusService.db.getMonitorById = sinon - .stub() - .returns({ status: false, save: sinon.stub() }); + statusService.db.getMonitorById = sinon.stub().returns({ status: false, save: sinon.stub() }); const result = await statusService.updateStatus({ monitorId: "test", status: true, @@ -87,9 +85,7 @@ describe("StatusService", () => { }); it("should return {statusChanged: true} if status has changed from up to down", async function () { - statusService.db.getMonitorById = sinon - .stub() - .returns({ status: true, save: sinon.stub() }); + statusService.db.getMonitorById = sinon.stub().returns({ status: true, save: sinon.stub() }); const result = await statusService.updateStatus({ monitorId: "test", status: false, diff --git a/server/tests/utils/dataUtils.test.js b/server/tests/utils/dataUtils.test.js index e39af5a75..ec9eabb40 100755 --- a/server/tests/utils/dataUtils.test.js +++ b/server/tests/utils/dataUtils.test.js @@ -51,13 +51,7 @@ describe("NormalizeData", function () { describe("calculatePercentile", function () { it("should return the lower value when upper is greater than or equal to the length of the sorted array", function () { - const checks = [ - { responseTime: 10 }, - { responseTime: 20 }, - { responseTime: 30 }, - { responseTime: 40 }, - { responseTime: 50 }, - ]; + const checks = [{ responseTime: 10 }, { responseTime: 20 }, { responseTime: 30 }, { responseTime: 40 }, { responseTime: 50 }]; const percentile = 100; const result = calculatePercentile(checks, percentile); diff --git a/server/tests/utils/imageProcessing.test.js b/server/tests/utils/imageProcessing.test.js index 29e47af6a..09330c317 100755 --- a/server/tests/utils/imageProcessing.test.js +++ b/server/tests/utils/imageProcessing.test.js @@ -12,9 +12,7 @@ describe("imageProcessing - GenerateAvatarImage", function () { // Stub the sharp function const toBufferStub = sinon.stub().resolves(Buffer.from("resized image buffer")); const resizeStub = sinon.stub().returns({ toBuffer: toBufferStub }); - const sharpStub = sinon - .stub(sharp.prototype, "resize") - .returns({ toBuffer: toBufferStub }); + const sharpStub = sinon.stub(sharp.prototype, "resize").returns({ toBuffer: toBufferStub }); const result = await GenerateAvatarImage(file); @@ -38,9 +36,7 @@ describe("imageProcessing - GenerateAvatarImage", function () { // Stub the sharp function to throw an error const toBufferStub = sinon.stub().rejects(new Error("Resizing failed")); const resizeStub = sinon.stub().returns({ toBuffer: toBufferStub }); - const sharpStub = sinon - .stub(sharp.prototype, "resize") - .returns({ toBuffer: toBufferStub }); + const sharpStub = sinon.stub(sharp.prototype, "resize").returns({ toBuffer: toBufferStub }); try { await GenerateAvatarImage(file); diff --git a/server/tests/utils/messages.test.js b/server/tests/utils/messages.test.js index 719af1be0..a7e98c477 100755 --- a/server/tests/utils/messages.test.js +++ b/server/tests/utils/messages.test.js @@ -3,25 +3,19 @@ describe("Messages", function () { describe("messages - errorMessages", function () { it("should have a DB_FIND_MONITOR_BY_ID function", function () { const monitorId = "12345"; - expect(errorMessages.DB_FIND_MONITOR_BY_ID(monitorId)).to.equal( - `Monitor with id ${monitorId} not found` - ); + expect(errorMessages.DB_FIND_MONITOR_BY_ID(monitorId)).to.equal(`Monitor with id ${monitorId} not found`); }); it("should have a DB_DELETE_CHECKS function", function () { const monitorId = "12345"; - expect(errorMessages.DB_DELETE_CHECKS(monitorId)).to.equal( - `No checks found for monitor with id ${monitorId}` - ); + expect(errorMessages.DB_DELETE_CHECKS(monitorId)).to.equal(`No checks found for monitor with id ${monitorId}`); }); }); describe("messages - successMessages", function () { it("should have a MONITOR_GET_BY_USER_ID function", function () { const userId = "12345"; - expect(successMessages.MONITOR_GET_BY_USER_ID(userId)).to.equal( - `Got monitor for ${userId} successfully"` - ); + expect(successMessages.MONITOR_GET_BY_USER_ID(userId)).to.equal(`Got monitor for ${userId} successfully"`); }); // Add more tests for other success messages as needed diff --git a/server/utils/dataUtils.js b/server/utils/dataUtils.js index d164b92fd..e7fd334dd 100755 --- a/server/utils/dataUtils.js +++ b/server/utils/dataUtils.js @@ -15,9 +15,7 @@ const calculatePercentileUptimeDetails = (arr, percentile) => { const upper = lower + 1; const weight = index % 1; if (upper >= sorted.length) return sorted[lower].avgResponseTime; - return ( - sorted[lower].avgResponseTime * (1 - weight) + sorted[upper].avgResponseTime * weight - ); + return sorted[lower].avgResponseTime * (1 - weight) + sorted[upper].avgResponseTime * weight; }; const NormalizeData = (checks, rangeMin, rangeMax) => { @@ -28,15 +26,11 @@ const NormalizeData = (checks, rangeMin, rangeMax) => { const normalizedChecks = checks.map((check) => { const originalResponseTime = check.responseTime; // Normalize the response time between 1 and 100 - let normalizedResponseTime = - rangeMin + ((check.responseTime - min) * (rangeMax - rangeMin)) / (max - min); + let normalizedResponseTime = rangeMin + ((check.responseTime - min) * (rangeMax - rangeMin)) / (max - min); // Put a floor on the response times so we don't have extreme outliers // Better visuals - normalizedResponseTime = Math.max( - rangeMin, - Math.min(rangeMax, normalizedResponseTime) - ); + normalizedResponseTime = Math.max(rangeMin, Math.min(rangeMax, normalizedResponseTime)); return { ...check, responseTime: normalizedResponseTime, @@ -60,15 +54,11 @@ const NormalizeDataUptimeDetails = (checks, rangeMin, rangeMax) => { const normalizedChecks = checks.map((check) => { const originalResponseTime = check.avgResponseTime; // Normalize the response time between 1 and 100 - let normalizedResponseTime = - rangeMin + ((check.avgResponseTime - min) * (rangeMax - rangeMin)) / (max - min); + let normalizedResponseTime = rangeMin + ((check.avgResponseTime - min) * (rangeMax - rangeMin)) / (max - min); // Put a floor on the response times so we don't have extreme outliers // Better visuals - normalizedResponseTime = Math.max( - rangeMin, - Math.min(rangeMax, normalizedResponseTime) - ); + normalizedResponseTime = Math.max(rangeMin, Math.min(rangeMax, normalizedResponseTime)); return { ...check, avgResponseTime: normalizedResponseTime, @@ -107,10 +97,4 @@ const safelyParseFloat = (value, defaultValue = 0) => { return parsedValue; }; -export { - safelyParseFloat, - calculatePercentile, - NormalizeData, - calculatePercentileUptimeDetails, - NormalizeDataUptimeDetails, -}; +export { safelyParseFloat, calculatePercentile, NormalizeData, calculatePercentileUptimeDetails, NormalizeDataUptimeDetails }; diff --git a/server/utils/logger.js b/server/utils/logger.js index 49b875d96..8cb57f9a7 100755 --- a/server/utils/logger.js +++ b/server/utils/logger.js @@ -2,47 +2,48 @@ import { createLogger, format, transports } from "winston"; import dotenv from "dotenv"; dotenv.config(); +const SERVICE_NAME = "Logger"; + class Logger { + static SERVICE_NAME = SERVICE_NAME; constructor() { this.logCache = []; this.maxCacheSize = 1000; - const consoleFormat = format.printf( - ({ level, message, service, method, details, timestamp, stack }) => { - if (message instanceof Object) { - message = JSON.stringify(message, null, 2); - } - - if (details instanceof Object) { - details = JSON.stringify(details, null, 2); - } - let msg = `${timestamp} ${level}:`; - service && (msg += ` [${service}]`); - method && (msg += `(${method})`); - message && (msg += ` ${message}`); - details && (msg += ` (details: ${details})`); - - if (typeof stack !== "undefined") { - const stackTrace = stack - ?.split("\n") - .slice(1) // Remove first line (error message) - .map((line) => { - const match = line.match(/at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)/); - if (match) { - return { - function: match[1], - file: match[2], - line: parseInt(match[3]), - column: parseInt(match[4]), - }; - } - return line.trim(); - }); - stack && (msg += ` (stack: ${JSON.stringify(stackTrace, null, 2)})`); - } - - return msg; + const consoleFormat = format.printf(({ level, message, service, method, details, timestamp, stack }) => { + if (message instanceof Object) { + message = JSON.stringify(message, null, 2); } - ); + + if (details instanceof Object) { + details = JSON.stringify(details, null, 2); + } + let msg = `${timestamp} ${level}:`; + service && (msg += ` [${service}]`); + method && (msg += `(${method})`); + message && (msg += ` ${message}`); + details && (msg += ` (details: ${details})`); + + if (typeof stack !== "undefined") { + const stackTrace = stack + ?.split("\n") + .slice(1) // Remove first line (error message) + .map((line) => { + const match = line.match(/at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)/); + if (match) { + return { + function: match[1], + file: match[2], + line: parseInt(match[3]), + column: parseInt(match[4]), + }; + } + return line.trim(); + }); + stack && (msg += ` (stack: ${JSON.stringify(stackTrace, null, 2)})`); + } + + return msg; + }); const logLevel = process.env.LOG_LEVEL || "info"; @@ -51,12 +52,7 @@ class Logger { format: format.combine(format.timestamp()), transports: [ new transports.Console({ - format: format.combine( - format.colorize(), - format.prettyPrint(), - format.json(), - consoleFormat - ), + format: format.combine(format.colorize(), format.prettyPrint(), format.json(), consoleFormat), }), new transports.File({ format: format.combine(format.json()), diff --git a/server/utils/roleUtils.js b/server/utils/roleUtils.js new file mode 100644 index 000000000..f359246b2 --- /dev/null +++ b/server/utils/roleUtils.js @@ -0,0 +1,13 @@ +export const ROLES = { + SUPERADMIN: "superadmin", + ADMIN: "admin", + USER: "user", + DEMO: "demo", +}; + +export const VALID_ROLES = [ROLES.ADMIN, ROLES.USER, ROLES.DEMO]; + +export const EDITABLE_ROLES = [ + { role: ROLES.ADMIN, _id: ROLES.ADMIN }, + { role: ROLES.USER, _id: ROLES.USER }, +]; diff --git a/server/utils/utils.js b/server/utils/utils.js index 57d181886..bcb4d8e5e 100755 --- a/server/utils/utils.js +++ b/server/utils/utils.js @@ -6,12 +6,7 @@ const ParseBoolean = (value) => { if (value === true || value === "true") { return true; - } else if ( - value === false || - value === "false" || - value === null || - value === undefined - ) { + } else if (value === false || value === "false" || value === null || value === undefined) { return false; } }; @@ -21,8 +16,7 @@ const getTokenFromHeaders = (headers) => { 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"); + if (parts.length !== 2 || parts[0] !== "Bearer") throw new Error("Invalid auth headers"); return parts[1]; }; diff --git a/server/validation/joi.js b/server/validation/joi.js index d44bb14eb..744b0ec74 100755 --- a/server/validation/joi.js +++ b/server/validation/joi.js @@ -1,4 +1,5 @@ import joi from "joi"; +import { ROLES, VALID_ROLES } from "../utils/roleUtils.js"; //**************************************** // Custom Validators @@ -7,9 +8,7 @@ import joi from "joi"; const roleValidatior = (role) => (value, helpers) => { const hasRole = role.some((role) => value.includes(role)); if (!hasRole) { - throw new joi.ValidationError( - `You do not have the required authorization. Required roles: ${role.join(", ")}` - ); + throw new joi.ValidationError(`You do not have the required authorization. Required roles: ${role.join(", ")}`); } return value; }; @@ -58,13 +57,9 @@ const registrationBodyValidation = joi.object({ inviteToken: joi.string().allow("").required(), }); -const editUserParamValidation = joi.object({ - userId: joi.string().required(), -}); - const editUserBodyValidation = joi.object({ - firstName: nameValidation.required(), - lastName: nameValidation.required(), + firstName: nameValidation.optional(), + lastName: nameValidation.optional(), profileImage: joi.any(), newPassword: joi.string().min(8).pattern(passwordPattern), password: joi.string().min(8).pattern(passwordPattern), @@ -78,7 +73,7 @@ const recoveryValidation = joi.object({ .required(), }); -const recoveryTokenValidation = joi.object({ +const recoveryTokenBodyValidation = joi.object({ recoveryToken: joi.string().required(), }); @@ -92,10 +87,6 @@ const deleteUserParamValidation = joi.object({ email: joi.string().email().required(), }); -const inviteRoleValidation = joi.object({ - roles: joi.custom(roleValidatior(["admin", "superadmin"])).required(), -}); - const inviteBodyValidation = joi.object({ email: joi.string().trim().email().required().messages({ "string.empty": "Email is required", @@ -134,11 +125,7 @@ const getMonitorsByTeamIdQueryValidation = joi.object({ .alternatives() .try( joi.string().valid("http", "ping", "pagespeed", "docker", "hardware", "port"), - joi - .array() - .items( - joi.string().valid("http", "ping", "pagespeed", "docker", "hardware", "port") - ) + joi.array().items(joi.string().valid("http", "ping", "pagespeed", "docker", "hardware", "port")) ), page: joi.number(), rowsPerPage: joi.number(), @@ -318,8 +305,6 @@ const getChecksQueryValidation = joi.object({ status: joi.boolean(), }); -const getTeamChecksParamValidation = joi.object({}); - const getTeamChecksQueryValidation = joi.object({ sortOrder: joi.string().valid("asc", "desc"), limit: joi.number(), @@ -454,8 +439,7 @@ const createStatusPageBodyValidation = joi.object({ .pattern(/^[a-zA-Z0-9_-]+$/) // Only allow alphanumeric, underscore, and hyphen .required() .messages({ - "string.pattern.base": - "URL can only contain letters, numbers, underscores, and hyphens", + "string.pattern.base": "URL can only contain letters, numbers, underscores, and hyphens", }), timezone: joi.string().optional(), color: joi.string().optional(), @@ -485,13 +469,9 @@ const imageValidation = joi fieldname: joi.string().required(), originalname: joi.string().required(), encoding: joi.string().required(), - mimetype: joi - .string() - .valid("image/jpeg", "image/png", "image/jpg") - .required() - .messages({ - "string.valid": "File must be a valid image (jpeg, jpg, or png)", - }), + mimetype: joi.string().valid("image/jpeg", "image/png", "image/jpg").required().messages({ + "string.valid": "File must be a valid image (jpeg, jpg, or png)", + }), size: joi.number().max(3145728).required().messages({ "number.max": "File size must be less than 3MB", }), @@ -577,6 +557,13 @@ const createNotificationBodyValidation = joi.object({ "string.empty": "Notification name is required", "any.required": "Notification name is required", }), + + type: joi.string().valid("email", "webhook", "slack", "discord", "pager_duty").required().messages({ + "string.empty": "Notification type is required", + "any.required": "Notification type is required", + "any.only": "Notification type must be email, webhook, or pager_duty", + }), + address: joi.when("type", { is: "email", then: joi.string().email().required().messages({ @@ -584,16 +571,12 @@ const createNotificationBodyValidation = joi.object({ "any.required": "E-mail address is required", "string.email": "Please enter a valid e-mail address", }), - }), - type: joi - .string() - .valid("email", "webhook", "slack", "discord", "pager_duty") - .required() - .messages({ - "string.empty": "Notification type is required", - "any.required": "Notification type is required", - "any.only": "Notification type must be email, webhook, or pager_duty", + otherwise: joi.string().uri().required().messages({ + "string.empty": "Webhook URL cannot be empty", + "any.required": "Webhook URL is required", + "string.uri": "Please enter a valid Webhook URL", }), + }), }); //**************************************** @@ -628,14 +611,43 @@ const sendTestEmailBodyValidation = joi.object({ systemEmailTLSServername: joi.string().allow("").optional(), }); +const getUserByIdParamValidation = joi.object({ + userId: joi.string().required(), +}); + +const editUserByIdParamValidation = joi.object({ + userId: joi.string().required(), +}); + +const editUserByIdBodyValidation = joi.object({ + firstName: nameValidation.required(), + lastName: nameValidation.required(), + email: joi.string().email().required(), + role: joi + .array() + .items(joi.string().valid(...VALID_ROLES)) + .min(1) + .required(), +}); + +const editSuperadminUserByIdBodyValidation = joi.object({ + firstName: nameValidation.required(), + lastName: nameValidation.required(), + email: joi.string().email().required(), + role: joi + .array() + .items(joi.string().valid(...VALID_ROLES, ROLES.SUPERADMIN)) + .min(1) + .required(), +}); + export { roleValidatior, loginValidation, registrationBodyValidation, recoveryValidation, - recoveryTokenValidation, + recoveryTokenBodyValidation, newPasswordValidation, - inviteRoleValidation, inviteBodyValidation, inviteVerificationBodyValidation, createMonitorBodyValidation, @@ -652,7 +664,6 @@ export { editMonitorBodyValidation, pauseMonitorParamValidation, getMonitorURLByQueryValidation, - editUserParamValidation, editUserBodyValidation, createAlertParamValidation, createAlertBodyValidation, @@ -666,7 +677,6 @@ export { createCheckBodyValidation, getChecksParamValidation, getChecksQueryValidation, - getTeamChecksParamValidation, getTeamChecksQueryValidation, ackCheckBodyValidation, ackAllChecksParamValidation, @@ -696,4 +706,8 @@ export { webhookConfigValidation, createAnnouncementValidation, sendTestEmailBodyValidation, + getUserByIdParamValidation, + editUserByIdParamValidation, + editUserByIdBodyValidation, + editSuperadminUserByIdBodyValidation, };