diff --git a/.github/scripts/download-translations.js b/.github/scripts/download-translations.js index 8f35be0de..7a8125ab8 100644 --- a/.github/scripts/download-translations.js +++ b/.github/scripts/download-translations.js @@ -6,7 +6,9 @@ import { URLSearchParams } from "url"; // POEditor API information const API_TOKEN = process.env.POEDITOR_API; const PROJECT_ID = process.env.POEDITOR_PROJECT_ID; -const LANGUAGES = (process.env.LANGUAGES || "tr,en").split(","); +const LANGUAGES = ( + process.env.LANGUAGES || "ar,zh-tw,cs,en,fi,fr,de,pt-br,ru,es,tr,ja" +).split(","); const EXPORT_FORMAT = process.env.EXPORT_FORMAT || "key_value_json"; // POEditor API endpoint diff --git a/.github/workflows/check-format.yml b/.github/workflows/check-format.yml index 76cb300f1..d3f34635c 100644 --- a/.github/workflows/check-format.yml +++ b/.github/workflows/check-format.yml @@ -39,24 +39,3 @@ jobs: - name: Check server formatting working-directory: server run: npm run format-check - close-pr-if-needed: - if: always() - runs-on: ubuntu-latest - needs: [format-client, format-server] - steps: - - name: Checkout repo - uses: actions/checkout@v4 - - - name: Get PR number - id: pr - run: echo "PR_NUMBER=$(jq -r .pull_request.number "$GITHUB_EVENT_PATH")" >> $GITHUB_ENV - - - name: Close PR using GitHub CLI - if: | - needs.format-client.result == 'failure' || - needs.format-server.result == 'failure' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh pr close "$PR_NUMBER" --delete-branch --comment "❌ Formatting check failed — PR auto-closed. - Please run \`npm run format\` in both `client` and `server` directories and push again." diff --git a/.github/workflows/deploy-images-on-release.yml b/.github/workflows/deploy-images-on-release.yml index cf06f7840..a9b2dfdb8 100644 --- a/.github/workflows/deploy-images-on-release.yml +++ b/.github/workflows/deploy-images-on-release.yml @@ -79,18 +79,6 @@ jobs: run: | docker push ghcr.io/bluewave-labs/checkmate-mongo:${{ steps.extract_tag.outputs.version }} - - name: Build Redis Docker image - run: | - docker build \ - -t ghcr.io/bluewave-labs/checkmate-redis:${{ steps.extract_tag.outputs.version }} \ - -f ./docker/dist/redis.Dockerfile \ - --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \ - . - - - name: Push Redis Docker image - run: | - docker push ghcr.io/bluewave-labs/checkmate-redis:${{ steps.extract_tag.outputs.version }} - docker-build-and-push-server-mono-multiarch: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/deploy-images.yml b/.github/workflows/deploy-images.yml index fa39fed85..53d7ab368 100644 --- a/.github/workflows/deploy-images.yml +++ b/.github/workflows/deploy-images.yml @@ -74,18 +74,6 @@ jobs: run: | docker push ghcr.io/bluewave-labs/checkmate-mongo:latest - - name: Build Redis Docker image - run: | - docker build \ - -t ghcr.io/bluewave-labs/checkmate-redis:latest \ - -f ./docker/dist/redis.Dockerfile \ - --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \ - . - - - name: Push Redis Docker image - run: | - docker push ghcr.io/bluewave-labs/checkmate-redis:latest - docker-build-and-push-server-mono-multiarch: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/poeditor-sync.yml b/.github/workflows/poeditor-sync.yml index 83dac7f09..a8b330290 100644 --- a/.github/workflows/poeditor-sync.yml +++ b/.github/workflows/poeditor-sync.yml @@ -7,7 +7,7 @@ on: languages: description: "Languages to synchronize (comma separated, e.g.: tr,en,es)" required: false - default: "ar,zh-tw,cs,en,fi,fr,de,pt-br,ru,es,tr" + default: "ar,zh-tw,cs,en,fi,fr,de,pt-br,ru,es,tr,ja" format: description: "Export format (key_value_json or json)" required: false diff --git a/.github/workflows/production-deploy.yml b/.github/workflows/production-deploy.yml index 7764c488b..532197710 100644 --- a/.github/workflows/production-deploy.yml +++ b/.github/workflows/production-deploy.yml @@ -71,17 +71,6 @@ jobs: - name: Push MongoDB Docker image run: docker push ghcr.io/bluewave-labs/checkmate:mongo-demo - - name: Build Redis Docker image - run: | - docker build \ - -t ghcr.io/bluewave-labs/checkmate:redis-demo \ - -f ./docker/prod/redis.Dockerfile \ - --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \ - . - - - name: Push Redis Docker image - run: docker push ghcr.io/bluewave-labs/checkmate:redis-demo - deploy-to-demo: needs: docker-build-and-push-server runs-on: ubuntu-latest diff --git a/.github/workflows/staging-deploy.yml b/.github/workflows/staging-deploy.yml index ea5477815..27b7e8690 100644 --- a/.github/workflows/staging-deploy.yml +++ b/.github/workflows/staging-deploy.yml @@ -71,17 +71,6 @@ jobs: - name: Push MongoDB Docker image run: docker push ghcr.io/bluewave-labs/checkmate:mongo-staging - - name: Build Redis Docker image - run: | - docker build \ - -t ghcr.io/bluewave-labs/checkmate:redis-staging \ - -f ./docker/staging/redis.Dockerfile \ - --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \ - . - - - name: Push Redis Docker image - run: docker push ghcr.io/bluewave-labs/checkmate:redis-staging - deploy-to-staging: needs: docker-build-and-push-server runs-on: ubuntu-latest 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..048cf0706 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ This repository contains both the frontend and the backend of Checkmate, an open-source, self-hosted monitoring tool for tracking server hardware, uptime, response times, and incidents in real-time with beautiful visualizations. Checkmate regularly checks whether a server/website is accessible and performs optimally, providing real-time alerts and reports on the monitored services' availability, downtime, and response time. -Checkmate also has an agent, called [Capture](https://github.com/bluewave-labs/capture), to retrieve data from remote servers. While Capture is not required to run Checkmate, it provides additional insights about your servers' CPU, RAM, disk, and temperature status. +Checkmate also has an agent, called [Capture](https://github.com/bluewave-labs/capture), to retrieve data from remote servers. While Capture is not required to run Checkmate, it provides additional insights about your servers' CPU, RAM, disk, and temperature status. Capture can run on Linux, Windows, Mac, Raspberry Pi, or any device that can run Go. Checkmate has been stress-tested with 1000+ active monitors without any particular issues or performance bottlenecks. @@ -26,41 +26,46 @@ 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. +### Using a Custom CA -## 🏁 Translations +If you need to monitor internal HTTPS endpoints with certificates from private Certificate Authorities (like Smallstep), see our [Custom CA Trust Guide](./docs/custom-ca-trust.md) for Docker configuration options. + +For more documentation, see the [docs directory](./docs/). + + +## 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,34 +75,44 @@ 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, network etc) - requires [Capture](https://github.com/bluewave-labs/capture) agent - Docker monitoring - Ping monitoring - SSL monitoring - Port monitoring +- Game server monitoring (3.0) - Incidents at a glance - Status pages -- E-mail, Webhooks, Discord, Telegram, Slack notifications +- E-mail, Webhooks, Discord and Slack notifications - Scheduled maintenance - JSON query monitoring -- Support for multiple languages +- Multi-language support for English, German, Japanese, Portuguese (Brazil), Russian, Turkish, Ukrainian, Vietnamese, Chinese (Traditional, Taiwan) -**Short term roadmap:** ([Milestone 2.2](https://github.com/bluewave-labs/Checkmate/milestone/8)) +**Short term roadmap:** +- Plugins that will help Checkmate get any information from a remote service (e.g database, etc) - Better notifications - Network monitoring - ..and a few more features -## 🏗️ Screenshots +If you would like to sponsor an additional feature, [see this page](https://checkmate.so/sponsored-features). + +## Screenshots

image @@ -114,7 +129,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 +145,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 are [Alex](http://github.com/ajhollid) (team lead), [Gorkem](http://github.com/gorkem-bwl/), [Owaise](http://github.com/Owaiseimdad), [Aryaman](https://github.com/Br0wnHammer), [Mert](https://github.com/mertssmnoglu) and [Karen](https://github.com/karenvicent) 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 +169,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/ArrowLeft/index.jsx b/client/src/Components/ArrowLeft/index.jsx new file mode 100644 index 000000000..7909441f3 --- /dev/null +++ b/client/src/Components/ArrowLeft/index.jsx @@ -0,0 +1,35 @@ +import LeftArrow from "../../assets/icons/left-arrow.svg?react"; +import LeftArrowDouble from "../../assets/icons/left-arrow-double.svg?react"; +import LeftArrowLong from "../../assets/icons/left-arrow-long.svg?react"; +import PropTypes from "prop-types"; + +const ArrowLeft = ({ type, color = "#667085", ...props }) => { + if (type === "double") { + return ( + + ); + } else if (type === "long") { + return ( + + ); + } else { + return ( + + ); + } +}; + +ArrowLeft.propTypes = { + color: PropTypes.string, + type: PropTypes.oneOf(["double", "long", "default"]), +}; +export default ArrowLeft; diff --git a/client/src/Components/ArrowRight/index.jsx b/client/src/Components/ArrowRight/index.jsx new file mode 100644 index 000000000..335bc19f2 --- /dev/null +++ b/client/src/Components/ArrowRight/index.jsx @@ -0,0 +1,28 @@ +import RightArrow from "../../assets/icons/right-arrow.svg?react"; +import RightArrowDouble from "../../assets/icons/right-arrow-double.svg?react"; +import PropTypes from "prop-types"; + +const ArrowRight = ({ type, color = "#667085", ...props }) => { + if (type === "double") { + return ( + + ); + } else { + return ( + + ); + } +}; + +ArrowRight.propTypes = { + type: PropTypes.oneOf(["double", "default"]), + color: PropTypes.string, +}; + +export default ArrowRight; diff --git a/client/src/Components/Avatar/index.jsx b/client/src/Components/Avatar/index.jsx index aa2050827..d089080ff 100644 --- a/client/src/Components/Avatar/index.jsx +++ b/client/src/Components/Avatar/index.jsx @@ -15,7 +15,7 @@ import { useTheme } from "@emotion/react"; * */ -const Avatar = ({ src, small, sx }) => { +const Avatar = ({ src, small, sx, onClick = () => {} }) => { const { user } = useSelector((state) => state.auth); const theme = useTheme(); @@ -31,6 +31,7 @@ const Avatar = ({ src, small, sx }) => { return ( * * @returns {React.ReactElement} Rendered CustomGauge component */ -const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, threshold = 50 }) => { +const CustomGauge = ({ + isLoading = false, + progress = 0, + radius = 70, + strokeWidth = 15, + precision = 1, + unit = "%", +}) => { const theme = useTheme(); // Calculate the length of the stroke for the circle const { circumference, totalSize, strokeLength } = useMemo( @@ -52,10 +61,28 @@ const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, threshold = const progressWithinRange = Math.max(MINIMUM_VALUE, Math.min(progress, MAXIMUM_VALUE)); - const fillColor = - progressWithinRange > threshold - ? theme.palette.error.lowContrast // CAIO_REVIEW - : theme.palette.accent.main; // CAIO_REVIEW + let fillColor; + if (progressWithinRange < 50) { + fillColor = theme.palette.success.main; + } else if (progressWithinRange < 80) { + fillColor = theme.palette.warning.lowContrast; + } else { + fillColor = theme.palette.error.lowContrast; + } + + if (isLoading) { + return ( + + + + ); + } return ( - {`${progressWithinRange.toFixed(1)}%`} + {`${progressWithinRange.toFixed(precision)}${unit}`} ); @@ -112,8 +139,10 @@ 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/Charts/Utils/chartUtils.jsx b/client/src/Components/Charts/Utils/chartUtils.jsx index 414e1ff9c..e24881c1c 100644 --- a/client/src/Components/Charts/Utils/chartUtils.jsx +++ b/client/src/Components/Charts/Utils/chartUtils.jsx @@ -88,7 +88,51 @@ PercentTick.propTypes = { */ const getFormattedPercentage = (value) => { if (typeof value !== "number") return value; - return `${(value * 100).toFixed(2)}.%`; + return `${(value * 100).toFixed(2)}%`; +}; + +/** + * Custom tick component for rendering network bytes per second. + * + * @param {Object} props - The properties object. + * @param {number} props.x - The x-coordinate for the tick. + * @param {number} props.y - The y-coordinate for the tick. + * @param {Object} props.payload - The payload object containing tick data. + * @param {number} props.index - The index of the tick. + * @returns {JSX.Element|null} The rendered tick component or null for the first tick. + */ +export const NetworkTick = ({ x, y, payload, index, formatter }) => { + const theme = useTheme(); + if (index === 0) return null; + + if (formatter === undefined) { + formatter = (value, space = false) => { + if (typeof value !== "number") return value; + // need to add space between value and unit + return `${(value / 1024).toFixed(1)}${space ? " " : ""}Kbps`; + }; + } + + return ( + + {formatter(payload?.value, true)} + + ); +}; + +NetworkTick.propTypes = { + x: PropTypes.number, + y: PropTypes.number, + payload: PropTypes.object, + index: PropTypes.number, + formatter: PropTypes.func, }; /** @@ -112,6 +156,7 @@ export const InfrastructureTooltip = ({ yLabel, dotColor, dateRange, + formatter = getFormattedPercentage, }) => { const uiTimezone = useSelector((state) => state.ui.timezone); const theme = useTheme(); @@ -166,8 +211,8 @@ export const InfrastructureTooltip = ({ sx={{ opacity: 0.8 }} > {yIdx >= 0 - ? `${yLabel} ${getFormattedPercentage(payload[0].payload[hardwareType][yIdx][metric])}` - : `${yLabel} ${getFormattedPercentage(payload[0].payload[yKey])}`} + ? `${yLabel} ${formatter(payload[0].payload[hardwareType][yIdx][metric])}` + : `${yLabel} ${formatter(payload[0].payload[yKey])}`} diff --git a/client/src/Components/Check/Check.jsx b/client/src/Components/Check/Check.jsx index 557414146..7f37a4dc1 100644 --- a/client/src/Components/Check/Check.jsx +++ b/client/src/Components/Check/Check.jsx @@ -51,9 +51,10 @@ const Check = ({ text, noHighlightText, variant = "info", outlined = false }) => sx={{ color: variant === "info" - ? theme.palette.primary.contrastTextTertiary + ? theme.palette.primary.contrastTextSecondary : colors[variant], - opacity: 0.8, + opacity: 0.9, + fontWeight: 450, }} > {noHighlightText && {noHighlightText}}{" "} diff --git a/client/src/Components/ConfigBox/index.jsx b/client/src/Components/ConfigBox/index.jsx index 44ae38472..01b35729e 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, @@ -21,11 +20,16 @@ const ConfigBox = styled(Stack)(({ theme }) => ({ borderRightColor: theme.palette.primary.lowContrast, paddingRight: theme.spacing(15), paddingLeft: theme.spacing(15), + backgroundColor: theme.palette.tertiary.background, + "& :is(h1, h2):first-of-type": { + fontWeight: 600, + marginBottom: theme.spacing(4), + }, }, "& > 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..144b16501 --- /dev/null +++ b/client/src/Components/Fallback/FallBackActionButtons.jsx @@ -0,0 +1,44 @@ +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..00ecafb4e --- /dev/null +++ b/client/src/Components/Fallback/FallbackBackground.jsx @@ -0,0 +1,40 @@ +import { useTheme } from "@emotion/react"; +import Box from "@mui/material/Box"; +import Background from "../../assets/Images/background-grid.svg?react"; +import SkeletonDark from "../../assets/Images/create-placeholder-dark.svg?react"; +import OutputAnimation from "../../assets/Animations/output.gif"; +import DarkmodeOutput from "../../assets/Animations/darkmodeOutput.gif"; +import { useSelector } from "react-redux"; +const FallbackBackground = () => { + const theme = useTheme(); + const mode = useSelector((state) => state.ui.mode); + return ( + <> + + + + + + ); +}; + +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..7a2ec5fe0 --- /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..69fee05c6 --- /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/GenericFallback/index.jsx b/client/src/Components/GenericFallback/index.jsx index af9ed2ea4..7f6d258ea 100644 --- a/client/src/Components/GenericFallback/index.jsx +++ b/client/src/Components/GenericFallback/index.jsx @@ -1,7 +1,7 @@ import { useTheme } from "@emotion/react"; import { Box, Stack } from "@mui/material"; -import Skeleton from "../../assets/Images/create-placeholder.svg?react"; -import SkeletonDark from "../../assets/Images/create-placeholder-dark.svg?react"; +import OutputAnimation from "../../assets/Animations/output.gif"; +import DarkmodeOutput from "../../assets/Animations/darkmodeOutput.gif"; import Background from "../../assets/Images/background-grid.svg?react"; import { useSelector } from "react-redux"; @@ -37,11 +37,18 @@ const GenericFallback = ({ children }) => { marginTop: "100px", }} > - {mode === "light" ? ( - - ) : ( - - )} + { + 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} + { const theme = useTheme(); return ( { ); }; -//TODO keep search state inside of component +//TODO keep search state inside of component. const Search = ({ label, id, @@ -68,6 +68,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 +147,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 +196,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; }} @@ -267,7 +282,9 @@ const Search = ({ }} sx={{ /* height: 34,*/ - "&.MuiAutocomplete-root .MuiAutocomplete-input": { p: 0 }, + "&.MuiAutocomplete-root .MuiAutocomplete-input": { + padding: `0 ${theme.spacing(5)}`, + }, ...sx, }} /> @@ -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..c5c488f91 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"; @@ -49,9 +50,16 @@ const Select = ({ onChange, onBlur, sx, + error = false, name = "", - labelControlSpacing = 2, + labelControlSpacing = 6, maxWidth, + //FieldWrapper's props + labelMb, + labelFontWeight, + labelVariant, + labelSx = {}, + fieldWrapperSx = {}, }) => { const theme = useTheme(); const itemStyles = { @@ -69,26 +77,24 @@ const Select = ({ }; return ( - - {label && ( - - {label} - - )} { @@ -151,7 +164,7 @@ const Select = ({ ))} - + ); }; @@ -161,6 +174,7 @@ Select.propTypes = { label: PropTypes.string, placeholder: PropTypes.string, isHidden: PropTypes.bool, + error: PropTypes.bool, value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]) .isRequired, items: PropTypes.arrayOf( 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..9b46204e3 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(); @@ -23,30 +30,44 @@ const LanguageSelector = () => { value={language} onChange={handleChange} size="small" - sx={{ minWidth: 80 }} + sx={{ + minWidth: 80, + "& .MuiSelect-select": { + display: "flex", + alignItems: "center", + justifyContent: "center", + }, + "& .MuiSelect-icon": { + alignSelf: "center", + }, + }} > {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 ( div { min-height: calc(100vh - var(--env-var-spacing-2) * 2); 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, + }} /> { + if (!user) return []; + + let filtered = [...items]; + + if (user.role?.includes("demo")) { + filtered = filtered.filter((item) => item.name !== "Password"); + } + + if (!user.role?.includes("superadmin")) { + filtered = filtered.filter((item) => item.name !== "Team"); + } + + return filtered; +}; + +const getRoleDisplayText = (user, t) => { + if (!user?.role) return ""; + + if (user.role.includes("superadmin")) return t("roles.superAdmin"); + if (user.role.includes("admin")) return t("roles.admin"); + if (user.role.includes("user")) return t("roles.teamMember"); + if (user.role.includes("demo")) return t("roles.demoUser"); + + return user.role; +}; + +const AuthFooter = ({ collapsed, accountMenuItems }) => { + const { t } = useTranslation(); + const theme = useTheme(); + const authState = useSelector((state) => state.auth); + const navigate = useNavigate(); + const dispatch = useDispatch(); + + const [anchorEl, setAnchorEl] = useState(null); + + const openPopup = (event) => { + setAnchorEl(event.currentTarget); + }; + + const closePopup = () => { + setAnchorEl(null); + }; + + const logout = async () => { + dispatch(clearAuthState()); + navigate("/login"); + }; + const renderAccountMenuItems = (user, items) => { + const filteredItems = getFilteredAccountMenuItems(user, items); + + return filteredItems.map((item) => ( + { + closePopup(); + navigate(item.path); + }} + sx={{ + gap: theme.spacing(2), + borderRadius: theme.shape.borderRadius, + pl: theme.spacing(4), + }} + > + {item.icon} + {item.name} + + )); + }; + return ( + + collapsed && openPopup(e)} + sx={{ + cursor: collapsed ? "pointer" : "default", + }} + /> + + + + + {authState.user?.firstName} {authState.user?.lastName} + + + {getRoleDisplayText(authState.user, t)} + + + + openPopup(event)} + > + + + + +

+ {collapsed && ( + + + + {authState.user?.firstName} {authState.user?.lastName} + + + {authState.user?.role} + + + + )} + {/* TODO Do we need two dividers? */} + {collapsed && } + {/* */} + {renderAccountMenuItems(authState.user, accountMenuItems)} + + + {t("menu.logOut", "Log out")} + + + + ); +}; + +AuthFooter.propTypes = { + collapsed: PropTypes.bool, + accountMenuItems: PropTypes.array, +}; + +export default AuthFooter; diff --git a/client/src/Components/Sidebar/components/collapseButton.jsx b/client/src/Components/Sidebar/components/collapseButton.jsx new file mode 100644 index 000000000..e7c382e5a --- /dev/null +++ b/client/src/Components/Sidebar/components/collapseButton.jsx @@ -0,0 +1,55 @@ +import IconButton from "@mui/material/IconButton"; +import ArrowRight from "../../ArrowRight"; +import ArrowLeft from "../../ArrowLeft"; +import { useTheme } from "@mui/material/styles"; +import { useDispatch } from "react-redux"; +import { toggleSidebar } from "../../../Features/UI/uiSlice"; +import PropTypes from "prop-types"; + +const CollapseButton = ({ collapsed }) => { + const theme = useTheme(); + const dispatch = useDispatch(); + const arrowIcon = collapsed ? ( + + ) : ( + + ); + return ( + { + dispatch(toggleSidebar()); + }} + > + {arrowIcon} + + ); +}; + +CollapseButton.propTypes = { + collapsed: PropTypes.bool.isRequired, +}; +export default CollapseButton; diff --git a/client/src/Components/Sidebar/components/logo.jsx b/client/src/Components/Sidebar/components/logo.jsx new file mode 100644 index 000000000..a0c0ff84e --- /dev/null +++ b/client/src/Components/Sidebar/components/logo.jsx @@ -0,0 +1,67 @@ +import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import { useTheme } from "@mui/material/styles"; +import { useNavigate } from "react-router"; +import { useTranslation } from "react-i18next"; +import PropTypes from "prop-types"; + +const Logo = ({ collapsed }) => { + const { t } = useTranslation(); + const theme = useTheme(); + const navigate = useNavigate(); + + return ( + navigate("/")} + sx={{ cursor: "pointer" }} + > + + C + + + {" "} + + {t("common.appName")} + + + + ); +}; + +Logo.propTypes = { + collapsed: PropTypes.bool, +}; + +export default Logo; diff --git a/client/src/Components/Sidebar/components/navItem.jsx b/client/src/Components/Sidebar/components/navItem.jsx new file mode 100644 index 000000000..2c569edde --- /dev/null +++ b/client/src/Components/Sidebar/components/navItem.jsx @@ -0,0 +1,97 @@ +import Tooltip from "@mui/material/Tooltip"; +import ListItemButton from "@mui/material/ListItemButton"; +import ListItemIcon from "@mui/material/ListItemIcon"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import { useTheme } from "@emotion/react"; +import PropTypes from "prop-types"; + +const NavItem = ({ item, collapsed, selected, onClick }) => { + const theme = useTheme(); + const iconStroke = selected + ? theme.palette.primary.contrastText + : theme.palette.primary.contrastTextTertiary; + + const buttonBgColor = selected ? theme.palette.secondary.main : "transparent"; + const buttonBgHoverColor = selected + ? theme.palette.secondary.main + : theme.palette.tertiary.main; + const fontWeight = selected ? 600 : 400; + return ( + + + + {item.icon} + + + + {item.name} + + + + + ); +}; + +NavItem.propTypes = { + item: PropTypes.object, + collapsed: PropTypes.bool, + selected: PropTypes.bool, + onClick: PropTypes.func, +}; +export default NavItem; diff --git a/client/src/Components/Sidebar/index.css b/client/src/Components/Sidebar/index.css deleted file mode 100644 index ef921acca..000000000 --- a/client/src/Components/Sidebar/index.css +++ /dev/null @@ -1,106 +0,0 @@ -/* TODO */ -aside .MuiList-root svg { - width: 20px; - height: 20px; - opacity: 0.9; -} -aside span.MuiTypography-root { - font-size: var(--env-var-font-size-medium); - line-height: 1; -} -aside .MuiStack-root + span.MuiTypography-root { - font-size: var(--env-var-font-size-medium-plus); -} -aside .MuiListSubheader-root { - font-size: var(--env-var-font-size-small); - font-weight: 500; - line-height: 1.5; - text-transform: uppercase; - margin-bottom: 2px; - opacity: 0.6; -} -aside p.MuiTypography-root { - font-size: var(--env-var-font-size-small); - opacity: 0.8; -} -aside .MuiListItemButton-root:not(.selected-path) > * { - opacity: 0.9; -} -aside .selected-path > * { - opacity: 1; -} -aside .selected-path span.MuiTypography-root { - font-weight: 600; -} -aside .MuiCollapse-wrapperInner .MuiList-root > .MuiListItemButton-root { - position: relative; -} -aside .MuiCollapse-wrapperInner .MuiList-root svg, -aside .MuiList-root .MuiListItemText-root + svg { - width: 18px; - height: 18px; -} - -.sidebar-popup li.MuiButtonBase-root:has(.MuiBox-root) { - padding-bottom: 0; -} -.sidebar-popup svg { - width: 16px; - height: 16px; - opacity: 0.9; -} - -/* TRANSITIONS */ -aside { - flex: 1; - transition: max-width 650ms cubic-bezier(0.36, -0.01, 0, 0.77); -} -.home-layout aside.collapsed { - max-width: 64px; -} - -aside.expanded .MuiTypography-root, -aside.expanded p.MuiTypography-root, -aside.expanded .MuiListItemText-root + svg, -aside.expanded .MuiAvatar-root + .MuiBox-root + .MuiIconButton-root { - visibility: visible; - animation: fadeIn 1s ease; -} - -aside.collapsed .MuiTypography-root, -aside.collapsed p.MuiTypography-root, -aside.collapsed .MuiListItemText-root + svg, -aside.collapsed .MuiAvatar-root + .MuiBox-root + .MuiIconButton-root { - opacity: 0; - visibility: hidden; -} - -aside .MuiListSubheader-root { - transition: padding 200ms ease; -} - -.sidebar-delay-fade { - opacity: 0; - visibility: hidden; - transition: opacity 0.3s ease; -} - -aside.expanded.sidebar-ready .sidebar-delay-fade { - opacity: 1; - visibility: visible; -} - -@keyframes fadeIn { - 0% { - opacity: 0; - visibility: hidden; - } - 30% { - opacity: 0; - visibility: hidden; - } - 100% { - opacity: 0.9; - visibility: visible; - } -} diff --git a/client/src/Components/Sidebar/index.jsx b/client/src/Components/Sidebar/index.jsx index 219ceff1e..6b7b6577c 100644 --- a/client/src/Components/Sidebar/index.jsx +++ b/client/src/Components/Sidebar/index.jsx @@ -1,26 +1,16 @@ -import React, { useEffect, useState, useRef } from "react"; -import { - Box, - Collapse, - Divider, - IconButton, - List, - ListItemButton, - ListItemIcon, - ListItemText, - Menu, - MenuItem, - Stack, - Tooltip, - Typography, -} from "@mui/material"; -import ThemeSwitch from "../ThemeSwitch"; -import Avatar from "../Avatar"; +import Stack from "@mui/material/Stack"; + +import List from "@mui/material/List"; +import Logo from "./components/logo"; +import CollapseButton from "./components/collapseButton"; +import Divider from "@mui/material/Divider"; +import NavItem from "./components/navItem"; +import AuthFooter from "./components/authFooter"; + import StarPrompt from "../StarPrompt"; import LockSvg from "../../assets/icons/lock.svg?react"; import UserSvg from "../../assets/icons/user.svg?react"; import TeamSvg from "../../assets/icons/user-two.svg?react"; -import LogoutSvg from "../../assets/icons/logout.svg?react"; import Support from "../../assets/icons/support.svg?react"; import Maintenance from "../../assets/icons/maintenance.svg?react"; import Monitors from "../../assets/icons/monitors.svg?react"; @@ -28,11 +18,6 @@ import Incidents from "../../assets/icons/incidents.svg?react"; import Integrations from "../../assets/icons/integrations.svg?react"; import PageSpeed from "../../assets/icons/page-speed.svg?react"; import Settings from "../../assets/icons/settings.svg?react"; -import ArrowDown from "../../assets/icons/down-arrow.svg?react"; -import ArrowUp from "../../assets/icons/up-arrow.svg?react"; -import ArrowRight from "../../assets/icons/right-arrow.svg?react"; -import ArrowLeft from "../../assets/icons/left-arrow.svg?react"; -import DotsVertical from "../../assets/icons/dots-vertical.svg?react"; import ChangeLog from "../../assets/icons/changeLog.svg?react"; import Docs from "../../assets/icons/docs.svg?react"; import StatusPages from "../../assets/icons/status-pages.svg?react"; @@ -40,17 +25,18 @@ import Discussions from "../../assets/icons/discussions.svg?react"; import Notifications from "../../assets/icons/notifications.svg?react"; import Logs from "../../assets/icons/logs.svg?react"; -import "./index.css"; - // Utils -import { useLocation, useNavigate } from "react-router"; -import { useTheme } from "@emotion/react"; -import { useDispatch, useSelector } from "react-redux"; +import { useTheme } from "@mui/material/styles"; +import { useSelector } from "react-redux"; import { useTranslation } from "react-i18next"; -import { clearAuthState } from "../../Features/Auth/authSlice"; -import { toggleSidebar } from "../../Features/UI/uiSlice"; -import { TurnedIn } from "@mui/icons-material"; -import { rules } from "eslint-plugin-react-refresh"; +import { useNavigate } from "react-router"; + +const URL_MAP = { + support: "https://discord.com/invite/NAb6H3UTjK", + discussions: "https://github.com/bluewave-labs/checkmate/discussions", + docs: "https://bluewavelabs.gitbook.io/checkmate", + changelog: "https://github.com/bluewave-labs/checkmate/releases", +}; const getMenu = (t) => [ { name: t("menu.uptime"), path: "uptime", icon: }, @@ -92,782 +78,94 @@ const getAccountMenuItems = (t) => [ { name: t("menu.team"), path: "account/team", icon: }, ]; -/* TODO this could be a key in nested Path would be the link */ -const URL_MAP = { - support: "https://discord.com/invite/NAb6H3UTjK", - discussions: "https://github.com/bluewave-labs/checkmate/discussions", - docs: "https://bluewavelabs.gitbook.io/checkmate", - changelog: "https://github.com/bluewave-labs/checkmate/releases", -}; - -const PATH_MAP = { - monitors: "Dashboard", - pagespeed: "Dashboard", - infrastructure: "Dashboard", - account: "Account", - settings: "Settings", -}; - -/** - * @component - * Sidebar component serves as a sidebar containing a menu. - * - * @returns {JSX.Element} The JSX element representing the Sidebar component. - */ - -function Sidebar() { +const Sidebar = () => { const theme = useTheme(); - const navigate = useNavigate(); - const location = useLocation(); - const dispatch = useDispatch(); const { t } = useTranslation(); - const authState = useSelector((state) => state.auth); + const navigate = useNavigate(); + // Redux state + const collapsed = useSelector((state) => state.ui.sidebar.collapsed); + const menu = getMenu(t); const otherMenuItems = getOtherMenuItems(t); const accountMenuItems = getAccountMenuItems(t); - const collapsed = useSelector((state) => state.ui.sidebar.collapsed); - const [open, setOpen] = useState({ Dashboard: false, Account: false, Other: false }); - const [anchorEl, setAnchorEl] = useState(null); - const [popup, setPopup] = useState(); - const { user } = useSelector((state) => state.auth); - const sidebarRef = useRef(null); - const [sidebarReady, setSidebarReady] = useState(false); - const TRANSITION_DURATION = 200; - let menu = getMenu(t); - menu = menu.filter((item) => { - if (item.path === "logs") { - return user.role?.includes("admin") || user.role?.includes("superadmin"); - } - return true; - }); - - useEffect(() => { - if (!collapsed) { - setSidebarReady(false); - const timeout = setTimeout(() => { - setSidebarReady(true); - }, TRANSITION_DURATION); - return () => clearTimeout(timeout); - } else { - setSidebarReady(false); - } - }, [collapsed]); - - const renderAccountMenuItems = () => { - let filteredAccountMenuItems = [...accountMenuItems]; - - // If the user is in demo mode, remove the "Password" option - if (user.role?.includes("demo")) { - filteredAccountMenuItems = filteredAccountMenuItems.filter( - (item) => item.name !== "Password" - ); - } - - // If the user is NOT a superadmin, remove the "Team" option - if (user.role && !user.role.includes("superadmin")) { - filteredAccountMenuItems = filteredAccountMenuItems.filter( - (item) => item.name !== "Team" - ); - } - - return filteredAccountMenuItems.map((item) => ( - { - closePopup(); - navigate(item.path); - }} - sx={{ - gap: theme.spacing(2), - borderRadius: theme.shape.borderRadius, - pl: theme.spacing(4), - }} - > - {item.icon} - {item.name} - - )); - }; - - const openPopup = (event, id) => { - setAnchorEl(event.currentTarget); - setPopup(id); - }; - const closePopup = () => { - setAnchorEl(null); - }; - - /** - * Handles logging out the user - * - */ - const logout = async () => { - // Clear auth state - dispatch(clearAuthState()); - navigate("/login"); - }; - - useEffect(() => { - const matchedKey = Object.keys(PATH_MAP).find((key) => - location.pathname.includes(key) - ); - - if (matchedKey) { - setOpen((prev) => ({ ...prev, [PATH_MAP[matchedKey]]: true })); - } - }, [location]); - - const iconColor = theme.palette.primary.contrastTextTertiary; - const sidebarClassName = `${collapsed ? "collapsed" : "expanded"} ${sidebarReady ? "sidebar-ready" : ""}`; - - /* TODO refactor this, there are a some ternaries and comments in the return */ return ( - + + { - setOpen((prev) => - Object.fromEntries(Object.keys(prev).map((key) => [key, false])) + > + {menu.map((item) => { + const selected = location.pathname.startsWith(`/${item.path}`); + return ( + navigate(`/${item.path}`)} + /> ); - dispatch(toggleSidebar()); - }} - > - {collapsed ? : } - - {/* TODO Alignment done using padding. Use single source of truth to that*/} - - {/* TODO Abstract logo into component */} - {/* TODO Turn logo into a link */} - navigate("/")} - sx={{ cursor: "pointer" }} - > - - C - - - {t("common.appName")} - - - - - - {menu.map((item) => { - return item.path ? ( - /* If item has a path */ - - navigate(`/${item.path}`)} - sx={{ - height: "37px", - gap: theme.spacing(4), - borderRadius: theme.shape.borderRadius, - px: theme.spacing(4), - pl: theme.spacing(5), - }} - > - {item.icon} - {item.name} - - - ) : collapsed ? ( - /* TODO Do we ever get here? If item does not have a path and collapsed state is true */ - - - openPopup(event, item.name)} - sx={{ - position: "relative", - gap: theme.spacing(4), - borderRadius: theme.shape.borderRadius, - px: theme.spacing(4), - }} - > - {item.icon} - {item.name} - - - - {item.nested.map((child) => { - if ( - child.name === "Team" && - authState.user?.role && - !authState.user.role.includes("superadmin") - ) { - return null; - } - - return ( - { - const url = URL_MAP[child.path]; - if (url) { - window.open(url, "_blank", "noreferrer"); - } else { - navigate(`/${child.path}`); - } - closePopup(); - }} - sx={{ - gap: theme.spacing(4), - opacity: 0.9, - /* TODO this has no effect? */ - "& svg": { - "& path": { - stroke: theme.palette.primary.contrastTextTertiary, - strokeWidth: 1.1, - }, - }, - }} - > - {child.icon} - {child.name} - - ); - })} - - - ) : ( - /* TODO Do we ever get here? If item does not have a path and collapsed state is false */ - - - setOpen((prev) => ({ - ...Object.fromEntries(Object.keys(prev).map((key) => [key, false])), - [item.name]: !prev[item.name], - })) - } - sx={{ - gap: theme.spacing(4), - borderRadius: theme.shape.borderRadius, - px: theme.spacing(4), - }} - > - {item.icon} - {item.name} - {open[`${item.name}`] ? : } - - - - {item.nested.map((child) => { - if ( - child.name === "Team" && - authState.user?.role && - !authState.user.role.includes("superadmin") - ) { - return null; - } - - return ( - { - const url = URL_MAP[child.path]; - if (url) { - window.open(url, "_blank", "noreferrer"); - } else { - navigate(`/${child.path}`); - } - }} - sx={{ - gap: theme.spacing(4), - borderRadius: theme.shape.borderRadius, - pl: theme.spacing(4), - "&::before": { - content: `""`, - position: "absolute", - top: 0, - left: "-7px", - height: "100%", - borderLeft: 1, - borderLeftColor: theme.palette.primary.lowContrast, - }, - "&:last-child::before": { - height: "50%", - }, - "&::after": { - content: `""`, - position: "absolute", - top: "45%", - left: "-8px", - height: "3px", - width: "3px", - borderRadius: "50%", - backgroundColor: theme.palette.primary.lowContrast, - }, - "&.selected-path::after": { - /* TODO what is this selector doing? */ - backgroundColor: theme.palette.primary.contrastTextTertiary, - transform: "scale(1.2)", - }, - }} - > - {child.icon} - {child.name} - - ); - })} - - - - ); - })} - - - + })} + {!collapsed && } - {otherMenuItems.map((item) => { - return item.path ? ( - - { + const url = URL_MAP[item.path]; + if (url) { + window.open(url, "_blank", "noreferrer"); + } else { + navigate(`/${item.path}`); } - onClick={() => { - const url = URL_MAP[item.path]; - if (url) { - window.open(url, "_blank", "noreferrer"); - } else { - navigate(`/${item.path}`); - } - }} - sx={{ - height: "37px", - gap: theme.spacing(4), - borderRadius: theme.shape.borderRadius, - px: theme.spacing(4), - pl: theme.spacing(5), - }} - > - {item.icon} - {item.name} {" "} - - - ) : null; + }} + /> + ); })} - - {collapsed ? ( - <> - - openPopup(event, "logout")} - sx={{ p: 0, "&:focus": { outline: "none" } }} - > - - - - - ) : ( - <> - - - - {authState.user?.firstName} {authState.user?.lastName} - - - {authState.user?.role?.includes("superadmin") - ? t("roles.superAdmin") - : authState.user?.role?.includes("admin") - ? t("roles.admin") - : authState.user?.role?.includes("user") - ? t("roles.teamMember") - : authState.user?.role?.includes("demo") - ? t("roles.demoUser") - : authState.user?.role} - - - - - - openPopup(event, "logout")} - > - - - - - - )} - - {collapsed && ( - - - - {authState.user?.firstName} {authState.user?.lastName} - - - {authState.user?.role} - - - - )} - {/* TODO Do we need two dividers? */} - {collapsed && } - {/* */} - {renderAccountMenuItems()} - - - {t("menu.logOut", "Log out")} - - - + ); -} +}; export default Sidebar; diff --git a/client/src/Components/Tab/index.jsx b/client/src/Components/Tab/index.jsx index 72709cc00..7609904d9 100644 --- a/client/src/Components/Tab/index.jsx +++ b/client/src/Components/Tab/index.jsx @@ -16,6 +16,7 @@ const CustomTabList = ({ value, onChange, children, ...props }) => { return ( - + ); diff --git a/client/src/Components/Table/index.jsx b/client/src/Components/Table/index.jsx index b6d31bebe..66db50fbe 100644 --- a/client/src/Components/Table/index.jsx +++ b/client/src/Components/Table/index.jsx @@ -70,6 +70,9 @@ const DataTable = ({ backgroundColor: theme.palette.primary.main, color: theme.palette.primary.contrastTextSecondary, }, + "& .MuiTableBody-root .MuiTableRow-root:last-child .MuiTableCell-root": { + borderBottom: "none", + }, }} > diff --git a/client/src/Components/Toast/body.jsx b/client/src/Components/Toast/body.jsx new file mode 100644 index 000000000..04d5aee99 --- /dev/null +++ b/client/src/Components/Toast/body.jsx @@ -0,0 +1,34 @@ +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; + +import { useTheme } from "@emotion/react"; +import PropTypes from "prop-types"; + +const ToastBody = ({ body }) => { + 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/monitorHooks.js b/client/src/Hooks/monitorHooks.js index 4f0e9c049..970b5d97f 100644 --- a/client/src/Hooks/monitorHooks.js +++ b/client/src/Hooks/monitorHooks.js @@ -202,6 +202,25 @@ const useFetchStatsByMonitorId = ({ return [monitor, audits, isLoading, networkError]; }; +const useFetchMonitorGames = ({ setGames, updateTrigger }) => { + const [isLoading, setIsLoading] = useState(true); + useEffect(() => { + const fetchGames = async () => { + try { + setIsLoading(true); + const res = await networkService.getMonitorGames(); + setGames(res.data.data); + } catch (error) { + createToast({ body: error.message }); + } finally { + setIsLoading(false); + } + }; + fetchGames(); + }, [setGames, updateTrigger]); + return [isLoading]; +}; + const useFetchMonitorById = ({ monitorId, setMonitor, updateTrigger }) => { const [isLoading, setIsLoading] = useState(true); useEffect(() => { @@ -300,6 +319,28 @@ const useCreateMonitor = () => { return [createMonitor, isLoading]; }; +const useFetchGlobalSettings = () => { + const [isLoading, setIsLoading] = useState(true); + const [globalSettings, setGlobalSettings] = useState(undefined); + useEffect(() => { + const fetchGlobalSettings = async () => { + try { + const res = await networkService.getAppSettings(); + setGlobalSettings(res?.data); + } catch (error) { + console.error("Failed to fetch global settings:", error); + createToast({ body: "Failed to load global settings" }); + } finally { + setIsLoading(false); + } + }; + + fetchGlobalSettings(); + }, []); + + return [globalSettings, isLoading]; +}; + const useDeleteMonitor = () => { const [isLoading, setIsLoading] = useState(false); const navigate = useNavigate(); @@ -335,7 +376,12 @@ const useUpdateMonitor = () => { expectedValue: monitor.expectedValue, ignoreTlsErrors: monitor.ignoreTlsErrors, jsonPath: monitor.jsonPath, - ...(monitor.type === "port" && { port: monitor.port }), + ...((monitor.type === "port" || monitor.type === "game") && { + port: monitor.port, + }), + ...(monitor.type == "game" && { + gameId: monitor.gameId, + }), ...(monitor.type === "hardware" && { thresholds: monitor.thresholds, secret: monitor.secret, @@ -499,6 +545,7 @@ export { useFetchUptimeMonitorById, useFetchHardwareMonitorById, useCreateMonitor, + useFetchGlobalSettings, useDeleteMonitor, useUpdateMonitor, usePauseMonitor, @@ -507,4 +554,5 @@ export { useDeleteMonitorStats, useCreateBulkMonitors, useExportMonitors, + useFetchMonitorGames, }; 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/Auth/Login/hooks/useLoadingSubmit.jsx b/client/src/Pages/Auth/Login/hooks/useLoadingSubmit.jsx new file mode 100644 index 000000000..a779165b6 --- /dev/null +++ b/client/src/Pages/Auth/Login/hooks/useLoadingSubmit.jsx @@ -0,0 +1,22 @@ +// Hook to avoid double submits and manage loading state +import { useState, useCallback } from "react"; + +const useLoadingSubmit = () => { + const [submitting, setSubmitting] = useState(false); + const executeSubmit = useCallback( + async (submitFunction) => { + if (submitting) return; + setSubmitting(true); + try { + return await submitFunction(); + } finally { + setSubmitting(false); + } + }, + [submitting] + ); + + return { submitting, executeSubmit }; +}; + +export default useLoadingSubmit; diff --git a/client/src/Pages/Auth/Login/hooks/useLoginForm.jsx b/client/src/Pages/Auth/Login/hooks/useLoginForm.jsx new file mode 100644 index 000000000..61814d723 --- /dev/null +++ b/client/src/Pages/Auth/Login/hooks/useLoginForm.jsx @@ -0,0 +1,19 @@ +import { useState } from "react"; + +const useLoginForm = () => { + const [form, setForm] = useState({ + email: "", + password: "", + }); + const onChange = (e) => { + let { name, value } = e.target; + if (name === "email") { + value = value.toLowerCase(); + } + const updatedForm = { ...form, [name]: value }; + setForm(updatedForm); + }; + return [form, onChange]; +}; + +export default useLoginForm; diff --git a/client/src/Pages/Auth/Login/hooks/useLoginSubmit.jsx b/client/src/Pages/Auth/Login/hooks/useLoginSubmit.jsx new file mode 100644 index 000000000..6bc5716ff --- /dev/null +++ b/client/src/Pages/Auth/Login/hooks/useLoginSubmit.jsx @@ -0,0 +1,40 @@ +import { useDispatch } from "react-redux"; +import { login } from "../../../../Features/Auth/authSlice"; +import { useNavigate } from "react-router-dom"; +import { createToast } from "../../../../Utils/toastUtils"; +import { useTranslation } from "react-i18next"; + +const useLoginSubmit = () => { + const dispatch = useDispatch(); + const navigate = useNavigate(); + const { t } = useTranslation(); + + const handleLoginSubmit = async (form, setErrors) => { + const action = await dispatch(login(form)); + if (action.payload.success) { + navigate("/uptime"); + createToast({ + body: t("auth.login.toasts.success"), + }); + } else { + if (action.payload) { + if (action.payload.msg === "Incorrect password") + setErrors({ + password: t("auth.login.errors.password.incorrect"), + }); + // dispatch errors + createToast({ + body: t("auth.login.toasts.incorrectPassword"), + }); + } else { + // unknown errors + createToast({ + body: t("common.toasts.unknownError"), + }); + } + } + }; + return [handleLoginSubmit]; +}; + +export default useLoginSubmit; diff --git a/client/src/Pages/Auth/Login/hooks/useValidateLoginForm.jsx b/client/src/Pages/Auth/Login/hooks/useValidateLoginForm.jsx new file mode 100644 index 000000000..e5d045be8 --- /dev/null +++ b/client/src/Pages/Auth/Login/hooks/useValidateLoginForm.jsx @@ -0,0 +1,31 @@ +import { useState } from "react"; +import { loginCredentials } from "../../../../Validation/validation"; + +const useValidateLoginForm = () => { + const [errors, setErrors] = useState({ + email: "", + password: "", + }); + const validateField = (name, value) => { + const { error } = loginCredentials.validate({ [name]: value }); + setErrors((prev) => ({ + ...prev, + [name]: error?.details?.[0]?.message || "", + })); + }; + const validateForm = (form) => { + const { error } = loginCredentials.validate(form, { abortEarly: false }); + if (error) { + const formErrors = {}; + for (const err of error.details) { + formErrors[err.path[0]] = err.message; + } + setErrors(formErrors); + return false; + } + return true; + }; + return [errors, setErrors, validateField, validateForm]; +}; + +export default useValidateLoginForm; diff --git a/client/src/Pages/Auth/Login/index.jsx b/client/src/Pages/Auth/Login/index.jsx index 098fbbf08..d06ecef6d 100644 --- a/client/src/Pages/Auth/Login/index.jsx +++ b/client/src/Pages/Auth/Login/index.jsx @@ -1,167 +1,102 @@ // Components import Stack from "@mui/material/Stack"; -import AuthHeader from "../components/AuthHeader"; import Button from "@mui/material/Button"; import TextInput from "../../../Components/Inputs/TextInput"; import { PasswordEndAdornment } from "../../../Components/Inputs/TextInput/Adornments"; -import { loginCredentials } from "../../../Validation/validation"; import TextLink from "../../../Components/TextLink"; -import Typography from "@mui/material/Typography"; - +import AuthPageWrapper from "../components/AuthPageWrapper"; // Utils -import { login } from "../../../Features/Auth/authSlice"; -import { useNavigate } from "react-router-dom"; -import { useDispatch } from "react-redux"; -import { useState } from "react"; import { useTheme } from "@mui/material/styles"; import { useTranslation } from "react-i18next"; -import { createToast } from "../../../Utils/toastUtils"; +import useLoginForm from "./hooks/useLoginForm"; +import useValidateLoginForm from "./hooks/useValidateLoginForm"; +import useLoginSubmit from "./hooks/useLoginSubmit"; +import useLoadingSubmit from "./hooks/useLoadingSubmit"; const Login = () => { - // Local state - const [form, setForm] = useState({ - email: "", - password: "", - }); - const [errors, setErrors] = useState({ - email: "", - password: "", - }); - // Hooks const { t } = useTranslation(); const theme = useTheme(); - const dispatch = useDispatch(); - const navigate = useNavigate(); + const [form, onChange] = useLoginForm(); + const [errors, setErrors, validateField, validateForm] = useValidateLoginForm(); + const [handleLoginSubmit] = useLoginSubmit(); + const { submitting, executeSubmit } = useLoadingSubmit(); // Handlers - const onChange = (e) => { - let { name, value } = e.target; - if (name === "email") { - value = value.toLowerCase(); - } - const updatedForm = { ...form, [name]: value }; - const { error } = loginCredentials.validate({ [name]: value }); - setForm(updatedForm); - setErrors((prev) => ({ - ...prev, - [name]: error?.details?.[0]?.message || "", - })); + const handleChange = (e) => { + onChange(e); + validateField(e.target.name, e.target.value); }; - const onSubmit = async (e) => { e.preventDefault(); - const toSubmit = { ...form }; - const { error } = loginCredentials.validate(toSubmit, { abortEarly: false }); - - if (error) { - const formErrors = {}; - for (const err of error.details) { - formErrors[err.path[0]] = err.message; - } - setErrors(formErrors); - return; - } - - const action = await dispatch(login(form)); - if (action.payload.success) { - navigate("/uptime"); - createToast({ - body: t("auth.login.toasts.success"), - }); - } else { - if (action.payload) { - if (action.payload.msg === "Incorrect password") - setErrors({ - password: t("auth.login.errors.password.incorrect"), - }); - // dispatch errors - createToast({ - body: t("auth.login.toasts.incorrectPassword"), - }); - } else { - // unknown errors - createToast({ - body: t("common.toasts.unknownError"), - }); - } - } + const isValid = validateForm(form); + if (!isValid) return; + await executeSubmit(() => handleLoginSubmit(form, setErrors)); }; - return ( - - - {t("auth.login.heading")} - - + } + /> + - - - + Login + - + + + ); }; - export default Login; diff --git a/client/src/Pages/Auth/Register/index.jsx b/client/src/Pages/Auth/Register/index.jsx index 97b27e5cf..2b45d2d15 100644 --- a/client/src/Pages/Auth/Register/index.jsx +++ b/client/src/Pages/Auth/Register/index.jsx @@ -1,10 +1,10 @@ // Components import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; -import AuthHeader from "../components/AuthHeader"; import TextInput from "../../../Components/Inputs/TextInput"; -import Check from "../../../Components/Check/Check"; import Button from "@mui/material/Button"; +import Box from "@mui/material/Box"; +import PasswordTooltip from "../components/PasswordTooltip"; // Utils import { useTheme } from "@emotion/react"; @@ -16,6 +16,7 @@ import { useParams } from "react-router-dom"; import { networkService } from "../../../main"; import { newOrChangedCredentials } from "../../../Validation/validation"; import { register } from "../../../Features/Auth/authSlice"; +import AuthPageWrapper from "../components/AuthPageWrapper"; import { createToast } from "../../../Utils/toastUtils"; import PropTypes from "prop-types"; @@ -195,59 +196,38 @@ const Register = ({ superAdminExists }) => { }; return ( - - - {t("auth.registration.heading.user")} - + + {superAdminExists + ? t("auth.registration.description.user") + : t("auth.registration.description.superAdmin")} + - - {superAdminExists - ? t("auth.registration.heading.user") - : t("auth.registration.heading.superAdmin")} - - - {superAdminExists - ? t("auth.registration.description.user") - : t("auth.registration.description.superAdmin")} - - (e.target.value = e.target.value.toLowerCase())} - onChange={onChange} - error={errors.email ? true : false} - helperText={errors.email ? t(errors.email) : ""} // Localization keys are in validation.js - /> { { error={errors.lastName ? true : false} helperText={errors.lastName ? t(errors.lastName) : ""} // Localization keys are in validation.js /> - - - - - - - - - - - + (e.target.value = e.target.value.toLowerCase())} + onChange={onChange} + error={errors.email ? true : false} + helperText={errors.email ? t(errors.email) : ""} // Localization keys are in validation.js + /> + + + + + + + - + ); }; diff --git a/client/src/Pages/Auth/components/AuthHeader.jsx b/client/src/Pages/Auth/components/AuthHeader.jsx index c6ad46a47..ce454c065 100644 --- a/client/src/Pages/Auth/components/AuthHeader.jsx +++ b/client/src/Pages/Auth/components/AuthHeader.jsx @@ -9,7 +9,7 @@ import ThemeSwitch from "../../../Components/ThemeSwitch"; import { useTheme } from "@mui/material/styles"; import { useTranslation } from "react-i18next"; -const AuthHeader = () => { +const AuthHeader = ({ hideLogo = false }) => { // Hooks const theme = useTheme(); const { t } = useTranslation(); @@ -27,8 +27,12 @@ const AuthHeader = () => { alignItems="center" gap={theme.spacing(4)} > - - {t("common.appName")} + {!hideLogo && ( + <> + + {t("common.appName")} + + )} { + const theme = useTheme(); + return ( + + + + + + + + + + + + + + + + {welcome} + + {heading} + + {children} + + + ); +}; + +export default AuthPageWrapper; + +AuthPageWrapper.propTypes = { + children: PropTypes.node, + heading: PropTypes.node, + welcome: PropTypes.node, +}; diff --git a/client/src/Pages/Auth/components/PasswordTooltip.jsx b/client/src/Pages/Auth/components/PasswordTooltip.jsx new file mode 100644 index 000000000..93a74403a --- /dev/null +++ b/client/src/Pages/Auth/components/PasswordTooltip.jsx @@ -0,0 +1,116 @@ +import Check from "../../../Components/Check/Check"; +import Stack from "@mui/material/Stack"; +import { Tooltip, useTheme } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import PropTypes from "prop-types"; + +const PasswordTooltip = ({ feedback, form, children }) => { + const theme = useTheme(); + const { t } = useTranslation(); + const hasPassword = form.password.length > 0; + const hasInvalidFeedback = Object.values(feedback).some( + (status) => status !== "success" + ); + const showPasswordTooltip = hasPassword && hasInvalidFeedback; + return ( + + + + + + + + + } + slotProps={{ + tooltip: { + sx: { + backgroundColor: theme.palette.tertiary.background, + border: `0.5px solid ${theme.palette.primary.lowContrast}90`, + borderRadius: theme.spacing(4), + color: theme.palette.primary.contrastText, + width: "auto", + maxWidth: { xs: "25vw", md: "none" }, + whiteSpace: { xs: "normal", md: "nowrap" }, + paddingTop: theme.spacing(8), + px: theme.spacing(8), + }, + }, + arrow: { + sx: { + color: theme.palette.tertiary.background, + }, + }, + }} + > + {children} + + ); +}; + +PasswordTooltip.propTypes = { + feedback: PropTypes.shape({ + length: PropTypes.string.isRequired, + special: PropTypes.string, + number: PropTypes.string, + uppercase: PropTypes.string, + lowercase: PropTypes.string, + confirm: PropTypes.string, + }), + form: PropTypes.shape({ + password: PropTypes.string.isRequired, + }), + children: PropTypes.node, +}; + +export default PasswordTooltip; diff --git a/client/src/Pages/Incidents/Components/StatusBoxes/StatusBox.jsx b/client/src/Pages/Incidents/Components/StatusBoxes/StatusBox.jsx index b19f017e7..7b20e3d51 100644 --- a/client/src/Pages/Incidents/Components/StatusBoxes/StatusBox.jsx +++ b/client/src/Pages/Incidents/Components/StatusBoxes/StatusBox.jsx @@ -2,18 +2,22 @@ import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; import { Box, Stack, Typography } from "@mui/material"; import Background from "../../../../assets/Images/background-grid.svg?react"; -import MonitorHeartOutlinedIcon from "@mui/icons-material/MonitorHeartOutlined"; -import TaskAltOutlinedIcon from "@mui/icons-material/TaskAltOutlined"; -import CancelOutlinedIcon from "@mui/icons-material/CancelOutlined"; -import WarningAmberRoundedIcon from "@mui/icons-material/WarningAmberRounded"; +import AlertIcon from "../../../../assets/icons/alert-icon.svg?react"; +import CheckIcon from "../../../../assets/icons/check-icon.svg?react"; +import CloseIcon from "../../../../assets/icons/close-icon.svg?react"; +import WarningIcon from "../../../../assets/icons/warning-icon.svg?react"; const StatusBox = ({ title, value, status }) => { const theme = useTheme(); let sharedStyles = { position: "absolute", right: 8, - opacity: 0.5, - "& svg path": { stroke: theme.palette.primary.contrastTextTertiary }, + "& svg": { + width: 20, + height: 20, + opacity: 0.9, + "& path": { stroke: theme.palette.primary.contrastTextTertiary, strokeWidth: 1.7 }, + }, }; let color; @@ -22,28 +26,28 @@ const StatusBox = ({ title, value, status }) => { color = theme.palette.success.lowContrast; icon = ( - + ); } else if (status === "down") { color = theme.palette.error.lowContrast; icon = ( - + ); } else if (status === "paused") { color = theme.palette.warning.lowContrast; icon = ( - + ); } else { color = theme.palette.accent.main; icon = ( - + ); } 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 = () => { - + { - return Object.keys(errors).filter((k) => k.startsWith(METRIC_PREFIX)).length > 0; -}; - -const getAlertError = (errors) => { - return Object.keys(errors).find((key) => key.startsWith(METRIC_PREFIX)) - ? errors[Object.keys(errors).find((key) => key.startsWith(METRIC_PREFIX))] - : null; -}; +// Utils +import NotificationsConfig from "../../../Components/NotificationConfig"; +import { capitalizeFirstLetter } from "../../../Utils/stringUtils"; +import { infrastructureMonitorValidation } from "../../../Validation/validation"; +import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications"; +import { useMonitorUtils } from "../../../Hooks/useMonitorUtils"; +import { useParams } from "react-router-dom"; +import { useSelector } from "react-redux"; +import { useState, useEffect } from "react"; +import { useTheme } from "@emotion/react"; +import { useTranslation } from "react-i18next"; +import { + useCreateMonitor, + useDeleteMonitor, + useFetchGlobalSettings, + useFetchHardwareMonitorById, + usePauseMonitor, + useUpdateMonitor, +} from "../../../Hooks/monitorHooks"; const CreateInfrastructureMonitor = () => { - const theme = useTheme(); const { user } = useSelector((state) => state.auth); const { monitorId } = useParams(); - const { t } = useTranslation(); - - // Determine if we are creating or editing const isCreate = typeof monitorId === "undefined"; - // Fetch monitor details if editing - const [monitor, isLoading, networkError] = useFetchHardwareMonitorById({ monitorId }); - const [notifications, notificationsAreLoading, notificationsError] = - useGetNotificationsByTeamId(); - const [updateMonitor, isUpdating] = useUpdateMonitor(); - const [createMonitor, isCreating] = useCreateMonitor(); + const theme = useTheme(); + const { t } = useTranslation(); // State const [errors, setErrors] = useState({}); const [https, setHttps] = useState(false); + const [isOpen, setIsOpen] = useState(false); + const [updateTrigger, setUpdateTrigger] = useState(false); const [infrastructureMonitor, setInfrastructureMonitor] = useState({ url: "", name: "", @@ -85,36 +63,107 @@ const CreateInfrastructureMonitor = () => { secret: "", }); + // Fetch monitor details if editing + const { statusColor, pagespeedStatusMsg, determineState } = useMonitorUtils(); + const [monitor, isLoading] = useFetchHardwareMonitorById({ + monitorId, + updateTrigger, + }); + const [createMonitor, isCreating] = useCreateMonitor(); + const [deleteMonitor, isDeleting] = useDeleteMonitor(); + const [globalSettings, globalSettingsLoading] = useFetchGlobalSettings(); + const [notifications, notificationsAreLoading] = useGetNotificationsByTeamId(); + const [pauseMonitor, isPausing] = usePauseMonitor(); + const [updateMonitor, isUpdating] = useUpdateMonitor(); + + const FREQUENCIES = [ + { _id: 0.25, name: t("time.fifteenSeconds") }, + { _id: 0.5, name: t("time.thirtySeconds") }, + { _id: 1, name: t("time.oneMinute") }, + { _id: 2, name: t("time.twoMinutes") }, + { _id: 5, name: t("time.fiveMinutes") }, + { _id: 10, name: t("time.tenMinutes") }, + ]; + const CRUMBS = [ + { name: "Infrastructure monitors", path: "/infrastructure" }, + ...(isCreate + ? [{ name: "Create", path: "/infrastructure/create" }] + : [ + { name: "Details", path: `/infrastructure/${monitorId}` }, + { name: "Configure", path: `/infrastructure/configure/${monitorId}` }, + ]), + ]; + const METRICS = ["cpu", "memory", "disk", "temperature"]; + const METRIC_PREFIX = "usage_"; + const MS_PER_MINUTE = 60000; + + const hasAlertError = (errors) => { + return Object.keys(errors).filter((k) => k.startsWith(METRIC_PREFIX)).length > 0; + }; + + const getAlertError = (errors) => { + const errorKey = Object.keys(errors).find((key) => key.startsWith(METRIC_PREFIX)); + return errorKey ? errors[errorKey] : null; + }; + // Populate form fields if editing useEffect(() => { - if (isCreate || !monitor) return; + if (isCreate) { + if (globalSettingsLoading) return; - setInfrastructureMonitor({ - url: monitor.url.replace(/^https?:\/\//, ""), - name: monitor.name || "", - notifications: monitor.notifications, - interval: monitor.interval / MS_PER_MINUTE, - cpu: monitor.thresholds?.usage_cpu !== undefined, - usage_cpu: monitor.thresholds?.usage_cpu ? monitor.thresholds.usage_cpu * 100 : "", + const gt = globalSettings?.data?.settings?.globalThresholds || {}; - memory: monitor.thresholds?.usage_memory !== undefined, - usage_memory: monitor.thresholds?.usage_memory - ? monitor.thresholds.usage_memory * 100 - : "", + setHttps(false); - disk: monitor.thresholds?.usage_disk !== undefined, - usage_disk: monitor.thresholds?.usage_disk - ? monitor.thresholds.usage_disk * 100 - : "", + setInfrastructureMonitor({ + url: "", + name: "", + notifications: [], + interval: 0.25, + cpu: gt.cpu !== undefined, + usage_cpu: gt.cpu !== undefined ? gt.cpu.toString() : "", + memory: gt.memory !== undefined, + usage_memory: gt.memory !== undefined ? gt.memory.toString() : "", + disk: gt.disk !== undefined, + usage_disk: gt.disk !== undefined ? gt.disk.toString() : "", + temperature: gt.temperature !== undefined, + usage_temperature: gt.temperature !== undefined ? gt.temperature.toString() : "", + secret: "", + }); + } else if (monitor) { + const { thresholds = {} } = monitor; - temperature: monitor.thresholds?.usage_temperature !== undefined, - usage_temperature: monitor.thresholds?.usage_temperature - ? monitor.thresholds.usage_temperature * 100 - : "", - secret: monitor.secret || "", - }); - setHttps(monitor.url.startsWith("https")); - }, [isCreate, monitor]); + setHttps(monitor.url.startsWith("https")); + + setInfrastructureMonitor({ + url: monitor.url.replace(/^https?:\/\//, ""), + name: monitor.name || "", + notifications: monitor.notifications || [], + interval: monitor.interval / MS_PER_MINUTE, + cpu: thresholds.usage_cpu !== undefined, + usage_cpu: + thresholds.usage_cpu !== undefined + ? (thresholds.usage_cpu * 100).toString() + : "", + memory: thresholds.usage_memory !== undefined, + usage_memory: + thresholds.usage_memory !== undefined + ? (thresholds.usage_memory * 100).toString() + : "", + disk: thresholds.usage_disk !== undefined, + usage_disk: + thresholds.usage_disk !== undefined + ? (thresholds.usage_disk * 100).toString() + : "", + temperature: thresholds.usage_temperature !== undefined, + usage_temperature: + thresholds.usage_temperature !== undefined + ? (thresholds.usage_temperature * 100).toString() + : "", + secret: monitor.secret || "", + }); + } + }, [isCreate, monitor, globalSettings, globalSettingsLoading]); // Handlers const onSubmit = async (event) => { @@ -197,6 +246,10 @@ const CreateInfrastructureMonitor = () => { : await updateMonitor({ monitor: form, redirect: "/infrastructure" }); }; + const triggerUpdate = () => { + setUpdateTrigger(!updateTrigger); + }; + const onChange = (event) => { const { value, name } = event.target; setInfrastructureMonitor({ @@ -223,19 +276,26 @@ const CreateInfrastructureMonitor = () => { }); }; + const handlePause = async () => { + await pauseMonitor({ monitorId, triggerUpdate }); + }; + + const handleRemove = async (event) => { + event.preventDefault(); + await deleteMonitor({ monitor, redirect: "/infrastructure" }); + }; + + const isBusy = + isLoading || + isUpdating || + isCreating || + isDeleting || + isPausing || + notificationsAreLoading; + return ( - + { gap={theme.spacing(12)} mt={theme.spacing(6)} > - - - {t(isCreate ? "infrastructureCreateYour" : "infrastructureEditYour")}{" "} - - - {t("monitor")} - - + + + + {!isCreate ? infrastructureMonitor.name : t("createYour") + " "} + + {isCreate ? ( + + {t("monitor")} + + ) : ( + <> + )} + + {!isCreate && ( + + + + + + + + {infrastructureMonitor.url?.replace(/^https?:\/\//, "") || "..."} + + + {t("editing")} + + + )} + + {!isCreate && ( + + + + + )} + - + { /> - + { disabled={!isCreate} /> {isCreate && ( - - {t("infrastructureProtocol")} + - + )} { - {t("notificationConfig.title")} + + {t("notificationConfig.title")} + {t("notificationConfig.description")} { label="Check frequency" value={infrastructureMonitor.interval || 15} onChange={onChange} - items={SELECT_VALUES} + items={FREQUENCIES} /> @@ -433,12 +614,23 @@ const CreateInfrastructureMonitor = () => { type="submit" variant="contained" color="accent" - loading={isLoading || isUpdating || isCreating || notificationsAreLoading} + loading={isBusy} > {t(isCreate ? "infrastructureCreateMonitor" : "infrastructureEditMonitor")} + {!isCreate && ( + setIsOpen(false)} + confirmationButtonLabel={t("delete")} + onConfirm={handleRemove} + /> + )} ); }; diff --git a/client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/index.jsx b/client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/index.jsx index 4dd978018..3c1c68f8b 100644 --- a/client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/index.jsx +++ b/client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/index.jsx @@ -2,18 +2,19 @@ import { Stack } from "@mui/material"; import Gauge from "./Gauge"; import SkeletonLayout from "./skeleton"; +import PropTypes from "prop-types"; // Utils import { useHardwareUtils } from "../../Hooks/useHardwareUtils"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; -const Gauges = ({ shouldRender, monitor }) => { +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/Components/NetworkStats/NetworkCharts.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx new file mode 100644 index 000000000..9157a3432 --- /dev/null +++ b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx @@ -0,0 +1,132 @@ +import PropTypes from "prop-types"; +import { Stack, Typography } from "@mui/material"; +import InfraAreaChart from "../../../../../Pages/Infrastructure/Details/Components/AreaChartBoxes/InfraAreaChart"; + +import { + TzTick, + InfrastructureTooltip, + NetworkTick, +} from "../../../../../Components/Charts/Utils/chartUtils"; +import { useTheme } from "@emotion/react"; +import { useTranslation } from "react-i18next"; +import { useHardwareUtils } from "../../Hooks/useHardwareUtils"; + +const NetworkCharts = ({ ethernetData, dateRange }) => { + const theme = useTheme(); + const { t } = useTranslation(); + + const { formatBytesPerSecondString, formatPacketsPerSecondString } = useHardwareUtils(); + + if (!ethernetData?.length) { + return {t("noNetworkStatsAvailable")}; + } + + const configs = [ + { + type: "network-bytes", + data: ethernetData, + dataKeys: ["bytesPerSec"], + heading: t("dataReceived"), + strokeColor: theme.palette.info.main, + gradientStartColor: theme.palette.info.main, + yLabel: t("rate"), + xTick: , + yTick: , + toolTip: ( + + ), + }, + { + type: "network-packets", + data: ethernetData, + dataKeys: ["packetsPerSec"], + heading: t("packetsReceivedRate"), + strokeColor: theme.palette.success.main, + gradientStartColor: theme.palette.success.main, + yLabel: t("rate"), + xTick: , + yTick: , + toolTip: ( + + ), + }, + { + type: "network-errors", + data: ethernetData, + dataKeys: ["errors"], + heading: t("networkErrors"), + strokeColor: theme.palette.error.main, + gradientStartColor: theme.palette.error.main, + yLabel: t("errors"), + xTick: , + toolTip: ( + Math.round(value).toLocaleString()} + /> + ), + }, + { + type: "network-drops", + data: ethernetData, + dataKeys: ["drops"], + heading: t("networkDrops"), + strokeColor: theme.palette.warning.main, + gradientStartColor: theme.palette.warning.main, + yLabel: t("drops"), + xTick: , + toolTip: ( + Math.round(value).toLocaleString()} + /> + ), + }, + ]; + + return ( + *": { + flexBasis: `calc(50% - ${theme.spacing(8)})`, + maxWidth: `calc(50% - ${theme.spacing(8)})`, + }, + }} + > + {configs.map((config) => ( + + ))} + + ); +}; + +NetworkCharts.propTypes = { + ethernetData: PropTypes.array.isRequired, + dateRange: PropTypes.string.isRequired, +}; + +export default NetworkCharts; diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx new file mode 100644 index 000000000..98ba90dae --- /dev/null +++ b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx @@ -0,0 +1,81 @@ +import PropTypes from "prop-types"; +import StatusBoxes from "../../../../../Components/StatusBoxes"; +import StatBox from "../../../../../Components/StatBox"; +import { Typography } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { useHardwareUtils } from "../../Hooks/useHardwareUtils"; + +function formatNumber(num) { + return num != null ? num.toLocaleString() : "0"; +} + +const NetworkStatBoxes = ({ shouldRender, net, ifaceName }) => { + const { t } = useTranslation(); + const { formatBytes } = useHardwareUtils(); + + const filtered = net?.filter((iface) => iface.name === ifaceName) || []; + + if (!net?.length) { + return {t("noNetworkStatsAvailable")}; + } + + return ( + + {filtered + .map((iface) => [ + , + , + , + , + , + , + ]) + .flat()} + + ); +}; + +NetworkStatBoxes.propTypes = { + shouldRender: PropTypes.bool.isRequired, + net: PropTypes.arrayOf( + PropTypes.shape({ + name: PropTypes.string.isRequired, + bytes_sent: PropTypes.number, + bytes_recv: PropTypes.number, + packets_sent: PropTypes.number, + packets_recv: PropTypes.number, + err_in: PropTypes.number, + err_out: PropTypes.number, + }) + ), + ifaceName: PropTypes.string.isRequired, +}; + +export default NetworkStatBoxes; diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx new file mode 100644 index 000000000..227523439 --- /dev/null +++ b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx @@ -0,0 +1,110 @@ +import PropTypes from "prop-types"; +import { useState, useEffect } from "react"; +import { FormControl, InputLabel, Select, MenuItem, Box } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { useTheme } from "@emotion/react"; +import NetworkStatBoxes from "./NetworkStatBoxes"; +import NetworkCharts from "./NetworkCharts"; +import MonitorTimeFrameHeader from "../../../../../Components/MonitorTimeFrameHeader"; + +const getAvailableInterfaces = (net) => { + return (net || []).map((iface) => iface.name).filter(Boolean); +}; + +const getNetworkInterfaceData = (checks, ifaceName) => { + if (!ifaceName) return []; + + // Transform backend data structure for the selected interface + // Backend already calculates deltas, we just reshape the data + return (checks || []) + .map((check) => { + const networkInterface = (check.net || []).find( + (iface) => iface.name === ifaceName + ); + if (!networkInterface) return null; + return { + _id: check._id, + bytesPerSec: networkInterface.deltaBytesRecv, + packetsPerSec: networkInterface.deltaPacketsRecv, + errors: networkInterface.deltaErrOut ?? 0, + drops: networkInterface.deltaDropOut ?? 0, + }; + }) + .filter(Boolean); +}; + +const Network = ({ net, checks, isLoading, dateRange, setDateRange }) => { + const { t } = useTranslation(); + const theme = useTheme(); + + const availableInterfaces = getAvailableInterfaces(net); + const [selectedInterface, setSelectedInterface] = useState(""); + + // Set default interface when data loads + useEffect(() => { + if (availableInterfaces.length > 0 && !selectedInterface) { + setSelectedInterface(availableInterfaces[0]); + } + }, [availableInterfaces, selectedInterface]); + + const ethernetData = getNetworkInterfaceData(checks, selectedInterface); + + return ( + <> + + + {availableInterfaces.length > 0 && ( + + {t("networkInterface")} + + + )} + + + + + ); +}; + +Network.propTypes = { + net: PropTypes.array, + checks: PropTypes.array, + isLoading: PropTypes.bool.isRequired, + dateRange: PropTypes.string.isRequired, + setDateRange: PropTypes.func.isRequired, +}; + +export default Network; diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/skeleton.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/skeleton.jsx new file mode 100644 index 000000000..52fcfad87 --- /dev/null +++ b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/skeleton.jsx @@ -0,0 +1,56 @@ +import { + Card, + CardContent, + Skeleton, + Table, + TableHead, + TableRow, + TableCell, + TableBody, +} from "@mui/material"; + +const SkeletonLayout = () => { + return ( + + + + + + + Name + Bytes Sent + Bytes Received + Packets Sent + Packets Received + Errors In + Errors Out + Drops In + Drops Out + + + + {Array.from({ length: 5 }).map((_, idx) => ( + + {Array.from({ length: 9 }).map((__, colIdx) => ( + + + + ))} + + ))} + +
+
+
+ ); +}; + +export default SkeletonLayout; diff --git a/client/src/Pages/Infrastructure/Details/Hooks/useHardwareUtils.jsx b/client/src/Pages/Infrastructure/Details/Hooks/useHardwareUtils.jsx index a2e4c5153..8edb6fb87 100644 --- a/client/src/Pages/Infrastructure/Details/Hooks/useHardwareUtils.jsx +++ b/client/src/Pages/Infrastructure/Details/Hooks/useHardwareUtils.jsx @@ -57,7 +57,7 @@ const useHardwareUtils = () => { if (GB >= 1) { return ( <> - {Number(GB.toFixed(0))} + {Number(GB.toFixed(2))} {space ? " " : ""} {t("gb")} @@ -65,7 +65,7 @@ const useHardwareUtils = () => { } else { return ( <> - {Number(MB.toFixed(0))} + {Number(MB.toFixed(2))} {space ? " " : ""} {t("mb")} @@ -73,6 +73,53 @@ const useHardwareUtils = () => { } }; + const formatBytesPerSecondString = (bytesPerSec, space = false) => { + if ( + bytesPerSec === undefined || + bytesPerSec === null || + typeof bytesPerSec !== "number" || + bytesPerSec === 0 + ) { + return `0${space ? " " : ""}B/s`; + } + + const GB = bytesPerSec / (1024 * 1024 * 1024); + const MB = bytesPerSec / (1024 * 1024); + const KB = bytesPerSec / 1024; + + if (GB >= 1) { + return `${Number(GB.toFixed(1))}${space ? " " : ""}GB/s`; + } else if (MB >= 1) { + return `${Number(MB.toFixed(1))}${space ? " " : ""}MB/s`; + } else if (KB >= 1) { + return `${Number(KB.toFixed(1))}${space ? " " : ""}KB/s`; + } else { + return `${Number(bytesPerSec.toFixed(1))}${space ? " " : ""}B/s`; + } + }; + + const formatPacketsPerSecondString = (packetsPerSec, space = false) => { + if ( + packetsPerSec === undefined || + packetsPerSec === null || + typeof packetsPerSec !== "number" || + packetsPerSec === 0 + ) { + return `0${space ? " " : ""}pps`; + } + + const M = packetsPerSec / (1000 * 1000); + const K = packetsPerSec / 1000; + + if (M >= 1) { + return `${Number(M.toFixed(1))}${space ? " " : ""}Mpps`; + } else if (K >= 1) { + return `${Number(K.toFixed(1))}${space ? " " : ""}Kpps`; + } else { + return `${Math.round(packetsPerSec)}${space ? " " : ""}pps`; + } + }; + /** * Converts a decimal value to a percentage * @@ -134,6 +181,8 @@ const useHardwareUtils = () => { decimalToPercentage, buildTemps, getDimensions, + formatBytesPerSecondString, + formatPacketsPerSecondString, }; }; diff --git a/client/src/Pages/Infrastructure/Details/index.jsx b/client/src/Pages/Infrastructure/Details/index.jsx index b57e0b91e..72f384101 100644 --- a/client/src/Pages/Infrastructure/Details/index.jsx +++ b/client/src/Pages/Infrastructure/Details/index.jsx @@ -1,5 +1,5 @@ // Components -import { Stack, Typography } from "@mui/material"; +import { Stack, Typography, Tab } from "@mui/material"; import Breadcrumbs from "../../../Components/Breadcrumbs"; import MonitorDetailsControlHeader from "../../../Components/MonitorDetailsControlHeader"; import MonitorTimeFrameHeader from "../../../Components/MonitorTimeFrameHeader"; @@ -7,6 +7,9 @@ import StatusBoxes from "./Components/StatusBoxes"; import GaugeBoxes from "./Components/GaugeBoxes"; import AreaChartBoxes from "./Components/AreaChartBoxes"; import GenericFallback from "../../../Components/GenericFallback"; +import NetworkStats from "./Components/NetworkStats"; +import CustomTabList from "../../../Components/Tab"; +import TabContext from "@mui/lab/TabContext"; // Utils import { useTheme } from "@emotion/react"; @@ -22,11 +25,10 @@ const BREADCRUMBS = [ { name: "details", path: "" }, ]; const InfrastructureDetails = () => { - // Redux state - // Local state const [dateRange, setDateRange] = useState("recent"); const [trigger, setTrigger] = useState(false); + const [tab, setTab] = useState("details"); // Utils const theme = useTheme(); @@ -87,23 +89,51 @@ const InfrastructureDetails = () => { monitor={monitor} triggerUpdate={triggerUpdate} /> - - - - + + setTab(v)} + > + + + + {tab === "details" && ( + <> + + + + + + )} + {tab === "network" && ( + + )} +
); }; 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..5bb227b7c --- /dev/null +++ b/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx @@ -0,0 +1,159 @@ +import Stack from "@mui/material/Stack"; +import CustomGauge from "../../../../../Components/Charts/CustomGauge"; +import Typography from "@mui/material/Typography"; + +// Utils +import { useTheme } from "@emotion/react"; +import PropTypes from "prop-types"; +import { getPercentage, formatBytes } from "../../utils/utils"; +import { useTranslation } from "react-i18next"; +import { Box } from "@mui/material"; + +const BaseContainer = ({children}) => { + const theme = useTheme() + return( + + {children} + + ); +}; + +const InfrastructureStyleGauge = ({ value, heading, metricOne, valueOne, metricTwo, valueTwo }) => { + const theme = useTheme(); + + const MetricRow = ({ label, value }) => ( + + {label} + + {value} + + + ); + + return( + + + + + + {heading} + + + + + {metricTwo && valueTwo && ( + + )} + + + + ); +}; + +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, +}; + +InfrastructureStyleGauge.propTypes = { + value: PropTypes.number, + heading: PropTypes.string, + metricOne: PropTypes.string, + valueOne: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + metricTwo: PropTypes.string, + valueTwo: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), +}; + +BaseContainer.propTypes = { + children: PropTypes.node.isRequired, +}; + +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..72ee47f48 --- /dev/null +++ b/client/src/Pages/Logs/Diagnostics/index.jsx @@ -0,0 +1,77 @@ +import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; +import Gauges from "./components/gauges"; +import Button from "@mui/material/Button"; +import StatBox from "../../../Components/StatBox"; +import StatusBoxes from "../../../Components/StatusBoxes"; +import { useTheme } from "@emotion/react"; +import { useTranslation } from "react-i18next"; +import { useFetchDiagnostics } from "../../../Hooks/logHooks"; +import { getHumanReadableDuration } from "../../../Utils/timeUtils"; +import { formatBytes, getPercentage } from "./utils/utils"; + +const Diagnostics = () => { + // Local state + + // Hooks + const theme = useTheme(); + const { t } = useTranslation(); + const [diagnostics, fetchDiagnostics, isLoading, error] = useFetchDiagnostics(); + // Setup + return ( + + + + + + + + + + + + + + + ); +}; + +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..d9ce2a25c 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"; @@ -50,13 +51,38 @@ const Logs = () => { ]; return ( - - {t("logsPage.description")} + + {t("logsPage.description")} + + {t("logsPage.logLevelSelect.title")} setRawInput(val)} + handleChange={handleTimezoneChange} + isAdorned={true} + unit="timezone" /> diff --git a/client/src/Pages/Settings/SettingsURL.jsx b/client/src/Pages/Settings/SettingsURL.jsx index 298dfa821..1ce2e8c73 100644 --- a/client/src/Pages/Settings/SettingsURL.jsx +++ b/client/src/Pages/Settings/SettingsURL.jsx @@ -15,7 +15,12 @@ const SettingsURL = ({ HEADING_SX, handleChange, showURL = false }) => { return ( - {t("settingsPage.urlSettings.title")} + + {t("settingsPage.urlSettings.title")} + {t("settingsPage.urlSettings.description")} diff --git a/client/src/Pages/Settings/index.jsx b/client/src/Pages/Settings/index.jsx index dae6bd680..92f58b4c0 100644 --- a/client/src/Pages/Settings/index.jsx +++ b/client/src/Pages/Settings/index.jsx @@ -8,6 +8,7 @@ import SettingsPagespeed from "./SettingsPagespeed"; import SettingsDemoMonitors from "./SettingsDemoMonitors"; import SettingsAbout from "./SettingsAbout"; import SettingsEmail from "./SettingsEmail"; +import SettingsGlobalThresholds from "./SettingsGlobalThresholds"; import Button from "@mui/material/Button"; // Utils import { settingsValidation } from "../../Validation/validation"; @@ -48,6 +49,7 @@ const Settings = () => { setIsApiKeySet, setIsEmailPasswordSet, }); + const [addDemoMonitors, isAddingDemoMonitors] = useAddDemoMonitors(); const [isSaving, saveError, saveSettings] = useSaveSettings({ @@ -149,6 +151,7 @@ const Settings = () => { error.details.forEach((err) => { newErrors[err.path[0]] = err.message; }); + setErrors(newErrors); } saveSettings(settingsData?.settings); @@ -190,6 +193,13 @@ const Settings = () => { handleChange={handleChange} errors={errors} /> + + 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 = ({ - - - {form.type === "http" && useAdvancedMatching && ( - <> - + )} { - {t("notificationConfig.title")} + + {t("notificationConfig.title")} + {t("notificationConfig.description")} @@ -365,7 +644,7 @@ const CreateMonitor = () => {
{ {t("distributedUptimeCreateAdvancedSettings")}
- + { { color={theme.palette.primary.contrastTextTertiary} opacity={0.8} > - {t("uptimeCreateJsonPath")}  + {t("uptimeCreateJsonPath") + " "} { > jmespath.org -  {t("uptimeCreateJsonPathQuery")} + {" " + t("uptimeCreateJsonPathQuery")} @@ -474,14 +755,32 @@ const CreateMonitor = () => { variant="contained" color="accent" disabled={!Object.values(errors).every((value) => value === undefined)} - loading={isCreating} + loading={isBusy} + sx={{ px: theme.spacing(12) }} > - {t("createMonitor")} + {t("settingsSave")} + + {!isCreate && ( + setIsOpen(false)} + confirmationButtonLabel={t("delete")} + onConfirm={handleRemove} + isLoading={isLoading} + /> + )} ); }; -export default CreateMonitor; +UptimeCreate.propTypes = { + isClone: PropTypes.bool, +}; + +export default UptimeCreate; diff --git a/client/src/Pages/Uptime/Configure/skeleton.jsx b/client/src/Pages/Uptime/Create/skeleton.jsx similarity index 100% rename from client/src/Pages/Uptime/Configure/skeleton.jsx rename to client/src/Pages/Uptime/Create/skeleton.jsx diff --git a/client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx b/client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx index 7d9f5a6bf..0efb06b6c 100644 --- a/client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx +++ b/client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx @@ -32,6 +32,7 @@ const getTypeOptions = () => [ { value: "ping", label: "Ping" }, { value: "docker", label: "Docker" }, { value: "port", label: "Port" }, + { value: "game", label: "Game" }, ]; // These functions were moved inline to ensure translations are applied correctly diff --git a/client/src/Pages/Uptime/Monitors/index.jsx b/client/src/Pages/Uptime/Monitors/index.jsx index 6e6f6caba..607092785 100644 --- a/client/src/Pages/Uptime/Monitors/index.jsx +++ b/client/src/Pages/Uptime/Monitors/index.jsx @@ -34,7 +34,7 @@ import { } from "../../../Hooks/monitorHooks"; import { useTranslation } from "react-i18next"; -const TYPES = ["http", "ping", "docker", "port"]; +const TYPES = ["http", "ping", "docker", "port", "game"]; const CreateMonitorButton = ({ shouldRender }) => { // Utils const navigate = useNavigate(); @@ -174,14 +174,9 @@ const UptimeMonitors = () => { ) { return ( diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index 3db8b772d..350d2f82d 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -14,7 +14,6 @@ import AuthNewPasswordConfirmed from "../Pages/Auth/NewPasswordConfirmed"; import Uptime from "../Pages/Uptime/Monitors"; import UptimeDetails from "../Pages/Uptime/Details"; import UptimeCreate from "../Pages/Uptime/Create"; -import UptimeConfigure from "../Pages/Uptime/Configure"; // PageSpeed import PageSpeed from "../Pages/PageSpeed/Monitors"; @@ -42,6 +41,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"; @@ -80,16 +80,20 @@ const Routes = () => { /> } /> + } + /> } /> } + element={} /> { path="account/team" element={} /> + + + + } + /> } The response from the axios GET request. + */ + async getMonitorGames() { + return this.axiosInstance.get(`/monitors/games`, { + headers: { + "Content-Type": "application/json", + }, + }); + } + /** * * ************************************ @@ -111,30 +125,6 @@ class NetworkService { return this.axiosInstance.post(`/monitors`, monitor); } - /** - * - * ************************************ - * Check the endpoint resolution - * ************************************ - * - * @async - * @param {Object} config - The configuration object. - * @param {Object} config.monitorURL - The monitor url to be sent in the request body. - * @returns {Promise} 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 +383,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 +396,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 +621,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 +996,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 +1110,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/constants.js b/client/src/Utils/Theme/constants.js index 1b905bb79..9df6665d0 100644 --- a/client/src/Utils/Theme/constants.js +++ b/client/src/Utils/Theme/constants.js @@ -87,8 +87,10 @@ const newColors = { offBlack: "#131315", gray0: "#FDFDFD", gray10: "#F4F4FF", + gray50: "#F9F9F9", gray100: "#F3F3F3", gray200: "#EFEFEF", + gray250: "#DADADA", gray500: "#A2A3A3", gray900: "#1c1c1c", blueGray50: "#E8F0FE", @@ -173,7 +175,7 @@ const newSemanticColors = { dark: newColors.blueGray800, }, lowContrast: { - light: newColors.gray200, + light: newColors.gray250, dark: newColors.blueGray600, }, }, @@ -206,6 +208,18 @@ const newSemanticColors = { light: newColors.blueGray800, dark: newColors.gray100, }, + background: { + light: newColors.gray50, + dark: newColors.offBlack, + }, + border: { + light: newColors.gray500, + dark: newColors.blueGray600, + }, + cardShadow: { + light: "0 0 0 1px rgba(0, 0, 0, 0.04), 0 12px 24px rgba(0, 0, 0, 0.08)", + dark: "0 2px 10px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(255, 255, 255, 0.04)", + }, }, success: { main: { diff --git a/client/src/Utils/Theme/globalTheme.js b/client/src/Utils/Theme/globalTheme.js index eb827a907..ce58153ec 100644 --- a/client/src/Utils/Theme/globalTheme.js +++ b/client/src/Utils/Theme/globalTheme.js @@ -1,5 +1,6 @@ import { typographyLevels } from "./constants"; const fontFamilyPrimary = '"Inter" , sans-serif'; +import { darken } from "@mui/material/styles"; // const fontFamilySecondary = '"Avenir", sans-serif'; /* TODO take the color out from here */ @@ -55,6 +56,20 @@ const baseTheme = (palette) => ({ styleOverrides: { root: ({ theme }) => ({ variants: [ + { + props: (props) => props.variant === "contained" && props.color === "accent", + style: { + backgroundColor: theme.palette.accent.main, + color: theme.palette.primary.contrastTextSecondaryDarkBg, + letterSpacing: "0.5px", + textShadow: "0 0 1px rgba(0, 0, 0, 0.15)", + "&:hover": { + backgroundColor: darken(theme.palette.accent.darker, 0.05), + boxShadow: `0 2px 6px rgba(0, 0, 0, 0.1)`, + transition: "all 0.2s ease-in-out", + }, + }, + }, { props: (props) => props.color === "accent", style: { @@ -217,16 +232,16 @@ const baseTheme = (palette) => ({ }, MuiList: { styleOverrides: { - root: { + root: ({ theme }) => ({ padding: 0, - }, + }), }, }, MuiListItemButton: { styleOverrides: { - root: { - transition: "none", - }, + root: ({ theme }) => ({ + transition: "background-color .3s", + }), }, }, MuiListItemText: { @@ -263,6 +278,7 @@ const baseTheme = (palette) => ({ }), }, }, + MuiTableHead: { styleOverrides: { root: ({ theme }) => ({ @@ -337,7 +353,7 @@ const baseTheme = (palette) => ({ }, "& .MuiInputBase-input.MuiOutlinedInput-input": { - padding: "0 var(--env-var-spacing-1-minus) !important", + padding: `0 ${theme.spacing(5)}`, }, "& .MuiOutlinedInput-root": { @@ -392,6 +408,7 @@ const baseTheme = (palette) => ({ "& .MuiOutlinedInput-root": { paddingTop: 0, paddingBottom: 0, + paddingRight: theme.spacing(5), }, "& fieldset": { borderColor: theme.palette.primary.lowContrast, @@ -553,6 +570,11 @@ const baseTheme = (palette) => ({ "&:hover .MuiOutlinedInput-notchedOutline": { borderColor: theme.palette.primary.lowContrast, }, + padding: `0 ${theme.spacing(5)}`, + minHeight: "34px", + display: "flex", + alignItems: "center", + lineHeight: 1, }), }, }, @@ -561,7 +583,7 @@ const baseTheme = (palette) => ({ root: ({ theme }) => ({ ml: "auto", "& .MuiButtonBase-root, & .MuiButtonBase-root:hover": { - borderColor: theme.palette.primary.contrastBorder, + borderColor: theme.palette.primary.lowContrast, width: "auto", whiteSpace: "nowrap", }, @@ -702,6 +724,14 @@ const baseTheme = (palette) => ({ }, ], }, + + MuiTooltip: { + styleOverrides: { + tooltip: () => ({ + fontSize: typographyLevels.m, + }), + }, + }, }, shape: { borderRadius: 2, 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..de248f87b 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."; @@ -183,12 +184,12 @@ const monitorValidation = joi.object({ .min(1) .max(65535) .when("type", { - is: "port", - then: joi.number().messages({ + is: joi.valid("port", "game"), + then: joi.required().messages({ "number.base": "Port must be a number.", "number.min": "Port must be at least 1.", "number.max": "Port must be at most 65535.", - "any.required": "Port is required for port monitors.", + "any.required": "Port is required for port and game monitors.", }), otherwise: joi.optional(), }), @@ -204,6 +205,14 @@ const monitorValidation = joi.object({ expectedValue: joi.string().allow(null, ""), jsonPath: joi.string().allow(null, ""), matchMethod: joi.string().allow(null, ""), + gameId: joi.when("type", { + is: "game", + then: joi.string().required().messages({ + "string.empty": "Game selection is required for game monitors.", + "any.required": "Game selection is required for game monitors.", + }), + otherwise: joi.string().allow(null, ""), + }), }); const imageValidation = joi.object({ @@ -301,6 +310,14 @@ const settingsValidation = joi.object({ systemEmailIgnoreTLS: joi.boolean(), systemEmailRequireTLS: joi.boolean(), systemEmailRejectUnauthorized: joi.boolean(), + globalThresholds: joi + .object({ + cpu: joi.number().min(1).max(100).allow("").optional(), + memory: joi.number().min(1).max(100).allow("").optional(), + disk: joi.number().min(1).max(100).allow("").optional(), + temperature: joi.number().min(1).max(150).allow("").optional(), + }) + .optional(), }); const dayjsValidator = (value, helpers) => { @@ -410,17 +427,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 +487,5 @@ export { statusPageValidation, logoImageValidation, notificationValidation, + editUserValidation, }; diff --git a/client/src/assets/Animations/darkmodeOutput.gif b/client/src/assets/Animations/darkmodeOutput.gif new file mode 100644 index 000000000..7629e30b4 Binary files /dev/null and b/client/src/assets/Animations/darkmodeOutput.gif differ diff --git a/client/src/assets/Animations/output.gif b/client/src/assets/Animations/output.gif new file mode 100644 index 000000000..c7823227a Binary files /dev/null and b/client/src/assets/Animations/output.gif differ diff --git a/client/src/assets/icons/alert-icon.svg b/client/src/assets/icons/alert-icon.svg new file mode 100644 index 000000000..1dc7fc2b7 --- /dev/null +++ b/client/src/assets/icons/alert-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/src/assets/icons/check-icon.svg b/client/src/assets/icons/check-icon.svg new file mode 100644 index 000000000..9a2cfac5a --- /dev/null +++ b/client/src/assets/icons/check-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/src/assets/icons/close-icon.svg b/client/src/assets/icons/close-icon.svg new file mode 100644 index 000000000..e1b6b690a --- /dev/null +++ b/client/src/assets/icons/close-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/src/assets/icons/left-arrow-double.svg b/client/src/assets/icons/left-arrow-double.svg index 6001a3ead..4cb2a5186 100644 --- a/client/src/assets/icons/left-arrow-double.svg +++ b/client/src/assets/icons/left-arrow-double.svg @@ -1,3 +1,4 @@ - - + + \ No newline at end of file diff --git a/client/src/assets/icons/left-arrow-long.svg b/client/src/assets/icons/left-arrow-long.svg index db9b827ed..56a774730 100644 --- a/client/src/assets/icons/left-arrow-long.svg +++ b/client/src/assets/icons/left-arrow-long.svg @@ -1,3 +1,4 @@ - - + + \ No newline at end of file diff --git a/client/src/assets/icons/left-arrow.svg b/client/src/assets/icons/left-arrow.svg index 8b38c408a..649d1681c 100644 --- a/client/src/assets/icons/left-arrow.svg +++ b/client/src/assets/icons/left-arrow.svg @@ -1,3 +1,4 @@ - - + + \ No newline at end of file diff --git a/client/src/assets/icons/right-arrow-double.svg b/client/src/assets/icons/right-arrow-double.svg index 4235134d7..6ad4882ad 100644 --- a/client/src/assets/icons/right-arrow-double.svg +++ b/client/src/assets/icons/right-arrow-double.svg @@ -1,3 +1,4 @@ - - + + \ No newline at end of file diff --git a/client/src/assets/icons/right-arrow.svg b/client/src/assets/icons/right-arrow.svg index 6b02062f2..77d022798 100644 --- a/client/src/assets/icons/right-arrow.svg +++ b/client/src/assets/icons/right-arrow.svg @@ -1,3 +1,4 @@ - - + + \ No newline at end of file diff --git a/client/src/assets/icons/warning-icon.svg b/client/src/assets/icons/warning-icon.svg new file mode 100644 index 000000000..468611758 --- /dev/null +++ b/client/src/assets/icons/warning-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/src/index.css b/client/src/index.css index f83cdc461..a5d305d46 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -28,6 +28,8 @@ html { --env-var-nav-bar-height: 70px; --env-var-side-bar-width: 250px; + --env-var-side-bar-collapsed-width: 64px; + --env-var-side-bar-auth-footer-height: 50px; --env-var-spacing-1: 12px; --env-var-spacing-1-plus: 16px; diff --git a/client/src/locales/ar.json b/client/src/locales/ar.json index b3b8fc4ce..33f245fd5 100644 --- a/client/src/locales/ar.json +++ b/client/src/locales/ar.json @@ -8,7 +8,6 @@ "settingsFailedToSave": "", "settingsStatsCleared": "", "settingsFailedToClearStats": "", - "settingsFailedToAddDemoMonitors": "", "settingsMonitorsDeleted": "", "settingsFailedToDeleteMonitors": "", "starPromptTitle": "", @@ -86,7 +85,8 @@ "networkError": "", "fallback": { "title": "", - "checks": [""] + "checks": [""], + "actionButton": "" }, "createButton": "", "createTitle": "", @@ -260,6 +260,7 @@ "notifyEmails": "", "seperateEmails": "", "checkFrequency": "", + "chooseGame": "", "matchMethod": "", "expectedValue": "", "deleteDialogTitle": "", @@ -331,7 +332,9 @@ "uploadSuccess": "", "validationFailed": "", "noFileSelected": "", - "fallbackPage": "" + "fallbackPage": "", + "invalidFileType": "", + "uploadFailed": "" }, "DeleteAccountTitle": "", "DeleteAccountButton": "", @@ -435,7 +438,8 @@ "http": "", "ping": "", "docker": "", - "port": "" + "port": "", + "game": "" }, "common": { "appName": "", @@ -537,6 +541,11 @@ "errors": { "incorrect": "" } + }, + "role": { + "errors": { + "min": "" + } } } }, @@ -675,7 +684,10 @@ "description": "", "webhookLabel": "", "webhookPlaceholder": "" - } + }, + "testNotification": "", + "dialogDeleteTitle": "", + "dialogDeleteConfirm": "" }, "notificationConfig": { "title": "", @@ -690,7 +702,6 @@ }, "advancedMatching": "", "sendTestNotifications": "", - "testNotificationsDisabled": "", "selectAll": "", "showAdminLoginLink": "", "logsPage": { @@ -698,7 +709,8 @@ "description": "", "tabs": { "queue": "", - "logs": "" + "logs": "", + "diagnostics": "" }, "toast": { "fetchLogsSuccess": "" @@ -753,7 +765,10 @@ "monitorActions": { "title": "", "import": "", - "export": "" + "export": "", + "deleteSuccess": "", + "deleteFailed": "", + "details": "" }, "settingsPage": { "aboutSettings": { @@ -785,7 +800,8 @@ "labelUser": "", "linkTransport": "", "placeholderUser": "", - "title": "" + "title": "", + "toastEmailRequiredFieldsError": "" }, "pageSpeedSettings": { "description": "", @@ -832,5 +848,124 @@ "selectEnabled": "", "title": "" } - } + }, + "statusPageCreate": { + "buttonSave": "" + }, + "incidentsOptionsHeaderFilterResolved": "", + "settingsSave": "", + "statusPageCreateAppearanceTitle": "", + "confirmPassword": "", + "monitorHooks": { + "failureAddDemoMonitors": "", + "successAddDemoMonitors": "" + }, + "settingsAppearance": "", + "settingsDisplayTimezone": "", + "settingsGeneralSettings": "", + "incidentsOptionsHeaderTotalIncidents": "", + "statusPage": { + "deleteSuccess": "", + "deleteFailed": "", + "createSuccess": "", + "updateSuccess": "", + "generalSettings": "", + "contents": "", + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "testNotificationsDisabled": "", + "incidentsTableResolvedAt": "", + "incidentsTableActionResolve": "", + "checkHooks": { + "failureResolveOne": "", + "failureResolveAll": "", + "failureResolveMonitor": "" + }, + "checkFormError": "", + "diagnosticsPage": { + "diagnosticDescription": "", + "statsDescription": "", + "gauges": { + "heapAllocationTitle": "", + "heapAllocationSubtitle": "", + "heapUsageTitle": "", + "heapUsageSubtitle": "", + "heapUtilizationTitle": "", + "heapUtilizationSubtitle": "", + "instantCpuUsageTitle": "", + "instantCpuUsageSubtitle": "" + }, + "stats": { + "eventLoopDelayTitle": "", + "uptimeTitle": "", + "usedHeapSizeTitle": "", + "totalHeapSizeTitle": "", + "osMemoryLimitTitle": "" + } + }, + "pageSpeedLighthouseAPI": "", + "time": { + "threeMinutes": "", + "fiveMinutes": "", + "tenMinutes": "", + "twentyMinutes": "", + "oneHour": "", + "oneDay": "", + "oneWeek": "" + }, + "general": { + "noOptionsFound": "" + }, + "infrastructureMonitor": { + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "maintenanceWindow": { + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "pageSpeed": { + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "uptimeMonitor": { + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "editUserPage": { + "form": { + "email": "", + "firstName": "", + "lastName": "", + "role": "", + "save": "" + }, + "table": { + "actionHeader": "", + "roleHeader": "" + }, + "title": "", + "toast": { + "successUserUpdate": "", + "validationErrors": "" + } + }, + "incidentsPageActionResolveMonitor": "", + "incidentsPageActionResolveAll": "" } diff --git a/client/src/locales/cs.json b/client/src/locales/cs.json index 2bb03f5ad..1fb579270 100644 --- a/client/src/locales/cs.json +++ b/client/src/locales/cs.json @@ -8,7 +8,6 @@ "settingsFailedToSave": "Nepodařilo se uložit nastavení", "settingsStatsCleared": "", "settingsFailedToClearStats": "", - "settingsFailedToAddDemoMonitors": "", "settingsMonitorsDeleted": "", "settingsFailedToDeleteMonitors": "", "starPromptTitle": "", @@ -86,7 +85,8 @@ "networkError": "", "fallback": { "title": "", - "checks": [""] + "checks": [""], + "actionButton": "" }, "createButton": "", "createTitle": "", @@ -260,6 +260,7 @@ "notifyEmails": "", "seperateEmails": "", "checkFrequency": "", + "chooseGame": "", "matchMethod": "", "expectedValue": "", "deleteDialogTitle": "", @@ -331,7 +332,9 @@ "uploadSuccess": "", "validationFailed": "", "noFileSelected": "", - "fallbackPage": "" + "fallbackPage": "", + "invalidFileType": "", + "uploadFailed": "" }, "DeleteAccountTitle": "", "DeleteAccountButton": "", @@ -435,7 +438,8 @@ "http": "", "ping": "", "docker": "", - "port": "" + "port": "", + "game": "" }, "common": { "appName": "Checkmate", @@ -537,6 +541,11 @@ "errors": { "incorrect": "" } + }, + "role": { + "errors": { + "min": "" + } } } }, @@ -675,7 +684,10 @@ "description": "", "webhookLabel": "", "webhookPlaceholder": "" - } + }, + "testNotification": "", + "dialogDeleteTitle": "", + "dialogDeleteConfirm": "" }, "notificationConfig": { "title": "", @@ -690,7 +702,6 @@ }, "advancedMatching": "", "sendTestNotifications": "", - "testNotificationsDisabled": "", "selectAll": "", "showAdminLoginLink": "", "logsPage": { @@ -698,7 +709,8 @@ "description": "", "tabs": { "queue": "", - "logs": "" + "logs": "", + "diagnostics": "" }, "toast": { "fetchLogsSuccess": "" @@ -753,7 +765,10 @@ "monitorActions": { "title": "", "import": "", - "export": "" + "export": "", + "deleteSuccess": "", + "deleteFailed": "", + "details": "" }, "settingsPage": { "aboutSettings": { @@ -785,7 +800,8 @@ "labelUser": "", "linkTransport": "", "placeholderUser": "", - "title": "" + "title": "", + "toastEmailRequiredFieldsError": "" }, "pageSpeedSettings": { "description": "", @@ -832,5 +848,124 @@ "selectEnabled": "", "title": "" } - } + }, + "statusPageCreate": { + "buttonSave": "" + }, + "incidentsOptionsHeaderFilterResolved": "", + "settingsSave": "", + "statusPageCreateAppearanceTitle": "", + "confirmPassword": "", + "monitorHooks": { + "failureAddDemoMonitors": "", + "successAddDemoMonitors": "" + }, + "settingsAppearance": "", + "settingsDisplayTimezone": "", + "settingsGeneralSettings": "", + "incidentsOptionsHeaderTotalIncidents": "", + "statusPage": { + "deleteSuccess": "", + "deleteFailed": "", + "createSuccess": "", + "updateSuccess": "", + "generalSettings": "", + "contents": "", + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "testNotificationsDisabled": "", + "incidentsTableResolvedAt": "", + "incidentsTableActionResolve": "", + "checkHooks": { + "failureResolveOne": "", + "failureResolveAll": "", + "failureResolveMonitor": "" + }, + "checkFormError": "", + "diagnosticsPage": { + "diagnosticDescription": "", + "statsDescription": "", + "gauges": { + "heapAllocationTitle": "", + "heapAllocationSubtitle": "", + "heapUsageTitle": "", + "heapUsageSubtitle": "", + "heapUtilizationTitle": "", + "heapUtilizationSubtitle": "", + "instantCpuUsageTitle": "", + "instantCpuUsageSubtitle": "" + }, + "stats": { + "eventLoopDelayTitle": "", + "uptimeTitle": "", + "usedHeapSizeTitle": "", + "totalHeapSizeTitle": "", + "osMemoryLimitTitle": "" + } + }, + "pageSpeedLighthouseAPI": "", + "time": { + "threeMinutes": "", + "fiveMinutes": "", + "tenMinutes": "", + "twentyMinutes": "", + "oneHour": "", + "oneDay": "", + "oneWeek": "" + }, + "general": { + "noOptionsFound": "" + }, + "infrastructureMonitor": { + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "maintenanceWindow": { + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "pageSpeed": { + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "uptimeMonitor": { + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "editUserPage": { + "form": { + "email": "", + "firstName": "", + "lastName": "", + "role": "", + "save": "" + }, + "table": { + "actionHeader": "", + "roleHeader": "" + }, + "title": "", + "toast": { + "successUserUpdate": "", + "validationErrors": "" + } + }, + "incidentsPageActionResolveMonitor": "", + "incidentsPageActionResolveAll": "" } diff --git a/client/src/locales/de.json b/client/src/locales/de.json index 1938dcccd..4ac77ef29 100644 --- a/client/src/locales/de.json +++ b/client/src/locales/de.json @@ -1,84 +1,83 @@ { "submit": "Absenden", "title": "Titel", - "distributedStatusHeaderText": "", - "distributedStatusSubHeaderText": "", + "distributedStatusHeaderText": "Echtzeit Abdeckung", + "distributedStatusSubHeaderText": "Genutzt von Millionen von Geräten weltweit, siehst Du die Systemleistung nach globaler Region, Land oder Stadt", "settingsDisabled": "Inaktiv", "settingsSuccessSaved": "Einstellungen erfolgreich gespeichert", "settingsFailedToSave": "Fehler beim Speichern der Einstellungen", "settingsStatsCleared": "Statistiken gelöscht", "settingsFailedToClearStats": "Fehler beim löschen der Statistiken", - "settingsFailedToAddDemoMonitors": "", - "settingsMonitorsDeleted": "", - "settingsFailedToDeleteMonitors": "", - "starPromptTitle": "", - "starPromptDescription": "", + "settingsMonitorsDeleted": "Alle Monitore erfolgreich gelöscht.", + "settingsFailedToDeleteMonitors": "Fehler beim Löschen aller Monitore", + "starPromptTitle": "Stern vergeben", + "starPromptDescription": "Sieh dir die neuesten Veröffentlichungen an und hilf der Community auf GitHub", "https": "HTTPS", "http": "HTTP", - "monitor": "", + "monitor": "monitor", "aboutus": "Über uns", "now": "Jetzt", "delete": "Löschen", "configure": "Einstellungen", "responseTime": "Antwortzeit", "ms": "ms", - "bar": "", - "area": "", + "bar": "Bar", + "area": "Bereich", "country": "Land", "city": "Stadt", "response": "Antwort", - "monitorStatusUp": "", - "monitorStatusDown": "", - "webhookSendSuccess": "", - "webhookSendError": "", - "webhookUnsupportedPlatform": "", - "distributedRightCategoryTitle": "", - "distributedStatusServerMonitors": "", - "distributedStatusServerMonitorsDescription": "", - "distributedUptimeCreateSelectURL": "", - "distributedUptimeCreateChecks": "", - "distributedUptimeCreateChecksDescription": "", - "distributedUptimeCreateIncidentNotification": "", - "distributedUptimeCreateIncidentDescription": "", + "monitorStatusUp": "Monitor {name} ({url}) ist jetzt ONLINE und antwortet", + "monitorStatusDown": "Monitor {name} ({url}) ist jetzt OFFLINE und antwortet nicht", + "webhookSendSuccess": "Webhook-Benachrichtigung erfolgreich gesendet", + "webhookSendError": "Fehler beim Senden der Webhook-Benachrichtigung an {platform}", + "webhookUnsupportedPlatform": "Nicht unterstützte Plattform: {platform}", + "distributedRightCategoryTitle": "Monitor", + "distributedStatusServerMonitors": "Server Monitore", + "distributedStatusServerMonitorsDescription": "Überwache den Status der zugehörigen Server", + "distributedUptimeCreateSelectURL": "Hier kannst Du die URL und Typ des Hosts auswählen", + "distributedUptimeCreateChecks": "Durchzuführende Prüfungen", + "distributedUptimeCreateChecksDescription": "Du kannst jederzeit Überprüfungen hinzufügen oder entfernen, nachdem Du Deine Seite hinzugefügt hast.\n", + "distributedUptimeCreateIncidentNotification": "Ereignisbenachrichtigungen", + "distributedUptimeCreateIncidentDescription": "Wenn es ein Ereignis gibt, benachrichtige die Benutzer.", "distributedUptimeCreateAdvancedSettings": "Erweiterte Einstellungen", - "distributedUptimeDetailsNoMonitorHistory": "", - "distributedUptimeDetailsStatusHeaderUptime": "", - "distributedUptimeDetailsStatusHeaderLastUpdate": "", + "distributedUptimeDetailsNoMonitorHistory": "Es gibt noch keine Überprüfungshistorie für diesen Monitor.", + "distributedUptimeDetailsStatusHeaderUptime": "Uptime:", + "distributedUptimeDetailsStatusHeaderLastUpdate": "Zuletzt aktualisiert", "notifications": { - "enableNotifications": "", - "testNotification": "", - "addOrEditNotifications": "", + "enableNotifications": "Aktiviere {{platform}} Benachrichtigungen\n", + "testNotification": "Test Benachrichtigung", + "addOrEditNotifications": "Benachrichtigungen hinzufügen oder bearbeiten", "slack": { "label": "Slack", - "description": "", + "description": "Um Slack-Benachrichtigungen zu aktivieren, erstelle eine Slack-App und aktiviere eingehende Webhooks. Danach gib einfach die Webhook-URL hier ein.", "webhookLabel": "Webhook URL", "webhookPlaceholder": "", - "webhookRequired": "" + "webhookRequired": "Slack webhook URL wird benötigt" }, "discord": { "label": "Discord", - "description": "", + "description": "Um Daten von Checkmate an einen Discord-Kanal über Discord-Benachrichtigungen mithilfe von Webhooks zu senden, kannst Du die eingehenden Webhooks von Discord verwenden.", "webhookLabel": "Discord Webhook URL", "webhookPlaceholder": "", - "webhookRequired": "" + "webhookRequired": "Discord webhook URL wird benötigt" }, "telegram": { "label": "Telegram", - "description": "", - "tokenLabel": "", + "description": "Um Telegram-Benachrichtigungen zu aktivieren, erstelle einen Telegram-Bot mit BotFather, einem offiziellen Bot zum Erstellen und Verwalten von Telegram-Bots. Dann hole dir das API-Token und die Chat-ID und notiere sie hier.", + "tokenLabel": "Dein bot token", "tokenPlaceholder": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", - "chatIdLabel": "", + "chatIdLabel": "Deine Chat ID", "chatIdPlaceholder": "-1001234567890", - "fieldsRequired": "" + "fieldsRequired": "Telegram token und Chat-ID werden benötigt" }, "webhook": { "label": "Webhooks", - "description": "", + "description": "Du kannst einen benutzerdefinierten Webhook einrichten, um Benachrichtigungen zu erhalten, wenn Ereignisse auftreten.", "urlLabel": "Webhook URL", "urlPlaceholder": "https://your-server.com/webhook", - "urlRequired": "" + "urlRequired": "Webhook URL wird benötigt" }, - "testNotificationDevelop": "", + "testNotificationDevelop": "Testbenachrichtigung 2", "integrationButton": "", "testSuccess": "", "testFailed": "", @@ -86,7 +85,8 @@ "networkError": "", "fallback": { "title": "", - "checks": [""] + "checks": [""], + "actionButton": "" }, "createButton": "", "createTitle": "", @@ -107,166 +107,167 @@ "failed": "" }, "test": { - "success": "", - "failed": "" + "success": "Test Benachrichtigung erfolgreich gesendet", + "failed": "Fehler beim senden der Test Benachrichtigung" } }, - "testLocale": "", - "add": "", - "monitors": "", - "distributedUptimeStatusCreateStatusPage": "", - "distributedUptimeStatusCreateStatusPageAccess": "", - "distributedUptimeStatusCreateStatusPageReady": "", - "distributedUptimeStatusBasicInfoHeader": "", - "distributedUptimeStatusBasicInfoDescription": "", - "distributedUptimeStatusLogoHeader": "", - "distributedUptimeStatusLogoDescription": "", - "distributedUptimeStatusLogoUploadButton": "", - "distributedUptimeStatusStandardMonitorsHeader": "", - "distributedUptimeStatusStandardMonitorsDescription": "", + "testLocale": "testLocale", + "add": "Hinzufügen", + "monitors": "Monitore", + "distributedUptimeStatusCreateStatusPage": "Statusseite", + "distributedUptimeStatusCreateStatusPageAccess": "Zugriff", + "distributedUptimeStatusCreateStatusPageReady": "Wenn deine Statusseite bereit ist, kannst du sie als veröffentlicht markieren.", + "distributedUptimeStatusBasicInfoHeader": "Grundlegende Informationen", + "distributedUptimeStatusBasicInfoDescription": "Definiere den Firmennamen und die Subdomain, auf die deine Statusseite verweist.", + "distributedUptimeStatusLogoHeader": "Logo", + "distributedUptimeStatusLogoDescription": "Lade ein Logo für deine Statusseite hoch", + "distributedUptimeStatusLogoUploadButton": "Logo hochladen", + "distributedUptimeStatusStandardMonitorsHeader": "Standardmonitore", + "distributedUptimeStatusStandardMonitorsDescription": "Füge Standardmonitore zu deiner Statusseite hinzu.", "distributedUptimeStatusCreateYour": "Erstelle Dein", "distributedUptimeStatusEditYour": "Ändere Dein", - "distributedUptimeStatusPublishedLabel": "", + "distributedUptimeStatusPublishedLabel": "Veröffentlicht und für die Öffentlichkeit sichtbar", "distributedUptimeStatusCompanyNameLabel": "Firmenname", - "distributedUptimeStatusPageAddressLabel": "", + "distributedUptimeStatusPageAddressLabel": "Die Adresse deiner Statusseite", "distributedUptimeStatus30Days": "30 Tage", "distributedUptimeStatus60Days": "60 Tage", "distributedUptimeStatus90Days": "90 Tage", - "distributedUptimeStatusPageNotSetUp": "", - "distributedUptimeStatusContactAdmin": "", - "distributedUptimeStatusPageNotPublic": "", - "distributedUptimeStatusPageDeleteDialog": "", - "distributedUptimeStatusPageDeleteConfirm": "", - "distributedUptimeStatusPageDeleteDescription": "", - "distributedUptimeStatusDevices": "", - "distributedUptimeStatusUpt": "", - "distributedUptimeStatusUptBurned": "", - "distributedUptimeStatusUptLogo": "", - "incidentsTableNoIncidents": "", - "incidentsTablePaginationLabel": "", - "incidentsTableMonitorName": "", - "incidentsTableStatus": "", - "incidentsTableDateTime": "", - "incidentsTableStatusCode": "", - "incidentsTableMessage": "", - "incidentsOptionsHeader": "", - "incidentsOptionsHeaderFilterBy": "", - "incidentsOptionsHeaderFilterAll": "", - "incidentsOptionsHeaderFilterDown": "", - "incidentsOptionsHeaderFilterCannotResolve": "", - "incidentsOptionsHeaderShow": "", - "incidentsOptionsHeaderLastHour": "", - "incidentsOptionsHeaderLastDay": "", - "incidentsOptionsHeaderLastWeek": "", - "incidentsOptionsPlaceholderAllServers": "", - "infrastructureCreateYour": "", - "infrastructureCreateGeneralSettingsDescription": "", - "infrastructureServerRequirement": "", - "infrastructureCustomizeAlerts": "", - "infrastructureAlertNotificationDescription": "", - "infrastructureCreateMonitor": "", - "infrastructureProtocol": "", - "infrastructureServerUrlLabel": "", - "infrastructureDisplayNameLabel": "", - "infrastructureAuthorizationSecretLabel": "", - "gb": "", - "mb": "", - "mem": "", - "memoryUsage": "", - "cpu": "", - "cpuUsage": "", - "cpuTemperature": "", - "diskUsage": "", - "used": "", - "total": "", - "cores": "", - "frequency": "", - "status": "", - "cpuPhysical": "", - "cpuLogical": "", - "cpuFrequency": "", - "avgCpuTemperature": "", - "memory": "", - "disk": "", - "uptime": "", - "os": "", - "host": "", - "actions": "", - "integrations": "", - "integrationsPrism": "", - "integrationsSlack": "", - "integrationsSlackInfo": "", - "integrationsDiscord": "", - "integrationsDiscordInfo": "", - "integrationsZapier": "", - "integrationsZapierInfo": "", - "commonSave": "", - "createYour": "", - "createMonitor": "", - "pause": "", - "resume": "", - "editing": "", - "url": "", - "access": "", - "timezone": "", - "features": "", - "administrator": "", - "loginHere": "", - "displayName": "", - "urlMonitor": "", - "portToMonitor": "", - "websiteMonitoring": "", - "websiteMonitoringDescription": "", - "pingMonitoring": "", - "pingMonitoringDescription": "", - "dockerContainerMonitoring": "", - "dockerContainerMonitoringDescription": "", - "portMonitoring": "", - "portMonitoringDescription": "", - "createMaintenanceWindow": "", - "createMaintenance": "", - "editMaintenance": "", - "maintenanceWindowName": "", + "distributedUptimeStatusPageNotSetUp": "Eine Statusseite ist nicht eingerichtet.", + "distributedUptimeStatusContactAdmin": "Bitte kontaktiere deinen Administrator", + "distributedUptimeStatusPageNotPublic": "Diese Statusseite ist nicht öffentlich.", + "distributedUptimeStatusPageDeleteDialog": "Möchtest du diese Statusseite löschen?", + "distributedUptimeStatusPageDeleteConfirm": "Ja, Statusseite löschen", + "distributedUptimeStatusPageDeleteDescription": "Sobald sie gelöscht ist, kann deine Statusseite nicht wiederhergestellt werden.", + "distributedUptimeStatusDevices": "Geräte", + "distributedUptimeStatusUpt": "UPT", + "distributedUptimeStatusUptBurned": "UPT Burned", + "distributedUptimeStatusUptLogo": "Upt Logo", + "incidentsTableNoIncidents": "Keine Ereignisse aufgezeichnet", + "incidentsTablePaginationLabel": "Ereignisse", + "incidentsTableMonitorName": "Monitor Name", + "incidentsTableStatus": "Status", + "incidentsTableDateTime": "Datum & Zeit", + "incidentsTableStatusCode": "Status Code", + "incidentsTableMessage": "Nachricht", + "incidentsOptionsHeader": "Ereignisse für", + "incidentsOptionsHeaderFilterBy": "Gefiltert nach:", + "incidentsOptionsHeaderFilterAll": "Alle", + "incidentsOptionsHeaderFilterDown": "offline", + "incidentsOptionsHeaderFilterCannotResolve": "Kann nicht aufgelöst werden", + "incidentsOptionsHeaderShow": "Anzeigen:", + "incidentsOptionsHeaderLastHour": "Letzte Stunde", + "incidentsOptionsHeaderLastDay": "Letzter Tag", + "incidentsOptionsHeaderLastWeek": "Letzte Woche", + "incidentsOptionsPlaceholderAllServers": "Alle Server", + "infrastructureCreateYour": "Erstelle dein", + "infrastructureCreateGeneralSettingsDescription": "Hier kannst du die URL des Hosts auswählen, zusammen mit dem Namen und dem Autorisierungs-token, um eine Verbindung zum Server-Agenten herzustellen.", + "infrastructureServerRequirement": "Der Server, den du überwachst, muss die folgende Software ausführen:", + "infrastructureCustomizeAlerts": "Benachrichtigungen anpassen", + "infrastructureAlertNotificationDescription": "Sende eine Benachrichtigung an Benutzer, wenn Schwellenwerte einen bestimmten Prozentsatz überschreiten.", + "infrastructureCreateMonitor": "Erstelle Infrastrukturmonitor", + "infrastructureProtocol": "Protokoll", + "infrastructureServerUrlLabel": "Server URL", + "infrastructureDisplayNameLabel": "Anzeigename", + "infrastructureAuthorizationSecretLabel": "Autorisierungs-token", + "gb": "GB", + "mb": "MB", + "mem": "Speicher", + "memoryUsage": "Speicher Nutzung", + "cpu": "CPU", + "cpuUsage": "CPU Nutzung", + "cpuTemperature": "CPU Temperatur", + "diskUsage": "Disk benutzt", + "used": "benutzt", + "total": "Total", + "cores": "Kerne", + "frequency": "Frequenz", + "status": "Status", + "cpuPhysical": "CPU (Physical)", + "cpuLogical": "CPU (Logical)", + "cpuFrequency": "CPU Frequenz", + "avgCpuTemperature": "Durchschn. CPU Termperatur", + "memory": "Speicher", + "disk": "Disk", + "uptime": "Uptime", + "os": "OS", + "host": "Host", + "actions": "Aktionen", + "integrations": "Integrationen", + "integrationsPrism": "Verbinde Prism mit deinem favorisierten Gerät", + "integrationsSlack": "Slack", + "integrationsSlackInfo": "Verbinde dich mit Slack und sieh Ereignisse in einem Kanal", + "integrationsDiscord": "Discord", + "integrationsDiscordInfo": "Verbinde dich mit Discord und sieh Ereignisse direkt in einem Kanal", + "integrationsZapier": "Zapier", + "integrationsZapierInfo": "Sende alle Ereignisse an Zapier und sieh sie dann überall", + "commonSave": "Speichern", + "createYour": "Erstelle Deinen", + "createMonitor": "Erstelle Monitor", + "pause": "Pausieren", + "resume": "Weiter", + "editing": "Bearbeiten...", + "url": "URL", + "access": "Zugriff", + "timezone": "Zeitzone", + "features": "Funktionen", + "administrator": "Administrator?", + "loginHere": "Hier anmelden", + "displayName": "Anzeigename", + "urlMonitor": "zu überwachende URL", + "portToMonitor": "zu überwachender Port", + "websiteMonitoring": "Websiten-Überwachung", + "websiteMonitoringDescription": "Verwende HTTP(s), um deine Website oder API-Endpunkt zu überwachen.", + "pingMonitoring": "Ping-Überwachung", + "pingMonitoringDescription": "Überprüfe, ob dein Server verfügbar ist oder nicht.", + "dockerContainerMonitoring": "Docker-Container-Überwachung", + "dockerContainerMonitoringDescription": "Überprüfe, ob dein Docker-Container läuft oder nicht.", + "portMonitoring": "Port-Überwachung", + "portMonitoringDescription": "Überprüfe, ob dein Port offen ist oder nicht.", + "createMaintenanceWindow": "Erstelle Wartungsfenster", + "createMaintenance": "Erstelle Wartungsfenster", + "editMaintenance": "Ändere Wartungsfenster", + "maintenanceWindowName": "Name des Wartungsfensters", "friendlyNameInput": "", - "friendlyNamePlaceholder": "", - "maintenanceRepeat": "", - "maintenance": "", - "duration": "", - "addMonitors": "", - "window": "", - "cancel": "", - "message": "", - "low": "", - "high": "", - "statusCode": "", - "date&Time": "", - "type": "", - "statusPageName": "", - "publicURL": "", - "repeat": "", - "edit": "", - "createA": "", - "remove": "", + "friendlyNamePlaceholder": "Wartung um __ : __ für ___ Minuten", + "maintenanceRepeat": "Wartungsfenster wiederholen", + "maintenance": "Wartungsfenster", + "duration": "Dauer", + "addMonitors": "Monitor hinzufügen", + "window": "Fenster", + "cancel": "Abbrechen", + "message": "Nachricht", + "low": "niedrig", + "high": "hoch", + "statusCode": "Status Code", + "date&Time": "Datum & Zeit", + "type": "Art", + "statusPageName": "Name der Statusseite", + "publicURL": "Öffentliche URL", + "repeat": "Wiederholen", + "edit": "Ändern", + "createA": "Erstelle ein", + "remove": "Löschen", "maintenanceWindowDescription": "", - "startTime": "", - "timeZoneInfo": "", - "monitorsToApply": "", - "nextWindow": "", - "notFoundButton": "", - "pageSpeedConfigureSettingsDescription": "", - "monitorDisplayName": "", - "whenNewIncident": "", - "notifySMS": "", - "notifyEmails": "", - "seperateEmails": "", - "checkFrequency": "", + "startTime": "Anfangszeit", + "timeZoneInfo": "Alle Daten und Zeiten sind in der Zeitzone GMT+0.", + "monitorsToApply": "Monitore, auf die das Wartungsfenster angewendet werden soll", + "nextWindow": "Nächstes Fenster", + "notFoundButton": "Gehe zum Haupt-Dashboard", + "pageSpeedConfigureSettingsDescription": "Hier kannst du die URL des Hosts auswählen, zusammen mit der Art des Monitors.", + "monitorDisplayName": "Anzeigename des Monitors", + "whenNewIncident": "Wenn es ein neues Ereignis gibt,", + "notifySMS": "Benachrichtige per SMS (kommt bald)", + "notifyEmails": "Benachrichtige auch per E-Mail an mehrere Adressen (kommt bald)", + "seperateEmails": "Du kannst mehrere E-Mails mit einem Komma trennen", + "checkFrequency": "Überprüfungsfrequenz", + "chooseGame": "", "matchMethod": "", - "expectedValue": "", - "deleteDialogTitle": "", - "deleteDialogDescription": "", + "expectedValue": "Erwarteter Wert", + "deleteDialogTitle": "Möchtest du diesen Monitor wirklich löschen?", + "deleteDialogDescription": "Sobald dieser Monitor gelöscht ist, kann er nicht wiederhergestellt werden.", "pageSpeedMonitor": "", "shown": "", - "ago": "", + "ago": "vor", "companyName": "", "pageSpeedDetailsPerformanceReport": "", "pageSpeedDetailsPerformanceReportCalculator": "", @@ -283,7 +284,7 @@ "statusPageCreateTabsContentFeaturesDescription": "", "showCharts": "", "showUptimePercentage": "", - "removeLogo": "", + "removeLogo": "Entferne Logo", "statusPageStatus": "", "statusPageStatusContactAdmin": "", "statusPageStatusNotPublic": "", @@ -304,129 +305,131 @@ "errorInvalidFieldId": "", "inviteNoTokenFound": "", "pageSpeedWarning": "", - "pageSpeedLearnMoreLink": "", - "pageSpeedAddApiKey": "", - "update": "", - "invalidFileFormat": "", - "invalidFileSize": "", + "pageSpeedLearnMoreLink": "Hier klicken", + "pageSpeedAddApiKey": "um einen API key einzutragen", + "update": "Update", + "invalidFileFormat": "Nicht unterstützes Dateiformat", + "invalidFileSize": "Dateigröße zu groß!", "ClickUpload": "", - "DragandDrop": "", - "MaxSize": "", - "SupportedFormats": "", - "FirstName": "", - "LastName": "", + "DragandDrop": "drag and drop", + "MaxSize": "Maximalgröße", + "SupportedFormats": "Unterstützte Formate", + "FirstName": "Vorname", + "LastName": "Zuname", "EmailDescriptionText": "", - "YourPhoto": "", + "YourPhoto": "Profilphoto", "PhotoDescriptionText": "", - "save": "", + "save": "Speichern", "DeleteDescriptionText": "", "DeleteAccountWarning": "", - "DeleteWarningTitle": "", + "DeleteWarningTitle": "Account wirklich löschen?", "bulkImport": { "title": "", "selectFileTips": "", "selectFileDescription": "", - "selectFile": "", + "selectFile": "Datei auswählen", "parsingFailed": "", - "uploadSuccess": "", - "validationFailed": "", - "noFileSelected": "", - "fallbackPage": "" + "uploadSuccess": "Monitore erfolgreich erstellt!", + "validationFailed": "Prüfung fehlgeschlagen", + "noFileSelected": "Keine Datei ausgewählt", + "fallbackPage": "", + "invalidFileType": "Ungültiger Dateityp", + "uploadFailed": "Upload fehlgeschlagen" }, - "DeleteAccountTitle": "", - "DeleteAccountButton": "", - "publicLink": "", - "maskedPageSpeedKeyPlaceholder": "", - "reset": "", - "ignoreTLSError": "", - "tlsErrorIgnored": "", - "ignoreTLSErrorDescription": "", - "createNew": "", + "DeleteAccountTitle": "Account löschen", + "DeleteAccountButton": "Account löschen", + "publicLink": "Öffentlicher Link", + "maskedPageSpeedKeyPlaceholder": "*************************************", + "reset": "Reset", + "ignoreTLSError": "TLS/SSL Fehler ignorieren", + "tlsErrorIgnored": "TLS/SSL Fehler ignoriert", + "ignoreTLSErrorDescription": "TLS/SSL Fehler ignorieren und Verfügbarkeit weiter prüfen", + "createNew": "Neu anlegen", "greeting": { - "prepend": "", + "prepend": "Hallo", "append": "", "overview": "" }, "roles": { - "superAdmin": "", - "admin": "", - "teamMember": "", - "demoUser": "" + "superAdmin": "Super admin", + "admin": "Admin", + "teamMember": "Team Mitglied", + "demoUser": "Demo user" }, "teamPanel": { - "teamMembers": "", + "teamMembers": "Team Mitglied", "filter": { - "all": "", - "member": "" + "all": "Alle", + "member": "Mitglied" }, - "inviteTeamMember": "", - "inviteNewTeamMember": "", + "inviteTeamMember": "Team Mirglied einladen", + "inviteNewTeamMember": "Neues Team Mirglied einladen", "inviteDescription": "", - "email": "", - "selectRole": "", - "inviteLink": "", - "cancel": "", - "noMembers": "", - "getToken": "", - "emailToken": "", + "email": "Email", + "selectRole": "Rolle auswählen", + "inviteLink": "Einladungslink", + "cancel": "Abbrechen", + "noMembers": "Es gibt keine Team Mitglieder mit dieser Rolle", + "getToken": "Token holen", + "emailToken": "E-mail token", "table": { - "name": "", - "email": "", - "role": "", - "created": "" + "name": "Name", + "email": "Email", + "role": "Rolle", + "created": "Angelegt" } }, "monitorState": { - "paused": "", + "paused": "Pausiert", "resumed": "", - "active": "" + "active": "Aktiv" }, "menu": { - "uptime": "", + "uptime": "Uptime", "pagespeed": "", "infrastructure": "", "incidents": "", - "statusPages": "", - "maintenance": "", + "statusPages": "Statusseite", + "maintenance": "Wartung", "integrations": "", - "settings": "", - "support": "", - "discussions": "", - "docs": "", - "changelog": "", - "profile": "", - "password": "", - "team": "", - "logOut": "", - "notifications": "", - "logs": "" + "settings": "Einstellungen", + "support": "Support", + "discussions": "Diskussionen", + "docs": "Anleitungen", + "changelog": "Änderungen", + "profile": "Profil", + "password": "Passwort", + "team": "Team", + "logOut": "Abmelden", + "notifications": "Benachrichtigungen", + "logs": "Logs" }, "settingsEmailUser": "", "state": "", - "statusBreadCrumbsStatusPages": "", - "statusBreadCrumbsDetails": "", - "commonSaving": "", + "statusBreadCrumbsStatusPages": "Statusseite", + "statusBreadCrumbsDetails": "Details", + "commonSaving": "Speichere...", "navControls": "", - "incidentsPageTitle": "", + "incidentsPageTitle": "Ereignisse", "passwordPanel": { - "passwordChangedSuccess": "", - "passwordInputIncorrect": "", - "currentPassword": "", - "enterCurrentPassword": "", - "newPassword": "", - "enterNewPassword": "", - "confirmNewPassword": "", - "passwordRequirements": "", - "saving": "" + "passwordChangedSuccess": "Dein Passwort wurde erfolgreich geändert.", + "passwordInputIncorrect": "Die Passworteingabe war nicht korrekt.", + "currentPassword": "Aktuelles Passwort", + "enterCurrentPassword": "Aktuelles Passwort eingeben", + "newPassword": "Neues Passwort", + "enterNewPassword": "Neues Passwort eingeben", + "confirmNewPassword": "Neues Passwort wiederholen", + "passwordRequirements": "Das Passwort muss mindestens 8 Zeichen, einen Großbuchstaben, eine Zahl und ein Sonderzeichen enthalten.", + "saving": "Speichere..." }, - "emailSent": "", - "failedToSendEmail": "", - "settingsTestEmailSuccess": "", - "settingsTestEmailFailed": "", - "settingsTestEmailFailedWithReason": "", - "settingsTestEmailUnknownError": "", + "emailSent": "Email erfolgreich gesendet", + "failedToSendEmail": "Email konnte nicht gesendet werden", + "settingsTestEmailSuccess": "Test-Email erfolgreich gesendet", + "settingsTestEmailFailed": "Test-Email konnte nicht gesendet werden", + "settingsTestEmailFailedWithReason": "Email konnte nicht gesendet werden: {{reason}}", + "settingsTestEmailUnknownError": "Unbekannter Fehler", "statusMsg": { - "paused": "", + "paused": "Monitoring pausiert", "up": "", "down": "", "pending": "" @@ -435,186 +438,192 @@ "http": "", "ping": "", "docker": "", - "port": "" + "port": "", + "game": "" }, "common": { - "appName": "", - "monitoringAgentName": "", + "appName": "Checkmate", + "monitoringAgentName": "Capture", "buttons": { "toggleTheme": "" }, "toasts": { - "networkError": "", - "checkConnection": "", - "unknownError": "" + "networkError": "Netzwerkfehler", + "checkConnection": "Bitte Verbindung prüfen", + "unknownError": "Unbekannter Fehler" } }, "auth": { "common": { "navigation": { - "continue": "", - "back": "" + "continue": "Weiter", + "back": "Zurück" }, "inputs": { "email": { - "label": "", - "placeholder": "", + "label": "Email", + "placeholder": "jordan.ellis@domain.com", "errors": { - "empty": "", + "empty": "Email adresse eingeben um fortzufahren", "invalid": "" } }, "password": { - "label": "", + "label": "Passwort", "rules": { "length": { - "beginning": "", - "highlighted": "" + "beginning": "Muß mindestens", + "highlighted": "8 Zeichen lang" }, "special": { - "beginning": "", - "highlighted": "" + "beginning": "Muß mindestens", + "highlighted": "ein Sonderzeichen enthalten" }, "number": { - "beginning": "", - "highlighted": "" + "beginning": "Muß mindestens", + "highlighted": "eine Zahl enthalten" }, "uppercase": { - "beginning": "", - "highlighted": "" + "beginning": "Muß mindestens", + "highlighted": "einen Großbuchstaben enthalten" }, "lowercase": { - "beginning": "", - "highlighted": "" + "beginning": "Muß mindestens", + "highlighted": "einen Kleinbuchstaben enthalten" }, "match": { "beginning": "", - "highlighted": "" + "highlighted": "muß übereinstimmen" } }, "errors": { - "empty": "", - "length": "", - "uppercase": "", - "lowercase": "", - "number": "", - "special": "", - "incorrect": "" + "empty": "Bitte Passwort eingeben", + "length": "Passwort muss mindestens 8 Zeichen lang sein", + "uppercase": "Passwort muss mindestens einen Großbuchstaben enthalten", + "lowercase": "Passwort muss mindestens einen Kleinbuchstaben enthalten", + "number": "Passwort muss mindestens eine Zahl enthalten", + "special": "Passwort muss mindestens ein Sonderzeichen enthalten", + "incorrect": "Das Passwort stimmt nicht mit dem gespeicherten überein" } }, "passwordConfirm": { - "label": "", - "placeholder": "", + "label": "Passwort bestätigen", + "placeholder": "Passwort nochmal eingeben", "errors": { - "empty": "", - "different": "" + "empty": "Passwort nochmal eingeben", + "different": "Die Passwörter stimmen nicht überein" } }, "firstName": { - "label": "", - "placeholder": "", + "label": "Name", + "placeholder": "Jordan", "errors": { - "empty": "", - "length": "", - "pattern": "" + "empty": "Bitte Name eingeben", + "length": "Name darf nicht mehr als 50 Zeichen lang sein", + "pattern": "Name darf nur aus Buchstaben, Leerstellen, Apostroph und Bindestrichen bestehen" } }, "lastName": { - "label": "", - "placeholder": "", + "label": "Zuname", + "placeholder": "Ellis", "errors": { - "empty": "", - "length": "", - "pattern": "" + "empty": "Bitte Zuname eingeben", + "length": "Zuname darf nicht länger als 50 Zeichen sein", + "pattern": "Zuname darf nur aus Buchstaben, Leerstellen, Apostroph und Bindestrichen bestehen" } } }, "errors": { - "validation": "" + "validation": "Fehler bei der prüfung der Daten" }, "fields": { "password": { "errors": { - "incorrect": "" + "incorrect": "Das Passwort stimmt nicht mit dem gespeicherten überein" + } + }, + "role": { + "errors": { + "min": "" } } } }, "login": { - "heading": "", + "heading": "Anmelden", "subheadings": { - "stepOne": "", - "stepTwo": "" + "stepOne": "Email eingeben", + "stepTwo": "Passwort eingeben" }, "links": { - "forgotPassword": "", - "register": "", - "forgotPasswordLink": "", - "registerLink": "" + "forgotPassword": "Passwort vergessen?", + "register": "Noch keinen Account?", + "forgotPasswordLink": "Passwort zurücksetzen", + "registerLink": "Hier resistrieren" }, "toasts": { - "success": "", - "incorrectPassword": "" + "success": "Willkommen zurück. Erfolgreich angemeldet.", + "incorrectPassword": "Falsches Passwort" }, "errors": { "password": { - "incorrect": "" + "incorrect": "Das Passwort stimmt nicht mit dem gespeicherten überein" } } }, "registration": { "heading": { - "superAdmin": "", - "user": "" + "superAdmin": "Super Admin anlegen", + "user": "Anmelden" }, "subheadings": { "stepOne": "", - "stepTwo": "", - "stepThree": "" + "stepTwo": "Email eingeben", + "stepThree": "Passwort eingeben" }, "description": { - "superAdmin": "", + "superAdmin": "Super admin account anlegen um zu beginnen", "user": "" }, "gettingStartedButton": { - "superAdmin": "", - "user": "" + "superAdmin": "Super admin account anlegen", + "user": "Normalen Benutzer anlegen" }, "termsAndPolicies": "", "links": { - "login": "" + "login": "Du hast bereits einen Benutzer? Anmelden" }, "toasts": { - "success": "" + "success": "Willkommen! Benutzer erfolgreich angelegt" } }, "forgotPassword": { - "heading": "", + "heading": "Passwort vergessen?", "subheadings": { - "stepOne": "", - "stepTwo": "", - "stepThree": "", - "stepFour": "" + "stepOne": "Kein Problem. Wir schicken Dir Infos zum zurücksetzen.", + "stepTwo": "Link zum Password zurücksetzen an geschickt. ", + "stepThree": "Das neue Passwort darf nicht gleich wie frühere Passwörter sein.", + "stepFour": "Das Passwort wurde zurückgesetzt. Unten klicken zum Anmelden." }, "buttons": { - "openEmail": "", - "resetPassword": "" + "openEmail": "Email app öffnen", + "resetPassword": "Passwort zurücksetzen" }, "imageAlts": { - "passwordKey": "", - "email": "", - "lock": "", - "passwordConfirm": "" + "passwordKey": "Password key icon", + "email": "Email icon", + "lock": "Lock icon", + "passwordConfirm": "Password confirm icon" }, "links": { - "login": "", - "resend": "" + "login": "Zurück zum Anmelden", + "resend": "Keine Email bekommen? Klicken um nochmal zu senden" }, "toasts": { - "sent": "", - "emailNotFound": "", - "redirect": "", - "success": "", + "sent": "Anleitung an gesendet.", + "emailNotFound": "Email nicht gefunden.", + "redirect": "Umleitung in ...", + "success": "Passwort erfolgreich zurückgesetzt.", "error": "" } } @@ -622,83 +631,86 @@ "errorPages": { "serverUnreachable": { "toasts": { - "reconnected": "", - "stillUnreachable": "" + "reconnected": "Verbindung zum Server wiederhergestellt.", + "stillUnreachable": "Server immer noch nicht erreichbar. Bitte später erneut versuchen." }, - "alertBox": "", + "alertBox": "Server Verbindungsfehler", "description": "", "retryButton": { - "default": "", - "processing": "" + "default": "Verbindung erneut versuchen", + "processing": "Verbinde..." } } }, "createNotifications": { "title": "", "nameSettings": { - "title": "", + "title": "Name", "description": "", - "nameLabel": "", - "namePlaceholder": "" + "nameLabel": "Name", + "namePlaceholder": "z.B. Slack Benachrichtigung" }, "typeSettings": { - "title": "", + "title": "Typ", "description": "", - "typeLabel": "" + "typeLabel": "Typ" }, "emailSettings": { - "title": "", - "description": "", - "emailLabel": "", - "emailPlaceholder": "" + "title": "Email", + "description": "Empfänger Email adresse", + "emailLabel": "Email addresse", + "emailPlaceholder": "z.B. john@example.com" }, "slackSettings": { - "title": "", - "description": "", - "webhookLabel": "", + "title": "Slack", + "description": "Slack webhook hier konfigurieren", + "webhookLabel": "Slack webhook URL", "webhookPlaceholder": "" }, "pagerdutySettings": { - "title": "", - "description": "", - "integrationKeyLabel": "", - "integrationKeyPlaceholder": "" + "title": "PagerDuty", + "description": "PagerDuty hier konfigurieren", + "integrationKeyLabel": "Integration key", + "integrationKeyPlaceholder": "1234567890" }, "discordSettings": { - "title": "", - "description": "", - "webhookLabel": "", - "webhookPlaceholder": "" + "title": "Discord", + "description": "Discord webhook hier konfigurieren", + "webhookLabel": "Discord Webhook URL", + "webhookPlaceholder": "https://your-server.com/webhook" }, "webhookSettings": { - "title": "", - "description": "", - "webhookLabel": "", - "webhookPlaceholder": "" - } + "title": "Webhook", + "description": "Webhook hier konfigurieren", + "webhookLabel": "Webhook URL", + "webhookPlaceholder": "https://your-server.com/webhook" + }, + "testNotification": "", + "dialogDeleteTitle": "", + "dialogDeleteConfirm": "" }, "notificationConfig": { - "title": "", + "title": "Benachrichtigungen", "description": "" }, "monitorStatus": { "checkingEvery": "", "withCaptureAgent": "", - "up": "", - "down": "", - "paused": "" + "up": "hoch", + "down": "runter", + "paused": "pausiert" }, "advancedMatching": "", - "sendTestNotifications": "", - "testNotificationsDisabled": "", - "selectAll": "", + "sendTestNotifications": "Test Benachrichtigung senden", + "selectAll": "Alle auswählen", "showAdminLoginLink": "", "logsPage": { - "title": "", + "title": "Logs", "description": "", "tabs": { - "queue": "", - "logs": "" + "queue": "Warteschlange", + "logs": "Server logs", + "diagnostics": "" }, "toast": { "fetchLogsSuccess": "" @@ -706,94 +718,98 @@ "logLevelSelect": { "title": "", "values": { - "all": "", - "info": "", - "warn": "", - "error": "", - "debug": "" + "all": "Alle", + "info": "Info", + "warn": "Warnung", + "error": "Fehler", + "debug": "Debug" } } }, "queuePage": { - "title": "", - "refreshButton": "", - "flushButton": "", + "title": "Warteschlange", + "refreshButton": "Auffrischen", + "flushButton": "Warteschlange leeren", "jobTable": { - "title": "", - "idHeader": "", - "urlHeader": "", - "typeHeader": "", - "activeHeader": "", + "title": "Aufgaben in der Warteschlange", + "idHeader": "Monitor ID", + "urlHeader": "URL", + "typeHeader": "Typ", + "activeHeader": "Aktiv", "lockedAtHeader": "", - "runCountHeader": "", - "failCountHeader": "", - "lastRunHeader": "", - "lastFinishedAtHeader": "", - "lastRunTookHeader": "" + "runCountHeader": "Anzahl Läufe", + "failCountHeader": "Anzahl Fehler", + "lastRunHeader": "Letzter Lauf um", + "lastFinishedAtHeader": "Zuletzt abgeschlossen um", + "lastRunTookHeader": "Letzter Lauf dauerte" }, "metricsTable": { "title": "", "metricHeader": "", - "valueHeader": "" + "valueHeader": "Wert" }, "failedJobTable": { - "title": "", - "monitorIdHeader": "", - "monitorUrlHeader": "", - "failCountHeader": "", - "failedAtHeader": "", - "failReasonHeader": "" + "title": "Anzahl Fehler", + "monitorIdHeader": "Monitor ID", + "monitorUrlHeader": "Monitor URL", + "failCountHeader": "Anzahl Fehler", + "failedAtHeader": "Zuletzt fehlgeschlagen um", + "failReasonHeader": "Fehlergrund" } }, "export": { - "title": "", - "success": "", - "failed": "" + "title": "Monitore exportieren", + "success": "Monitore erfolgreich exportiert", + "failed": "Fehler beim exportieren der Monitore" }, "monitorActions": { - "title": "", - "import": "", - "export": "" + "title": "Export/Import", + "import": "Monitore importieren", + "export": "Monitore exportieren", + "deleteSuccess": "", + "deleteFailed": "", + "details": "" }, "settingsPage": { "aboutSettings": { - "labelDevelopedBy": "", - "labelVersion": "", - "title": "" + "labelDevelopedBy": "Entwickelt von Bluewave Labs", + "labelVersion": "Version", + "title": "Über" }, "demoMonitorsSettings": { - "buttonAddMonitors": "", - "description": "", - "title": "" + "buttonAddMonitors": "Demo Monitor hinzufügen", + "description": "Beispiel Monitor zur Demonstration hinzufügen.", + "title": "Demo Monitore" }, "emailSettings": { - "buttonSendTestEmail": "", + "buttonSendTestEmail": "Test-Email senden", "description": "", "descriptionTransport": "", "labelAddress": "", "labelConnectionHost": "", "labelHost": "", - "labelIgnoreTLS": "", - "labelPassword": "", - "labelPasswordSet": "", + "labelIgnoreTLS": "STARTTLS abschalten: TLS nicht nutzen, selbst wenn der Server es unterstützt", + "labelPassword": "Email password - Password für Anmeldung", + "labelPasswordSet": "Passwort erstellt. Reset drücken um es zu ändern.", "labelPool": "", - "labelPort": "", - "labelRejectUnauthorized": "", + "labelPort": "Email port - Port zum verbinden", + "labelRejectUnauthorized": "Ungültige Zertifikate ablehnen. Verbindungen mit self-signed oder nicht vertrauenswürdigen Zertifikaten ablehnen.", "labelRequireTLS": "", - "labelSecure": "", + "labelSecure": "SSL verwenden (empfohlen): Verbindung mit SSL/TLS verschlüsseln", "labelTLSServername": "", "labelUser": "", "linkTransport": "", - "placeholderUser": "", - "title": "" + "placeholderUser": "Leer lassen, wenn nicht benötigt", + "title": "Email", + "toastEmailRequiredFieldsError": "" }, "pageSpeedSettings": { "description": "", - "labelApiKeySet": "", - "labelApiKey": "", - "title": "" + "labelApiKeySet": "API key erstellt. Reset drücken um ihn zu ändern.", + "labelApiKey": "PageSpeed API key", + "title": "Google PageSpeed API key" }, - "saveButtonLabel": "", + "saveButtonLabel": "Speichern", "statsSettings": { "clearAllStatsButton": "", "clearAllStatsDescription": "", @@ -802,35 +818,154 @@ "clearAllStatsDialogTitle": "", "description": "", "labelTTL": "", - "labelTTLOptional": "", + "labelTTLOptional": "0 für unendlich", "title": "" }, "systemResetSettings": { - "buttonRemoveAllMonitors": "", - "description": "", - "dialogConfirm": "", - "dialogDescription": "", - "dialogTitle": "", - "title": "" + "buttonRemoveAllMonitors": "Alle Monitore löschen", + "description": "Alle Monitore löschen", + "dialogConfirm": "Ja, alle Monitore löschen", + "dialogDescription": "Gelöschte Monitore können nicht wiederhergestellt werden.", + "dialogTitle": "Wirklich alle Monitore löschen?", + "title": "System zurücksetzen" }, "timezoneSettings": { "description": "", - "label": "", - "title": "" + "label": "Zeitzone", + "title": "Zeitzone" }, - "title": "", + "title": "Einstellungen", "uiSettings": { "description": "", - "labelLanguage": "", + "labelLanguage": "Sprache", "labelTheme": "", "title": "" }, "urlSettings": { "description": "", "label": "", - "selectDisabled": "", - "selectEnabled": "", + "selectDisabled": "Inaktiv", + "selectEnabled": "Aktiv", "title": "" } - } + }, + "statusPageCreate": { + "buttonSave": "Speichern" + }, + "incidentsOptionsHeaderFilterResolved": "", + "settingsSave": "Speichern", + "statusPageCreateAppearanceTitle": "", + "confirmPassword": "Passwort bestätigen", + "monitorHooks": { + "failureAddDemoMonitors": "Demo Monitor konnte nicht hinzugefügt werden", + "successAddDemoMonitors": "Demo Monitor erfolgreich hinzugefügt" + }, + "settingsAppearance": "", + "settingsDisplayTimezone": "", + "settingsGeneralSettings": "", + "incidentsOptionsHeaderTotalIncidents": "", + "statusPage": { + "deleteSuccess": "", + "deleteFailed": "", + "createSuccess": "", + "updateSuccess": "", + "generalSettings": "", + "contents": "", + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "testNotificationsDisabled": "", + "incidentsTableResolvedAt": "", + "incidentsTableActionResolve": "", + "checkHooks": { + "failureResolveOne": "", + "failureResolveAll": "", + "failureResolveMonitor": "" + }, + "checkFormError": "", + "diagnosticsPage": { + "diagnosticDescription": "", + "statsDescription": "", + "gauges": { + "heapAllocationTitle": "", + "heapAllocationSubtitle": "", + "heapUsageTitle": "", + "heapUsageSubtitle": "", + "heapUtilizationTitle": "", + "heapUtilizationSubtitle": "", + "instantCpuUsageTitle": "", + "instantCpuUsageSubtitle": "" + }, + "stats": { + "eventLoopDelayTitle": "", + "uptimeTitle": "", + "usedHeapSizeTitle": "", + "totalHeapSizeTitle": "", + "osMemoryLimitTitle": "" + } + }, + "pageSpeedLighthouseAPI": "", + "time": { + "threeMinutes": "", + "fiveMinutes": "", + "tenMinutes": "", + "twentyMinutes": "", + "oneHour": "", + "oneDay": "", + "oneWeek": "" + }, + "general": { + "noOptionsFound": "" + }, + "infrastructureMonitor": { + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "maintenanceWindow": { + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "pageSpeed": { + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "uptimeMonitor": { + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "editUserPage": { + "form": { + "email": "", + "firstName": "", + "lastName": "", + "role": "", + "save": "" + }, + "table": { + "actionHeader": "", + "roleHeader": "" + }, + "title": "", + "toast": { + "successUserUpdate": "", + "validationErrors": "" + } + }, + "incidentsPageActionResolveMonitor": "", + "incidentsPageActionResolveAll": "" } diff --git a/client/src/locales/en.json b/client/src/locales/en.json index ca4e716dd..857825c9f 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": { @@ -82,7 +87,7 @@ "highlighted": "one lower character" }, "match": { - "beginning": "Confirm password and password", + "beginning": "Passwords", "highlighted": "must match" }, "number": { @@ -149,7 +154,7 @@ "incorrect": "The password you provided does not match our records" } }, - "heading": "Log In", + "heading": "Log in to continue", "links": { "forgotPassword": "Forgot password?", "forgotPasswordLink": "Reset password", @@ -163,7 +168,8 @@ "toasts": { "incorrectPassword": "Incorrect password", "success": "Welcome back! You're successfully logged in." - } + }, + "welcome": "Welcome back to Checkmate!" }, "registration": { "description": { @@ -189,12 +195,20 @@ "termsAndPolicies": "By creating an account, you agree to our Terms of Service and Privacy Policy.", "toasts": { "success": "Welcome! Your account was created successfully." - } + }, + "welcome": "Welcome to Checkmate!" } }, "avgCpuTemperature": "Average CPU Temperature", "bar": "Bar", "basicInformation": "Basic Information", + "bytesPerSecond": "Bytes per second", + "bytesReceived": "Bytes Received", + "bytesSent": "Bytes Sent", + "dataReceived": "Data Received", + "dataSent": "Data Sent", + "dataRate": "Data Rate", + "rate": "Rate", "bulkImport": { "fallbackPage": "Import a file to upload a list of servers in bulk", "invalidFileType": "Invalid file type", @@ -211,9 +225,11 @@ "cancel": "Cancel", "checkFormError": "Please check the form for errors.", "checkFrequency": "Check frequency", + "chooseGame": "Choose game", "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 +264,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 +296,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 +308,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,8 +318,38 @@ "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", + "drops": "Drops", + "errors": "Errors", + "errorsIn": "Errors In", + "errorsOut": "Errors Out", + "networkErrors": "Network Errors", + "networkDrops": "Network Drops", + "networkInterface": "Network Interface", + "selectInterface": "Select Interface", + "details": "Details", "displayName": "Display name", "distributedRightCategoryTitle": "Monitor", "distributedStatusHeaderText": "Real-time, real-device coverage", @@ -351,8 +397,26 @@ "dockerContainerMonitoringDescription": "Check whether your Docker container is running or not.", "duration": "Duration", "edit": "Edit", - "editMaintenance": "Edit maintenance", "editing": "Editing...", + "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" + } + }, "emailSent": "Email sent successfully", "errorInvalidFieldId": "Invalid field ID provided", "errorInvalidTypeId": "Invalid notification type provided", @@ -382,6 +446,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 +463,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", + "incidentsPageActionResolveAll": "Resolve all incidents", + "incidentsPageActionResolveMonitor": "Resolve monitor 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 +493,17 @@ "infrastructureDisplayNameLabel": "Display name", "infrastructureEditMonitor": "Save Infrastructure Monitor", "infrastructureEditYour": "Edit your", + "infrastructureMonitor": { + "fallback": { + "actionButton": "Let's create your first infrastructure monitor!", + "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:" + } + }, "infrastructureMonitorCreated": "Infrastructure monitor created successfully!", "infrastructureMonitorUpdated": "Infrastructure monitor updated successfully!", "infrastructureProtocol": "Protocol", @@ -443,7 +522,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 +534,7 @@ } }, "tabs": { + "diagnostics": "Diagnostics", "logs": "Server logs", "queue": "Job queue" }, @@ -467,10 +547,30 @@ "maintenance": "maintenance", "maintenanceRepeat": "Maintenance Repeat", "maintenanceTableActionMenuDialogTitle": "Do you really want to remove this maintenance window?", + "maintenanceWindow": { + "fallback": { + "actionButton": "Let's create your first maintenance window!", + "checks": [ + "Mark your maintenance periods", + "Eliminate any misunderstandings", + "Stop sending alerts in maintenance windows" + ], + "title": "A maintenance window is used to:" + } + }, "maintenanceWindowDescription": "Your pings won't be sent during this time frame", "maintenanceWindowName": "Maintenance Window Name", "maskedPageSpeedKeyPlaceholder": "*************************************", "matchMethod": "Match Method", + "matchMethodOptions": { + "equal": "Equal", + "equalPlaceholder": "success", + "include": "Include", + "includePlaceholder": "ok", + "regex": "Regex", + "regexPlaceholder": "^(success|ok)$", + "text": "Match Method" + }, "mb": "MB", "mem": "Mem", "memory": "Memory", @@ -498,8 +598,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", @@ -510,6 +610,7 @@ "failureAddDemoMonitors": "Failed to add demo monitors", "successAddDemoMonitors": "Successfully added demo monitors" }, + "monitors": "monitors", "monitorState": { "active": "Active", "paused": "Paused", @@ -524,11 +625,39 @@ }, "monitorStatusDown": "Monitor {name} ({url}) is DOWN and not responding", "monitorStatusUp": "Monitor {name} ({url}) is now UP and responding", - "monitors": "monitors", "monitorsToApply": "Monitors to apply maintenance window to", + "monitorType": { + "docker": { + "label": "Container ID", + "namePlaceholder": "My Container", + "placeholder": "abcd1234" + }, + "http": { + "label": "URL to monitor", + "namePlaceholder": "Google", + "placeholder": "google.com" + }, + "ping": { + "label": "IP address to monitor", + "namePlaceholder": "Google", + "placeholder": "1.1.1.1" + }, + "port": { + "label": "URL to monitor", + "namePlaceholder": "Localhost:5173", + "placeholder": "localhost" + }, + "game": { + "label": "URL to monitor", + "namePlaceholder": "localhost:5173", + "placeholder": "localhost" + } + }, "ms": "ms", "navControls": "Controls", + "network": "Network", "nextWindow": "Next window", + "noNetworkStatsAvailable": "No network stats available.", "notFoundButton": "Go to the main dashboard", "notificationConfig": { "description": "Select the notifications channels you want to use", @@ -559,12 +688,13 @@ }, "enableNotifications": "Enable {{platform}} notifications", "fallback": { + "actionButton": "Let's create your first notification channel!", "checks": [ "Alert teams about downtime or performance issues", "Let engineers know when incidents happen", "Keep administrators informed of system changes" ], - "title": "notification channel" + "title": "A notification channel is used to:" }, "fetch": { "failed": "Failed to fetch notifications", @@ -609,6 +739,17 @@ "notifySMS": "Notify via SMS (coming soon)", "now": "Now", "os": "OS", + "pageSpeed": { + "fallback": { + "actionButton": "Let's create your first PageSpeed monitor!", + "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:" + } + }, "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.", @@ -628,19 +769,25 @@ "passwordRequirements": "New password must contain at least 8 characters and must have at least one uppercase letter, one lowercase letter, one number and one special character.", "saving": "Saving..." }, + "packetsPerSecond": "Packets per second", + "packetsReceived": "Packets Received", + "packetsReceivedRate": "Packets Received Rate", + "packetsSent": "Packets Sent", "pause": "Pause", "pingMonitoring": "Ping monitoring", "pingMonitoringDescription": "Check whether your server is available or not.", "portMonitoring": "Port monitoring", "portMonitoringDescription": "Check whether your port is open or not.", + "gameServerMonitoring": "Game Server Monitoring", + "gameServerMonitoringDescription": "Check whether your game server is running or not", "portToMonitor": "Port to monitor", "publicLink": "Public link", "publicURL": "Public URL", "queuePage": { "failedJobTable": { "failCountHeader": "Fail count", - "failReasonHeader": "Fail reason", "failedAtHeader": "Last failed at", + "failReasonHeader": "Fail reason", "monitorIdHeader": "Monitor ID", "monitorUrlHeader": "Monitor URL", "title": "Failed jobs" @@ -758,6 +905,10 @@ "title": "Display timezone" }, "title": "Settings", + "globalThresholds": { + "title": "Global Thresholds", + "description": "Configure global CPU, Memory, Disk, and Temperature thresholds. If a value is provided, it will automatically be enabled for monitoring." + }, "uiSettings": { "description": "Switch between light and dark mode, or change user interface language.", "labelLanguage": "Language", @@ -781,8 +932,8 @@ "settingsTestEmailUnknownError": "Unknown error", "showAdminLoginLink": "Show \"Administrator? Login Here\" link on the status page", "showCharts": "Show charts", - "showUptimePercentage": "Show uptime percentage", "shown": "Shown", + "showUptimePercentage": "Show uptime percentage", "starPromptDescription": "See the latest releases and help grow the community on GitHub", "starPromptTitle": "Star Checkmate", "startTime": "Start time", @@ -797,6 +948,23 @@ "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", + "fallback": { + "actionButton": "Let's create your first status page!", + "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:" + }, + "generalSettings": "General settings", + "updateSuccess": "Status page updated successfully" + }, "statusPageCreate": { "buttonSave": "Save" }, @@ -816,14 +984,6 @@ "statusPageStatusNoPage": "There's no status page here.", "statusPageStatusNotPublic": "This status page is not public.", "statusPageStatusServiceStatus": "Service status", - "statusPage": { - "deleteSuccess": "Status page deleted successfully", - "deleteFailed": "Failed to delete status page", - "createSuccess": "Status page created successfully", - "updateSuccess": "Status page updated successfully", - "generalSettings": "General settings", - "contents": "Contents" - }, "submit": "Submit", "teamPanel": { "cancel": "Cancel", @@ -851,22 +1011,30 @@ "testLocale": "testLocale", "testNotificationsDisabled": "There are no notifications setup for this monitor. You need to add one by clicking 'Configure' button", "time": { - "threeMinutes": "3 minutes", + "fifteenSeconds": "15 seconds", "fiveMinutes": "5 minutes", - "tenMinutes": "10 minutes", - "twentyMinutes": "20 minutes", - "oneHour": "1 hour", + "fourMinutes": "4 minutes", "oneDay": "1 day", - "oneWeek": "1 week" + "oneHour": "1 hour", + "oneMinute": "1 minute", + "oneWeek": "1 week", + "tenMinutes": "10 minutes", + "thirtySeconds": "30 seconds", + "threeMinutes": "3 minutes", + "twentyMinutes": "20 minutes", + "twoMinutes": "2 minutes" }, - "timeZoneInfo": "All dates and times are in GMT+0 time zone.", "timezone": "Timezone", + "timeZoneInfo": "All dates and times are in GMT+0 time zone.", "title": "Title", "tlsErrorIgnored": "TLS/SSL errors ignored", "total": "Total", "type": "Type", "update": "Update", "uptime": "Uptime", + "uptimeAdvancedMatching": { + "jsonPath": "JSON Path" + }, "uptimeCreate": "The expected value is used to match against response result, and the match determines the status.", "uptimeCreateJsonPath": "This expression will be evaluated against the reponse JSON data and the result will be used to match against the expected value. See", "uptimeCreateJsonPathQuery": "for query language documentation.", @@ -874,7 +1042,20 @@ "docker": "Enter the Docker ID of your container. Docker IDs must be the full 64 char Docker ID. You can run docker inspect to get the full container ID.", "http": "Enter the URL or IP to monitor (e.g., https://example.com/ or 192.168.1.100) and add a clear display name that appears on the dashboard.", "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." + "port": "Enter the URL or IP of the server, the port number and a clear display name that appears on the dashboard.", + "game": "Enter the IP address or hostname and the port number to ping (e.g., 192.168.1.100 or example.com) and choose game type." + }, + "uptimeMonitor": { + "fallback": { + "actionButton": "Let's create your first uptime monitor!", + "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:" + } }, "url": "URL", "urlMonitor": "URL to monitor", diff --git a/client/src/locales/es.json b/client/src/locales/es.json index 0db51b102..1e2ee3b6c 100644 --- a/client/src/locales/es.json +++ b/client/src/locales/es.json @@ -1,84 +1,83 @@ { "submit": "Enviar", "title": "Titulo", - "distributedStatusHeaderText": "", - "distributedStatusSubHeaderText": "", - "settingsDisabled": "", - "settingsSuccessSaved": "", - "settingsFailedToSave": "", - "settingsStatsCleared": "", - "settingsFailedToClearStats": "", - "settingsFailedToAddDemoMonitors": "", - "settingsMonitorsDeleted": "", - "settingsFailedToDeleteMonitors": "", + "distributedStatusHeaderText": "Cobertura en tiempo real, para dispositivos reales", + "distributedStatusSubHeaderText": "Potenciado por millones de dispositivos alrededor del mundo, vea el rendimiento del sistema por región global, país o ciudad", + "settingsDisabled": "Deshabilitado", + "settingsSuccessSaved": "Ajustes guardados correctamente", + "settingsFailedToSave": "Fallo al guardar los ajustes", + "settingsStatsCleared": "Estadísticas eliminadas correctamente", + "settingsFailedToClearStats": "Fallo al eliminar las estadísticas", + "settingsMonitorsDeleted": "Todos los monitores eliminados correctamente", + "settingsFailedToDeleteMonitors": "Fallo al eliminar todos los monitores", "starPromptTitle": "", - "starPromptDescription": "", - "https": "", - "http": "", - "monitor": "", - "aboutus": "", - "now": "", - "delete": "", - "configure": "", - "responseTime": "", - "ms": "", - "bar": "", - "area": "", - "country": "", - "city": "", - "response": "", - "monitorStatusUp": "", - "monitorStatusDown": "", - "webhookSendSuccess": "", - "webhookSendError": "", - "webhookUnsupportedPlatform": "", - "distributedRightCategoryTitle": "", - "distributedStatusServerMonitors": "", - "distributedStatusServerMonitorsDescription": "", - "distributedUptimeCreateSelectURL": "", - "distributedUptimeCreateChecks": "", - "distributedUptimeCreateChecksDescription": "", - "distributedUptimeCreateIncidentNotification": "", - "distributedUptimeCreateIncidentDescription": "", - "distributedUptimeCreateAdvancedSettings": "", - "distributedUptimeDetailsNoMonitorHistory": "", - "distributedUptimeDetailsStatusHeaderUptime": "", - "distributedUptimeDetailsStatusHeaderLastUpdate": "", + "starPromptDescription": "Vea las últimas versiones y ayude a incrementar la comunidad en Github", + "https": "HTTPS", + "http": "HTTP", + "monitor": "monitor", + "aboutus": "Acerca de", + "now": "Ahora", + "delete": "Eliminar", + "configure": "Configurar", + "responseTime": "Tiempo de Respuesta", + "ms": "ms", + "bar": "Barra", + "area": "Área", + "country": "PAIS", + "city": "CIUDAD", + "response": "RESPUESTA", + "monitorStatusUp": "El monitor {name} ({url}) está ahora activo y respondiendo", + "monitorStatusDown": "El monitor {name} ({url}) está inactivo y no está respondiendo", + "webhookSendSuccess": "Notificación webhook enviada correctamente", + "webhookSendError": "Error al enviar notificación webhook a {platform}", + "webhookUnsupportedPlatform": "Plataforma no soportada: {platform}", + "distributedRightCategoryTitle": "Monitor", + "distributedStatusServerMonitors": "Monitores de Servidores", + "distributedStatusServerMonitorsDescription": "Monitoree el estado de los servidores relacionados", + "distributedUptimeCreateSelectURL": "Aquí puede seleccionar la URL del anfitrión, junto con el tipo de monitor.", + "distributedUptimeCreateChecks": "Verificaciones a realizar", + "distributedUptimeCreateChecksDescription": "Usted siempre puede agregar o eliminar verificaciones luego de añadir su sitio.", + "distributedUptimeCreateIncidentNotification": "Notificaciones de incidentes", + "distributedUptimeCreateIncidentDescription": "Cuando ocurra un incidente, notificar a usuarios.", + "distributedUptimeCreateAdvancedSettings": "Ajustes avanzados", + "distributedUptimeDetailsNoMonitorHistory": "No hay historial de verificaciones para este monitor aún.", + "distributedUptimeDetailsStatusHeaderUptime": "Tiempo de funcionamiento:", + "distributedUptimeDetailsStatusHeaderLastUpdate": "Actualizado por última vez", "notifications": { - "enableNotifications": "", - "testNotification": "", - "addOrEditNotifications": "", + "enableNotifications": "Habilitar notificaciones para {{platform}}", + "testNotification": "Notificación de prueba", + "addOrEditNotifications": "Agregar o eliminar notificaciones", "slack": { - "label": "", - "description": "", - "webhookLabel": "", + "label": "Slack", + "description": "Para habilitar notificaciones de Slack, cree una aplicación de Slack y habilite los webhooks entrantes. Luego de eso, simplemente introduzca la URL del webhook aquí.", + "webhookLabel": "URL del webhook", "webhookPlaceholder": "", "webhookRequired": "" }, "discord": { - "label": "", - "description": "", - "webhookLabel": "", + "label": "Discord", + "description": "Para enviar datos a un canal de Discord desde Checkmate a través de notificaciones de Discord utilizando webhooks, puede utilizar la característica de webhooks entrantes de Discord.", + "webhookLabel": "URL de Webhooks de Discord", "webhookPlaceholder": "", "webhookRequired": "" }, "telegram": { - "label": "", - "description": "", - "tokenLabel": "", - "tokenPlaceholder": "", - "chatIdLabel": "", - "chatIdPlaceholder": "", + "label": "Telegram", + "description": "Para habilitar las notificaciones de Telegram, cree un bot de Telegram utilizando BotFather, un bot oficial para crear y administrar bots de Telegram. Entonces, obtenga una clave API y un ID de chat y escríbalos aquí.", + "tokenLabel": "La clave de su bot", + "tokenPlaceholder": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", + "chatIdLabel": "El ID de su Chat", + "chatIdPlaceholder": "-1001234567890", "fieldsRequired": "" }, "webhook": { - "label": "", - "description": "", - "urlLabel": "", - "urlPlaceholder": "", + "label": "Webhooks", + "description": "Puede establecer un webhook personalizado para recibir notificaciones cuando ocurra un incidente.", + "urlLabel": "URL del webhook", + "urlPlaceholder": "https://your-server.com/webhook", "urlRequired": "" }, - "testNotificationDevelop": "", + "testNotificationDevelop": "Notificación de prueba 2", "integrationButton": "", "testSuccess": "", "testFailed": "", @@ -86,7 +85,8 @@ "networkError": "", "fallback": { "title": "", - "checks": [""] + "checks": [""], + "actionButton": "" }, "createButton": "", "createTitle": "", @@ -111,10 +111,10 @@ "failed": "" } }, - "testLocale": "", - "add": "", - "monitors": "", - "distributedUptimeStatusCreateStatusPage": "", + "testLocale": "testLocale", + "add": "Agregar", + "monitors": "monitores", + "distributedUptimeStatusCreateStatusPage": "página de estado", "distributedUptimeStatusCreateStatusPageAccess": "", "distributedUptimeStatusCreateStatusPageReady": "", "distributedUptimeStatusBasicInfoHeader": "", @@ -260,6 +260,7 @@ "notifyEmails": "", "seperateEmails": "", "checkFrequency": "", + "chooseGame": "", "matchMethod": "", "expectedValue": "", "deleteDialogTitle": "", @@ -331,7 +332,9 @@ "uploadSuccess": "", "validationFailed": "", "noFileSelected": "", - "fallbackPage": "" + "fallbackPage": "", + "invalidFileType": "", + "uploadFailed": "" }, "DeleteAccountTitle": "", "DeleteAccountButton": "", @@ -435,7 +438,8 @@ "http": "", "ping": "", "docker": "", - "port": "" + "port": "", + "game": "" }, "common": { "appName": "", @@ -537,6 +541,11 @@ "errors": { "incorrect": "" } + }, + "role": { + "errors": { + "min": "" + } } } }, @@ -675,7 +684,10 @@ "description": "", "webhookLabel": "", "webhookPlaceholder": "" - } + }, + "testNotification": "", + "dialogDeleteTitle": "", + "dialogDeleteConfirm": "" }, "notificationConfig": { "title": "", @@ -690,7 +702,6 @@ }, "advancedMatching": "", "sendTestNotifications": "", - "testNotificationsDisabled": "", "selectAll": "", "showAdminLoginLink": "", "logsPage": { @@ -698,7 +709,8 @@ "description": "", "tabs": { "queue": "", - "logs": "" + "logs": "", + "diagnostics": "" }, "toast": { "fetchLogsSuccess": "" @@ -753,7 +765,10 @@ "monitorActions": { "title": "", "import": "", - "export": "" + "export": "", + "deleteSuccess": "", + "deleteFailed": "", + "details": "" }, "settingsPage": { "aboutSettings": { @@ -785,7 +800,8 @@ "labelUser": "", "linkTransport": "", "placeholderUser": "", - "title": "" + "title": "", + "toastEmailRequiredFieldsError": "" }, "pageSpeedSettings": { "description": "", @@ -832,5 +848,124 @@ "selectEnabled": "", "title": "" } - } + }, + "statusPageCreate": { + "buttonSave": "" + }, + "incidentsOptionsHeaderFilterResolved": "", + "settingsSave": "", + "statusPageCreateAppearanceTitle": "", + "confirmPassword": "", + "monitorHooks": { + "failureAddDemoMonitors": "", + "successAddDemoMonitors": "" + }, + "settingsAppearance": "", + "settingsDisplayTimezone": "", + "settingsGeneralSettings": "", + "incidentsOptionsHeaderTotalIncidents": "", + "statusPage": { + "deleteSuccess": "", + "deleteFailed": "", + "createSuccess": "", + "updateSuccess": "", + "generalSettings": "", + "contents": "", + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "testNotificationsDisabled": "", + "incidentsTableResolvedAt": "", + "incidentsTableActionResolve": "", + "checkHooks": { + "failureResolveOne": "", + "failureResolveAll": "", + "failureResolveMonitor": "" + }, + "checkFormError": "", + "diagnosticsPage": { + "diagnosticDescription": "", + "statsDescription": "", + "gauges": { + "heapAllocationTitle": "", + "heapAllocationSubtitle": "", + "heapUsageTitle": "", + "heapUsageSubtitle": "", + "heapUtilizationTitle": "", + "heapUtilizationSubtitle": "", + "instantCpuUsageTitle": "", + "instantCpuUsageSubtitle": "" + }, + "stats": { + "eventLoopDelayTitle": "", + "uptimeTitle": "", + "usedHeapSizeTitle": "", + "totalHeapSizeTitle": "", + "osMemoryLimitTitle": "" + } + }, + "pageSpeedLighthouseAPI": "", + "time": { + "threeMinutes": "", + "fiveMinutes": "", + "tenMinutes": "", + "twentyMinutes": "", + "oneHour": "", + "oneDay": "", + "oneWeek": "" + }, + "general": { + "noOptionsFound": "" + }, + "infrastructureMonitor": { + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "maintenanceWindow": { + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "pageSpeed": { + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "uptimeMonitor": { + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "editUserPage": { + "form": { + "email": "", + "firstName": "", + "lastName": "", + "role": "", + "save": "" + }, + "table": { + "actionHeader": "", + "roleHeader": "" + }, + "title": "", + "toast": { + "successUserUpdate": "", + "validationErrors": "" + } + }, + "incidentsPageActionResolveMonitor": "", + "incidentsPageActionResolveAll": "" } diff --git a/client/src/locales/fi.json b/client/src/locales/fi.json index 90b30e925..cce87a89f 100644 --- a/client/src/locales/fi.json +++ b/client/src/locales/fi.json @@ -8,7 +8,6 @@ "settingsFailedToSave": "", "settingsStatsCleared": "", "settingsFailedToClearStats": "", - "settingsFailedToAddDemoMonitors": "", "settingsMonitorsDeleted": "", "settingsFailedToDeleteMonitors": "", "starPromptTitle": "", @@ -86,7 +85,8 @@ "networkError": "", "fallback": { "title": "", - "checks": [""] + "checks": [""], + "actionButton": "" }, "createButton": "", "createTitle": "", @@ -260,6 +260,7 @@ "notifyEmails": "", "seperateEmails": "", "checkFrequency": "", + "chooseGame": "", "matchMethod": "", "expectedValue": "", "deleteDialogTitle": "", @@ -331,7 +332,9 @@ "uploadSuccess": "", "validationFailed": "", "noFileSelected": "Ei valittua tiedostoa", - "fallbackPage": "" + "fallbackPage": "", + "invalidFileType": "", + "uploadFailed": "" }, "DeleteAccountTitle": "Poista tili", "DeleteAccountButton": "Poista tili", @@ -435,7 +438,8 @@ "http": "", "ping": "", "docker": "", - "port": "" + "port": "", + "game": "" }, "common": { "appName": "", @@ -537,6 +541,11 @@ "errors": { "incorrect": "" } + }, + "role": { + "errors": { + "min": "" + } } } }, @@ -675,7 +684,10 @@ "description": "", "webhookLabel": "", "webhookPlaceholder": "" - } + }, + "testNotification": "", + "dialogDeleteTitle": "", + "dialogDeleteConfirm": "" }, "notificationConfig": { "title": "", @@ -690,7 +702,6 @@ }, "advancedMatching": "", "sendTestNotifications": "", - "testNotificationsDisabled": "", "selectAll": "", "showAdminLoginLink": "", "logsPage": { @@ -698,7 +709,8 @@ "description": "", "tabs": { "queue": "", - "logs": "" + "logs": "", + "diagnostics": "" }, "toast": { "fetchLogsSuccess": "" @@ -753,7 +765,10 @@ "monitorActions": { "title": "", "import": "", - "export": "" + "export": "", + "deleteSuccess": "", + "deleteFailed": "", + "details": "" }, "settingsPage": { "aboutSettings": { @@ -785,7 +800,8 @@ "labelUser": "", "linkTransport": "", "placeholderUser": "", - "title": "" + "title": "", + "toastEmailRequiredFieldsError": "" }, "pageSpeedSettings": { "description": "", @@ -832,5 +848,124 @@ "selectEnabled": "", "title": "" } - } + }, + "statusPageCreate": { + "buttonSave": "" + }, + "incidentsOptionsHeaderFilterResolved": "", + "settingsSave": "", + "statusPageCreateAppearanceTitle": "", + "confirmPassword": "", + "monitorHooks": { + "failureAddDemoMonitors": "", + "successAddDemoMonitors": "" + }, + "settingsAppearance": "", + "settingsDisplayTimezone": "", + "settingsGeneralSettings": "", + "incidentsOptionsHeaderTotalIncidents": "", + "statusPage": { + "deleteSuccess": "", + "deleteFailed": "", + "createSuccess": "", + "updateSuccess": "", + "generalSettings": "", + "contents": "", + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "testNotificationsDisabled": "", + "incidentsTableResolvedAt": "", + "incidentsTableActionResolve": "", + "checkHooks": { + "failureResolveOne": "", + "failureResolveAll": "", + "failureResolveMonitor": "" + }, + "checkFormError": "", + "diagnosticsPage": { + "diagnosticDescription": "", + "statsDescription": "", + "gauges": { + "heapAllocationTitle": "", + "heapAllocationSubtitle": "", + "heapUsageTitle": "", + "heapUsageSubtitle": "", + "heapUtilizationTitle": "", + "heapUtilizationSubtitle": "", + "instantCpuUsageTitle": "", + "instantCpuUsageSubtitle": "" + }, + "stats": { + "eventLoopDelayTitle": "", + "uptimeTitle": "", + "usedHeapSizeTitle": "", + "totalHeapSizeTitle": "", + "osMemoryLimitTitle": "" + } + }, + "pageSpeedLighthouseAPI": "", + "time": { + "threeMinutes": "", + "fiveMinutes": "", + "tenMinutes": "", + "twentyMinutes": "", + "oneHour": "", + "oneDay": "", + "oneWeek": "" + }, + "general": { + "noOptionsFound": "" + }, + "infrastructureMonitor": { + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "maintenanceWindow": { + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "pageSpeed": { + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "uptimeMonitor": { + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "editUserPage": { + "form": { + "email": "", + "firstName": "", + "lastName": "", + "role": "", + "save": "" + }, + "table": { + "actionHeader": "", + "roleHeader": "" + }, + "title": "", + "toast": { + "successUserUpdate": "", + "validationErrors": "" + } + }, + "incidentsPageActionResolveMonitor": "", + "incidentsPageActionResolveAll": "" } diff --git a/client/src/locales/fr.json b/client/src/locales/fr.json index fd2173b7b..52d5d8c7e 100644 --- a/client/src/locales/fr.json +++ b/client/src/locales/fr.json @@ -1,79 +1,78 @@ { "submit": "Envoyer", "title": "Titre", - "distributedStatusHeaderText": "", - "distributedStatusSubHeaderText": "", + "distributedStatusHeaderText": "Couverture en temps réel, sur des appareils en temps réel", + "distributedStatusSubHeaderText": "Alimenté par des millions d'appareils à travers le monde, consultez les performances du système par région, pays ou ville", "settingsDisabled": "Désactivé", - "settingsSuccessSaved": "", - "settingsFailedToSave": "", - "settingsStatsCleared": "", - "settingsFailedToClearStats": "", - "settingsFailedToAddDemoMonitors": "", - "settingsMonitorsDeleted": "", - "settingsFailedToDeleteMonitors": "", - "starPromptTitle": "", - "starPromptDescription": "", - "https": "", - "http": "", - "monitor": "", + "settingsSuccessSaved": "Les paramètres ont bien été sauvegardés", + "settingsFailedToSave": "Les paramètres n'ont pas pu être sauvegardés", + "settingsStatsCleared": "Les statistiques ont été réinitialisées avec succès", + "settingsFailedToClearStats": "Les statistiques n'ont pas pu être réinitialisées", + "settingsMonitorsDeleted": "Tous les moniteurs ont bien été supprimés", + "settingsFailedToDeleteMonitors": "La suppression de tous les moniteurs a échoué", + "starPromptTitle": "Ajouter Checkmate aux favoris", + "starPromptDescription": "Voir les dernières versions et aider la communauté à grandir sur Github", + "https": "HTTPS", + "http": "HTTP", + "monitor": "Moniteur", "aboutus": "A propos de nous", - "now": "", + "now": "Maintenant", "delete": "Supprimer", - "configure": "", - "responseTime": "", - "ms": "", - "bar": "", + "configure": "Configurer", + "responseTime": "Temps de réponse", + "ms": "ms", + "bar": "Graphique", "area": "Zone", "country": "PAYS", "city": "VILLE", - "response": "", - "monitorStatusUp": "", - "monitorStatusDown": "", - "webhookSendSuccess": "", - "webhookSendError": "", - "webhookUnsupportedPlatform": "", + "response": "Réponse", + "monitorStatusUp": "Moniteur {name} ({url}) est désormais EN LIGNE et répond aux requêtes", + "monitorStatusDown": "Moniteur {name} ({url}) est désormais HORS LIGNE et ne répond plus", + "webhookSendSuccess": "La notification webhook a bien été envoyée", + "webhookSendError": "Une erreur s'est produite lors de l'envoi de la notification webhook sur {platform}", + "webhookUnsupportedPlatform": "La plateforme {platform} n'est pas supportée actuellement", "distributedRightCategoryTitle": "Moniteur", - "distributedStatusServerMonitors": "", - "distributedStatusServerMonitorsDescription": "", - "distributedUptimeCreateSelectURL": "", - "distributedUptimeCreateChecks": "", - "distributedUptimeCreateChecksDescription": "", + "distributedStatusServerMonitors": "Monitoring des serveurs", + "distributedStatusServerMonitorsDescription": "Statut des moniteurs relatifs aux serveurs", + "distributedUptimeCreateSelectURL": "Ici vous pouvez sélectionner l'URL de l'hôte et le type de moniteur", + "distributedUptimeCreateChecks": "Vérifications à effectuer", + "distributedUptimeCreateChecksDescription": "Vous pourrez ajouter ou supprimer des vérifications après l'ajout de votre site", "distributedUptimeCreateIncidentNotification": "Notifications d'incidents", - "distributedUptimeCreateIncidentDescription": "", + "distributedUptimeCreateIncidentDescription": "Quand il y a un incident, notifier les utilisateurs", "distributedUptimeCreateAdvancedSettings": "Paramètres avancés", - "distributedUptimeDetailsNoMonitorHistory": "", - "distributedUptimeDetailsStatusHeaderUptime": "", - "distributedUptimeDetailsStatusHeaderLastUpdate": "", + "distributedUptimeDetailsNoMonitorHistory": "Il n'y a pas encore d'historique de vérification pour ce moniteur", + "distributedUptimeDetailsStatusHeaderUptime": "Temps en ligne :", + "distributedUptimeDetailsStatusHeaderLastUpdate": "Dernière mise à jour", "notifications": { - "enableNotifications": "", + "enableNotifications": "Activer les notifications sur {{platform}}", "testNotification": "Notification de test", "addOrEditNotifications": "Ajouter ou éditer des notifications", "slack": { - "label": "", - "description": "", + "label": "Slack", + "description": "Pour activer les notifications Slack, créez une application Slack et activez les webhooks entrants. Ensuite, indiquer l'URL du webhook ici.", "webhookLabel": "URL du webhook", - "webhookPlaceholder": "", - "webhookRequired": "" + "webhookPlaceholder": "https://hooks.slack.com/services/...", + "webhookRequired": "L'URL du webhook Slack est requise" }, "discord": { - "label": "", - "description": "", + "label": "Discord", + "description": "Pour envoyer des données sur un canal Discord depuis Checkmate, utilisez l'intégration Webhook Discord.", "webhookLabel": "URL du webhook Discord", - "webhookPlaceholder": "", - "webhookRequired": "" + "webhookPlaceholder": "https://discord.com/api/webhooks/...", + "webhookRequired": "L'URL du webhook Discord est requise" }, "telegram": { - "label": "", - "description": "", - "tokenLabel": "", - "tokenPlaceholder": "", + "label": "Telegram", + "description": "Pour activer les notifications Telegram, créez un bot Telegram en utilisant BotFather, le robot officiel pour créer et gérer ses propres robots. Indiquez ensuite votre clé API et l'ID du chat ici.", + "tokenLabel": "Token du robot", + "tokenPlaceholder": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", "chatIdLabel": "Votre Chat ID", "chatIdPlaceholder": "-1001234567890", - "fieldsRequired": "" + "fieldsRequired": "Le token Telegram et l'ID du chat sont requis." }, "webhook": { - "label": "", - "description": "", + "label": "Webhooks", + "description": "Vous pouvez indiquer un webhook personnalisé pour recevoir des notifications lorsqu'un incident se produit.", "urlLabel": "URL du webhook", "urlPlaceholder": "https://votre-serveur.fr/webhook", "urlRequired": "L'URL du webhook est nécessaire" @@ -82,115 +81,120 @@ "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": "", + "unsupportedType": "Type de notification non supporté", + "networkError": "Une erreur réseau s'est produite", "fallback": { - "title": "", - "checks": [""] + "title": "canal de notification", + "checks": [ + "Alertez les équipes en cas de temps d'arrêt ou de problèmes de performances", + "Informez les ingénieurs lorsque des incidents se produisent", + "Tenez les administrateurs informés des changements apportés au système" + ], + "actionButton": "" }, - "createButton": "", - "createTitle": "", + "createButton": "Créer un canal de notification", + "createTitle": "Canal de notification", "create": { - "success": "", - "failed": "" + "success": "La notification a été créée avec succès", + "failed": "Une erreur s'est produite lors de la création de la notification" }, "fetch": { - "success": "", - "failed": "" + "success": "Les notifications ont été récupérées avec succès", + "failed": "Une erreur s'est produite lors de la récupération des notifications" }, "delete": { - "success": "", - "failed": "" + "success": "Notification supprimée avec succès", + "failed": "Une erreur s'est produite lors de la suppression de la notification" }, "edit": { - "success": "", - "failed": "" + "success": "Notification modifiée avec succès", + "failed": "Une erreur s'est produite lors de la modification de la notification" }, "test": { - "success": "", - "failed": "" + "success": "Notification de test envoyée avec succès", + "failed": "Une erreur s'est produite lors de l'envoi de la notification de test" } }, - "testLocale": "", + "testLocale": "testLocale", "add": "Ajouter", - "monitors": "", + "monitors": "moniteurs", "distributedUptimeStatusCreateStatusPage": "page de statut", "distributedUptimeStatusCreateStatusPageAccess": "Accès", - "distributedUptimeStatusCreateStatusPageReady": "", - "distributedUptimeStatusBasicInfoHeader": "", - "distributedUptimeStatusBasicInfoDescription": "", + "distributedUptimeStatusCreateStatusPageReady": "Si votre page de statut est prête, vous pouvez l'indiquer en \"Publiée\".", + "distributedUptimeStatusBasicInfoHeader": "Informations de base", + "distributedUptimeStatusBasicInfoDescription": "Définir le nom de la société et le sous domaine sur lequel votre page de statut pointe.", "distributedUptimeStatusLogoHeader": "Logo", - "distributedUptimeStatusLogoDescription": "", + "distributedUptimeStatusLogoDescription": "Ajoutez un logo pour votre page de statut", "distributedUptimeStatusLogoUploadButton": "Téléverser un logo", - "distributedUptimeStatusStandardMonitorsHeader": "", - "distributedUptimeStatusStandardMonitorsDescription": "", - "distributedUptimeStatusCreateYour": "", - "distributedUptimeStatusEditYour": "", - "distributedUptimeStatusPublishedLabel": "", + "distributedUptimeStatusStandardMonitorsHeader": "Moniteurs standards", + "distributedUptimeStatusStandardMonitorsDescription": "Ajouter un moniteur standard à votre page de statut", + "distributedUptimeStatusCreateYour": "Créer votre", + "distributedUptimeStatusEditYour": "Editer votre", + "distributedUptimeStatusPublishedLabel": "Publiée et visible publiquement", "distributedUptimeStatusCompanyNameLabel": "Nom de l'entreprise", "distributedUptimeStatusPageAddressLabel": "Adresse de votre page de statut", "distributedUptimeStatus30Days": "30 jours", "distributedUptimeStatus60Days": "60 jours", "distributedUptimeStatus90Days": "90 jours", - "distributedUptimeStatusPageNotSetUp": "", + "distributedUptimeStatusPageNotSetUp": "Aucune page de statut n'est mise en place.", "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": "", + "distributedUptimeStatusDevices": "Équipements", + "distributedUptimeStatusUpt": "UPT", "distributedUptimeStatusUptBurned": "", - "distributedUptimeStatusUptLogo": "", - "incidentsTableNoIncidents": "", + "distributedUptimeStatusUptLogo": "Logo Upt", + "incidentsTableNoIncidents": "Aucun incident enregistrée", "incidentsTablePaginationLabel": "incidents", - "incidentsTableMonitorName": "", - "incidentsTableStatus": "", + "incidentsTableMonitorName": "Nom du moniteur", + "incidentsTableStatus": "Statut", "incidentsTableDateTime": "Date et heure", - "incidentsTableStatusCode": "", + "incidentsTableStatusCode": "Code de statut", "incidentsTableMessage": "Message", - "incidentsOptionsHeader": "", - "incidentsOptionsHeaderFilterBy": "", - "incidentsOptionsHeaderFilterAll": "", - "incidentsOptionsHeaderFilterDown": "", - "incidentsOptionsHeaderFilterCannotResolve": "", - "incidentsOptionsHeaderShow": "", + "incidentsOptionsHeader": "Incidents pour :", + "incidentsOptionsHeaderFilterBy": "Filtrer par :", + "incidentsOptionsHeaderFilterAll": "Tout", + "incidentsOptionsHeaderFilterDown": "Hors ligne", + "incidentsOptionsHeaderFilterCannotResolve": "Impossible de résoudre", + "incidentsOptionsHeaderShow": "Voir:", "incidentsOptionsHeaderLastHour": "Dernière heure", "incidentsOptionsHeaderLastDay": "Dernier jour", "incidentsOptionsHeaderLastWeek": "Dernière semaine", - "incidentsOptionsPlaceholderAllServers": "", - "infrastructureCreateYour": "", - "infrastructureCreateGeneralSettingsDescription": "", - "infrastructureServerRequirement": "", - "infrastructureCustomizeAlerts": "", - "infrastructureAlertNotificationDescription": "", - "infrastructureCreateMonitor": "", + "incidentsOptionsPlaceholderAllServers": "Tous les serveurs", + "infrastructureCreateYour": "Créer votre", + "infrastructureCreateGeneralSettingsDescription": "Ici, vous pouvez sélectionner l'URL de l'hôte, ainsi que le nom familier et le secret d'autorisation pour vous connecter à l'agent sur le serveur.", + "infrastructureServerRequirement": "Le serveur que vous surveillez doit exécuter le", + "infrastructureCustomizeAlerts": "Personnaliser les alertes", + "infrastructureAlertNotificationDescription": "Envoyer une notification aux utilisateurs lorsque le seuil dépasse un pourcentage spécifié.", + "infrastructureCreateMonitor": "Créer un moniteur d'infrastructure", "infrastructureProtocol": "Protocole", "infrastructureServerUrlLabel": "URL du serveur", - "infrastructureDisplayNameLabel": "", - "infrastructureAuthorizationSecretLabel": "", - "gb": "", - "mb": "", - "mem": "", - "memoryUsage": "", + "infrastructureDisplayNameLabel": "Nom d'affichage", + "infrastructureAuthorizationSecretLabel": "Secret d'autorisation", + "gb": "GB", + "mb": "MB", + "mem": "Mém", + "memoryUsage": "Utilisation de la mémoire", "cpu": "CPU", - "cpuUsage": "", - "cpuTemperature": "", + "cpuUsage": "Utilisation CPU", + "cpuTemperature": "Température CPU", "diskUsage": "Utilisation du disque", - "used": "", - "total": "", - "cores": "", - "frequency": "", - "status": "", - "cpuPhysical": "", - "cpuLogical": "", + "used": "Utilisé", + "total": "Total", + "cores": "Coeurs", + "frequency": "Fréquence", + "status": "Statut", + "cpuPhysical": "CPU (Physique)", + "cpuLogical": "CPU (Logique)", "cpuFrequency": "Fréquence CPU", "avgCpuTemperature": "Température moyenne du CPU", - "memory": "", + "memory": "Mémoire", "disk": "Disque", - "uptime": "", - "os": "", - "host": "", + "uptime": "Temps en ligne", + "os": "OS", + "host": "Hôte", "actions": "Actions", "integrations": "Intégrations", "integrationsPrism": "Connecter Prism à votre service préféré.", @@ -202,211 +206,214 @@ "integrationsZapierInfo": "Envoyer tous les incidents à Zapier", "commonSave": "Sauvegarder", "createYour": "Créez votre", - "createMonitor": "", - "pause": "", - "resume": "", - "editing": "", + "createMonitor": "Ajouter un moniteur", + "pause": "Mettre en pause", + "resume": "Reprendre", + "editing": "Edition...", "url": "URL", "access": "Accès", "timezone": "Fuseau horaire", - "features": "", + "features": "Fonctionnalités", "administrator": "Administrateur ?", - "loginHere": "", + "loginHere": "Connexion", "displayName": "Nom d'affichage", "urlMonitor": "URL à suivre", - "portToMonitor": "", - "websiteMonitoring": "", - "websiteMonitoringDescription": "", - "pingMonitoring": "", - "pingMonitoringDescription": "", - "dockerContainerMonitoring": "", - "dockerContainerMonitoringDescription": "", - "portMonitoring": "", - "portMonitoringDescription": "", - "createMaintenanceWindow": "", + "portToMonitor": "Port du moniteur", + "websiteMonitoring": "Monitoring d'un site internet", + "websiteMonitoringDescription": "Utiliser HTTP(s) pour vérifier votre site ou votre API.", + "pingMonitoring": "Monitoring par ping", + "pingMonitoringDescription": "Vérifier si le serveur est disponible ou pas", + "dockerContainerMonitoring": "Monitoring d'un container Docker", + "dockerContainerMonitoringDescription": "Vérifier si votre container Docker fonctionne ou pas.", + "portMonitoring": "Monitoring d'un port", + "portMonitoringDescription": "Vérifier si votre port est ouvert ou non.", + "createMaintenanceWindow": "Créer une fenêtre de maintenance", "createMaintenance": "Créer une maintenance", - "editMaintenance": "", - "maintenanceWindowName": "", - "friendlyNameInput": "", - "friendlyNamePlaceholder": "", - "maintenanceRepeat": "", - "maintenance": "", - "duration": "", + "editMaintenance": "Modifier la maintenance", + "maintenanceWindowName": "Nom de la fenêtre de maintenance", + "friendlyNameInput": "Nom", + "friendlyNamePlaceholder": "Maintenance à __ : __ pendant ___ minutes", + "maintenanceRepeat": "Répéter la maintenance", + "maintenance": "maintenance", + "duration": "Durée", "addMonitors": "Ajouter des moniteurs", - "window": "", + "window": "fenêtre", "cancel": "Annuler", - "message": "", - "low": "", - "high": "", - "statusCode": "", + "message": "Message", + "low": "bas", + "high": "haut", + "statusCode": "Code de statut", "date&Time": "Date et heure", - "type": "", - "statusPageName": "", - "publicURL": "", - "repeat": "", - "edit": "", + "type": "Type", + "statusPageName": "Nom de la page de statut", + "publicURL": "URL publique", + "repeat": "Répéter", + "edit": "Modifier", "createA": "Créer un", - "remove": "", - "maintenanceWindowDescription": "", - "startTime": "", - "timeZoneInfo": "", - "monitorsToApply": "", - "nextWindow": "", - "notFoundButton": "", - "pageSpeedConfigureSettingsDescription": "", - "monitorDisplayName": "", + "remove": "Supprimer", + "maintenanceWindowDescription": "Vos pings ne seront pas envoyés pendant cette fenêtre de temps.", + "startTime": "Date de démarrage", + "timeZoneInfo": "Toutes les dates et heures sont dans le fuseau GMT+0", + "monitorsToApply": "Moniteurs sur lesquels appliquer la fenêtre de maintenance", + "nextWindow": "Prochaine fenêtre de maintenance", + "notFoundButton": "Aller au tableau de bord principal", + "pageSpeedConfigureSettingsDescription": "Vous pouvez sélectionner l'URL de hôte et le type de moniteur.", + "monitorDisplayName": "Nom du moniteur", "whenNewIncident": "Lors d'un nouvel incident,", - "notifySMS": "", - "notifyEmails": "", - "seperateEmails": "", + "notifySMS": "Notification par SMS (bientôt)", + "notifyEmails": "Envoyer également une notification par email à plusieurs adresses (bientôt).", + "seperateEmails": "Vous pouvez ajouter plusieurs adresses emails en les séparant par une virgule", "checkFrequency": "Vérifier la fréquence", - "matchMethod": "", - "expectedValue": "", - "deleteDialogTitle": "", - "deleteDialogDescription": "", - "pageSpeedMonitor": "", - "shown": "", + "chooseGame": "", + "matchMethod": "Méthode de rapprochement", + "expectedValue": "Valeur attendue", + "deleteDialogTitle": "Voulez-vous vraiment supprimer ce moniteur ?", + "deleteDialogDescription": "Une fois supprimé, le moniteur ne peut pas être récupéré.", + "pageSpeedMonitor": "Moniteur PageSpeed", + "shown": "Visibles", "ago": "depuis", - "companyName": "", - "pageSpeedDetailsPerformanceReport": "", - "pageSpeedDetailsPerformanceReportCalculator": "", + "companyName": "Nom de la société", + "pageSpeedDetailsPerformanceReport": "Les valeurs sont estimatives et peuvent varier.", + "pageSpeedDetailsPerformanceReportCalculator": "Voir le calculateur", "checkingEvery": "Vérification tous les", - "statusPageCreateSettings": "", - "basicInformation": "", - "statusPageCreateBasicInfoDescription": "", - "statusPageCreateSelectTimeZoneDescription": "", - "statusPageCreateAppearanceDescription": "", - "statusPageCreateSettingsCheckboxLabel": "", - "statusPageCreateBasicInfoStatusPageAddress": "", - "statusPageCreateTabsContent": "", - "statusPageCreateTabsContentDescription": "", - "statusPageCreateTabsContentFeaturesDescription": "", - "showCharts": "", - "showUptimePercentage": "", - "removeLogo": "", - "statusPageStatus": "", - "statusPageStatusContactAdmin": "", - "statusPageStatusNotPublic": "", - "statusPageStatusNoPage": "", - "statusPageStatusServiceStatus": "", - "deleteStatusPage": "", + "statusPageCreateSettings": "Si votre page de statut est prête, vous pouvez la rendre publique.", + "basicInformation": "Informations de base", + "statusPageCreateBasicInfoDescription": "Définir le nom de la société et le sous-domaine vers lequel pointe votre page de statut.", + "statusPageCreateSelectTimeZoneDescription": "Sélectionnez le fuseau horaire qui sera utilisé pour votre page de statut", + "statusPageCreateAppearanceDescription": "Définir le style par défaut de votre page de statut.", + "statusPageCreateSettingsCheckboxLabel": "Publiée et visible au public", + "statusPageCreateBasicInfoStatusPageAddress": "L'adresse de votre page de statut", + "statusPageCreateTabsContent": "Serveurs de votre page de statut", + "statusPageCreateTabsContentDescription": "Vous pouvez ajouter autant de moniteurs à votre page de statut que vous le souhaitez. Vous pouvez également les réordonner pour améliorer l'expérience utilisateur.", + "statusPageCreateTabsContentFeaturesDescription": "Voir plus de détails sur la page de statut", + "showCharts": "Voir les graphiques", + "showUptimePercentage": "Voir le pourcentage de temps en ligne", + "removeLogo": "Supprimer le logo", + "statusPageStatus": "Aucune page de statut publique n'est déployée", + "statusPageStatusContactAdmin": "Merci de contacter votre administrateur", + "statusPageStatusNotPublic": "Cette page de statut n'est pas piblique", + "statusPageStatusNoPage": "Il n'y a aucune page de statut ici", + "statusPageStatusServiceStatus": "Statut des services", + "deleteStatusPage": "Voulez-vous supprimer cette page de statut ?", "deleteStatusPageConfirm": "Oui, supprimer la page de statut", - "deleteStatusPageDescription": "", - "uptimeCreate": "", - "uptimeCreateJsonPath": "", - "uptimeCreateJsonPathQuery": "", - "maintenanceTableActionMenuDialogTitle": "", - "infrastructureEditYour": "", - "infrastructureEditMonitor": "", - "infrastructureMonitorCreated": "", - "infrastructureMonitorUpdated": "", - "errorInvalidTypeId": "", - "errorInvalidFieldId": "", - "inviteNoTokenFound": "", - "pageSpeedWarning": "", - "pageSpeedLearnMoreLink": "", - "pageSpeedAddApiKey": "", - "update": "", + "deleteStatusPageDescription": "Une fois supprimée, votre page de statut ne pourra plus être récupérée.", + "uptimeCreate": "La valeur attendue est utilisée pour comparer le résultat de la réponse et la correspondance détermine le statut du moniteur.", + "uptimeCreateJsonPath": "Cette expression sera évaluée par rapport aux données JSON de la réponse et le résultat sera utilisé pour la comparaison avec la valeur attendue. Voir", + "uptimeCreateJsonPathQuery": "pour la documentation sur le langage de requête.", + "maintenanceTableActionMenuDialogTitle": "Voulez-vous vraiment supprimer cette fenêtre de maintenance ?", + "infrastructureEditYour": "Modifier votre", + "infrastructureEditMonitor": "Sauvegarder le moniteur d'infrastructure", + "infrastructureMonitorCreated": "Le moniteur d'infrastructure a été créé avec succès !", + "infrastructureMonitorUpdated": "Le moniteur d'infrastructure a été modifié avec succès !", + "errorInvalidTypeId": "Le type de notification fourni est invalide", + "errorInvalidFieldId": "Le champ ID fourni est invalide", + "inviteNoTokenFound": "Aucun jeton d'invitation fourni", + "pageSpeedWarning": "Attention : vous n'avez pas encore ajouté de clé API Google PageSpeed. Sans celle-ci, le moniteur PageSpeed ne fonctionnera pas.", + "pageSpeedLearnMoreLink": "Cliquer ici", + "pageSpeedAddApiKey": "pour ajouter votre clé API.", + "update": "Mettre à jour", "invalidFileFormat": "Format de fichier non supporté !", "invalidFileSize": "Le fichier est trop volumineux !", "ClickUpload": "Cliquez pour téléverser", - "DragandDrop": "", - "MaxSize": "", - "SupportedFormats": "", - "FirstName": "", - "LastName": "", - "EmailDescriptionText": "", + "DragandDrop": "glisser et déposer", + "MaxSize": "Taille maximum", + "SupportedFormats": "Formats supportés", + "FirstName": "Prénom", + "LastName": "Nom", + "EmailDescriptionText": "Il s'agit de votre adresse email actuelle : elle ne peut pas être changée.", "YourPhoto": "Photo de profil", - "PhotoDescriptionText": "", - "save": "", - "DeleteDescriptionText": "", - "DeleteAccountWarning": "", + "PhotoDescriptionText": "La photo sera affichée sur votre page de profil.", + "save": "Enregistrer", + "DeleteDescriptionText": "Cela supprimera le compte et toutes les données associées du serveur. Cette opération est irréversible.", + "DeleteAccountWarning": "Supprimer votre compte signifie que vous ne pourrez pas vous reconnecter et que toutes vos données seront supprimés. Ceci est irréversible.", "DeleteWarningTitle": "Vous supprimez vraiment ce compte ?", "bulkImport": { - "title": "", - "selectFileTips": "", - "selectFileDescription": "", - "selectFile": "", - "parsingFailed": "", - "uploadSuccess": "", + "title": "Import en masse", + "selectFileTips": "Sélectionnez un fichier CSV pour l'upload", + "selectFileDescription": "Vous pouvez télécharger notre ou exemple.", + "selectFile": "Sélectionner un fichier", + "parsingFailed": "L'analyse du fichier a rencontré un problème", + "uploadSuccess": "Moniteurs créés avec succès !", "validationFailed": "Echec de la validation", - "noFileSelected": "", - "fallbackPage": "" + "noFileSelected": "Aucun fichier sélectionné", + "fallbackPage": "Importer un fichier pour ajouter une liste de serveurs en masse", + "invalidFileType": "Type de fichier invalide", + "uploadFailed": "Le téléchargement a échoué" }, "DeleteAccountTitle": "Supprimer le compte", "DeleteAccountButton": "Supprimer le compte", - "publicLink": "", - "maskedPageSpeedKeyPlaceholder": "", - "reset": "", - "ignoreTLSError": "", - "tlsErrorIgnored": "", - "ignoreTLSErrorDescription": "", + "publicLink": "Lien public", + "maskedPageSpeedKeyPlaceholder": "*************************************", + "reset": "Réinitialiser", + "ignoreTLSError": "Ignorer les erreurs TLS/SSL", + "tlsErrorIgnored": "Erreurs TLS/SSL ignorées", + "ignoreTLSErrorDescription": "Ignorer les erreurs TLS/SSL et continuer à vérifier l'accessibilité du site web", "createNew": "Créer un nouveau", "greeting": { - "prepend": "", + "prepend": "Salut !", "append": "L'après-midi est votre libre, rendons-le épique !", - "overview": "" + "overview": "Voici un aperçu de vos moniteurs {{type}}." }, "roles": { - "superAdmin": "", + "superAdmin": "Super admin", "admin": "Admin", - "teamMember": "", - "demoUser": "" + "teamMember": "Membre de l'équipe", + "demoUser": "Utilisateur de démonstration" }, "teamPanel": { "teamMembers": "Membres de l'équipe", "filter": { "all": "Tous", - "member": "" + "member": "Membre" }, - "inviteTeamMember": "", - "inviteNewTeamMember": "", - "inviteDescription": "", + "inviteTeamMember": "Inviter un membre de l'équipe", + "inviteNewTeamMember": "Inviter un nouveau membre d'équipe", + "inviteDescription": "Lorsque vous ajoutez un nouveau membre à l'équipe, il aura accès à tous les moniteurs.", "email": "Email", - "selectRole": "", + "selectRole": "Sélectionnez un rôle", "inviteLink": "Lien d'invitation", "cancel": "Annuler", - "noMembers": "", - "getToken": "", - "emailToken": "", + "noMembers": "Il n'y a aucun membre d'équipe avec ce rôle", + "getToken": "Récupérer un token", + "emailToken": "Token de l'email", "table": { - "name": "", + "name": "Nom", "email": "Email", - "role": "", + "role": "Rôle", "created": "Créée" } }, "monitorState": { - "paused": "", - "resumed": "", + "paused": "Pause", + "resumed": "Reprendre", "active": "Actif" }, "menu": { - "uptime": "", - "pagespeed": "", - "infrastructure": "", - "incidents": "", - "statusPages": "", - "maintenance": "", + "uptime": "Temps en ligne", + "pagespeed": "Vitesse", + "infrastructure": "Infrastructure", + "incidents": "Incidents", + "statusPages": "Pages de statut", + "maintenance": "Maintenance", "integrations": "Intégrations", - "settings": "", - "support": "", + "settings": "Paramètres", + "support": "Support", "discussions": "Discussions", - "docs": "", + "docs": "Documentation", "changelog": "Changelog", - "profile": "", - "password": "", - "team": "", + "profile": "Profil", + "password": "Mot de passe", + "team": "Équipe", "logOut": "Déconnexion", - "notifications": "", - "logs": "" + "notifications": "Notifications", + "logs": "Journaux" }, - "settingsEmailUser": "", - "state": "", - "statusBreadCrumbsStatusPages": "", - "statusBreadCrumbsDetails": "", + "settingsEmailUser": "Utilisateur SMTP - Utilisateur pour l'authentification, écrase l'adresse email si spécifiée", + "state": "État", + "statusBreadCrumbsStatusPages": "Pages de statut", + "statusBreadCrumbsDetails": "Détails", "commonSaving": "Sauvegarde...", - "navControls": "", + "navControls": "Contrôle", "incidentsPageTitle": "Incidents", "passwordPanel": { "passwordChangedSuccess": "Votre mot de passe a été modifié avec succès.", @@ -419,418 +426,550 @@ "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": "", + "emailSent": "Email envoyé avec succès", + "failedToSendEmail": "Une erreur s'est produite lors de l'envoi de l'email", + "settingsTestEmailSuccess": "Email de test envoyé avec succès", + "settingsTestEmailFailed": "Une erreur s'est produite lors de l'envoi de l'email de test", + "settingsTestEmailFailedWithReason": "Une erreur s'est produite lors de l'envoi de l'email de test : {{reason}}", "settingsTestEmailUnknownError": "Erreur inconnue", "statusMsg": { - "paused": "", - "up": "", - "down": "", - "pending": "" + "paused": "Le moniteur est en pause.", + "up": "Votre site est en ligne.", + "down": "Votre site est hors-ligne.", + "pending": "En attente..." }, "uptimeGeneralInstructions": { - "http": "", - "ping": "", - "docker": "", - "port": "" + "http": "Entrez l'URL ou l'IP du moniteur (par exemple https://exemple.fr ou 192.168.1.100) et ajoutez un nom familier qui apparaîtra sur le tableau de bord.", + "ping": "Entrez l'adresse IP ou le nom d'hôte à tester (par exemple, 192.168.1.100 ou exemple.fr) et ajoutez un nom familier qui apparaîtra sur le tableau de bord.", + "docker": "Entrer l'ID Docker du container. Les identifiants Docker doivent être les 64 caractères de l'ID Docker. Vous pouvez utiliser la commande docker inspect pour avoir l'ID complet.", + "port": "Entrez l'URL ou l'adresse IP du serveur, le numéro de port et un nom d'affichage familier qui apparaîtra sur le tableau de bord.", + "game": "" }, "common": { - "appName": "", - "monitoringAgentName": "", + "appName": "Checkmate", + "monitoringAgentName": "Capture", "buttons": { - "toggleTheme": "" + "toggleTheme": "Afficher le mode clair & sombre" }, "toasts": { - "networkError": "", - "checkConnection": "", - "unknownError": "" + "networkError": "Erreur de réseau", + "checkConnection": "Merci de vérifier votre connexion", + "unknownError": "Erreur inconnue" } }, "auth": { "common": { "navigation": { - "continue": "", - "back": "" + "continue": "Continuer", + "back": "Retour" }, "inputs": { "email": { - "label": "", - "placeholder": "", + "label": "Email", + "placeholder": "jean.dupont@domaine.fr", "errors": { - "empty": "", - "invalid": "" + "empty": "Pour continuer, merci d'indiquer votre adresse email", + "invalid": "Merci de vérifier la validité de l'adresse email saisie" } }, "password": { - "label": "", + "label": "Mot de passe", "rules": { "length": { - "beginning": "", - "highlighted": "" + "beginning": "Doit contenir au moins", + "highlighted": "Longueur de 8 caractères" }, "special": { - "beginning": "", - "highlighted": "" + "beginning": "Doit contenir au moins", + "highlighted": "un caractère spécial" }, "number": { - "beginning": "", - "highlighted": "" + "beginning": "Doit contenir au moins", + "highlighted": "un nombre" }, "uppercase": { - "beginning": "", - "highlighted": "" + "beginning": "Doit contenir au moins", + "highlighted": "un caractère majuscule" }, "lowercase": { - "beginning": "", - "highlighted": "" + "beginning": "Doit contenir au moins", + "highlighted": "un caractère miniscule" }, "match": { - "beginning": "", - "highlighted": "" + "beginning": "Confirmer le mot de passe et le mot de passe", + "highlighted": "doit valoir" } }, "errors": { - "empty": "", - "length": "", - "uppercase": "", - "lowercase": "", - "number": "", - "special": "", - "incorrect": "" + "empty": "Merci d'entrer votre mot de passe", + "length": "Le mot de passe doit faire 8 caractères minimum", + "uppercase": "Le mot de passe doit contenir au minimum une lettre majuscule", + "lowercase": "Le mot de passe doit contenir au minimum une lettre minuscule", + "number": "Le mot de passe doit contenir au minimum un chiffre", + "special": "Le mot de passe doit contenir au minimum un caractère spécial", + "incorrect": "Le mot de passe fourni est erroné" } }, "passwordConfirm": { - "label": "", - "placeholder": "", + "label": "Confirmer le mot de passe", + "placeholder": "Entrez à nouveau le mot de passe pour confirmer", "errors": { - "empty": "", - "different": "" + "empty": "Merci d'entrer votre mot de passe à nouveau pour le confirmer (aide avec la typo)", + "different": "Les mots de passe indiqués ne sont pas identiques, l'un d'entre eux est probablement mal écrit" } }, "firstName": { - "label": "", - "placeholder": "", + "label": "Prénom", + "placeholder": "Jean", "errors": { - "empty": "", - "length": "", - "pattern": "" + "empty": "Merci d'entrer votre prénom", + "length": "Le prénom doit être inférieur à 50 caractères", + "pattern": "Le prénom ne peut contenir que des lettres, des espaces, des apostrophes ou des tirets" } }, "lastName": { - "label": "", - "placeholder": "", + "label": "Nom", + "placeholder": "Dupont", "errors": { - "empty": "", - "length": "", - "pattern": "" + "empty": "Merci d'entrer votre nom", + "length": "Le nom doit être inférieur à 50 caractères", + "pattern": "Le nom ne peut contenir que des lettres, des espaces, des apostrophes ou des tirets" } } }, "errors": { - "validation": "" + "validation": "Une erreur s'est produite lors de la validation des données." }, "fields": { "password": { "errors": { - "incorrect": "" + "incorrect": "Le mot de passe fourni est erroné" + } + }, + "role": { + "errors": { + "min": "" } } } }, "login": { - "heading": "", + "heading": "Connexion", "subheadings": { - "stepOne": "", - "stepTwo": "" + "stepOne": "Entrez votre adresse email", + "stepTwo": "Entrez votre mot de passe" }, "links": { - "forgotPassword": "", - "register": "", - "forgotPasswordLink": "", - "registerLink": "" + "forgotPassword": "Mot de passe oublié ?", + "register": "Pas encore de compte ?", + "forgotPasswordLink": "Réinitialiser le mot de passe", + "registerLink": "Inscrivez vous ici" }, "toasts": { - "success": "", - "incorrectPassword": "" + "success": "Bon retour ! Vous êtes connecté avec succès.", + "incorrectPassword": "Mot de passe incorrect" }, "errors": { "password": { - "incorrect": "" + "incorrect": "Le mot de passe fourni est erroné" } } }, "registration": { "heading": { - "superAdmin": "", - "user": "" + "superAdmin": "Créer un super admin", + "user": "Inscription" }, "subheadings": { - "stepOne": "", - "stepTwo": "", - "stepThree": "" + "stepOne": "Entrez votre informations personnelles", + "stepTwo": "Entrez votre adresse email", + "stepThree": "Créez votre mot de passe" }, "description": { - "superAdmin": "", - "user": "" + "superAdmin": "Créer le compte \"SuperAdmin\" pour commencer", + "user": "inscription comme utilisateur et demander à l'administrateur principal l'accès aux moniteurs" }, "gettingStartedButton": { - "superAdmin": "", - "user": "" + "superAdmin": "Créer un compte super admin", + "user": "Inscription comme utilisateur normal" }, - "termsAndPolicies": "", + "termsAndPolicies": "En créant un compte, vous comprenez et acceptez les Conditions d'Utilisation et la Politique de Confidentialité.", "links": { - "login": "" + "login": "Déjà inscrit ? Connexion" }, "toasts": { - "success": "" + "success": "Bienvenue ! Votre compte a été créé avec succès." } }, "forgotPassword": { - "heading": "", + "heading": "Mot de passe oublié ?", "subheadings": { - "stepOne": "", - "stepTwo": "", - "stepThree": "", - "stepFour": "" + "stepOne": "Pas d'inquiétude, nous allons vous envoyer des informations pour réinitialiser votre mot de passe.", + "stepTwo": "Nous avons envoyer un lien de réinitialisation du mot de passe à l'email ", + "stepThree": "Votre nouveau mot de passe doit être différent des précédents.", + "stepFour": "Votre mot de passe a été réinitialisé avec succès. Cliquez ci-dessous pour vous connecter." }, "buttons": { - "openEmail": "", - "resetPassword": "" + "openEmail": "Ouvrir votre application email", + "resetPassword": "Réinitialiser le mot de passe" }, "imageAlts": { - "passwordKey": "", - "email": "", - "lock": "", - "passwordConfirm": "" + "passwordKey": "Icône du mot de passe", + "email": "Icône de l'email", + "lock": "Icône de verrouillage", + "passwordConfirm": "Icône de confirmation du mot de passe" }, "links": { - "login": "", - "resend": "" + "login": "Retourner à la page de connexion", + "resend": "Vous n'avez pas reçu l'email ? Cliquez ici pour le renvoyer" }, "toasts": { - "sent": "", - "emailNotFound": "", - "redirect": "", - "success": "", - "error": "" + "sent": "Instructions envoyées sur l'adresse email .", + "emailNotFound": "L'email n'a pas été trouvé.", + "redirect": "Redirection dans ...", + "success": "Votre mot de passe", + "error": "Une erreur s'est produite lors de la réinitialisation du mot de passe. Réessayez ultérieurement ou contactez le support." } } }, "errorPages": { "serverUnreachable": { "toasts": { - "reconnected": "", - "stillUnreachable": "" + "reconnected": "Connexion rétablie avec le serveur.", + "stillUnreachable": "Le serveur est toujours inaccessible. Réessayez ultérieurement." }, - "alertBox": "", - "description": "", + "alertBox": "Erreur de connexion au serveur", + "description": "Nous ne parvenons pas à nous connecter au serveur. Veuillez vérifier votre connexion Internet ou vérifier votre configuration de déploiement si le problème persiste.", "retryButton": { - "default": "", - "processing": "" + "default": "Réessayer la connexion", + "processing": "Connexion..." } } }, "createNotifications": { - "title": "", + "title": "Créer un canal de notification", "nameSettings": { - "title": "", - "description": "", - "nameLabel": "", - "namePlaceholder": "" + "title": "Nom", + "description": "Une description pour votre intégration.", + "nameLabel": "Nom", + "namePlaceholder": "e.g. notifications Slack" }, "typeSettings": { - "title": "", - "description": "", - "typeLabel": "" + "title": "Type", + "description": "Sélectionnez le type de canal de notification que vous souhaitez créer.", + "typeLabel": "Type" }, "emailSettings": { - "title": "", - "description": "", - "emailLabel": "", - "emailPlaceholder": "" + "title": "Email", + "description": "Adresse email du destinataire", + "emailLabel": "Adresse email", + "emailPlaceholder": "e.g. jean@exemple.fr" }, "slackSettings": { - "title": "", - "description": "", - "webhookLabel": "", - "webhookPlaceholder": "" + "title": "Slack", + "description": "Configurez votre webhook Slack ici", + "webhookLabel": "URL du webhook Slack", + "webhookPlaceholder": "https://hooks.slack.com/services/..." }, "pagerdutySettings": { - "title": "", - "description": "", - "integrationKeyLabel": "", - "integrationKeyPlaceholder": "" + "title": "PagerDuty", + "description": "Configurez l'intégration PagerDuty ici", + "integrationKeyLabel": "Clé d'intégration", + "integrationKeyPlaceholder": "1234567890" }, "discordSettings": { - "title": "", - "description": "", - "webhookLabel": "", - "webhookPlaceholder": "" + "title": "Discord", + "description": "Configurez votre webhook Discord ici", + "webhookLabel": "URL du webhook Discord", + "webhookPlaceholder": "https://your-server.com/webhook" }, "webhookSettings": { - "title": "", - "description": "", - "webhookLabel": "", - "webhookPlaceholder": "" - } + "title": "Webhook", + "description": "Configurez votre webhook ici", + "webhookLabel": "URL du webhook", + "webhookPlaceholder": "https://your-server.com/webhook" + }, + "testNotification": "Notification de test", + "dialogDeleteTitle": "Voulez-vous vraiment supprimer cette notification ?", + "dialogDeleteConfirm": "Supprimer" }, "notificationConfig": { - "title": "", - "description": "" + "title": "Notifications", + "description": "Sélectionnez les canaux de notification que vous souhaitez utiliser" }, "monitorStatus": { - "checkingEvery": "", - "withCaptureAgent": "", - "up": "", - "down": "", - "paused": "" + "checkingEvery": "Vérifier toutes les {{interval}}", + "withCaptureAgent": "avec l'agent Capture {{version}}", + "up": "en ligne", + "down": "hors ligne", + "paused": "en pause" }, - "advancedMatching": "", - "sendTestNotifications": "", - "testNotificationsDisabled": "", - "selectAll": "", - "showAdminLoginLink": "", + "advancedMatching": "Filtrage avancé", + "sendTestNotifications": "Envoyer une notification de test", + "selectAll": "Sélectionner tout", + "showAdminLoginLink": "Voir le lien \"Administrateur ? Connectez-vous ici\" sur la page de statut", "logsPage": { - "title": "", - "description": "", + "title": "Journaux", + "description": "Journaux systèmes - 1000 dernières lignes", "tabs": { - "queue": "", - "logs": "" + "queue": "File d'attente des tâches", + "logs": "Journaux des serveurs", + "diagnostics": "Diagnostic" }, "toast": { - "fetchLogsSuccess": "" + "fetchLogsSuccess": "Journaux récupérés avec succès" }, "logLevelSelect": { - "title": "", + "title": "Niveau de journalisation", "values": { - "all": "", - "info": "", - "warn": "", - "error": "", - "debug": "" + "all": "Tout", + "info": "Info", + "warn": "Attention", + "error": "Erreur", + "debug": "Debug" } } }, "queuePage": { - "title": "", - "refreshButton": "", - "flushButton": "", + "title": "File d'attente", + "refreshButton": "Rafraîchir", + "flushButton": "Vider la file d'attente", "jobTable": { - "title": "", - "idHeader": "", - "urlHeader": "", - "typeHeader": "", - "activeHeader": "", - "lockedAtHeader": "", - "runCountHeader": "", - "failCountHeader": "", - "lastRunHeader": "", - "lastFinishedAtHeader": "", - "lastRunTookHeader": "" + "title": "Tâches actuellement dans la file d'attente", + "idHeader": "ID du moniteur", + "urlHeader": "URL", + "typeHeader": "Type", + "activeHeader": "Actif", + "lockedAtHeader": "Verrouillé le", + "runCountHeader": "Nombre de tentatives", + "failCountHeader": "Nombre d'échecs", + "lastRunHeader": "Dernière tentative le", + "lastFinishedAtHeader": "Dernière finalisation à", + "lastRunTookHeader": "Dernière exécution effectuée" }, "metricsTable": { - "title": "", - "metricHeader": "", - "valueHeader": "" + "title": "Métriques des files", + "metricHeader": "Métrique", + "valueHeader": "Valeur" }, "failedJobTable": { - "title": "", - "monitorIdHeader": "", - "monitorUrlHeader": "", - "failCountHeader": "", - "failedAtHeader": "", - "failReasonHeader": "" + "title": "Tâches en échec", + "monitorIdHeader": "ID du moniteur", + "monitorUrlHeader": "URL du moniteur", + "failCountHeader": "Nombre de tentatives échouées", + "failedAtHeader": "Dernier échec à", + "failReasonHeader": "Raison de l'échec" } }, "export": { - "title": "", - "success": "", - "failed": "" + "title": "Export des moniteurs", + "success": "Les moniteurs ont été exportés avec succès !", + "failed": "Une erreur s'est produite lors de l'export des moniteurs" }, "monitorActions": { - "title": "", - "import": "", - "export": "" + "title": "Export/Import", + "import": "Importer des moniteurs", + "export": "Exporter des moniteurs", + "deleteSuccess": "Le moniteur a été supprimé avec succès", + "deleteFailed": "Une erreur s'est produite lors de la suppression du moniteur", + "details": "Détails" }, "settingsPage": { "aboutSettings": { - "labelDevelopedBy": "", - "labelVersion": "", - "title": "" + "labelDevelopedBy": "Développé par Bluewave Labs", + "labelVersion": "Version", + "title": "A propos" }, "demoMonitorsSettings": { - "buttonAddMonitors": "", - "description": "", - "title": "" + "buttonAddMonitors": "Ajouter un moniteur de démonstration", + "description": "Ajouter un moniteur à des fins de démonstration.", + "title": "Moniteurs de démonstration" }, "emailSettings": { - "buttonSendTestEmail": "", - "description": "", - "descriptionTransport": "", - "labelAddress": "", - "labelConnectionHost": "", - "labelHost": "", - "labelIgnoreTLS": "", - "labelPassword": "", - "labelPasswordSet": "", - "labelPool": "", - "labelPort": "", - "labelRejectUnauthorized": "", - "labelRequireTLS": "", - "labelSecure": "", - "labelTLSServername": "", - "labelUser": "", - "linkTransport": "", - "placeholderUser": "", - "title": "" + "buttonSendTestEmail": "Envoyer un mail de test", + "description": "Configurez les paramètres emails pour le système. Il s'agit du système utilisé pour les notifications & alertes.", + "descriptionTransport": "Ceci créé un transport SMTP pour NodeMailer", + "labelAddress": "Adresse email - utilisée pour la connexion", + "labelConnectionHost": "Hôte SMTP - Nom d'hôte à utiliser dans l'introduction HELO/EHLO", + "labelHost": "Hôte SMTP - Nom d'hôte ou IP sur lequel se connecter", + "labelIgnoreTLS": "Désactiver STARTTLS : ne pas utiliser TLS même si le serveur le supporte", + "labelPassword": "Mot de passe SMTP - Mot de passe requis pour l'authentification", + "labelPasswordSet": "Le mot de passe est renseigné. Réinitialisez le pour le changer.", + "labelPool": "Activer le pool de connexion SMTP afin de réutiliser les connexions existantes pour améliorer la performance", + "labelPort": "Port SMTP - Port auquel se connecter", + "labelRejectUnauthorized": "Rejeter les certificats non valides : rejeter les connexions avec des certificats auto-signés ou non fiables", + "labelRequireTLS": "Forcer STARTTLS : nécessite la mise à niveau TLS, échouer si ce n'est pas supporté", + "labelSecure": "Utiliser le SSL (recommandé) : chiffrer la connexion SSL/TLS", + "labelTLSServername": "Nom du serveur TLS - Nom d'hôte facultatif pour la validation TLS lorsque l'hôte est une adresse IP", + "labelUser": "Utilisateur SMTP - Nom d'utilisateur pour l'authentification, remplace l'adresse email si spécifié", + "linkTransport": "Voir les spécifications ici", + "placeholderUser": "Laisser vide si non requis", + "title": "Email", + "toastEmailRequiredFieldsError": "L'adresse email, l'hôte, le port et le mot de passe sont obligatoires." }, "pageSpeedSettings": { - "description": "", - "labelApiKeySet": "", - "labelApiKey": "", - "title": "" + "description": "Entrez votre clé API Google PageSpeed pour activer la surveillance Google PageSpeed. Cliquez sur Réinitialiser pour mettre à jour la clé.", + "labelApiKeySet": "La clé API est générée. Cliquez sur \"Réinitialiser\" pour la modifier.", + "labelApiKey": "Clé API PageSpeed", + "title": "Clé API Google PageSpeed" }, - "saveButtonLabel": "", + "saveButtonLabel": "Sauvegarder", "statsSettings": { - "clearAllStatsButton": "", - "clearAllStatsDescription": "", - "clearAllStatsDialogConfirm": "", - "clearAllStatsDialogDescription": "", - "clearAllStatsDialogTitle": "", - "description": "", - "labelTTL": "", - "labelTTLOptional": "", - "title": "" + "clearAllStatsButton": "Supprimer toutes les statistiques", + "clearAllStatsDescription": "Supprimer toutes les statistiques. Cette action est irréversible.", + "clearAllStatsDialogConfirm": "Oui, supprimer toutes les statistiques", + "clearAllStatsDialogDescription": "Une fois supprimés, l'historique et les statistiques du moniteur ne peuvent plus être récupérés.", + "clearAllStatsDialogTitle": "Voulez-vous supprimer toutes les statistiques ?", + "description": "Définissez la durée pendant laquelle vous souhaitez conserver les données historiques. Vous pouvez également effacer toutes les données existantes.", + "labelTTL": "Le nombre de jours dont vous souhaitez garder l'historique.", + "labelTTLOptional": "0 pour une durée infinie", + "title": "Historique du moniteur" }, "systemResetSettings": { - "buttonRemoveAllMonitors": "", - "description": "", - "dialogConfirm": "", - "dialogDescription": "", - "dialogTitle": "", - "title": "" + "buttonRemoveAllMonitors": "Supprimer tous les moniteurs", + "description": "Supprimer toutes les moniteurs du système.", + "dialogConfirm": "Oui, supprimer tous les moniteurs", + "dialogDescription": "Une fois supprimés, les moniteurs ne peuvent plus être récupérés.", + "dialogTitle": "Voulez-vous supprimer tous les moniteurs ?", + "title": "Réinitialisation du système" }, "timezoneSettings": { - "description": "", - "label": "", - "title": "" + "description": "Sélectionnez le fuseau horaire utilisé pour afficher les dates et les heures dans l'application.", + "label": "Fuseau horaire d'affichage", + "title": "Fuseau horaire d'affichage" }, - "title": "", + "title": "Paramètres", "uiSettings": { - "description": "", - "labelLanguage": "", - "labelTheme": "", - "title": "" + "description": "Changer entre le mode clair et sombre, ou changer le langage de l'interface utilisateur.", + "labelLanguage": "Langage", + "labelTheme": "Thème", + "title": "Apparence" }, "urlSettings": { - "description": "", - "label": "", - "selectDisabled": "", - "selectEnabled": "", - "title": "" + "description": "Affichez l'adresse IP ou l'URL du moniteur sur la page de statut publique. Si cette option est désactivée, seul le nom du moniteur sera affiché afin de protéger les informations sensibles.", + "label": "Afficher l'IP/URL sur la page de statut", + "selectDisabled": "Désactivé", + "selectEnabled": "Activé", + "title": "IP/URL du moniteur sur la page de statut" } - } + }, + "statusPageCreate": { + "buttonSave": "Sauvegarder" + }, + "incidentsOptionsHeaderFilterResolved": "Résolu", + "settingsSave": "Sauvegarder", + "statusPageCreateAppearanceTitle": "Apparence", + "confirmPassword": "Confirmer le mot de passe", + "monitorHooks": { + "failureAddDemoMonitors": "Une erreur s'est produite lors de l'ajout des moniteurs de démonstration", + "successAddDemoMonitors": "Les moniteurs de démonstration ont bien été ajoutés" + }, + "settingsAppearance": "Apparance", + "settingsDisplayTimezone": "Fuseau horaire d'affichage", + "settingsGeneralSettings": "Paramètres généraux", + "incidentsOptionsHeaderTotalIncidents": "Nombre d'incidents", + "statusPage": { + "deleteSuccess": "La page de statut a été supprimée avec succès", + "deleteFailed": "Une erreur s'est produite lors de l'ajout de la page de statut", + "createSuccess": "La page de statut a été ajoutée avec succès", + "updateSuccess": "La page de statut a été modifiée avec succès", + "generalSettings": "Paramètres généraux", + "contents": "Contenus", + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "testNotificationsDisabled": "Il n'y aucune notification paramétrée pour ce moniteur. Vous pouvez en ajouter une en cliquant sur le bouton 'Configurer'", + "incidentsTableResolvedAt": "Résolu le", + "incidentsTableActionResolve": "Résoudre", + "checkHooks": { + "failureResolveOne": "Une erreur s'est produite lors de la résolution de l'incident.", + "failureResolveAll": "Une erreur s'est produite lors de la résolution des incidents.", + "failureResolveMonitor": "" + }, + "checkFormError": "Merci de vérifier les erreurs du formulaire", + "diagnosticsPage": { + "diagnosticDescription": "Diagnostic du système", + "statsDescription": "Statistiques du système", + "gauges": { + "heapAllocationTitle": "Allocation du tas", + "heapAllocationSubtitle": "% de mémoire disponible", + "heapUsageTitle": "Utilisation du tas", + "heapUsageSubtitle": "% de mémoire disponible", + "heapUtilizationTitle": "Utilisation du tas", + "heapUtilizationSubtitle": "% d'allocation", + "instantCpuUsageTitle": "Utilisation instantanée du CPU", + "instantCpuUsageSubtitle": "% de CPU utilisé" + }, + "stats": { + "eventLoopDelayTitle": "Délai de la boucle d'évènements", + "uptimeTitle": "Temps en ligne", + "usedHeapSizeTitle": "Taille du tas utilisé", + "totalHeapSizeTitle": "Taille total du tas", + "osMemoryLimitTitle": "Limite de mémoire de l'OS" + } + }, + "pageSpeedLighthouseAPI": "Utilisez l'API Lighthouse PageSpeed pour suivre votre site web", + "time": { + "threeMinutes": "3 minutes", + "fiveMinutes": "5 minutes", + "tenMinutes": "10 minutes", + "twentyMinutes": "20 minutes", + "oneHour": "1 heure", + "oneDay": "1 jour", + "oneWeek": "1 semaine" + }, + "general": { + "noOptionsFound": "" + }, + "infrastructureMonitor": { + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "maintenanceWindow": { + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "pageSpeed": { + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "uptimeMonitor": { + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "editUserPage": { + "form": { + "email": "", + "firstName": "", + "lastName": "", + "role": "", + "save": "" + }, + "table": { + "actionHeader": "", + "roleHeader": "" + }, + "title": "", + "toast": { + "successUserUpdate": "", + "validationErrors": "" + } + }, + "incidentsPageActionResolveMonitor": "", + "incidentsPageActionResolveAll": "" } diff --git a/client/src/locales/ja.json b/client/src/locales/ja.json new file mode 100644 index 000000000..80245d64b --- /dev/null +++ b/client/src/locales/ja.json @@ -0,0 +1,975 @@ +{ + "submit": "送信", + "title": "タイトル", + "distributedStatusHeaderText": "リアルタイム、実デバイス対応", + "distributedStatusSubHeaderText": "世界中の数百万台のデバイスを活用し、グローバル地域、国、都市別にシステムパフォーマンスを表示", + "settingsDisabled": "無効", + "settingsSuccessSaved": "設定が正常に保存されました", + "settingsFailedToSave": "設定の保存に失敗しました", + "settingsStatsCleared": "統計が正常にクリアされました", + "settingsFailedToClearStats": "統計のクリアに失敗しました", + "settingsMonitorsDeleted": "すべてのモニターが正常に削除されました", + "settingsFailedToDeleteMonitors": "すべてのモニターの削除に失敗しました", + "starPromptTitle": "Checkmateにスター", + "starPromptDescription": "GitHubで最新リリースを確認し、コミュニティの成長を支援", + "https": "HTTPS", + "http": "HTTP", + "monitor": "モニター", + "aboutus": "私たちについて", + "now": "今", + "delete": "削除", + "configure": "設定", + "responseTime": "レスポンス時間", + "ms": "ミリ秒", + "bar": "バー", + "area": "エリア", + "country": "国", + "city": "都市", + "response": "レスポンス", + "monitorStatusUp": "モニター {name} ({url}) がアップして応答中です", + "monitorStatusDown": "モニター {name} ({url}) がダウンして応答していません", + "webhookSendSuccess": "Webhook通知が正常に送信されました", + "webhookSendError": "{platform}へのWebhook通知送信エラー", + "webhookUnsupportedPlatform": "サポートされていないプラットフォーム: {platform}", + "distributedRightCategoryTitle": "モニター", + "distributedStatusServerMonitors": "サーバーモニター", + "distributedStatusServerMonitorsDescription": "関連サーバーのステータスを監視", + "distributedUptimeCreateSelectURL": "ここでホストのURL、モニタータイプを選択できます。", + "distributedUptimeCreateChecks": "実行するチェック", + "distributedUptimeCreateChecksDescription": "サイト追加後はいつでもチェックを追加・削除できます。", + "distributedUptimeCreateIncidentNotification": "インシデント通知", + "distributedUptimeCreateIncidentDescription": "インシデント発生時、ユーザーに通知。", + "distributedUptimeCreateAdvancedSettings": "詳細設定", + "distributedUptimeDetailsNoMonitorHistory": "このモニターのチェック履歴はまだありません。", + "distributedUptimeDetailsStatusHeaderUptime": "稼働時間:", + "distributedUptimeDetailsStatusHeaderLastUpdate": "最終更新", + "notifications": { + "enableNotifications": "{{platform}}通知を有効化", + "testNotification": "テスト通知", + "addOrEditNotifications": "通知を追加または編集", + "slack": { + "label": "Slack", + "description": "Slack通知を有効にするには、Slackアプリを作成し、incoming webhookを有効にします。その後、ここでWebhook URLを提供するだけです。", + "webhookLabel": "Webhook URL", + "webhookPlaceholder": "https://hooks.slack.com/services/...", + "webhookRequired": "Slack Webhook URLが必要です" + }, + "discord": { + "label": "Discord", + "description": "Webhookを使用してDiscord通知経由でCheckmateからDiscordチャンネルにデータを送信するには、DiscordのIncoming Webhook機能を使用できます。", + "webhookLabel": "Discord Webhook URL", + "webhookPlaceholder": "https://discord.com/api/webhooks/...", + "webhookRequired": "Discord Webhook URLが必要です" + }, + "telegram": { + "label": "Telegram", + "description": "Telegram通知を有効にするには、Telegramボットを作成・管理するための公式ボットであるBotFatherを使用してTelegramボットを作成します。次に、APIトークンとチャットIDを取得してここに記入します。", + "tokenLabel": "ボットトークン", + "tokenPlaceholder": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", + "chatIdLabel": "チャットID", + "chatIdPlaceholder": "-1001234567890", + "fieldsRequired": "TelegramトークンとチャットIDが必要です" + }, + "webhook": { + "label": "Webhook", + "description": "インシデント発生時に通知を受信するためのカスタムWebhookを設定できます。", + "urlLabel": "Webhook URL", + "urlPlaceholder": "https://your-server.com/webhook", + "urlRequired": "Webhook URLが必要です" + }, + "testNotificationDevelop": "テスト通知2", + "integrationButton": "通知統合", + "testSuccess": "テスト通知が正常に送信されました!", + "testFailed": "テスト通知の送信に失敗しました", + "unsupportedType": "サポートされていない通知タイプ", + "networkError": "ネットワークエラーが発生しました", + "fallback": { + "title": "通知チャンネル", + "checks": [ + "ダウンタイムやパフォーマンス問題についてチームにアラート", + "インシデント発生時にエンジニアに通知", + "システム変更について管理者に情報提供" + ], + "actionButton": "" + }, + "createButton": "通知チャンネルを作成", + "createTitle": "通知チャンネル", + "create": { + "success": "通知が正常に作成されました", + "failed": "通知の作成に失敗しました" + }, + "fetch": { + "success": "通知が正常に取得されました", + "failed": "通知の取得に失敗しました" + }, + "delete": { + "success": "通知が正常に削除されました", + "failed": "通知の削除に失敗しました" + }, + "edit": { + "success": "通知が正常に更新されました", + "failed": "通知の更新に失敗しました" + }, + "test": { + "success": "テスト通知が正常に送信されました", + "failed": "テスト通知の送信に失敗しました" + } + }, + "testLocale": "テストロケール", + "add": "追加", + "monitors": "モニター", + "distributedUptimeStatusCreateStatusPage": "ステータスページ", + "distributedUptimeStatusCreateStatusPageAccess": "アクセス", + "distributedUptimeStatusCreateStatusPageReady": "ステータスページの準備ができたら、公開済みとしてマークできます。", + "distributedUptimeStatusBasicInfoHeader": "基本情報", + "distributedUptimeStatusBasicInfoDescription": "会社名とステータスページが指すサブドメインを定義。", + "distributedUptimeStatusLogoHeader": "ロゴ", + "distributedUptimeStatusLogoDescription": "ステータスページ用のロゴをアップロード", + "distributedUptimeStatusLogoUploadButton": "ロゴをアップロード", + "distributedUptimeStatusStandardMonitorsHeader": "標準モニター", + "distributedUptimeStatusStandardMonitorsDescription": "標準モニターをステータスページに添付。", + "distributedUptimeStatusCreateYour": "作成する", + "distributedUptimeStatusEditYour": "編集する", + "distributedUptimeStatusPublishedLabel": "公開済みで一般に表示", + "distributedUptimeStatusCompanyNameLabel": "会社名", + "distributedUptimeStatusPageAddressLabel": "ステータスページアドレス", + "distributedUptimeStatus30Days": "30日", + "distributedUptimeStatus60Days": "60日", + "distributedUptimeStatus90Days": "90日", + "distributedUptimeStatusPageNotSetUp": "ステータスページが設定されていません。", + "distributedUptimeStatusContactAdmin": "管理者にお問い合わせください", + "distributedUptimeStatusPageNotPublic": "このステータスページは非公開です。", + "distributedUptimeStatusPageDeleteDialog": "このステータスページを削除しますか?", + "distributedUptimeStatusPageDeleteConfirm": "はい、ステータスページを削除", + "distributedUptimeStatusPageDeleteDescription": "削除されると、ステータスページは復元できません。", + "distributedUptimeStatusDevices": "デバイス", + "distributedUptimeStatusUpt": "UPT", + "distributedUptimeStatusUptBurned": "UPT消費", + "distributedUptimeStatusUptLogo": "Uptロゴ", + "incidentsTableNoIncidents": "記録されたインシデントはありません", + "incidentsTablePaginationLabel": "インシデント", + "incidentsTableMonitorName": "モニター名", + "incidentsTableStatus": "ステータス", + "incidentsTableDateTime": "日付と時刻", + "incidentsTableStatusCode": "ステータスコード", + "incidentsTableMessage": "メッセージ", + "incidentsOptionsHeader": "インシデント対象:", + "incidentsOptionsHeaderFilterBy": "フィルター:", + "incidentsOptionsHeaderFilterAll": "すべて", + "incidentsOptionsHeaderFilterDown": "ダウン", + "incidentsOptionsHeaderFilterCannotResolve": "解決不可", + "incidentsOptionsHeaderShow": "表示:", + "incidentsOptionsHeaderLastHour": "最終時間", + "incidentsOptionsHeaderLastDay": "最終日", + "incidentsOptionsHeaderLastWeek": "最終週", + "incidentsOptionsPlaceholderAllServers": "すべてのサーバー", + "infrastructureCreateYour": "作成する", + "infrastructureCreateGeneralSettingsDescription": "ここでホストのURL、フレンドリーネーム、サーバーエージェントに接続するための認証シークレットを選択できます。", + "infrastructureServerRequirement": "監視中のサーバーで実行されている必要があります", + "infrastructureCustomizeAlerts": "アラートをカスタマイズ", + "infrastructureAlertNotificationDescription": "しきい値が指定された割合を超えた場合にユーザーに通知を送信。", + "infrastructureCreateMonitor": "インフラストラクチャモニターを作成", + "infrastructureProtocol": "プロトコル", + "infrastructureServerUrlLabel": "サーバーURL", + "infrastructureDisplayNameLabel": "表示名", + "infrastructureAuthorizationSecretLabel": "認証シークレット", + "gb": "GB", + "mb": "MB", + "mem": "メモリ", + "memoryUsage": "メモリ使用率", + "cpu": "CPU", + "cpuUsage": "CPU使用率", + "cpuTemperature": "CPU温度", + "diskUsage": "ディスク使用率", + "used": "使用済み", + "total": "合計", + "cores": "コア", + "frequency": "頻度", + "status": "ステータス", + "cpuPhysical": "CPU(物理)", + "cpuLogical": "CPU(論理)", + "cpuFrequency": "CPU周波数", + "avgCpuTemperature": "平均CPU温度", + "memory": "メモリ", + "disk": "ディスク", + "uptime": "稼働時間", + "os": "OS", + "host": "ホスト", + "actions": "アクション", + "integrations": "統合", + "integrationsPrism": "Prismをお気に入りのサービスに接続。", + "integrationsSlack": "Slack", + "integrationsSlackInfo": "Slackと接続してチャンネルでインシデントを表示", + "integrationsDiscord": "Discord", + "integrationsDiscordInfo": "Discordと接続してチャンネルでインシデントを直接表示", + "integrationsZapier": "Zapier", + "integrationsZapierInfo": "すべてのインシデントをZapierに送信し、どこでも表示", + "commonSave": "保存", + "createYour": "作成する", + "createMonitor": "モニターを作成", + "pause": "一時停止", + "resume": "再開", + "editing": "編集中...", + "url": "URL", + "access": "アクセス", + "timezone": "タイムゾーン", + "features": "機能", + "administrator": "管理者?", + "loginHere": "こちらでログイン", + "displayName": "表示名", + "urlMonitor": "監視するURL", + "portToMonitor": "監視するポート", + "websiteMonitoring": "ウェブサイト監視", + "websiteMonitoringDescription": "HTTP(s)を使用してウェブサイトやAPIエンドポイントを監視。", + "pingMonitoring": "Ping監視", + "pingMonitoringDescription": "サーバーが利用可能かどうかをチェック。", + "dockerContainerMonitoring": "Dockerコンテナ監視", + "dockerContainerMonitoringDescription": "Dockerコンテナが実行中かどうかをチェック。", + "portMonitoring": "ポート監視", + "portMonitoringDescription": "ポートが開いているかどうかをチェック。", + "createMaintenanceWindow": "メンテナンスウィンドウを作成", + "createMaintenance": "メンテナンスを作成", + "editMaintenance": "メンテナンスを編集", + "maintenanceWindowName": "メンテナンスウィンドウ名", + "friendlyNameInput": "フレンドリーネーム", + "friendlyNamePlaceholder": "__:__で___分間のメンテナンス", + "maintenanceRepeat": "メンテナンス繰り返し", + "maintenance": "メンテナンス", + "duration": "期間", + "addMonitors": "モニターを追加", + "window": "ウィンドウ", + "cancel": "キャンセル", + "message": "メッセージ", + "low": "低", + "high": "高", + "statusCode": "ステータスコード", + "date&Time": "日付と時刻", + "type": "タイプ", + "statusPageName": "ステータスページ名", + "publicURL": "公開URL", + "repeat": "繰り返し", + "edit": "編集", + "createA": "作成する", + "remove": "削除", + "maintenanceWindowDescription": "この時間帯はpingが送信されません", + "startTime": "開始時刻", + "timeZoneInfo": "すべての日付と時刻はGMT+0タイムゾーンです。", + "monitorsToApply": "メンテナンスウィンドウを適用するモニター", + "nextWindow": "次のウィンドウ", + "notFoundButton": "メインダッシュボードに移動", + "pageSpeedConfigureSettingsDescription": "ここでホストのURL、モニタータイプを選択できます。", + "monitorDisplayName": "モニター表示名", + "whenNewIncident": "新しいインシデントが発生した場合、", + "notifySMS": "SMS経由で通知(近日公開)", + "notifyEmails": "複数のアドレスにメールでも通知(近日公開)", + "seperateEmails": "複数のメールはカンマで区切ることができます", + "checkFrequency": "チェック頻度", + "chooseGame": "", + "matchMethod": "マッチ方法", + "expectedValue": "期待値", + "deleteDialogTitle": "本当にこのモニターを削除しますか?", + "deleteDialogDescription": "削除されると、このモニターは復元できません。", + "pageSpeedMonitor": "PageSpeedモニター", + "shown": "表示", + "ago": "前", + "companyName": "会社名", + "pageSpeedDetailsPerformanceReport": "値は推定値で変動する可能性があります。", + "pageSpeedDetailsPerformanceReportCalculator": "計算機を見る", + "checkingEvery": "チェック間隔", + "statusPageCreateSettings": "ステータスページの準備ができたら、公開済みとしてマークできます。", + "basicInformation": "基本情報", + "statusPageCreateBasicInfoDescription": "会社名とステータスページが指すサブドメインを定義。", + "statusPageCreateSelectTimeZoneDescription": "ステータスページが表示されるタイムゾーンを選択。", + "statusPageCreateAppearanceDescription": "公開ステータスページのデフォルトの外観を定義。", + "statusPageCreateSettingsCheckboxLabel": "公開済みで一般に表示", + "statusPageCreateBasicInfoStatusPageAddress": "ステータスページアドレス", + "statusPageCreateTabsContent": "ステータスページサーバー", + "statusPageCreateTabsContentDescription": "監視している任意の数のサーバーをステータスページに追加できます。最適な表示体験のために並び替えることもできます。", + "statusPageCreateTabsContentFeaturesDescription": "ステータスページでより多くの詳細を表示", + "showCharts": "チャートを表示", + "showUptimePercentage": "稼働率を表示", + "removeLogo": "ロゴを削除", + "statusPageStatus": "公開ステータスページが設定されていません。", + "statusPageStatusContactAdmin": "管理者にお問い合わせください", + "statusPageStatusNotPublic": "このステータスページは非公開です。", + "statusPageStatusNoPage": "ここにはステータスページがありません。", + "statusPageStatusServiceStatus": "サービスステータス", + "deleteStatusPage": "このステータスページを削除しますか?", + "deleteStatusPageConfirm": "はい、ステータスページを削除", + "deleteStatusPageDescription": "削除されると、ステータスページは復元できません。", + "uptimeCreate": "期待値はレスポンス結果と照合するために使用され、マッチがステータスを決定します。", + "uptimeCreateJsonPath": "この式はレスポンスJSONデータに対して評価され、結果は期待値との照合に使用されます。参照", + "uptimeCreateJsonPathQuery": "クエリ言語ドキュメント。", + "maintenanceTableActionMenuDialogTitle": "本当にこのメンテナンスウィンドウを削除しますか?", + "infrastructureEditYour": "編集する", + "infrastructureEditMonitor": "インフラストラクチャモニターを保存", + "infrastructureMonitorCreated": "インフラストラクチャモニターが正常に作成されました!", + "infrastructureMonitorUpdated": "インフラストラクチャモニターが正常に更新されました!", + "errorInvalidTypeId": "無効な通知タイプが提供されました", + "errorInvalidFieldId": "無効なフィールドIDが提供されました", + "inviteNoTokenFound": "招待トークンが見つかりません", + "pageSpeedWarning": "警告: Google PageSpeed APIキーがまだ追加されていません。これがないとPageSpeedモニターは機能しません。", + "pageSpeedLearnMoreLink": "こちらをクリック", + "pageSpeedAddApiKey": "APIキーを追加する。", + "update": "更新", + "invalidFileFormat": "サポートされていないファイル形式!", + "invalidFileSize": "ファイルサイズが大きすぎます!", + "ClickUpload": "クリックしてアップロード", + "DragandDrop": "ドラッグアンドドロップ", + "MaxSize": "最大サイズ", + "SupportedFormats": "対応フォーマット", + "FirstName": "名", + "LastName": "姓", + "EmailDescriptionText": "こちらは現在ご使用のメールアドレスです — 変更することはできません。", + "YourPhoto": "プロフィール写真", + "PhotoDescriptionText": "この写真はプロフィールページに表示されます。", + "save": "保存", + "DeleteDescriptionText": "これにより、アカウントと関連するすべてのデータがサーバーから削除されます。この操作は元に戻せません。", + "DeleteAccountWarning": "アカウントを削除すると、再度サインインできなくなり、すべてのデータが削除されます。この操作は元に戻せません。", + "DeleteWarningTitle": "本当にこのアカウントを削除しますか?", + "bulkImport": { + "title": "一括インポート", + "selectFileTips": "アップロードするCSVファイルを選択", + "selectFileDescription": "またはサンプルをダウンロードできます", + "selectFile": "ファイルを選択", + "parsingFailed": "解析に失敗しました", + "uploadSuccess": "モニターが正常に作成されました!", + "validationFailed": "検証に失敗しました", + "noFileSelected": "ファイルが選択されていません", + "fallbackPage": "サーバーのリストを一括アップロードするためのファイルをインポート", + "invalidFileType": "無効なファイルタイプ", + "uploadFailed": "アップロードに失敗しました" + }, + "DeleteAccountTitle": "アカウントを削除", + "DeleteAccountButton": "アカウントを削除", + "publicLink": "公開リンク", + "maskedPageSpeedKeyPlaceholder": "*************************************", + "reset": "リセット", + "ignoreTLSError": "TLS/SSLエラーを無視", + "tlsErrorIgnored": "TLS/SSLエラーが無視されました", + "ignoreTLSErrorDescription": "TLS/SSLエラーを無視してウェブサイトの可用性をチェック続行", + "createNew": "新規作成", + "greeting": { + "prepend": "こんにちは", + "append": "午後はあなたの遊び場です—壮大なものにしましょう!", + "overview": "{{type}}モニターの概要です。" + }, + "roles": { + "superAdmin": "スーパー管理者", + "admin": "管理者", + "teamMember": "チームメンバー", + "demoUser": "デモユーザー" + }, + "teamPanel": { + "teamMembers": "チームメンバー", + "filter": { + "all": "すべて", + "member": "メンバー" + }, + "inviteTeamMember": "チームメンバーを招待", + "inviteNewTeamMember": "新しいチームメンバーを招待", + "inviteDescription": "新しいチームメンバーを追加すると、すべてのモニターへのアクセス権が付与されます。", + "email": "メール", + "selectRole": "役割を選択", + "inviteLink": "招待リンク", + "cancel": "キャンセル", + "noMembers": "この役割のチームメンバーはいません", + "getToken": "トークンを取得", + "emailToken": "メールトークン", + "table": { + "name": "名前", + "email": "メール", + "role": "役割", + "created": "作成日" + } + }, + "monitorState": { + "paused": "一時停止", + "resumed": "再開", + "active": "アクティブ" + }, + "menu": { + "uptime": "稼働時間", + "pagespeed": "ページ速度", + "infrastructure": "インフラストラクチャ", + "incidents": "インシデント", + "statusPages": "ステータスページ", + "maintenance": "メンテナンス", + "integrations": "統合", + "settings": "設定", + "support": "サポート", + "discussions": "ディスカッション", + "docs": "ドキュメント", + "changelog": "変更履歴", + "profile": "プロフィール", + "password": "パスワード", + "team": "チーム", + "logOut": "ログアウト", + "notifications": "通知", + "logs": "ログ" + }, + "settingsEmailUser": "メールユーザー - 認証用ユーザー名、指定されている場合はメールアドレスを上書き", + "state": "状態", + "statusBreadCrumbsStatusPages": "ステータスページ", + "statusBreadCrumbsDetails": "詳細", + "commonSaving": "保存中...", + "navControls": "コントロール", + "incidentsPageTitle": "インシデント", + "passwordPanel": { + "passwordChangedSuccess": "パスワードが正常に変更されました。", + "passwordInputIncorrect": "パスワード入力が間違っています。", + "currentPassword": "現在のパスワード", + "enterCurrentPassword": "現在のパスワードを入力", + "newPassword": "新しいパスワード", + "enterNewPassword": "新しいパスワードを入力", + "confirmNewPassword": "新しいパスワードの確認", + "passwordRequirements": "新しいパスワードは少なくとも8文字で、少なくとも1つの大文字、1つの小文字、1つの数字、1つの特殊文字を含む必要があります。", + "saving": "保存中..." + }, + "emailSent": "メールが正常に送信されました", + "failedToSendEmail": "メールの送信に失敗しました", + "settingsTestEmailSuccess": "テストメールが正常に送信されました", + "settingsTestEmailFailed": "テストメールの送信に失敗しました", + "settingsTestEmailFailedWithReason": "テストメールの送信に失敗しました: {{reason}}", + "settingsTestEmailUnknownError": "不明なエラー", + "statusMsg": { + "paused": "監視が一時停止されています。", + "up": "サイトが稼働中です。", + "down": "サイトがダウンしています。", + "pending": "保留中..." + }, + "uptimeGeneralInstructions": { + "http": "監視するURLまたはIPを入力(例: https://example.com/ または 192.168.1.100)し、ダッシュボードに表示される明確な表示名を追加。", + "ping": "pingするIPアドレスまたはホスト名を入力(例: 192.168.1.100 または example.com)し、ダッシュボードに表示される明確な表示名を追加。", + "docker": "コンテナのDocker IDを入力。Docker IDは64文字のフルDocker IDである必要があります。docker inspect を実行してフルコンテナIDを取得できます。", + "port": "サーバーのURLまたはIP、ポート番号、ダッシュボードに表示される明確な表示名を入力。", + "game": "" + }, + "common": { + "appName": "Checkmate", + "monitoringAgentName": "Capture", + "buttons": { + "toggleTheme": "ライト&ダークを切り替え" + }, + "toasts": { + "networkError": "ネットワークエラー", + "checkConnection": "接続を確認してください", + "unknownError": "不明なエラー" + } + }, + "auth": { + "common": { + "navigation": { + "continue": "続行", + "back": "戻る" + }, + "inputs": { + "email": { + "label": "メール", + "placeholder": "jordan.ellis@domain.com", + "errors": { + "empty": "続行するには、メールアドレスを入力してください", + "invalid": "入力されたメールアドレスの有効性を再確認してください" + } + }, + "password": { + "label": "パスワード", + "rules": { + "length": { + "beginning": "少なくとも", + "highlighted": "8文字である必要があります" + }, + "special": { + "beginning": "少なくとも", + "highlighted": "1つの特殊文字を含む必要があります" + }, + "number": { + "beginning": "少なくとも", + "highlighted": "1つの数字を含む必要があります" + }, + "uppercase": { + "beginning": "少なくとも", + "highlighted": "1つの大文字を含む必要があります" + }, + "lowercase": { + "beginning": "少なくとも", + "highlighted": "1つの小文字を含む必要があります" + }, + "match": { + "beginning": "パスワードの確認とパスワードは", + "highlighted": "一致する必要があります" + } + }, + "errors": { + "empty": "パスワードを入力してください", + "length": "パスワードは少なくとも8文字である必要があります", + "uppercase": "パスワードは少なくとも1つの大文字を含む必要があります", + "lowercase": "パスワードは少なくとも1つの小文字を含む必要があります", + "number": "パスワードは少なくとも1つの数字を含む必要があります", + "special": "パスワードは少なくとも1つの特殊文字を含む必要があります", + "incorrect": "入力されたパスワードが記録と一致しません" + } + }, + "passwordConfirm": { + "label": "パスワードの確認", + "placeholder": "確認のためにパスワードを再入力", + "errors": { + "empty": "確認のためにパスワードを再度入力してください(タイプミスを防ぐため)", + "different": "入力されたパスワードが一致しないため、どちらかが間違っています" + } + }, + "firstName": { + "label": "名前", + "placeholder": "太郎", + "errors": { + "empty": "名前を入力してください", + "length": "名前は50文字未満である必要があります", + "pattern": "名前は文字、スペース、アポストロフィ、またはハイフンのみを含む必要があります" + } + }, + "lastName": { + "label": "姓", + "placeholder": "田中", + "errors": { + "empty": "姓を入力してください", + "length": "姓は50文字未満である必要があります", + "pattern": "姓は文字、スペース、アポストロフィ、またはハイフンのみを含む必要があります" + } + } + }, + "errors": { + "validation": "データの検証エラー。" + }, + "fields": { + "password": { + "errors": { + "incorrect": "入力されたパスワードが記録と一致しません" + } + }, + "role": { + "errors": { + "min": "" + } + } + } + }, + "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": "例: Slack通知" + }, + "typeSettings": { + "title": "タイプ", + "description": "作成したい通知チャンネルのタイプを選択。", + "typeLabel": "タイプ" + }, + "emailSettings": { + "title": "メール", + "description": "送信先メールアドレス。", + "emailLabel": "メールアドレス", + "emailPlaceholder": "例: john@example.com" + }, + "slackSettings": { + "title": "Slack", + "description": "Slack Webhookをここで設定", + "webhookLabel": "Slack Webhook URL", + "webhookPlaceholder": "https://hooks.slack.com/services/..." + }, + "pagerdutySettings": { + "title": "PagerDuty", + "description": "PagerDuty統合をここで設定", + "integrationKeyLabel": "統合キー", + "integrationKeyPlaceholder": "1234567890" + }, + "discordSettings": { + "title": "Discord", + "description": "Discord Webhookをここで設定", + "webhookLabel": "Discord Webhook URL", + "webhookPlaceholder": "https://your-server.com/webhook" + }, + "webhookSettings": { + "title": "Webhook", + "description": "Webhookをここで設定", + "webhookLabel": "Webhook URL", + "webhookPlaceholder": "https://your-server.com/webhook" + }, + "testNotification": "テスト通知", + "dialogDeleteTitle": "この通知を削除してもよろしいですか?", + "dialogDeleteConfirm": "削除" + }, + "notificationConfig": { + "title": "通知", + "description": "使用したい通知チャンネルを選択" + }, + "monitorStatus": { + "checkingEvery": "{{interval}}ごとにチェック", + "withCaptureAgent": "Captureエージェント{{version}}で", + "up": "アップ", + "down": "ダウン", + "paused": "一時停止" + }, + "advancedMatching": "高度なマッチング", + "sendTestNotifications": "テスト通知を送信", + "selectAll": "すべて選択", + "showAdminLoginLink": "ステータスページに「管理者?こちらでログイン」リンクを表示", + "logsPage": { + "title": "ログ", + "description": "システムログ - 最新1000行", + "tabs": { + "queue": "ジョブキュー", + "logs": "サーバーログ", + "diagnostics": "診断" + }, + "toast": { + "fetchLogsSuccess": "ログが正常に取得されました" + }, + "logLevelSelect": { + "title": "ログレベル", + "values": { + "all": "すべて", + "info": "情報", + "warn": "警告", + "error": "エラー", + "debug": "デバッグ" + } + } + }, + "queuePage": { + "title": "キュー", + "refreshButton": "更新", + "flushButton": "キューをフラッシュ", + "jobTable": { + "title": "現在キューにあるジョブ", + "idHeader": "モニターID", + "urlHeader": "URL", + "typeHeader": "タイプ", + "activeHeader": "アクティブ", + "lockedAtHeader": "ロック日時", + "runCountHeader": "実行回数", + "failCountHeader": "失敗回数", + "lastRunHeader": "最終実行日時", + "lastFinishedAtHeader": "最終完了日時", + "lastRunTookHeader": "最終実行時間" + }, + "metricsTable": { + "title": "キューメトリック", + "metricHeader": "メトリック", + "valueHeader": "値" + }, + "failedJobTable": { + "title": "失敗したジョブ", + "monitorIdHeader": "モニターID", + "monitorUrlHeader": "モニターURL", + "failCountHeader": "失敗回数", + "failedAtHeader": "最終失敗日時", + "failReasonHeader": "失敗理由" + } + }, + "export": { + "title": "モニターをエクスポート", + "success": "モニターが正常にエクスポートされました!", + "failed": "モニターのエクスポートに失敗しました" + }, + "monitorActions": { + "title": "エクスポート/インポート", + "import": "モニターをインポート", + "export": "モニターをエクスポート", + "deleteSuccess": "モニターが正常に削除されました", + "deleteFailed": "モニターの削除に失敗しました", + "details": "詳細" + }, + "settingsPage": { + "aboutSettings": { + "labelDevelopedBy": "Bluewave Labsによる開発", + "labelVersion": "バージョン", + "title": "について" + }, + "demoMonitorsSettings": { + "buttonAddMonitors": "デモモニターを追加", + "description": "デモンストレーション目的でサンプルモニターを追加。", + "title": "デモモニター" + }, + "emailSettings": { + "buttonSendTestEmail": "テストメールを送信", + "description": "システムのメール設定を構成。これは通知とアラートの送信に使用されます。", + "descriptionTransport": "これはNodeMailer用のSMTPトランスポートを構築します", + "labelAddress": "メールアドレス - 認証に使用", + "labelConnectionHost": "メール接続ホスト - HELO/EHLO挨拶で使用するホスト名", + "labelHost": "メールホスト - 接続するホスト名またはIPアドレス", + "labelIgnoreTLS": "STARTTLSを無効化: サーバーがサポートしていてもTLSを使用しない", + "labelPassword": "メールパスワード - 認証用パスワード", + "labelPasswordSet": "パスワードが設定されています。変更するにはリセットをクリック。", + "labelPool": "接続プーリングを有効化: パフォーマンス向上のため既存接続を再利用", + "labelPort": "メールポート - 接続するポート", + "labelRejectUnauthorized": "無効な証明書を拒否: 自己署名または信頼されていない証明書での接続を拒否", + "labelRequireTLS": "STARTTLSを強制: TLSアップグレードを必須とし、サポートされていない場合は失敗", + "labelSecure": "SSL使用(推奨): SSL/TLSを使用して接続を暗号化", + "labelTLSServername": "TLS Servername - ホストがIPの場合のTLS検証用オプションホスト名", + "labelUser": "メールユーザー - 認証用ユーザー名、指定されている場合はメールアドレスを上書き", + "linkTransport": "仕様はこちらを参照", + "placeholderUser": "不要な場合は空白のまま", + "title": "メール", + "toastEmailRequiredFieldsError": "メールアドレス、ホスト、ポート、パスワードが必要です" + }, + "pageSpeedSettings": { + "description": "Google PageSpeed APIキーを入力してGoogle PageSpeed監視を有効化。キーを更新するにはリセットをクリック。", + "labelApiKeySet": "APIキーが設定されています。変更するにはリセットをクリック。", + "labelApiKey": "PageSpeed APIキー", + "title": "Google PageSpeed APIキー" + }, + "saveButtonLabel": "保存", + "statsSettings": { + "clearAllStatsButton": "すべての統計をクリア", + "clearAllStatsDescription": "すべての統計をクリア。この操作は元に戻せません。", + "clearAllStatsDialogConfirm": "はい、すべての統計をクリア", + "clearAllStatsDialogDescription": "削除されると、監視履歴と統計は復元できません。", + "clearAllStatsDialogTitle": "すべての統計をクリアしますか?", + "description": "履歴データを保持する期間を定義。既存のデータをすべてクリアすることもできます。", + "labelTTL": "監視履歴を保持したい日数。", + "labelTTLOptional": "0で無限", + "title": "モニター履歴" + }, + "systemResetSettings": { + "buttonRemoveAllMonitors": "すべてのモニターを削除", + "description": "システムからすべてのモニターを削除。", + "dialogConfirm": "はい、すべてのモニターを削除", + "dialogDescription": "削除されると、モニターは復元できません。", + "dialogTitle": "すべてのモニターを削除しますか?", + "title": "システムリセット" + }, + "timezoneSettings": { + "description": "アプリケーション全体で日付と時刻を表示するために使用するタイムゾーンを選択。", + "label": "表示タイムゾーン", + "title": "表示タイムゾーン" + }, + "title": "設定", + "uiSettings": { + "description": "ライトモードとダークモードを切り替えるか、ユーザーインターフェース言語を変更。", + "labelLanguage": "言語", + "labelTheme": "テーマモード", + "title": "外観" + }, + "urlSettings": { + "description": "公開ステータスページでモニターのIPアドレスまたはURLを表示。無効にすると、機密情報を保護するためモニター名のみが表示されます。", + "label": "ステータスページでIP/URLを表示", + "selectDisabled": "無効", + "selectEnabled": "有効", + "title": "ステータスページのモニターIP/URL" + } + }, + "statusPageCreate": { + "buttonSave": "保存" + }, + "incidentsOptionsHeaderFilterResolved": "解決済み", + "settingsSave": "保存", + "statusPageCreateAppearanceTitle": "外観", + "confirmPassword": "パスワードの確認", + "monitorHooks": { + "failureAddDemoMonitors": "デモモニターの追加に失敗しました", + "successAddDemoMonitors": "デモモニターが正常に追加されました" + }, + "settingsAppearance": "外観", + "settingsDisplayTimezone": "表示タイムゾーン", + "settingsGeneralSettings": "一般設定", + "incidentsOptionsHeaderTotalIncidents": "総インシデント", + "statusPage": { + "deleteSuccess": "ステータスページが正常に削除されました", + "deleteFailed": "ステータスページの削除に失敗しました", + "createSuccess": "ステータスページが正常に作成されました", + "updateSuccess": "ステータスページが正常に更新されました", + "generalSettings": "一般設定", + "contents": "コンテンツ", + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "testNotificationsDisabled": "このモニターには通知が設定されていません。「設定」ボタンをクリックして追加する必要があります", + "incidentsTableResolvedAt": "解決日時", + "incidentsTableActionResolve": "解決", + "checkHooks": { + "failureResolveOne": "インシデントの解決に失敗しました。", + "failureResolveAll": "すべてのインシデントの解決に失敗しました。", + "failureResolveMonitor": "" + }, + "checkFormError": "フォームでエラーを確認してください。", + "diagnosticsPage": { + "diagnosticDescription": "システム診断", + "statsDescription": "システム統計", + "gauges": { + "heapAllocationTitle": "ヒープ割り当て", + "heapAllocationSubtitle": "利用可能メモリの%", + "heapUsageTitle": "ヒープ使用量", + "heapUsageSubtitle": "利用可能メモリの%", + "heapUtilizationTitle": "ヒープ利用率", + "heapUtilizationSubtitle": "割り当て済みの%", + "instantCpuUsageTitle": "瞬間CPU使用率", + "instantCpuUsageSubtitle": "CPUが使用した1秒の%" + }, + "stats": { + "eventLoopDelayTitle": "イベントループ遅延", + "uptimeTitle": "稼働時間", + "usedHeapSizeTitle": "使用ヒープサイズ", + "totalHeapSizeTitle": "総ヒープサイズ", + "osMemoryLimitTitle": "OSメモリ制限" + } + }, + "pageSpeedLighthouseAPI": "Lighthouse PageSpeed APIを使用してウェブサイトを監視", + "time": { + "threeMinutes": "3分", + "fiveMinutes": "5分", + "tenMinutes": "10分", + "twentyMinutes": "20分", + "oneHour": "1時間", + "oneDay": "1日", + "oneWeek": "1週間" + }, + "general": { + "noOptionsFound": "" + }, + "infrastructureMonitor": { + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "maintenanceWindow": { + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "pageSpeed": { + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "uptimeMonitor": { + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "editUserPage": { + "form": { + "email": "", + "firstName": "", + "lastName": "", + "role": "", + "save": "" + }, + "table": { + "actionHeader": "", + "roleHeader": "" + }, + "title": "", + "toast": { + "successUserUpdate": "", + "validationErrors": "" + } + }, + "incidentsPageActionResolveMonitor": "", + "incidentsPageActionResolveAll": "" +} diff --git a/client/src/locales/pt-BR.json b/client/src/locales/pt-BR.json index f9ccc81b4..4b4bd04a9 100644 --- a/client/src/locales/pt-BR.json +++ b/client/src/locales/pt-BR.json @@ -8,7 +8,6 @@ "settingsFailedToSave": "Falha ao salvar as configurações", "settingsStatsCleared": "Estatísticas limpas com sucesso", "settingsFailedToClearStats": "Falha ao limpar estatísticas", - "settingsFailedToAddDemoMonitors": "Falha ao adicionar monitores de demonstração", "settingsMonitorsDeleted": "Todos os monitores foram excluídos com sucesso", "settingsFailedToDeleteMonitors": "Falha ao excluir todos os monitores", "starPromptTitle": "Star Checkmate", @@ -86,7 +85,8 @@ "networkError": "Ocorreu um erro de rede", "fallback": { "title": "", - "checks": [""] + "checks": [""], + "actionButton": "" }, "createButton": "", "createTitle": "", @@ -260,6 +260,7 @@ "notifyEmails": "Também notificar por e-mail para vários endereços (em breve)", "seperateEmails": "Você pode separar vários e-mails com uma vírgula", "checkFrequency": "Verifique a frequência", + "chooseGame": "", "matchMethod": "Método de correspondência", "expectedValue": "Valor esperado", "deleteDialogTitle": "Você realmente deseja excluir este monitor?", @@ -331,7 +332,9 @@ "uploadSuccess": "Monitores criados com sucesso!", "validationFailed": "Falha na validação", "noFileSelected": "Nenhum arquivo selecionado", - "fallbackPage": "Importe um arquivo para enviar uma lista de servidores em massa" + "fallbackPage": "Importe um arquivo para enviar uma lista de servidores em massa", + "invalidFileType": "", + "uploadFailed": "" }, "DeleteAccountTitle": "Remover conta", "DeleteAccountButton": "Remover conta", @@ -432,47 +435,48 @@ "pending": "Pendente..." }, "uptimeGeneralInstructions": { - "http": "", - "ping": "", - "docker": "", - "port": "" + "http": "Insira a URL ou IP para monitorar (ex. https://exemplo.com.br/ ou 192.168.1.100) e adicione uma descrição que aparecerá na dashboard.", + "ping": "Insira o endereço IP ou nome de domínio para ping (ex. 192.168.1.100 ou exemplo.com.br) e adicione uma descrição que aparecerá na dashboard.", + "docker": "Insira o Docker Id do seu container. Docker Ids devem ser todos os 64 caracteres. Você pode executar docker inspect para descobrir o Id completo.", + "port": "Insira a URL ou o IP do servidor, aporta e uma descrição que aparecerá na dashboard.", + "game": "" }, "common": { - "appName": "", - "monitoringAgentName": "", + "appName": "Checkmate", + "monitoringAgentName": "Capture", "buttons": { - "toggleTheme": "" + "toggleTheme": "Trocar claro e escuro" }, "toasts": { - "networkError": "", - "checkConnection": "", - "unknownError": "" + "networkError": "Erro de rede", + "checkConnection": "Por favor, verifique a conexão", + "unknownError": "Erro desconhecido" } }, "auth": { "common": { "navigation": { - "continue": "", - "back": "" + "continue": "Continuar", + "back": "Voltar" }, "inputs": { "email": { - "label": "", - "placeholder": "", + "label": "Email", + "placeholder": "joao.silva@dominio.com.br", "errors": { - "empty": "", - "invalid": "" + "empty": "Para continuar, insira seu endereço de email", + "invalid": "Por favor, confirme o endereço de email informado" } }, "password": { - "label": "", + "label": "Senha", "rules": { "length": { - "beginning": "", - "highlighted": "" + "beginning": "Deve ter no mínimo", + "highlighted": "8 caracteres de comprimento" }, "special": { - "beginning": "", + "beginning": "Deve conter pelo menos", "highlighted": "" }, "number": { @@ -493,9 +497,9 @@ } }, "errors": { - "empty": "", - "length": "", - "uppercase": "", + "empty": "Por favor, digite sua senha", + "length": "A senha deve ter pelo menos 8 caracteres", + "uppercase": "A senha deve conter pelo menos 1 letra maiúscula", "lowercase": "", "number": "", "special": "", @@ -511,50 +515,55 @@ } }, "firstName": { - "label": "", - "placeholder": "", + "label": "Nome", + "placeholder": "João", "errors": { - "empty": "", - "length": "", - "pattern": "" + "empty": "Por favor, insira seu nome", + "length": "Nome deve ter menos de 50 caracteres", + "pattern": "Nome deve conter apenas letras, espaços, apóstrofos ou hífens" } }, "lastName": { - "label": "", - "placeholder": "", + "label": "Sobrenome", + "placeholder": "Silva", "errors": { - "empty": "", - "length": "", - "pattern": "" + "empty": "Por favor, insira seu sobrenome", + "length": "Sobrenome deve ter menos de 50 caracteres", + "pattern": "Sobrenome deve conter apenas letras, espaços, apóstrofos ou hífens" } } }, "errors": { - "validation": "" + "validation": "Erro de validação de dados." }, "fields": { "password": { "errors": { "incorrect": "" } + }, + "role": { + "errors": { + "min": "" + } } } }, "login": { - "heading": "", + "heading": "Entrar", "subheadings": { - "stepOne": "", - "stepTwo": "" + "stepOne": "Insira seu email", + "stepTwo": "Insira sua senha" }, "links": { - "forgotPassword": "", - "register": "", + "forgotPassword": "Esqueceu sua senha?", + "register": "Não tem uma conta?", "forgotPasswordLink": "", "registerLink": "" }, "toasts": { - "success": "", - "incorrectPassword": "" + "success": "Bem-vindo de volta! Você está autenticado.", + "incorrectPassword": "Senha incorreta" }, "errors": { "password": { @@ -564,8 +573,8 @@ }, "registration": { "heading": { - "superAdmin": "", - "user": "" + "superAdmin": "Crie um usuário administrador", + "user": "Se inscrever" }, "subheadings": { "stepOne": "", @@ -604,11 +613,11 @@ "passwordKey": "", "email": "", "lock": "", - "passwordConfirm": "" + "passwordConfirm": "Ícone de confirmação de senha" }, "links": { - "login": "", - "resend": "" + "login": "Voltar para o login", + "resend": "Não recebeu o email? Clique para reenviar" }, "toasts": { "sent": "", @@ -675,7 +684,10 @@ "description": "", "webhookLabel": "", "webhookPlaceholder": "" - } + }, + "testNotification": "", + "dialogDeleteTitle": "", + "dialogDeleteConfirm": "" }, "notificationConfig": { "title": "", @@ -690,7 +702,6 @@ }, "advancedMatching": "", "sendTestNotifications": "", - "testNotificationsDisabled": "", "selectAll": "", "showAdminLoginLink": "", "logsPage": { @@ -698,7 +709,8 @@ "description": "", "tabs": { "queue": "", - "logs": "" + "logs": "", + "diagnostics": "" }, "toast": { "fetchLogsSuccess": "" @@ -753,7 +765,10 @@ "monitorActions": { "title": "", "import": "", - "export": "" + "export": "", + "deleteSuccess": "", + "deleteFailed": "", + "details": "" }, "settingsPage": { "aboutSettings": { @@ -785,7 +800,8 @@ "labelUser": "", "linkTransport": "", "placeholderUser": "", - "title": "" + "title": "", + "toastEmailRequiredFieldsError": "" }, "pageSpeedSettings": { "description": "", @@ -832,5 +848,124 @@ "selectEnabled": "", "title": "" } - } + }, + "statusPageCreate": { + "buttonSave": "" + }, + "incidentsOptionsHeaderFilterResolved": "", + "settingsSave": "", + "statusPageCreateAppearanceTitle": "", + "confirmPassword": "", + "monitorHooks": { + "failureAddDemoMonitors": "", + "successAddDemoMonitors": "" + }, + "settingsAppearance": "", + "settingsDisplayTimezone": "", + "settingsGeneralSettings": "", + "incidentsOptionsHeaderTotalIncidents": "", + "statusPage": { + "deleteSuccess": "", + "deleteFailed": "", + "createSuccess": "", + "updateSuccess": "", + "generalSettings": "", + "contents": "", + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "testNotificationsDisabled": "", + "incidentsTableResolvedAt": "", + "incidentsTableActionResolve": "", + "checkHooks": { + "failureResolveOne": "", + "failureResolveAll": "", + "failureResolveMonitor": "" + }, + "checkFormError": "", + "diagnosticsPage": { + "diagnosticDescription": "", + "statsDescription": "", + "gauges": { + "heapAllocationTitle": "", + "heapAllocationSubtitle": "", + "heapUsageTitle": "", + "heapUsageSubtitle": "", + "heapUtilizationTitle": "", + "heapUtilizationSubtitle": "", + "instantCpuUsageTitle": "", + "instantCpuUsageSubtitle": "" + }, + "stats": { + "eventLoopDelayTitle": "", + "uptimeTitle": "", + "usedHeapSizeTitle": "", + "totalHeapSizeTitle": "", + "osMemoryLimitTitle": "" + } + }, + "pageSpeedLighthouseAPI": "", + "time": { + "threeMinutes": "", + "fiveMinutes": "", + "tenMinutes": "", + "twentyMinutes": "", + "oneHour": "", + "oneDay": "", + "oneWeek": "" + }, + "general": { + "noOptionsFound": "" + }, + "infrastructureMonitor": { + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "maintenanceWindow": { + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "pageSpeed": { + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "uptimeMonitor": { + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "editUserPage": { + "form": { + "email": "", + "firstName": "", + "lastName": "", + "role": "", + "save": "" + }, + "table": { + "actionHeader": "", + "roleHeader": "" + }, + "title": "", + "toast": { + "successUserUpdate": "", + "validationErrors": "" + } + }, + "incidentsPageActionResolveMonitor": "", + "incidentsPageActionResolveAll": "" } diff --git a/client/src/locales/ru.json b/client/src/locales/ru.json index e0f66b98e..25e1038aa 100644 --- a/client/src/locales/ru.json +++ b/client/src/locales/ru.json @@ -8,10 +8,9 @@ "settingsFailedToSave": "Не удалось сохранить настройки", "settingsStatsCleared": "Статистика успешно очищена", "settingsFailedToClearStats": "Не удалось очистить статистику", - "settingsFailedToAddDemoMonitors": "Не удалось добавить демонстрационные мониторы", "settingsMonitorsDeleted": "Успешно удалены все мониторы", "settingsFailedToDeleteMonitors": "Не удалось удалить все мониторы", - "starPromptTitle": "Star Checkmate", + "starPromptTitle": "", "starPromptDescription": "Ознакомьтесь с последними релизами и помогите развить сообщество на GitHub", "https": "HTTPS", "http": "HTTP", @@ -86,7 +85,8 @@ "networkError": "Произошла сетевая ошибка", "fallback": { "title": "", - "checks": [""] + "checks": [""], + "actionButton": "" }, "createButton": "", "createTitle": "", @@ -260,6 +260,7 @@ "notifyEmails": "Также уведомлять по электронной почте на несколько адресов (скоро)", "seperateEmails": "Вы можете разделить несколько адресов электронной почты запятой.", "checkFrequency": "Проверить частоту", + "chooseGame": "", "matchMethod": "Метод сопоставления", "expectedValue": "Ожидаемое значение", "deleteDialogTitle": "Вы действительно хотите удалить этот монитор?", @@ -331,7 +332,9 @@ "uploadSuccess": "Мониторы успешно созданы!", "validationFailed": "Не удалось выполнить проверку", "noFileSelected": "Файл не выбран", - "fallbackPage": "Импортируйте файл для массовой загрузки списка серверов" + "fallbackPage": "Импортируйте файл для массовой загрузки списка серверов", + "invalidFileType": "", + "uploadFailed": "" }, "DeleteAccountTitle": "Удалить аккаунт", "DeleteAccountButton": "Удалить аккаунт", @@ -435,7 +438,8 @@ "http": "", "ping": "", "docker": "", - "port": "" + "port": "", + "game": "" }, "common": { "appName": "", @@ -537,6 +541,11 @@ "errors": { "incorrect": "" } + }, + "role": { + "errors": { + "min": "" + } } } }, @@ -675,7 +684,10 @@ "description": "", "webhookLabel": "", "webhookPlaceholder": "" - } + }, + "testNotification": "", + "dialogDeleteTitle": "", + "dialogDeleteConfirm": "" }, "notificationConfig": { "title": "", @@ -690,7 +702,6 @@ }, "advancedMatching": "", "sendTestNotifications": "", - "testNotificationsDisabled": "", "selectAll": "", "showAdminLoginLink": "", "logsPage": { @@ -698,7 +709,8 @@ "description": "", "tabs": { "queue": "", - "logs": "" + "logs": "", + "diagnostics": "" }, "toast": { "fetchLogsSuccess": "" @@ -753,7 +765,10 @@ "monitorActions": { "title": "", "import": "", - "export": "" + "export": "", + "deleteSuccess": "", + "deleteFailed": "", + "details": "" }, "settingsPage": { "aboutSettings": { @@ -785,7 +800,8 @@ "labelUser": "", "linkTransport": "", "placeholderUser": "", - "title": "" + "title": "", + "toastEmailRequiredFieldsError": "" }, "pageSpeedSettings": { "description": "", @@ -832,5 +848,124 @@ "selectEnabled": "", "title": "" } - } + }, + "statusPageCreate": { + "buttonSave": "" + }, + "incidentsOptionsHeaderFilterResolved": "", + "settingsSave": "", + "statusPageCreateAppearanceTitle": "", + "confirmPassword": "", + "monitorHooks": { + "failureAddDemoMonitors": "", + "successAddDemoMonitors": "" + }, + "settingsAppearance": "", + "settingsDisplayTimezone": "", + "settingsGeneralSettings": "", + "incidentsOptionsHeaderTotalIncidents": "", + "statusPage": { + "deleteSuccess": "", + "deleteFailed": "", + "createSuccess": "", + "updateSuccess": "", + "generalSettings": "", + "contents": "", + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "testNotificationsDisabled": "", + "incidentsTableResolvedAt": "", + "incidentsTableActionResolve": "", + "checkHooks": { + "failureResolveOne": "", + "failureResolveAll": "", + "failureResolveMonitor": "" + }, + "checkFormError": "", + "diagnosticsPage": { + "diagnosticDescription": "", + "statsDescription": "", + "gauges": { + "heapAllocationTitle": "", + "heapAllocationSubtitle": "", + "heapUsageTitle": "", + "heapUsageSubtitle": "", + "heapUtilizationTitle": "", + "heapUtilizationSubtitle": "", + "instantCpuUsageTitle": "", + "instantCpuUsageSubtitle": "" + }, + "stats": { + "eventLoopDelayTitle": "", + "uptimeTitle": "", + "usedHeapSizeTitle": "", + "totalHeapSizeTitle": "", + "osMemoryLimitTitle": "" + } + }, + "pageSpeedLighthouseAPI": "", + "time": { + "threeMinutes": "", + "fiveMinutes": "", + "tenMinutes": "", + "twentyMinutes": "", + "oneHour": "", + "oneDay": "", + "oneWeek": "" + }, + "general": { + "noOptionsFound": "" + }, + "infrastructureMonitor": { + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "maintenanceWindow": { + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "pageSpeed": { + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "uptimeMonitor": { + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "editUserPage": { + "form": { + "email": "", + "firstName": "", + "lastName": "", + "role": "", + "save": "" + }, + "table": { + "actionHeader": "", + "roleHeader": "" + }, + "title": "", + "toast": { + "successUserUpdate": "", + "validationErrors": "" + } + }, + "incidentsPageActionResolveMonitor": "", + "incidentsPageActionResolveAll": "" } diff --git a/client/src/locales/tr.json b/client/src/locales/tr.json index 845b24aaf..9e20242fb 100644 --- a/client/src/locales/tr.json +++ b/client/src/locales/tr.json @@ -8,7 +8,6 @@ "settingsFailedToSave": "Ayarlar kaydedilemedi", "settingsStatsCleared": "İstatistikler başarıyla temizlendi", "settingsFailedToClearStats": "İstatistikler temizlenemedi", - "settingsFailedToAddDemoMonitors": "Demo monitörler eklenemedi", "settingsMonitorsDeleted": "Tüm monitörler başarıyla silindi", "settingsFailedToDeleteMonitors": "Monitörler silinemedi", "starPromptTitle": "Checkmate yıldızla değerlendirin", @@ -86,7 +85,8 @@ "networkError": "Ağ hatası oluştu", "fallback": { "title": "", - "checks": [""] + "checks": [""], + "actionButton": "" }, "createButton": "Bildirim kanalı oluştur", "createTitle": "Bildirim kanalı", @@ -95,16 +95,16 @@ "failed": "Bildirim kanalı oluşturulamadı" }, "fetch": { - "success": "", - "failed": "" + "success": "Bildirimler başarıyla alındı", + "failed": "Bildirimler alınamadı" }, "delete": { - "success": "", - "failed": "" + "success": "Bildirim başarıyla silindi", + "failed": "Bildirim silinemedi" }, "edit": { - "success": "", - "failed": "" + "success": "Bildirim başarıyla güncellendi", + "failed": "Bildirim güncellenemedi" }, "test": { "success": "Test bildirimi başarıyla gönderildi", @@ -260,6 +260,7 @@ "notifyEmails": "Ayrıca birden fazla eposta adresine bildirim gönderebilirsiniz (yakında geliyor)", "seperateEmails": "Birden fazla epostayı virgülle ayırabilirsiniz", "checkFrequency": "Frekansı denetle", + "chooseGame": "Oyun seç", "matchMethod": "Eşleşme yöntemi", "expectedValue": "Beklenen değer", "deleteDialogTitle": "Gerçekten bu monitörü silmek istiyor musunuz?", @@ -331,7 +332,9 @@ "uploadSuccess": "Monitör başarıyla oluşturuldu!", "validationFailed": "Doğrulama başarısız oldu", "noFileSelected": "Hiçbir dosya seçilmedi", - "fallbackPage": "Toplu olarak sunucu listesini yüklemek için bir dosyayı içe aktarın" + "fallbackPage": "Toplu olarak sunucu listesini yüklemek için bir dosyayı içe aktarın", + "invalidFileType": "", + "uploadFailed": "" }, "DeleteAccountTitle": "Hesabı sil", "DeleteAccountButton": "Hesabı sil", @@ -340,11 +343,11 @@ "reset": "Sıfırla", "ignoreTLSError": "TLS/SSL hatalarını gözardı et", "tlsErrorIgnored": "TLS/SSL hataları gözardı ediliyor", - "ignoreTLSErrorDescription": "", + "ignoreTLSErrorDescription": "TLS/SSL hatalarını gözardı et ve websitesinin ayakta olduğunu denetle", "createNew": "Yeni oluştur", "greeting": { "prepend": "Merhaba", - "append": "", + "append": "Öğleden sonra muhteşem geçsin!", "overview": "" }, "roles": { @@ -365,28 +368,28 @@ "email": "Eposta", "selectRole": "Rolü seçin", "inviteLink": "Davet linki", - "cancel": "", - "noMembers": "", + "cancel": "İptal", + "noMembers": "Bu role sahip hiç üye yok", "getToken": "", "emailToken": "", "table": { - "name": "", - "email": "", - "role": "", - "created": "" + "name": "İsim", + "email": "Eposta", + "role": "Rol", + "created": "Oluşturulma" } }, "monitorState": { - "paused": "", - "resumed": "", - "active": "" + "paused": "Durduruldu", + "resumed": "Yeniden başlatıldı", + "active": "Aktif" }, "menu": { "uptime": "", - "pagespeed": "", + "pagespeed": "Sayfa hızı", "infrastructure": "", - "incidents": "", - "statusPages": "", + "incidents": "Olaylar", + "statusPages": "Durum sayfaları", "maintenance": "Bakım", "integrations": "Entegrasyonlar", "settings": "Ayarlar", @@ -399,7 +402,7 @@ "team": "Ekip", "logOut": "Çıkış yap", "notifications": "Bildirimler", - "logs": "" + "logs": "Günlükler" }, "settingsEmailUser": "E-posta kullanıcısı – Kimlik doğrulama için kullanıcı adı; belirtilirse e-posta adresinin yerine geçer.", "state": "Durum", @@ -435,7 +438,8 @@ "http": "", "ping": "", "docker": "", - "port": "" + "port": "", + "game": "" }, "common": { "appName": "Checkmate", @@ -512,11 +516,11 @@ }, "firstName": { "label": "", - "placeholder": "", + "placeholder": "Jordan", "errors": { - "empty": "", - "length": "", - "pattern": "" + "empty": "Lütfen adınızı girin", + "length": "İsim 50 karakterden az olmalıdır", + "pattern": "İsim yalnızca harfler, boşluklar, kesme işaretleri veya tireler içerebilir" } }, "lastName": { @@ -525,23 +529,28 @@ "errors": { "empty": "Lütfen soyadınızı girin", "length": "Soyadı en çok 50 karakter olmalıdır", - "pattern": "" + "pattern": "Soyad yalnızca harfler, boşluklar, kesme işaretleri veya tireler içerebilir" } } }, "errors": { - "validation": "" + "validation": "Veriyi işlerken hata oluştu." }, "fields": { "password": { "errors": { "incorrect": "" } + }, + "role": { + "errors": { + "min": "" + } } } }, "login": { - "heading": "", + "heading": "Giriş yap", "subheadings": { "stepOne": "Eposta adresinizi girin", "stepTwo": "Parolanızı girin" @@ -580,7 +589,7 @@ "superAdmin": "Süper yönetici hesabı oluşturun", "user": "" }, - "termsAndPolicies": "", + "termsAndPolicies": "Bir hesap oluşturarak Hizmet Şartları ve Gizlilik Politikası'nı kabul etmiş olursunuz.", "links": { "login": "Bir hesabınız mı var? Giriş yapın" }, @@ -593,8 +602,8 @@ "subheadings": { "stepOne": "Endişelenmeyin, sıfırlamak için gerekli bilgileri göndereceğiz.", "stepTwo": "Parola sıfırlama için gerekli bilgileri hesabına ilettik.", - "stepThree": "", - "stepFour": "" + "stepThree": "Parolanız daha önce kullandığınız paroladan farklı olmalıdır.", + "stepFour": "Parolanız başarıyla sıfırlandı. Buraya tıklayarak giriş yapabilirsiniz." }, "buttons": { "openEmail": "Eposta uygulamasını aç", @@ -612,7 +621,7 @@ }, "toasts": { "sent": "", - "emailNotFound": "", + "emailNotFound": "Eposta bulunamadı.", "redirect": "", "success": "", "error": "" @@ -675,7 +684,10 @@ "description": "", "webhookLabel": "", "webhookPlaceholder": "https://sunucu-adı.com/webhook\n" - } + }, + "testNotification": "Test bildirimi", + "dialogDeleteTitle": "Gerçekten bu bildirimi silmek istiyor musunuz?", + "dialogDeleteConfirm": "Sil" }, "notificationConfig": { "title": "Bildirimler", @@ -688,20 +700,20 @@ "down": "çalışmıyor", "paused": "beklemede" }, - "advancedMatching": "", + "advancedMatching": "Gelişmiş eşleştirme", "sendTestNotifications": "Bildirimleri dene", - "testNotificationsDisabled": "", "selectAll": "Tümünü seç", "showAdminLoginLink": "", "logsPage": { - "title": "", - "description": "", + "title": "Günlükler", + "description": "Sistem günlükleri - son 1000 satır", "tabs": { - "queue": "", - "logs": "" + "queue": "İş kuyruğu", + "logs": "Sunucu günlükleri", + "diagnostics": "Teşhis" }, "toast": { - "fetchLogsSuccess": "" + "fetchLogsSuccess": "Günlükler başarıyla alındı" }, "logLevelSelect": { "title": "", @@ -723,37 +735,40 @@ "idHeader": "", "urlHeader": "", "typeHeader": "", - "activeHeader": "", + "activeHeader": "Aktif", "lockedAtHeader": "", "runCountHeader": "", "failCountHeader": "", "lastRunHeader": "", - "lastFinishedAtHeader": "", + "lastFinishedAtHeader": "Son tamamlama", "lastRunTookHeader": "" }, "metricsTable": { "title": "", - "metricHeader": "", - "valueHeader": "" + "metricHeader": "Metrik", + "valueHeader": "Değer" }, "failedJobTable": { - "title": "", - "monitorIdHeader": "", - "monitorUrlHeader": "", - "failCountHeader": "", + "title": "Tamamlanamayan işler", + "monitorIdHeader": "Monitör ID", + "monitorUrlHeader": "Monitör URL", + "failCountHeader": "Hata sayısı", "failedAtHeader": "", "failReasonHeader": "" } }, "export": { - "title": "", - "success": "", - "failed": "" + "title": "Monitörleri dışarı al", + "success": "Monitörler başarıyla dışarıya alındı", + "failed": "Monitörleri dışarıya alırken bir hata oluştu" }, "monitorActions": { - "title": "", + "title": "Dışarı/İçeri al", "import": "", - "export": "" + "export": "", + "deleteSuccess": "Monitör başarıyla silindi", + "deleteFailed": "Monitör silinemedi", + "details": "Detaylar" }, "settingsPage": { "aboutSettings": { @@ -784,16 +799,17 @@ "labelTLSServername": "", "labelUser": "", "linkTransport": "", - "placeholderUser": "", - "title": "" + "placeholderUser": "Eğer gerekmiyorsa boş bırakabilirsiniz", + "title": "Eposta", + "toastEmailRequiredFieldsError": "" }, "pageSpeedSettings": { - "description": "", - "labelApiKeySet": "", - "labelApiKey": "", - "title": "" + "description": "Google PageSpeed izlemeyi etkinleştirmek için Google PageSpeed API anahtarınızı girin. Anahtarı güncellemek için Sıfırla'ya tıklayın.", + "labelApiKeySet": "API key tanımlandı. Sıfırla düğmesine tıklayarak değiştirebilirsiniz.", + "labelApiKey": "PageSpeed API anahtarı", + "title": "Google PageSpeed API anahtarı" }, - "saveButtonLabel": "", + "saveButtonLabel": "Kaydet", "statsSettings": { "clearAllStatsButton": "", "clearAllStatsDescription": "", @@ -802,35 +818,154 @@ "clearAllStatsDialogTitle": "", "description": "", "labelTTL": "", - "labelTTLOptional": "", - "title": "" + "labelTTLOptional": "Sonsuz için 0", + "title": "İzleme geçmişi" }, "systemResetSettings": { - "buttonRemoveAllMonitors": "", - "description": "", - "dialogConfirm": "", + "buttonRemoveAllMonitors": "Tüm monitörleri sil", + "description": "Sistemden tüm monitörleri sil", + "dialogConfirm": "Evet, tüm monitörleri sil", "dialogDescription": "", - "dialogTitle": "", - "title": "" + "dialogTitle": "Gerçekten tüm monitörleri silmek istiyor musunuz?", + "title": "Sistemi sıfırla" }, "timezoneSettings": { "description": "", - "label": "", - "title": "" + "label": "Görünüm zaman dilimi", + "title": "Görünüm zaman dilimi" }, - "title": "", + "title": "Ayarlar", "uiSettings": { - "description": "", - "labelLanguage": "", - "labelTheme": "", - "title": "" + "description": "Aydınlık ve karanlık mod arasında geçiş yapın ya da kullanıcı arayüzü dilini değiştirin.", + "labelLanguage": "Dil", + "labelTheme": "Tema modu", + "title": "Görünüm" }, "urlSettings": { "description": "", "label": "", - "selectDisabled": "", - "selectEnabled": "", + "selectDisabled": "Kapalı", + "selectEnabled": "Etkin", "title": "" } - } + }, + "statusPageCreate": { + "buttonSave": "" + }, + "incidentsOptionsHeaderFilterResolved": "", + "settingsSave": "", + "statusPageCreateAppearanceTitle": "", + "confirmPassword": "", + "monitorHooks": { + "failureAddDemoMonitors": "", + "successAddDemoMonitors": "" + }, + "settingsAppearance": "", + "settingsDisplayTimezone": "", + "settingsGeneralSettings": "", + "incidentsOptionsHeaderTotalIncidents": "", + "statusPage": { + "deleteSuccess": "Durum sayfası başarıyla silindi", + "deleteFailed": "Durum sayfası silinemedi", + "createSuccess": "Durum sayfası başarıyla oluşturuldu", + "updateSuccess": "Durum sayfası başarıyla güncellendi", + "generalSettings": "Genel ayarlar", + "contents": "İçerikler", + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "testNotificationsDisabled": "Bu izleyici için ayarlanmış bir bildirim yok. 'Yapılandır' düğmesine tıklayarak bir bildirim eklemeniz gerekiyor.", + "incidentsTableResolvedAt": "", + "incidentsTableActionResolve": "Çözümle", + "checkHooks": { + "failureResolveOne": "Olay çözümlenirken bir hata oluştu.", + "failureResolveAll": "Tüm olayları çözümlerken bir hata oluştu.", + "failureResolveMonitor": "" + }, + "checkFormError": "Formda hatalar var. Lütfen kontrol edin.", + "diagnosticsPage": { + "diagnosticDescription": "", + "statsDescription": "", + "gauges": { + "heapAllocationTitle": "", + "heapAllocationSubtitle": "", + "heapUsageTitle": "", + "heapUsageSubtitle": "", + "heapUtilizationTitle": "", + "heapUtilizationSubtitle": "", + "instantCpuUsageTitle": "", + "instantCpuUsageSubtitle": "" + }, + "stats": { + "eventLoopDelayTitle": "", + "uptimeTitle": "", + "usedHeapSizeTitle": "", + "totalHeapSizeTitle": "", + "osMemoryLimitTitle": "" + } + }, + "pageSpeedLighthouseAPI": "", + "time": { + "threeMinutes": "", + "fiveMinutes": "", + "tenMinutes": "", + "twentyMinutes": "", + "oneHour": "1 saat", + "oneDay": "1 gün", + "oneWeek": "1 hafta" + }, + "general": { + "noOptionsFound": "" + }, + "infrastructureMonitor": { + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "maintenanceWindow": { + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "pageSpeed": { + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "uptimeMonitor": { + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "editUserPage": { + "form": { + "email": "", + "firstName": "", + "lastName": "", + "role": "", + "save": "" + }, + "table": { + "actionHeader": "", + "roleHeader": "" + }, + "title": "", + "toast": { + "successUserUpdate": "", + "validationErrors": "" + } + }, + "incidentsPageActionResolveMonitor": "", + "incidentsPageActionResolveAll": "" } diff --git a/client/src/locales/uk.json b/client/src/locales/uk.json new file mode 100644 index 000000000..22b96487c --- /dev/null +++ b/client/src/locales/uk.json @@ -0,0 +1,838 @@ +{ + "submit": "", + "title": "", + "distributedStatusHeaderText": "", + "distributedStatusSubHeaderText": "", + "settingsDisabled": "", + "settingsSuccessSaved": "", + "settingsFailedToSave": "", + "settingsStatsCleared": "", + "settingsFailedToClearStats": "", + "settingsFailedToAddDemoMonitors": "", + "settingsMonitorsDeleted": "", + "settingsFailedToDeleteMonitors": "", + "starPromptTitle": "", + "starPromptDescription": "", + "https": "", + "http": "", + "monitor": "", + "aboutus": "", + "now": "", + "delete": "", + "configure": "", + "responseTime": "", + "ms": "", + "bar": "", + "area": "", + "country": "", + "city": "", + "response": "", + "monitorStatusUp": "", + "monitorStatusDown": "", + "webhookSendSuccess": "", + "webhookSendError": "", + "webhookUnsupportedPlatform": "", + "distributedRightCategoryTitle": "", + "distributedStatusServerMonitors": "", + "distributedStatusServerMonitorsDescription": "", + "distributedUptimeCreateSelectURL": "", + "distributedUptimeCreateChecks": "", + "distributedUptimeCreateChecksDescription": "", + "distributedUptimeCreateIncidentNotification": "", + "distributedUptimeCreateIncidentDescription": "", + "distributedUptimeCreateAdvancedSettings": "", + "distributedUptimeDetailsNoMonitorHistory": "", + "distributedUptimeDetailsStatusHeaderUptime": "", + "distributedUptimeDetailsStatusHeaderLastUpdate": "", + "notifications": { + "enableNotifications": "", + "testNotification": "", + "addOrEditNotifications": "", + "slack": { + "label": "", + "description": "", + "webhookLabel": "", + "webhookPlaceholder": "", + "webhookRequired": "" + }, + "discord": { + "label": "", + "description": "", + "webhookLabel": "", + "webhookPlaceholder": "", + "webhookRequired": "" + }, + "telegram": { + "label": "", + "description": "", + "tokenLabel": "", + "tokenPlaceholder": "", + "chatIdLabel": "", + "chatIdPlaceholder": "", + "fieldsRequired": "" + }, + "webhook": { + "label": "", + "description": "", + "urlLabel": "", + "urlPlaceholder": "", + "urlRequired": "" + }, + "testNotificationDevelop": "", + "integrationButton": "", + "testSuccess": "", + "testFailed": "", + "unsupportedType": "", + "networkError": "", + "fallback": { + "title": "", + "checks": [""] + }, + "createButton": "", + "createTitle": "", + "create": { + "success": "", + "failed": "" + }, + "fetch": { + "success": "", + "failed": "" + }, + "delete": { + "success": "", + "failed": "" + }, + "edit": { + "success": "", + "failed": "" + }, + "test": { + "success": "", + "failed": "" + } + }, + "testLocale": "", + "add": "", + "monitors": "", + "distributedUptimeStatusCreateStatusPage": "", + "distributedUptimeStatusCreateStatusPageAccess": "", + "distributedUptimeStatusCreateStatusPageReady": "", + "distributedUptimeStatusBasicInfoHeader": "", + "distributedUptimeStatusBasicInfoDescription": "", + "distributedUptimeStatusLogoHeader": "", + "distributedUptimeStatusLogoDescription": "", + "distributedUptimeStatusLogoUploadButton": "", + "distributedUptimeStatusStandardMonitorsHeader": "", + "distributedUptimeStatusStandardMonitorsDescription": "", + "distributedUptimeStatusCreateYour": "", + "distributedUptimeStatusEditYour": "", + "distributedUptimeStatusPublishedLabel": "", + "distributedUptimeStatusCompanyNameLabel": "", + "distributedUptimeStatusPageAddressLabel": "", + "distributedUptimeStatus30Days": "", + "distributedUptimeStatus60Days": "", + "distributedUptimeStatus90Days": "", + "distributedUptimeStatusPageNotSetUp": "", + "distributedUptimeStatusContactAdmin": "", + "distributedUptimeStatusPageNotPublic": "", + "distributedUptimeStatusPageDeleteDialog": "", + "distributedUptimeStatusPageDeleteConfirm": "", + "distributedUptimeStatusPageDeleteDescription": "", + "distributedUptimeStatusDevices": "", + "distributedUptimeStatusUpt": "", + "distributedUptimeStatusUptBurned": "", + "distributedUptimeStatusUptLogo": "", + "incidentsTableNoIncidents": "", + "incidentsTablePaginationLabel": "", + "incidentsTableMonitorName": "", + "incidentsTableStatus": "", + "incidentsTableDateTime": "", + "incidentsTableStatusCode": "", + "incidentsTableMessage": "", + "incidentsOptionsHeader": "", + "incidentsOptionsHeaderFilterBy": "", + "incidentsOptionsHeaderFilterAll": "", + "incidentsOptionsHeaderFilterDown": "", + "incidentsOptionsHeaderFilterCannotResolve": "", + "incidentsOptionsHeaderShow": "", + "incidentsOptionsHeaderLastHour": "", + "incidentsOptionsHeaderLastDay": "", + "incidentsOptionsHeaderLastWeek": "", + "incidentsOptionsPlaceholderAllServers": "", + "infrastructureCreateYour": "", + "infrastructureCreateGeneralSettingsDescription": "", + "infrastructureServerRequirement": "", + "infrastructureCustomizeAlerts": "", + "infrastructureAlertNotificationDescription": "", + "infrastructureCreateMonitor": "", + "infrastructureProtocol": "", + "infrastructureServerUrlLabel": "", + "infrastructureDisplayNameLabel": "", + "infrastructureAuthorizationSecretLabel": "", + "gb": "", + "mb": "", + "mem": "", + "memoryUsage": "", + "cpu": "", + "cpuUsage": "", + "cpuTemperature": "", + "diskUsage": "", + "used": "", + "total": "", + "cores": "", + "frequency": "", + "status": "", + "cpuPhysical": "", + "cpuLogical": "", + "cpuFrequency": "", + "avgCpuTemperature": "", + "memory": "", + "disk": "", + "uptime": "", + "os": "", + "host": "", + "actions": "", + "integrations": "", + "integrationsPrism": "", + "integrationsSlack": "", + "integrationsSlackInfo": "", + "integrationsDiscord": "", + "integrationsDiscordInfo": "", + "integrationsZapier": "", + "integrationsZapierInfo": "", + "commonSave": "", + "createYour": "", + "createMonitor": "", + "pause": "", + "resume": "", + "editing": "", + "url": "", + "access": "", + "timezone": "", + "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": "", + "message": "", + "low": "", + "high": "", + "statusCode": "", + "date&Time": "", + "type": "", + "statusPageName": "", + "publicURL": "", + "repeat": "", + "edit": "", + "createA": "", + "remove": "", + "maintenanceWindowDescription": "", + "startTime": "", + "timeZoneInfo": "", + "monitorsToApply": "", + "nextWindow": "", + "notFoundButton": "", + "pageSpeedConfigureSettingsDescription": "", + "monitorDisplayName": "", + "whenNewIncident": "", + "notifySMS": "", + "notifyEmails": "", + "seperateEmails": "", + "checkFrequency": "", + "chooseGame": "", + "matchMethod": "", + "expectedValue": "", + "deleteDialogTitle": "", + "deleteDialogDescription": "", + "pageSpeedMonitor": "", + "shown": "", + "ago": "", + "companyName": "", + "pageSpeedDetailsPerformanceReport": "", + "pageSpeedDetailsPerformanceReportCalculator": "", + "checkingEvery": "", + "statusPageCreateSettings": "", + "basicInformation": "", + "statusPageCreateBasicInfoDescription": "", + "statusPageCreateSelectTimeZoneDescription": "", + "statusPageCreateAppearanceDescription": "", + "statusPageCreateSettingsCheckboxLabel": "", + "statusPageCreateBasicInfoStatusPageAddress": "", + "statusPageCreateTabsContent": "", + "statusPageCreateTabsContentDescription": "", + "statusPageCreateTabsContentFeaturesDescription": "", + "showCharts": "", + "showUptimePercentage": "", + "removeLogo": "", + "statusPageStatus": "", + "statusPageStatusContactAdmin": "", + "statusPageStatusNotPublic": "", + "statusPageStatusNoPage": "", + "statusPageStatusServiceStatus": "", + "deleteStatusPage": "", + "deleteStatusPageConfirm": "", + "deleteStatusPageDescription": "", + "uptimeCreate": "", + "uptimeCreateJsonPath": "", + "uptimeCreateJsonPathQuery": "", + "maintenanceTableActionMenuDialogTitle": "", + "infrastructureEditYour": "", + "infrastructureEditMonitor": "", + "infrastructureMonitorCreated": "", + "infrastructureMonitorUpdated": "", + "errorInvalidTypeId": "", + "errorInvalidFieldId": "", + "inviteNoTokenFound": "", + "pageSpeedWarning": "", + "pageSpeedLearnMoreLink": "", + "pageSpeedAddApiKey": "", + "update": "", + "invalidFileFormat": "", + "invalidFileSize": "", + "ClickUpload": "", + "DragandDrop": "", + "MaxSize": "", + "SupportedFormats": "", + "FirstName": "", + "LastName": "", + "EmailDescriptionText": "", + "YourPhoto": "", + "PhotoDescriptionText": "", + "save": "", + "DeleteDescriptionText": "", + "DeleteAccountWarning": "", + "DeleteWarningTitle": "", + "bulkImport": { + "title": "", + "selectFileTips": "", + "selectFileDescription": "", + "selectFile": "", + "parsingFailed": "", + "uploadSuccess": "", + "validationFailed": "", + "noFileSelected": "", + "fallbackPage": "" + }, + "DeleteAccountTitle": "", + "DeleteAccountButton": "", + "publicLink": "", + "maskedPageSpeedKeyPlaceholder": "", + "reset": "", + "ignoreTLSError": "", + "tlsErrorIgnored": "", + "ignoreTLSErrorDescription": "", + "createNew": "", + "greeting": { + "prepend": "", + "append": "", + "overview": "" + }, + "roles": { + "superAdmin": "", + "admin": "", + "teamMember": "", + "demoUser": "" + }, + "teamPanel": { + "teamMembers": "", + "filter": { + "all": "", + "member": "" + }, + "inviteTeamMember": "", + "inviteNewTeamMember": "", + "inviteDescription": "", + "email": "", + "selectRole": "", + "inviteLink": "", + "cancel": "", + "noMembers": "", + "getToken": "", + "emailToken": "", + "table": { + "name": "", + "email": "", + "role": "", + "created": "" + } + }, + "monitorState": { + "paused": "", + "resumed": "", + "active": "" + }, + "menu": { + "uptime": "", + "pagespeed": "", + "infrastructure": "", + "incidents": "", + "statusPages": "", + "maintenance": "", + "integrations": "", + "settings": "", + "support": "", + "discussions": "", + "docs": "", + "changelog": "", + "profile": "", + "password": "", + "team": "", + "logOut": "", + "notifications": "", + "logs": "" + }, + "settingsEmailUser": "", + "state": "", + "statusBreadCrumbsStatusPages": "", + "statusBreadCrumbsDetails": "", + "commonSaving": "", + "navControls": "", + "incidentsPageTitle": "", + "passwordPanel": { + "passwordChangedSuccess": "", + "passwordInputIncorrect": "", + "currentPassword": "", + "enterCurrentPassword": "", + "newPassword": "", + "enterNewPassword": "", + "confirmNewPassword": "", + "passwordRequirements": "", + "saving": "" + }, + "emailSent": "", + "failedToSendEmail": "", + "settingsTestEmailSuccess": "", + "settingsTestEmailFailed": "", + "settingsTestEmailFailedWithReason": "", + "settingsTestEmailUnknownError": "", + "statusMsg": { + "paused": "", + "up": "", + "down": "", + "pending": "" + }, + "uptimeGeneralInstructions": { + "http": "", + "ping": "", + "docker": "", + "port": "", + "game": "" + }, + "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/vi.json b/client/src/locales/vi.json new file mode 100644 index 000000000..22b96487c --- /dev/null +++ b/client/src/locales/vi.json @@ -0,0 +1,838 @@ +{ + "submit": "", + "title": "", + "distributedStatusHeaderText": "", + "distributedStatusSubHeaderText": "", + "settingsDisabled": "", + "settingsSuccessSaved": "", + "settingsFailedToSave": "", + "settingsStatsCleared": "", + "settingsFailedToClearStats": "", + "settingsFailedToAddDemoMonitors": "", + "settingsMonitorsDeleted": "", + "settingsFailedToDeleteMonitors": "", + "starPromptTitle": "", + "starPromptDescription": "", + "https": "", + "http": "", + "monitor": "", + "aboutus": "", + "now": "", + "delete": "", + "configure": "", + "responseTime": "", + "ms": "", + "bar": "", + "area": "", + "country": "", + "city": "", + "response": "", + "monitorStatusUp": "", + "monitorStatusDown": "", + "webhookSendSuccess": "", + "webhookSendError": "", + "webhookUnsupportedPlatform": "", + "distributedRightCategoryTitle": "", + "distributedStatusServerMonitors": "", + "distributedStatusServerMonitorsDescription": "", + "distributedUptimeCreateSelectURL": "", + "distributedUptimeCreateChecks": "", + "distributedUptimeCreateChecksDescription": "", + "distributedUptimeCreateIncidentNotification": "", + "distributedUptimeCreateIncidentDescription": "", + "distributedUptimeCreateAdvancedSettings": "", + "distributedUptimeDetailsNoMonitorHistory": "", + "distributedUptimeDetailsStatusHeaderUptime": "", + "distributedUptimeDetailsStatusHeaderLastUpdate": "", + "notifications": { + "enableNotifications": "", + "testNotification": "", + "addOrEditNotifications": "", + "slack": { + "label": "", + "description": "", + "webhookLabel": "", + "webhookPlaceholder": "", + "webhookRequired": "" + }, + "discord": { + "label": "", + "description": "", + "webhookLabel": "", + "webhookPlaceholder": "", + "webhookRequired": "" + }, + "telegram": { + "label": "", + "description": "", + "tokenLabel": "", + "tokenPlaceholder": "", + "chatIdLabel": "", + "chatIdPlaceholder": "", + "fieldsRequired": "" + }, + "webhook": { + "label": "", + "description": "", + "urlLabel": "", + "urlPlaceholder": "", + "urlRequired": "" + }, + "testNotificationDevelop": "", + "integrationButton": "", + "testSuccess": "", + "testFailed": "", + "unsupportedType": "", + "networkError": "", + "fallback": { + "title": "", + "checks": [""] + }, + "createButton": "", + "createTitle": "", + "create": { + "success": "", + "failed": "" + }, + "fetch": { + "success": "", + "failed": "" + }, + "delete": { + "success": "", + "failed": "" + }, + "edit": { + "success": "", + "failed": "" + }, + "test": { + "success": "", + "failed": "" + } + }, + "testLocale": "", + "add": "", + "monitors": "", + "distributedUptimeStatusCreateStatusPage": "", + "distributedUptimeStatusCreateStatusPageAccess": "", + "distributedUptimeStatusCreateStatusPageReady": "", + "distributedUptimeStatusBasicInfoHeader": "", + "distributedUptimeStatusBasicInfoDescription": "", + "distributedUptimeStatusLogoHeader": "", + "distributedUptimeStatusLogoDescription": "", + "distributedUptimeStatusLogoUploadButton": "", + "distributedUptimeStatusStandardMonitorsHeader": "", + "distributedUptimeStatusStandardMonitorsDescription": "", + "distributedUptimeStatusCreateYour": "", + "distributedUptimeStatusEditYour": "", + "distributedUptimeStatusPublishedLabel": "", + "distributedUptimeStatusCompanyNameLabel": "", + "distributedUptimeStatusPageAddressLabel": "", + "distributedUptimeStatus30Days": "", + "distributedUptimeStatus60Days": "", + "distributedUptimeStatus90Days": "", + "distributedUptimeStatusPageNotSetUp": "", + "distributedUptimeStatusContactAdmin": "", + "distributedUptimeStatusPageNotPublic": "", + "distributedUptimeStatusPageDeleteDialog": "", + "distributedUptimeStatusPageDeleteConfirm": "", + "distributedUptimeStatusPageDeleteDescription": "", + "distributedUptimeStatusDevices": "", + "distributedUptimeStatusUpt": "", + "distributedUptimeStatusUptBurned": "", + "distributedUptimeStatusUptLogo": "", + "incidentsTableNoIncidents": "", + "incidentsTablePaginationLabel": "", + "incidentsTableMonitorName": "", + "incidentsTableStatus": "", + "incidentsTableDateTime": "", + "incidentsTableStatusCode": "", + "incidentsTableMessage": "", + "incidentsOptionsHeader": "", + "incidentsOptionsHeaderFilterBy": "", + "incidentsOptionsHeaderFilterAll": "", + "incidentsOptionsHeaderFilterDown": "", + "incidentsOptionsHeaderFilterCannotResolve": "", + "incidentsOptionsHeaderShow": "", + "incidentsOptionsHeaderLastHour": "", + "incidentsOptionsHeaderLastDay": "", + "incidentsOptionsHeaderLastWeek": "", + "incidentsOptionsPlaceholderAllServers": "", + "infrastructureCreateYour": "", + "infrastructureCreateGeneralSettingsDescription": "", + "infrastructureServerRequirement": "", + "infrastructureCustomizeAlerts": "", + "infrastructureAlertNotificationDescription": "", + "infrastructureCreateMonitor": "", + "infrastructureProtocol": "", + "infrastructureServerUrlLabel": "", + "infrastructureDisplayNameLabel": "", + "infrastructureAuthorizationSecretLabel": "", + "gb": "", + "mb": "", + "mem": "", + "memoryUsage": "", + "cpu": "", + "cpuUsage": "", + "cpuTemperature": "", + "diskUsage": "", + "used": "", + "total": "", + "cores": "", + "frequency": "", + "status": "", + "cpuPhysical": "", + "cpuLogical": "", + "cpuFrequency": "", + "avgCpuTemperature": "", + "memory": "", + "disk": "", + "uptime": "", + "os": "", + "host": "", + "actions": "", + "integrations": "", + "integrationsPrism": "", + "integrationsSlack": "", + "integrationsSlackInfo": "", + "integrationsDiscord": "", + "integrationsDiscordInfo": "", + "integrationsZapier": "", + "integrationsZapierInfo": "", + "commonSave": "", + "createYour": "", + "createMonitor": "", + "pause": "", + "resume": "", + "editing": "", + "url": "", + "access": "", + "timezone": "", + "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": "", + "message": "", + "low": "", + "high": "", + "statusCode": "", + "date&Time": "", + "type": "", + "statusPageName": "", + "publicURL": "", + "repeat": "", + "edit": "", + "createA": "", + "remove": "", + "maintenanceWindowDescription": "", + "startTime": "", + "timeZoneInfo": "", + "monitorsToApply": "", + "nextWindow": "", + "notFoundButton": "", + "pageSpeedConfigureSettingsDescription": "", + "monitorDisplayName": "", + "whenNewIncident": "", + "notifySMS": "", + "notifyEmails": "", + "seperateEmails": "", + "checkFrequency": "", + "chooseGame": "", + "matchMethod": "", + "expectedValue": "", + "deleteDialogTitle": "", + "deleteDialogDescription": "", + "pageSpeedMonitor": "", + "shown": "", + "ago": "", + "companyName": "", + "pageSpeedDetailsPerformanceReport": "", + "pageSpeedDetailsPerformanceReportCalculator": "", + "checkingEvery": "", + "statusPageCreateSettings": "", + "basicInformation": "", + "statusPageCreateBasicInfoDescription": "", + "statusPageCreateSelectTimeZoneDescription": "", + "statusPageCreateAppearanceDescription": "", + "statusPageCreateSettingsCheckboxLabel": "", + "statusPageCreateBasicInfoStatusPageAddress": "", + "statusPageCreateTabsContent": "", + "statusPageCreateTabsContentDescription": "", + "statusPageCreateTabsContentFeaturesDescription": "", + "showCharts": "", + "showUptimePercentage": "", + "removeLogo": "", + "statusPageStatus": "", + "statusPageStatusContactAdmin": "", + "statusPageStatusNotPublic": "", + "statusPageStatusNoPage": "", + "statusPageStatusServiceStatus": "", + "deleteStatusPage": "", + "deleteStatusPageConfirm": "", + "deleteStatusPageDescription": "", + "uptimeCreate": "", + "uptimeCreateJsonPath": "", + "uptimeCreateJsonPathQuery": "", + "maintenanceTableActionMenuDialogTitle": "", + "infrastructureEditYour": "", + "infrastructureEditMonitor": "", + "infrastructureMonitorCreated": "", + "infrastructureMonitorUpdated": "", + "errorInvalidTypeId": "", + "errorInvalidFieldId": "", + "inviteNoTokenFound": "", + "pageSpeedWarning": "", + "pageSpeedLearnMoreLink": "", + "pageSpeedAddApiKey": "", + "update": "", + "invalidFileFormat": "", + "invalidFileSize": "", + "ClickUpload": "", + "DragandDrop": "", + "MaxSize": "", + "SupportedFormats": "", + "FirstName": "", + "LastName": "", + "EmailDescriptionText": "", + "YourPhoto": "", + "PhotoDescriptionText": "", + "save": "", + "DeleteDescriptionText": "", + "DeleteAccountWarning": "", + "DeleteWarningTitle": "", + "bulkImport": { + "title": "", + "selectFileTips": "", + "selectFileDescription": "", + "selectFile": "", + "parsingFailed": "", + "uploadSuccess": "", + "validationFailed": "", + "noFileSelected": "", + "fallbackPage": "" + }, + "DeleteAccountTitle": "", + "DeleteAccountButton": "", + "publicLink": "", + "maskedPageSpeedKeyPlaceholder": "", + "reset": "", + "ignoreTLSError": "", + "tlsErrorIgnored": "", + "ignoreTLSErrorDescription": "", + "createNew": "", + "greeting": { + "prepend": "", + "append": "", + "overview": "" + }, + "roles": { + "superAdmin": "", + "admin": "", + "teamMember": "", + "demoUser": "" + }, + "teamPanel": { + "teamMembers": "", + "filter": { + "all": "", + "member": "" + }, + "inviteTeamMember": "", + "inviteNewTeamMember": "", + "inviteDescription": "", + "email": "", + "selectRole": "", + "inviteLink": "", + "cancel": "", + "noMembers": "", + "getToken": "", + "emailToken": "", + "table": { + "name": "", + "email": "", + "role": "", + "created": "" + } + }, + "monitorState": { + "paused": "", + "resumed": "", + "active": "" + }, + "menu": { + "uptime": "", + "pagespeed": "", + "infrastructure": "", + "incidents": "", + "statusPages": "", + "maintenance": "", + "integrations": "", + "settings": "", + "support": "", + "discussions": "", + "docs": "", + "changelog": "", + "profile": "", + "password": "", + "team": "", + "logOut": "", + "notifications": "", + "logs": "" + }, + "settingsEmailUser": "", + "state": "", + "statusBreadCrumbsStatusPages": "", + "statusBreadCrumbsDetails": "", + "commonSaving": "", + "navControls": "", + "incidentsPageTitle": "", + "passwordPanel": { + "passwordChangedSuccess": "", + "passwordInputIncorrect": "", + "currentPassword": "", + "enterCurrentPassword": "", + "newPassword": "", + "enterNewPassword": "", + "confirmNewPassword": "", + "passwordRequirements": "", + "saving": "" + }, + "emailSent": "", + "failedToSendEmail": "", + "settingsTestEmailSuccess": "", + "settingsTestEmailFailed": "", + "settingsTestEmailFailedWithReason": "", + "settingsTestEmailUnknownError": "", + "statusMsg": { + "paused": "", + "up": "", + "down": "", + "pending": "" + }, + "uptimeGeneralInstructions": { + "http": "", + "ping": "", + "docker": "", + "port": "", + "game": "" + }, + "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/zh-TW.json b/client/src/locales/zh-TW.json index 0c86b1e16..90a1aa30c 100644 --- a/client/src/locales/zh-TW.json +++ b/client/src/locales/zh-TW.json @@ -8,7 +8,6 @@ "settingsFailedToSave": "", "settingsStatsCleared": "", "settingsFailedToClearStats": "", - "settingsFailedToAddDemoMonitors": "", "settingsMonitorsDeleted": "", "settingsFailedToDeleteMonitors": "", "starPromptTitle": "", @@ -86,7 +85,8 @@ "networkError": "", "fallback": { "title": "", - "checks": [""] + "checks": [""], + "actionButton": "" }, "createButton": "", "createTitle": "", @@ -260,6 +260,7 @@ "notifyEmails": "", "seperateEmails": "", "checkFrequency": "", + "chooseGame": "", "matchMethod": "", "expectedValue": "", "deleteDialogTitle": "", @@ -331,7 +332,9 @@ "uploadSuccess": "", "validationFailed": "", "noFileSelected": "", - "fallbackPage": "" + "fallbackPage": "", + "invalidFileType": "", + "uploadFailed": "" }, "DeleteAccountTitle": "", "DeleteAccountButton": "", @@ -435,7 +438,8 @@ "http": "", "ping": "", "docker": "", - "port": "" + "port": "", + "game": "" }, "common": { "appName": "", @@ -537,6 +541,11 @@ "errors": { "incorrect": "" } + }, + "role": { + "errors": { + "min": "" + } } } }, @@ -675,7 +684,10 @@ "description": "", "webhookLabel": "", "webhookPlaceholder": "" - } + }, + "testNotification": "", + "dialogDeleteTitle": "", + "dialogDeleteConfirm": "" }, "notificationConfig": { "title": "", @@ -690,7 +702,6 @@ }, "advancedMatching": "", "sendTestNotifications": "", - "testNotificationsDisabled": "", "selectAll": "", "showAdminLoginLink": "", "logsPage": { @@ -698,7 +709,8 @@ "description": "", "tabs": { "queue": "", - "logs": "" + "logs": "", + "diagnostics": "" }, "toast": { "fetchLogsSuccess": "" @@ -753,7 +765,10 @@ "monitorActions": { "title": "", "import": "", - "export": "" + "export": "", + "deleteSuccess": "", + "deleteFailed": "", + "details": "" }, "settingsPage": { "aboutSettings": { @@ -785,7 +800,8 @@ "labelUser": "", "linkTransport": "", "placeholderUser": "", - "title": "" + "title": "", + "toastEmailRequiredFieldsError": "" }, "pageSpeedSettings": { "description": "", @@ -832,5 +848,124 @@ "selectEnabled": "", "title": "" } - } + }, + "statusPageCreate": { + "buttonSave": "" + }, + "incidentsOptionsHeaderFilterResolved": "", + "settingsSave": "", + "statusPageCreateAppearanceTitle": "", + "confirmPassword": "", + "monitorHooks": { + "failureAddDemoMonitors": "", + "successAddDemoMonitors": "" + }, + "settingsAppearance": "", + "settingsDisplayTimezone": "", + "settingsGeneralSettings": "", + "incidentsOptionsHeaderTotalIncidents": "", + "statusPage": { + "deleteSuccess": "", + "deleteFailed": "", + "createSuccess": "", + "updateSuccess": "", + "generalSettings": "", + "contents": "", + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "testNotificationsDisabled": "", + "incidentsTableResolvedAt": "", + "incidentsTableActionResolve": "", + "checkHooks": { + "failureResolveOne": "", + "failureResolveAll": "", + "failureResolveMonitor": "" + }, + "checkFormError": "", + "diagnosticsPage": { + "diagnosticDescription": "", + "statsDescription": "", + "gauges": { + "heapAllocationTitle": "", + "heapAllocationSubtitle": "", + "heapUsageTitle": "", + "heapUsageSubtitle": "", + "heapUtilizationTitle": "", + "heapUtilizationSubtitle": "", + "instantCpuUsageTitle": "", + "instantCpuUsageSubtitle": "" + }, + "stats": { + "eventLoopDelayTitle": "", + "uptimeTitle": "", + "usedHeapSizeTitle": "", + "totalHeapSizeTitle": "", + "osMemoryLimitTitle": "" + } + }, + "pageSpeedLighthouseAPI": "", + "time": { + "threeMinutes": "", + "fiveMinutes": "", + "tenMinutes": "", + "twentyMinutes": "", + "oneHour": "", + "oneDay": "", + "oneWeek": "" + }, + "general": { + "noOptionsFound": "" + }, + "infrastructureMonitor": { + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "maintenanceWindow": { + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "pageSpeed": { + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "uptimeMonitor": { + "fallback": { + "checks": [""], + "title": "", + "actionButton": "" + } + }, + "editUserPage": { + "form": { + "email": "", + "firstName": "", + "lastName": "", + "role": "", + "save": "" + }, + "table": { + "actionHeader": "", + "roleHeader": "" + }, + "title": "", + "toast": { + "successUserUpdate": "", + "validationErrors": "" + } + }, + "incidentsPageActionResolveMonitor": "", + "incidentsPageActionResolveAll": "" } diff --git a/client/vite.config.js b/client/vite.config.js index 44a5905c5..df686d751 100644 --- a/client/vite.config.js +++ b/client/vite.config.js @@ -5,7 +5,7 @@ import { execSync } from "child_process"; export default defineConfig(({ mode }) => { const env = loadEnv(mode, process.cwd(), ""); - let version = 2.3; + let version = "3.0-beta"; return { base: "/", diff --git a/docker/.gitignore b/docker/.gitignore index 9a6df883a..76567d514 100755 --- a/docker/.gitignore +++ b/docker/.gitignore @@ -12,3 +12,13 @@ dist/docker-compose-test.yaml /dist-mono/redis/data/* /dist-arm/mongo/data/* *.env + +# Custom CA certificates and keys (should not be committed) +dev/certs/* +!dev/certs/.gitkeep + +# Certificate artifacts +*.key +*.csr +*.srl +*.log diff --git a/docker/dev/certs/.gitkeep b/docker/dev/certs/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/docker/dev/docker-compose.custom-ca-example.yaml b/docker/dev/docker-compose.custom-ca-example.yaml new file mode 100644 index 000000000..a5d7e2f43 --- /dev/null +++ b/docker/dev/docker-compose.custom-ca-example.yaml @@ -0,0 +1,30 @@ +# Dev/Test only: Not required in production +# Example Docker Compose configuration for custom CA trust +# This file demonstrates how to configure Checkmate to trust custom CAs +# +# Usage: docker-compose -f docker-compose.yaml -f docker-compose.custom-ca-example.yaml up + +services: + server: + image: uptime_server:latest + restart: always + ports: + - "52345:52345" + env_file: + - server.env + environment: + # Mount your custom CA certificate + NODE_EXTRA_CA_CERTS: /certs/custom-ca.pem + volumes: + # Mount the certs directory (read-only for security) + - ./certs:/certs:ro + depends_on: + - redis + - mongodb + # Optional: Add healthcheck to ensure the server starts properly + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:52345/api/v1/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s diff --git a/docker/dev/docker-compose.yaml b/docker/dev/docker-compose.yaml index 292d0b1d5..a2c66c027 100755 --- a/docker/dev/docker-compose.yaml +++ b/docker/dev/docker-compose.yaml @@ -19,27 +19,15 @@ services: env_file: - server.env depends_on: - - redis - mongodb - redis: - image: uptime_redis:latest - restart: always - volumes: - - ./redis/data:/data - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 30s - timeout: 10s - retries: 5 - start_period: 5s mongodb: image: uptime_mongo:latest restart: always - command: ["mongod", "--quiet", "--replSet", "rs0", "--bind_ip_all"] + command: ["mongod", "--quiet", "--bind_ip_all"] volumes: - ./mongo/data:/data/db healthcheck: - test: echo "try { rs.status() } catch (err) { rs.initiate({_id:'rs0',members:[{_id:0,host:'mongodb:27017'}]}) }" | mongosh --port 27017 --quiet + test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')", "--quiet"] interval: 5s timeout: 30s start_period: 0s diff --git a/docker/dev/nginx-test/docker-compose.nginx-test.yaml b/docker/dev/nginx-test/docker-compose.nginx-test.yaml new file mode 100644 index 000000000..766e839bd --- /dev/null +++ b/docker/dev/nginx-test/docker-compose.nginx-test.yaml @@ -0,0 +1,13 @@ +# Dev/Test only: Not required in production +# Docker Compose configuration for nginx test service + +services: + nginx-test: + image: nginx:alpine + ports: + - "8443:443" + volumes: + - ../certs/host-int-cert.pem:/etc/nginx/certs/server.crt:ro + - ../certs/host-int-key.pem:/etc/nginx/certs/server.key:ro + - ./nginx.conf:/etc/nginx/nginx.conf:ro + restart: unless-stopped diff --git a/docker/dev/nginx-test/nginx.conf b/docker/dev/nginx-test/nginx.conf new file mode 100644 index 000000000..81c4c9240 --- /dev/null +++ b/docker/dev/nginx-test/nginx.conf @@ -0,0 +1,14 @@ +# Dev/Test only: Not required in production +# Nginx configuration for testing custom CA trust functionality + +events {} +http { + server { + listen 443 ssl; + ssl_certificate /etc/nginx/certs/server.crt; + ssl_certificate_key /etc/nginx/certs/server.key; + location / { + return 200 "hello from tls\n"; + } + } +} diff --git a/docker/dev/redis.Dockerfile b/docker/dev/redis.Dockerfile deleted file mode 100755 index af68ec61e..000000000 --- a/docker/dev/redis.Dockerfile +++ /dev/null @@ -1,2 +0,0 @@ -FROM redis -EXPOSE 6379 \ No newline at end of file diff --git a/docker/dev/server.Dockerfile b/docker/dev/server.Dockerfile index d9d99a09d..a3542c168 100755 --- a/docker/dev/server.Dockerfile +++ b/docker/dev/server.Dockerfile @@ -10,4 +10,4 @@ COPY ./server/ ./ EXPOSE 52345 -CMD ["node", "index.js"] \ No newline at end of file +CMD ["node", "src/index.js"] \ No newline at end of file diff --git a/docker/dist-arm/docker-compose.yaml b/docker/dist-arm/docker-compose.yaml index 921d3b8c1..96bbf8f3f 100644 --- a/docker/dist-arm/docker-compose.yaml +++ b/docker/dist-arm/docker-compose.yaml @@ -7,7 +7,7 @@ services: environment: - UPTIME_APP_API_BASE_URL=http://localhost:52345/api/v1 - UPTIME_APP_CLIENT_HOST=http://localhost - - DB_CONNECTION_STRING=mongodb://mongodb:27017/uptime_db?replicaSet=rs0 + - DB_CONNECTION_STRING=mongodb://mongodb:27017/uptime_db - REDIS_URL=redis://redis:6379 - CLIENT_HOST=http://localhost - JWT_SECRET=my_secret @@ -22,22 +22,10 @@ services: - "27017:27017" volumes: - ./mongo/data:/data/db - - mongo-init: - image: mongo:4.4.18 - depends_on: - - mongodb - entrypoint: > - bash -c " - echo 'Waiting for MongoDB to be ready...' && - until mongo --host mongodb --eval 'db.adminCommand(\"ping\")' > /dev/null 2>&1; do - sleep 2 - done && - echo 'MongoDB is up. Initiating replica set...' && - mongo --host mongodb --eval ' - rs.initiate({ - _id: \"rs0\", - members: [{ _id: 0, host: \"mongodb:27017\" }] - }) - ' || echo 'Replica set may already be initialized.' - " + healthcheck: + test: ["CMD", "mongo", "--eval", "db.runCommand({ ping: 1 })", "--quiet"] + interval: 5s + timeout: 30s + start_period: 0s + start_interval: 1s + retries: 30 diff --git a/docker/dist-arm/server.Dockerfile b/docker/dist-arm/server.Dockerfile index 981d1cf8f..c6d31a893 100644 --- a/docker/dist-arm/server.Dockerfile +++ b/docker/dist-arm/server.Dockerfile @@ -22,4 +22,4 @@ RUN chmod +x ./scripts/inject-vars.sh EXPOSE 52345 -CMD ./scripts/inject-vars.sh && node ./index.js +CMD ./scripts/inject-vars.sh && node ./src/index.js diff --git a/docker/dist-mono/build_images.sh b/docker/dist-mono/build_images.sh index b56b9052c..fd3a163cc 100755 --- a/docker/dist-mono/build_images.sh +++ b/docker/dist-mono/build_images.sh @@ -8,7 +8,6 @@ cd ../.. services=("mono_mongo" "mono_redis" "mono_server") dockerfiles=( "./docker/dist-mono/mongoDB.Dockerfile" - "./docker/dist-mono/redis.Dockerfile" "./docker/dist-mono/server.Dockerfile" ) diff --git a/docker/dist-mono/docker-compose.yaml b/docker/dist-mono/docker-compose.yaml index 0dd97aafb..0e0b49445 100755 --- a/docker/dist-mono/docker-compose.yaml +++ b/docker/dist-mono/docker-compose.yaml @@ -7,8 +7,7 @@ services: environment: - UPTIME_APP_API_BASE_URL=http://localhost:52345/api/v1 - UPTIME_APP_CLIENT_HOST=http://localhost - - DB_CONNECTION_STRING=mongodb://mongodb:27017/uptime_db?replicaSet=rs0 - - REDIS_URL=redis://redis:6379 + - DB_CONNECTION_STRING=mongodb://mongodb:27017/uptime_db - CLIENT_HOST=http://localhost - JWT_SECRET=my_secret depends_on: @@ -16,11 +15,11 @@ services: mongodb: image: ghcr.io/bluewave-labs/checkmate-mongo:latest restart: always - command: ["mongod", "--quiet", "--replSet", "rs0", "--bind_ip_all"] + command: ["mongod", "--quiet", "--bind_ip_all"] volumes: - ./mongo/data:/data/db healthcheck: - test: echo "try { rs.status() } catch (err) { rs.initiate({_id:'rs0',members:[{_id:0,host:'mongodb:27017'}]}) }" | mongosh --port 27017 --quiet + test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')", "--quiet"] interval: 5s timeout: 30s start_period: 0s diff --git a/docker/dist-mono/redis.Dockerfile b/docker/dist-mono/redis.Dockerfile deleted file mode 100755 index af68ec61e..000000000 --- a/docker/dist-mono/redis.Dockerfile +++ /dev/null @@ -1,2 +0,0 @@ -FROM redis -EXPOSE 6379 \ No newline at end of file diff --git a/docker/dist-mono/server.Dockerfile b/docker/dist-mono/server.Dockerfile index f28c834ea..4063e4d7a 100644 --- a/docker/dist-mono/server.Dockerfile +++ b/docker/dist-mono/server.Dockerfile @@ -22,4 +22,4 @@ RUN chmod +x ./scripts/inject-vars.sh EXPOSE 52345 -CMD ./scripts/inject-vars.sh && node ./index.js +CMD ./scripts/inject-vars.sh && node ./src/index.js diff --git a/docker/dist/build_images.sh b/docker/dist/build_images.sh index ccfa9cc85..84fbb7d5a 100755 --- a/docker/dist/build_images.sh +++ b/docker/dist/build_images.sh @@ -7,15 +7,14 @@ cd ../../ declare -A services=( ["bluewaveuptime/uptime_client"]="./docker/dist/client.Dockerfile" ["bluewaveuptime/uptime_database_mongo"]="./docker/dist/mongoDB.Dockerfile" - ["bluewaveuptime/uptime_redis"]="./docker/dist/redis.Dockerfile" ["bluewaveuptime/uptime_server"]="./docker/dist/server.Dockerfile" ) for service in "${!services[@]}"; do - docker buildx build \ - --platform linux/amd64,linux/arm64 \ + docker build \ -f "${services[$service]}" \ -t "$service" \ + . if [ $? -ne 0 ]; then echo "Error building $service image. Exiting..." diff --git a/docker/dist/docker-compose.yaml b/docker/dist/docker-compose.yaml index ff2f1b7ea..2e845daab 100755 --- a/docker/dist/docker-compose.yaml +++ b/docker/dist/docker-compose.yaml @@ -18,8 +18,7 @@ services: depends_on: - mongodb environment: - - DB_CONNECTION_STRING=mongodb://mongodb:27017/uptime_db?replicaSet=rs0 - - REDIS_URL=redis://redis:6379 + - DB_CONNECTION_STRING=mongodb://mongodb:27017/uptime_db - CLIENT_HOST=http://localhost - JWT_SECRET=my_secret # volumes: @@ -29,9 +28,9 @@ services: restart: always volumes: - ./mongo/data:/data/db - command: ["mongod", "--quiet", "--replSet", "rs0", "--bind_ip_all"] + command: ["mongod", "--quiet", "--bind_ip_all"] healthcheck: - test: echo "try { rs.status() } catch (err) { rs.initiate({_id:'rs0',members:[{_id:0,host:'mongodb:27017'}]}) }" | mongosh --port 27017 --quiet + test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')", "--quiet"] interval: 5s timeout: 30s start_period: 0s diff --git a/docker/dist/redis.Dockerfile b/docker/dist/redis.Dockerfile deleted file mode 100755 index af68ec61e..000000000 --- a/docker/dist/redis.Dockerfile +++ /dev/null @@ -1,2 +0,0 @@ -FROM redis -EXPOSE 6379 \ No newline at end of file diff --git a/docker/dist/server.Dockerfile b/docker/dist/server.Dockerfile index d9d99a09d..a3542c168 100755 --- a/docker/dist/server.Dockerfile +++ b/docker/dist/server.Dockerfile @@ -10,4 +10,4 @@ COPY ./server/ ./ EXPOSE 52345 -CMD ["node", "index.js"] \ No newline at end of file +CMD ["node", "src/index.js"] \ No newline at end of file diff --git a/docker/prod/build_images.sh b/docker/prod/build_images.sh index 23e91355c..49f8c7841 100755 --- a/docker/prod/build_images.sh +++ b/docker/prod/build_images.sh @@ -8,7 +8,6 @@ cd ../../ declare -A services=( ["uptime_client"]="./docker/prod/client.Dockerfile" ["uptime_database_mongo"]="./docker/prod/mongoDB.Dockerfile" - ["uptime_redis"]="./docker/prod/redis.Dockerfile" ["uptime_server"]="./docker/prod/server.Dockerfile" ) diff --git a/docker/prod/docker-compose.yaml b/docker/prod/docker-compose.yaml index 980a0e2f5..6e29fd90a 100755 --- a/docker/prod/docker-compose.yaml +++ b/docker/prod/docker-compose.yaml @@ -37,16 +37,26 @@ services: mongodb: image: ghcr.io/bluewave-labs/checkmate:mongo-demo restart: always - command: ["mongod", "--quiet", "--replSet", "rs0", "--bind_ip_all"] + command: ["mongod", "--quiet", "--bind_ip_all"] volumes: - ./mongo/data:/data/db - # - ./mongo/init/init.js:/docker-entrypoint-initdb.d/init.js // No longer needed env_file: - mongo.env healthcheck: - test: echo "try { rs.status() } catch (err) { rs.initiate({_id:'rs0',members:[{_id:0,host:'mongodb:27017'}]}) }" | mongosh --port 27017 --quiet + test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')", "--quiet"] interval: 5s timeout: 30s start_period: 0s start_interval: 1s retries: 30 + capture: + image: ghcr.io/bluewave-labs/capture:latest + container_name: capture + restart: unless-stopped + ports: + - "59232:59232" + environment: + - API_SECRET=my_secret + - GIN_MODE=release + volumes: + - /etc/os-release:/etc/os-release:ro diff --git a/docker/prod/redis.Dockerfile b/docker/prod/redis.Dockerfile deleted file mode 100755 index af68ec61e..000000000 --- a/docker/prod/redis.Dockerfile +++ /dev/null @@ -1,2 +0,0 @@ -FROM redis -EXPOSE 6379 \ No newline at end of file diff --git a/docker/prod/server.Dockerfile b/docker/prod/server.Dockerfile index c80f468f8..94018c07d 100755 --- a/docker/prod/server.Dockerfile +++ b/docker/prod/server.Dockerfile @@ -12,4 +12,4 @@ COPY ./server ./ EXPOSE 52345 -CMD ["node", "index.js"] \ No newline at end of file +CMD ["node", "src/index.js"] \ No newline at end of file diff --git a/docker/staging/build_images.sh b/docker/staging/build_images.sh index ccfd451ba..a11aebf7c 100755 --- a/docker/staging/build_images.sh +++ b/docker/staging/build_images.sh @@ -8,7 +8,6 @@ cd ../../ declare -A services=( ["uptime_client"]="./docker/staging/client.Dockerfile" ["uptime_database_mongo"]="./docker/staging/mongoDB.Dockerfile" - ["uptime_redis"]="./docker/staging/redis.Dockerfile" ["uptime_server"]="./docker/staging/server.Dockerfile" ) diff --git a/docker/staging/docker-compose.yaml b/docker/staging/docker-compose.yaml index b9ca52b6e..f4ed5ce6f 100755 --- a/docker/staging/docker-compose.yaml +++ b/docker/staging/docker-compose.yaml @@ -31,36 +31,32 @@ services: env_file: - server.env depends_on: - - redis - mongodb - redis: - image: ghcr.io/bluewave-labs/checkmate:redis-staging - restart: always - ports: - - "6379:6379" - volumes: - - ./redis/data:/data - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 30s - timeout: 10s - retries: 5 - start_period: 5s mongodb: image: ghcr.io/bluewave-labs/checkmate:mongo-staging restart: always - command: ["mongod", "--quiet", "--replSet", "rs0", "--bind_ip_all"] + command: ["mongod", "--quiet", "--bind_ip_all"] ports: - "27017:27017" volumes: - ./mongo/data:/data/db - # - ./mongo/init/02_create_users.js:/docker-entrypoint-initdb.d/02_create_users.js // No longer needed env_file: - mongo.env healthcheck: - test: echo "try { rs.status() } catch (err) { rs.initiate({_id:'rs0',members:[{_id:0,host:'mongodb:27017'}]}) }" | mongosh --port 27017 --quiet + test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')", "--quiet"] interval: 5s timeout: 30s start_period: 0s start_interval: 1s retries: 30 + capture: + image: ghcr.io/bluewave-labs/capture:latest + container_name: capture + restart: unless-stopped + ports: + - "59232:59232" + environment: + - API_SECRET=my_secret + - GIN_MODE=release + volumes: + - /etc/os-release:/etc/os-release:ro diff --git a/docker/staging/redis.Dockerfile b/docker/staging/redis.Dockerfile deleted file mode 100755 index af68ec61e..000000000 --- a/docker/staging/redis.Dockerfile +++ /dev/null @@ -1,2 +0,0 @@ -FROM redis -EXPOSE 6379 \ No newline at end of file diff --git a/docker/staging/server.Dockerfile b/docker/staging/server.Dockerfile index c80f468f8..94018c07d 100755 --- a/docker/staging/server.Dockerfile +++ b/docker/staging/server.Dockerfile @@ -12,4 +12,4 @@ COPY ./server ./ EXPOSE 52345 -CMD ["node", "index.js"] \ No newline at end of file +CMD ["node", "src/index.js"] \ No newline at end of file diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..dc245e21e --- /dev/null +++ b/docs/README.md @@ -0,0 +1,38 @@ +# Checkmate Documentation + +Welcome to the Checkmate documentation. This directory contains guides and references for various aspects of Checkmate deployment and configuration. + +## Available Documentation + +### Deployment & Configuration +- **[Custom CA Trust Guide](./custom-ca-trust.md)** - Configure Checkmate to trust custom Certificate Authorities +- **[Custom CA Quick Reference](./custom-ca-quick-reference.md)** - Quick setup guide for custom CA trust + +## Docker Configuration + +### Custom CA Trust +If you need to monitor internal HTTPS endpoints with certificates from private Certificate Authorities (like Smallstep), see our custom CA trust documentation: + +- **Full Guide**: [Custom CA Trust Guide](./custom-ca-trust.md) +- **Quick Reference**: [Custom CA Quick Reference](./custom-ca-quick-reference.md) + +### Example Configurations +The `docker/dev/` directory contains example configurations: +- `docker-compose.custom-ca-example.yaml` - Example Docker Compose with custom CA +- `server-custom-ca.Dockerfile` - Example Dockerfile for OS-level CA trust +- `export-smallstep-ca.sh` - Helper script for Smallstep CA export + +## Contributing to Documentation + +If you find issues or want to improve the documentation: + +1. Check existing issues and discussions +2. Submit a pull request with your changes +3. Follow the same markdown formatting style +4. Include practical examples and code snippets + +## Getting Help + +- [GitHub Discussions](https://github.com/bluewave-labs/checkmate/discussions) +- [Discord Channel](https://discord.gg/NAb6H3UTjK) +- [Documentation Portal](https://docs.checkmate.so/) diff --git a/docs/custom-ca-quick-reference.md b/docs/custom-ca-quick-reference.md new file mode 100644 index 000000000..71ea3b954 --- /dev/null +++ b/docs/custom-ca-quick-reference.md @@ -0,0 +1,60 @@ +# Custom CA Trust - Quick Reference + +## Fast Setup (Node-level) + +1. **Export your CA certificate:** + ```bash + # For Smallstep + step certificate inspect --format pem step-ca/root_ca.crt > docker/dev/certs/custom-ca.pem + ``` + +2. **Use the provided example override:** + ```bash + docker-compose -f docker-compose.yaml -f docker-compose.custom-ca-example.yaml up -d + ``` + +3. **Or manually update docker-compose.yaml:** + ```yaml + services: + server: + environment: + NODE_EXTRA_CA_CERTS: /certs/custom-ca.pem + volumes: + - ./certs:/certs:ro + ``` + +## Alternative Setup (OS-level) + +1. **Use custom Dockerfile:** + ```bash + docker-compose -f docker-compose.yaml -f docker-compose.custom-ca.yaml up + ``` + +## File Structure +``` +docker/dev/ +├── docker-compose.yaml +├── docker-compose.custom-ca-example.yaml # Example config +├── certs/ +│ ├── README.md +│ └── custom-ca.pem # Your CA certificate +└── export-smallstep-ca.sh # Helper script +``` + +## Environment Variables +- `NODE_EXTRA_CA_CERTS=/certs/custom-ca.pem` - Node.js trust +- Mount `./certs:/certs:ro` - Volume mount + +## Security +- ✅ Only trust CAs you control +- ✅ Use read-only volume mounts +- ✅ Keep certificates out of version control +- ❌ Never trust untrusted CAs + +## Troubleshooting +- Check container logs: `docker-compose logs server` +- Verify certificate: `docker exec -it ls -la /certs/` +- Test connection: `docker exec -it wget --ca-certificate=/certs/custom-ca.pem https://your-internal-site.com` + +## Full Documentation +See [Custom CA Trust Guide](./custom-ca-trust.md) for detailed instructions. diff --git a/docs/custom-ca-trust.md b/docs/custom-ca-trust.md new file mode 100644 index 000000000..b3a19831f --- /dev/null +++ b/docs/custom-ca-trust.md @@ -0,0 +1,234 @@ +# Custom Certificate Authority Trust + +This guide explains how to configure Checkmate to trust custom Certificate Authorities (CAs) when running in Docker, particularly useful for internal/private CAs like Smallstep. + +## Understanding Certificate Authorities + +Certificate Authorities (CAs) are entities that issue and manage digital certificates. While public CAs (like Let's Encrypt, DigiCert) are trusted by default in most systems, private or internal CAs (like those issued by Smallstep, internal PKI systems) require explicit trust configuration. + +When Checkmate monitors HTTPS endpoints with certificates from private CAs, it may show them as "DOWN" due to certificate validation failures, even if the service is actually accessible. + +## Node-level Trust Approach + +The simplest approach is to mount your custom CA certificate and configure Node.js to trust it using the `NODE_EXTRA_CA_CERTS` environment variable. + +### Docker Compose Configuration + +Add a volume mount for your CA certificate and set the environment variable: + +```yaml +services: + server: + image: uptime_server:latest + restart: always + ports: + - "52345:52345" + env_file: + - server.env + environment: + NODE_EXTRA_CA_CERTS: /certs/custom-ca.pem + volumes: + - ./certs:/certs:ro + depends_on: + - redis + - mongodb +``` + +### Directory Structure + +Create a `certs` directory in your Docker Compose project root: + +``` +docker/dev/ +├── docker-compose.yaml +├── certs/ +│ └── custom-ca.pem +└── ... +``` + +## OS-level Trust Approach (Debian-based) + +For more comprehensive trust configuration, you can create a derived Dockerfile that installs your CA at the OS level. + +### Custom Dockerfile + +Create `docker/dev/server-custom-ca.Dockerfile`: + +```dockerfile +FROM node:20-alpine + +# Install ca-certificates for Alpine +RUN apk add --no-cache ca-certificates + +WORKDIR /app + +COPY ./server/package*.json ./ + +RUN npm install + +COPY ./server/ ./ + +# Copy your custom CA certificate +COPY ./certs/custom-ca.crt /usr/local/share/ca-certificates/ + +# Update CA certificates +RUN update-ca-certificates + +EXPOSE 52345 + +CMD ["node", "src/index.js"] +``` + +### Docker Compose Override + +Create `docker/dev/docker-compose.custom-ca.yaml`: + +```yaml +services: + server: + build: + context: . + dockerfile: server-custom-ca.Dockerfile + restart: always + ports: + - "52345:52345" + env_file: + - server.env + depends_on: + - redis + - mongodb +``` + +Run with: `docker-compose -f docker-compose.yaml -f docker-compose.custom-ca.yaml up` + +## Alpine Linux Considerations + +Since Checkmate uses Alpine Linux as the base image, you need to install the `ca-certificates` package: + +```dockerfile +# Install ca-certificates for Alpine +RUN apk add --no-cache ca-certificates + +# Copy and update CA certificates +COPY ./certs/custom-ca.crt /usr/local/share/ca-certificates/ +RUN update-ca-certificates +``` + +## Smallstep CA Configuration + +If you're using Smallstep as your internal CA, you can export the root CA certificate: + +### Export Smallstep Root CA + +```bash +# Export the root CA certificate +step certificate inspect --format pem step-ca/root_ca.crt > custom-ca.pem + +# Or if you have the CA URL configured +step certificate inspect --format pem $(step path)/certs/root_ca.crt > custom-ca.pem +``` + +### Using the Exported Certificate + +1. Copy the exported `custom-ca.pem` to your `docker/dev/certs/` directory +2. Use either the Node-level or OS-level approach above +3. Restart your Checkmate server container + +## Security Considerations + +⚠️ **Important Security Warning**: Only trust CAs that you control or explicitly trust. Adding untrusted CAs can compromise the security of your monitoring system. + +- **Private CAs**: Only trust CAs from your organization's PKI infrastructure +- **Self-signed certificates**: Consider using proper CA infrastructure instead +- **Certificate validation**: Ensure your CA certificates are valid and not expired +- **Access control**: Limit access to the CA certificate files in production + +## Troubleshooting + +### Verify CA Trust + +Test if your CA is trusted by the container: + +```bash +# Enter the running container +docker exec -it sh + +# Check if the CA is in the trust store +ls -la /usr/local/share/ca-certificates/ +cat /etc/ssl/certs/ca-certificates.crt | grep -A 5 -B 5 "YOUR_CA_NAME" +``` + +### Common Issues + +1. **Permission denied**: Ensure the CA certificate file has proper read permissions +2. **Certificate format**: Use PEM format (.pem, .crt) for best compatibility +3. **Container restart**: Always restart the container after adding new CA certificates +4. **Path issues**: Verify the certificate path in your volume mounts + +## Example: Complete Working Setup + +Here's a complete example for a Smallstep CA setup. This demonstrates both the baseline failure (expected) and the custom CA success: + +### Baseline Test (Should Fail) +1. **Start Checkmate without custom CA trust:** + ```bash + cd docker/dev + docker-compose up -d + ``` + +2. **Test connection to internal HTTPS endpoint:** + ```bash + # This should fail with TLS error (unknown CA) + docker-compose exec server node -e " + const https = require('https'); + https.get('https://your-internal-site.com', res => { + console.log('STATUS:', res.statusCode); + }).on('error', e => { + console.error('ERR:', e.message); + }); + " + ``` + **Expected result**: TLS error due to unknown CA + +### Custom CA Test (Should Succeed) +1. **Export Smallstep Root CA:** + ```bash + step certificate inspect --format pem step-ca/root_ca.crt > docker/dev/certs/smallstep-root-ca.pem + ``` + +2. **Update docker-compose.yaml with custom CA trust:** + ```yaml + services: + server: + environment: + NODE_EXTRA_CA_CERTS: /certs/smallstep-root-ca.pem + volumes: + - ./certs:/certs:ro + ``` + +3. **Restart with custom CA:** + ```bash + docker-compose down + docker-compose -f docker-compose.yaml -f docker-compose.custom-ca-example.yaml up -d + ``` + +4. **Test the same connection:** + ```bash + # This should now succeed + docker-compose exec server node -e " + const https = require('https'); + https.get('https://your-internal-site.com', res => { + console.log('STATUS:', res.statusCode); + }).on('error', e => { + console.error('ERR:', e.message); + }); + " + ``` + **Expected result**: HTTP 200 OK + +### Verification +- **Baseline**: TLS failure proves default behavior (unknown CA) +- **Custom CA**: TLS success proves custom CA trust is working +- **Both tests must pass** to confirm the feature works correctly + +Your Checkmate server should now trust certificates issued by your Smallstep CA, allowing you to monitor internal HTTPS endpoints without disabling SSL validation. diff --git a/scripts/dev/setup-custom-ca.sh b/scripts/dev/setup-custom-ca.sh new file mode 100755 index 000000000..699f70bbd --- /dev/null +++ b/scripts/dev/setup-custom-ca.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash + +# Dev/Test only: Not required in production +# This script generates test certificates for development and testing purposes + +set -euo pipefail + +# Colors for output +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Configuration +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +CERTS_DIR="$REPO_ROOT/docker/dev/certs" + +# Function to print colored output +print_status() { + local status=$1 + local message=$2 + case $status in + "PASS") + echo -e "${GREEN}[PASS]${NC} $message" + ;; + "INFO") + echo -e "${BLUE}[INFO]${NC} $message" + ;; + "WARN") + echo -e "${YELLOW}[WARN]${NC} $message" + ;; + esac +} + +# Function to check if a command exists +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +# Function to generate certificates using mkcert +generate_certs_mkcert() { + print_status "INFO" "Generating certificates using mkcert..." + + # Install mkcert CA (ignore if already done) + mkcert -install 2>/dev/null || true + + # Copy root CA + local ca_root + ca_root=$(mkcert -CAROOT) + cp "$ca_root/rootCA.pem" "$CERTS_DIR/custom-ca.pem" + + # Generate server certificate + mkcert -key-file "$CERTS_DIR/host-int-key.pem" \ + -cert-file "$CERTS_DIR/host-int-cert.pem" \ + host.docker.internal + + print_status "PASS" "Certificates generated using mkcert" +} + +# Function to generate certificates using OpenSSL +generate_certs_openssl() { + print_status "INFO" "Generating certificates using OpenSSL..." + + # Create CA private key + openssl genrsa -out "$CERTS_DIR/ca.key" 2048 + + # Create CA certificate + openssl req -new -x509 -days 365 -key "$CERTS_DIR/ca.key" \ + -out "$CERTS_DIR/custom-ca.pem" \ + -subj "/C=US/ST=Test/L=Test/O=Test CA/CN=Test Root CA" + + # Create server private key + openssl genrsa -out "$CERTS_DIR/host-int-key.pem" 2048 + + # Create server certificate signing request + openssl req -new -key "$CERTS_DIR/host-int-key.pem" \ + -out "$CERTS_DIR/host-int-cert.csr" \ + -subj "/C=US/ST=Test/L=Test/O=Test/CN=host.docker.internal" + + # Create extfile for SAN + cat > "$CERTS_DIR/san.ext" << EOF +subjectAltName=DNS:host.docker.internal,IP:127.0.0.1 +EOF + + # Sign server certificate with CA + openssl x509 -req -days 365 \ + -in "$CERTS_DIR/host-int-cert.csr" \ + -CA "$CERTS_DIR/custom-ca.pem" \ + -CAkey "$CERTS_DIR/ca.key" \ + -CAcreateserial \ + -out "$CERTS_DIR/host-int-cert.pem" \ + -extfile "$CERTS_DIR/san.ext" + + # Clean up temporary files + rm -f "$CERTS_DIR/ca.key" "$CERTS_DIR/host-int-cert.csr" "$CERTS_DIR/san.ext" "$CERTS_DIR/.srl" + + print_status "PASS" "Certificates generated using OpenSSL" +} + +# Main function +main() { + print_status "INFO" "Setting up custom CA certificates for Checkmate testing" + echo "================================================================" + + # Create certs directory if missing + print_status "INFO" "Creating certificates directory..." + mkdir -p "$CERTS_DIR" + + # Generate certificates + if command_exists mkcert; then + generate_certs_mkcert + else + print_status "WARN" "mkcert not found, falling back to OpenSSL" + generate_certs_openssl + fi + + # Create duplicate for compatibility with existing overrides + print_status "INFO" "Creating duplicate CA file for compatibility..." + cp "$CERTS_DIR/custom-ca.pem" "$CERTS_DIR/smallstep-root-ca.pem" + + # Verify certificates exist + if [ ! -f "$CERTS_DIR/custom-ca.pem" ] || [ ! -f "$CERTS_DIR/host-int-cert.pem" ] || [ ! -f "$CERTS_DIR/host-int-key.pem" ]; then + echo "Error: Failed to generate required certificates" + exit 1 + fi + + # Print summary + echo "" + print_status "PASS" "All required certificates generated successfully" + echo "" + echo "Certificate summary:" + echo "====================" + ls -l "$CERTS_DIR" + echo "" + echo "CA certificate preview:" + echo "======================" + head -3 "$CERTS_DIR/custom-ca.pem" + echo "" + print_status "INFO" "Certificates are ready for use with Checkmate custom CA trust" +} + +# Run main function +main "$@" diff --git a/scripts/test-custom-ca.sh b/scripts/test-custom-ca.sh new file mode 100755 index 000000000..bdf5b62c7 --- /dev/null +++ b/scripts/test-custom-ca.sh @@ -0,0 +1,454 @@ +#!/usr/bin/env bash + +# Dev/Test only: Not required in production +# This script tests the custom CA trust functionality in development environment + +set -euo pipefail + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Configuration +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +DEV_DIR="${ROOT_DIR}/docker/dev" +CERT_DIR="${DEV_DIR}/certs" +NGINX_DIR="${DEV_DIR}/nginx-test" +COMPOSE_BASELINE="${DEV_DIR}/docker-compose.yaml" +COMPOSE_CUSTOM_CA="${DEV_DIR}/docker-compose.custom-ca-example.yaml" +COMPOSE_NGINX_TEST="${NGINX_DIR}/docker-compose.nginx-test.yaml" + +# Test configuration +NGINX_PORT=8443 +CHECKMATE_PORT=52345 +HEALTH_ENDPOINT="http://localhost:$CHECKMATE_PORT/api/v1/health" +TEST_URL="https://host.docker.internal:$NGINX_PORT" +MAX_WAIT=60 +WAIT_INTERVAL=2 + +# Global flag for cleanup behavior +CLEAN_CERTS=false + +# Function to print colored output +print_status() { + local status=$1 + local message=$2 + case $status in + "PASS") + echo -e "${GREEN}[PASS]${NC} $message" + ;; + "FAIL") + echo -e "${RED}[FAIL]${NC} $message" + ;; + "INFO") + echo -e "${BLUE}[INFO]${NC} $message" + ;; + "WARN") + echo -e "${YELLOW}[WARN]${NC} $message" + ;; + esac +} + +# Function to check if a command exists +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +# Function to check if certificates exist and are valid +check_certificates() { + local ca_file="${CERT_DIR}/custom-ca.pem" + local cert_file="${CERT_DIR}/host-int-cert.pem" + local key_file="${CERT_DIR}/host-int-key.pem" + + if [ ! -f "$ca_file" ] || [ ! -f "$cert_file" ] || [ ! -f "$key_file" ]; then + return 1 + fi + + # Check if files are not empty + if [ ! -s "$ca_file" ] || [ ! -s "$cert_file" ] || [ ! -s "$key_file" ]; then + return 1 + fi + + # Check if CA file starts with BEGIN CERTIFICATE + if ! head -1 "$ca_file" | grep -q "BEGIN CERTIFICATE"; then + return 1 + fi + + return 0 +} + +# Function to wait for service to be ready +wait_for_service() { + local url=$1 + local service_name=$2 + local elapsed=0 + + print_status "INFO" "Waiting for $service_name to be ready..." + + while [ $elapsed -lt $MAX_WAIT ]; do + if curl -s -f "$url" >/dev/null 2>&1; then + print_status "PASS" "$service_name is ready after ${elapsed}s" + return 0 + fi + + sleep $WAIT_INTERVAL + elapsed=$((elapsed + WAIT_INTERVAL)) + echo -n "." + done + + print_status "FAIL" "$service_name failed to start within ${MAX_WAIT}s" + return 1 +} + +# Function to get container name by image +get_container_name() { + local image_name=$1 + docker ps --filter "ancestor=$image_name" --format "{{.Names}}" | head -n1 +} + +# Function to run Node.js probe test +run_probe_test() { + local container_name=$1 + local test_name=$2 + local expected_exit_code=$3 + + print_status "INFO" "Running $test_name probe test..." + + local probe_script=" +const https = require('https'); +https.get('https://host.docker.internal:8443', res => { + console.log('STATUS', res.statusCode); + process.exit(res.statusCode===200?0:1); +}).on('error', e => { + console.error('ERR', e.code||e.message); + process.exit(1); +}); +" + + local exit_code + if docker exec -i "$container_name" node -e "$probe_script" 2>/dev/null; then + exit_code=$? + else + exit_code=$? + fi + + if [ $exit_code -eq $expected_exit_code ]; then + print_status "PASS" "$test_name probe test completed with expected exit code $expected_exit_code" + return 0 + else + print_status "FAIL" "$test_name probe test failed with exit code $exit_code (expected $expected_exit_code)" + return 1 + fi +} + +# Function to setup certificates if needed +setup_certificates_if_needed() { + if check_certificates; then + print_status "INFO" "Certificates already exist and are valid, skipping generation" + return 0 + fi + + print_status "INFO" "Certificates missing or invalid, generating new ones..." + if [ -x "${ROOT_DIR}/scripts/dev/setup-custom-ca.sh" ]; then + "${ROOT_DIR}/scripts/dev/setup-custom-ca.sh" + else + print_status "FAIL" "Certificate setup script not found at scripts/dev/setup-custom-ca.sh" + return 1 + fi +} + +# Function to create nginx test configuration +create_nginx_test_config() { + print_status "INFO" "Setting up nginx test configuration..." + + mkdir -p "$NGINX_DIR" + + # Create nginx.conf if it doesn't exist + if [ ! -f "${NGINX_DIR}/nginx.conf" ]; then + cat > "${NGINX_DIR}/nginx.conf" << 'EOF' +events {} +http { + server { + listen 443 ssl; + ssl_certificate /etc/nginx/certs/server.crt; + ssl_certificate_key /etc/nginx/certs/server.key; + location / { + return 200 "hello from tls\n"; + } + } +} +EOF + fi + + # Create docker-compose.nginx-test.yaml if it doesn't exist + if [ ! -f "$COMPOSE_NGINX_TEST" ]; then + cat > "$COMPOSE_NGINX_TEST" << EOF +services: + nginx-test: + image: nginx:alpine + ports: + - "$NGINX_PORT:443" + volumes: + - ../certs/host-int-cert.pem:/etc/nginx/certs/server.crt:ro + - ../certs/host-int-key.pem:/etc/nginx/certs/server.key:ro + - ./nginx.conf:/etc/nginx/nginx.conf:ro + restart: unless-stopped +EOF + fi + + print_status "PASS" "Nginx test configuration created" +} + +# Function to start nginx test service +start_nginx_test() { + print_status "INFO" "Starting nginx test service..." + + cd "$NGINX_DIR" + docker-compose -f docker-compose.nginx-test.yaml up -d + + # Wait for nginx to be ready + local elapsed=0 + while [ $elapsed -lt $MAX_WAIT ]; do + if curl -s -f -k "https://localhost:$NGINX_PORT" >/dev/null 2>&1; then + print_status "PASS" "Nginx test service is ready" + cd "$ROOT_DIR" + return 0 + fi + + sleep $WAIT_INTERVAL + elapsed=$((elapsed + WAIT_INTERVAL)) + echo -n "." + done + + print_status "FAIL" "Nginx test service failed to start" + cd "$ROOT_DIR" + return 1 +} + +# Function to stop nginx test service +stop_nginx_test() { + print_status "INFO" "Stopping nginx test service..." + cd "$NGINX_DIR" + docker-compose -f docker-compose.nginx-test.yaml down 2>/dev/null || true + cd "$ROOT_DIR" +} + +# Function to run baseline test +run_baseline_test() { + print_status "INFO" "Running baseline test (should fail due to unknown CA)..." + + # Start baseline Checkmate + cd "$DEV_DIR" + docker-compose -f docker-compose.yaml up -d --build + + # Wait for Checkmate to be ready + if ! wait_for_service "$HEALTH_ENDPOINT" "Checkmate baseline"; then + cd "$ROOT_DIR" + return 1 + fi + + # Get container name + local container_name + container_name=$(get_container_name "uptime_server") + if [ -z "$container_name" ]; then + print_status "FAIL" "Could not find Checkmate server container" + cd "$ROOT_DIR" + return 1 + fi + + # Run probe test (should fail) + if run_probe_test "$container_name" "baseline" 1; then + print_status "PASS" "Baseline test completed - TLS failure as expected" + cd "$ROOT_DIR" + return 0 + else + print_status "FAIL" "Baseline test failed - unexpected behavior" + cd "$ROOT_DIR" + return 1 + fi +} + +# Function to run custom CA test +run_custom_ca_test() { + print_status "INFO" "Running custom CA test (should succeed)..." + + # Stop baseline Checkmate + cd "$DEV_DIR" + docker-compose -f docker-compose.yaml down + + # Start Checkmate with custom CA + docker-compose -f docker-compose.yaml -f docker-compose.custom-ca-example.yaml up -d --build + + # Wait for Checkmate to be ready + if ! wait_for_service "$HEALTH_ENDPOINT" "Checkmate custom CA"; then + cd "$ROOT_DIR" + return 1 + fi + + # Get container name + local container_name + container_name=$(get_container_name "uptime_server") + if [ -z "$container_name" ]; then + print_status "FAIL" "Could not find Checkmate server container" + cd "$ROOT_DIR" + return 1 + fi + + # Run probe test (should succeed) + if run_probe_test "$container_name" "custom CA" 0; then + print_status "PASS" "Custom CA test completed - TLS success as expected" + cd "$ROOT_DIR" + return 0 + else + print_status "FAIL" "Custom CA test failed - unexpected behavior" + cd "$ROOT_DIR" + return 1 + fi +} + +# Function to cleanup everything +cleanup() { + print_status "INFO" "Cleaning up test environment..." + + # Stop all services + cd "$DEV_DIR" + docker-compose -f docker-compose.yaml down 2>/dev/null || true + docker-compose -f docker-compose.yaml -f docker-compose.custom-ca-example.yaml down 2>/dev/null || true + + stop_nginx_test + + # Only remove certificates if --clean was specified + if [ "$CLEAN_CERTS" = true ]; then + print_status "INFO" "Removing certificates as requested with --clean" + rm -rf "${CERT_DIR:?}"/* + else + print_status "INFO" "Preserving certificates for future test runs" + fi + + cd "$ROOT_DIR" + print_status "PASS" "Cleanup completed" +} + +# Function to show usage +show_usage() { + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Options:" + echo " --clean Clean up all test containers and certificates" + echo " --help Show this help message" + echo "" + echo "This script tests the custom CA trust functionality in Checkmate." +} + +# Main function +main() { + local clean_only=false + + # Parse command line arguments + while [[ $# -gt 0 ]]; do + case $1 in + --clean) + clean_only=true + CLEAN_CERTS=true + shift + ;; + --help) + show_usage + exit 0 + ;; + *) + echo "Unknown option: $1" + show_usage + exit 1 + ;; + esac + done + + print_status "INFO" "Starting Checkmate Custom CA Trust Test" + echo "==================================================" + + if [ "$clean_only" = true ]; then + cleanup + exit 0 + fi + + # Check prerequisites + if ! command_exists docker; then + print_status "FAIL" "Docker is not installed or not in PATH" + exit 1 + fi + + if ! command_exists docker-compose; then + print_status "FAIL" "docker-compose is not installed or not in PATH" + exit 1 + fi + + # Check if required files exist + if [ ! -f "$COMPOSE_BASELINE" ]; then + print_status "FAIL" "Baseline docker-compose.yaml not found at $COMPOSE_BASELINE" + exit 1 + fi + + if [ ! -f "$COMPOSE_CUSTOM_CA" ]; then + print_status "FAIL" "Custom CA docker-compose override not found at $COMPOSE_CUSTOM_CA" + exit 1 + fi + + # Setup test environment + setup_certificates_if_needed + create_nginx_test_config + start_nginx_test + + # Run tests + local baseline_result=false + local custom_ca_result=false + + if run_baseline_test; then + baseline_result=true + fi + + if run_custom_ca_test; then + custom_ca_result=true + fi + + # Print summary + echo "" + echo "==================================================" + print_status "INFO" "Test Summary" + echo "==================================================" + + if [ "$baseline_result" = true ]; then + print_status "PASS" "Baseline: TLS failure as expected" + else + print_status "FAIL" "Baseline: Unexpected behavior" + fi + + if [ "$custom_ca_result" = true ]; then + print_status "PASS" "Custom CA: TLS success (STATUS 200)" + else + print_status "FAIL" "Custom CA: Unexpected behavior" + fi + + if [ "$baseline_result" = true ] && [ "$custom_ca_result" = true ]; then + echo "" + print_status "PASS" "All tests passed! Custom CA trust is working correctly." + echo "" + print_status "INFO" "To clean up, run: $0 --clean" + exit 0 + else + echo "" + print_status "FAIL" "Some tests failed. Custom CA trust may not be working correctly." + echo "" + print_status "INFO" "To clean up, run: $0 --clean" + exit 1 + fi +} + +# Trap cleanup on script exit +trap cleanup EXIT + +# Run main function +main "$@" 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/authController.js b/server/controllers/authController.js deleted file mode 100755 index 1d8c2af0e..000000000 --- a/server/controllers/authController.js +++ /dev/null @@ -1,452 +0,0 @@ -import { - registrationBodyValidation, - loginValidation, - editUserParamValidation, - editUserBodyValidation, - recoveryValidation, - recoveryTokenValidation, - newPasswordValidation, -} 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; - this.settingsService = settingsService; - this.emailService = emailService; - this.jobQueue = jobQueue; - this.stringService = stringService; - this.logger = logger; - } - - /** - * 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. - * @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). - */ - registerUser = async (req, res, next) => { - try { - 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, - }); - } - - res.success({ - msg: this.stringService.authCreateUser, - data: { user: newUser, token: token }, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "registerController")); - } - }; - - /** - * Logs in a user by validating the user's credentials and issuing 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. - */ - loginUser = async (req, res, next) => { - try { - 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; - - return res.success({ - msg: this.stringService.authLoginUser, - data: { - user: userWithoutPassword, - token: token, - }, - }); - } catch (error) { - error.status = 401; - next(handleError(error, 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. - * @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). - */ - editUser = async (req, res, next) => { - try { - await editUserParamValidation.validateAsync(req.params); - 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; - } - - 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")); - } - }; - - /** - * Checks if a superadmin account exists in the database. - * @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. - */ - checkSuperadminExists = async (req, res, next) => { - try { - const superAdminExists = await this.db.checkSuperadmin(req, res); - - return res.success({ - msg: this.stringService.authAdminExists, - data: superAdminExists, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "checkSuperadminController")); - } - }; - /** - * Requests a recovery token for a user. The user's email is validated and a recovery token is created and sent via 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). - */ - requestRecovery = async (req, res, next) => { - try { - 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 - ); - - return res.success({ - msg: this.stringService.authCreateRecoveryToken, - data: msgId, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "recoveryRequestController")); - } - }; - /** - * Validates a recovery token. The recovery token is validated and if valid, a success message is returned. - * @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). - */ - 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); - - return res.success({ - msg: this.stringService.authVerifyRecoveryToken, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "validateRecoveryTokenController")); - } - }; - - /** - * 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. - * @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). - */ - resetPassword = async (req, res, next) => { - try { - 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); - - return res.success({ - msg: this.stringService.authResetPassword, - data: { user, token }, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "resetPasswordController")); - } - }; - - /** - * Deletes a user and all associated monitors, checks, and alerts. - * - * @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. - */ - 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); - - 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); - - return res.success({ - msg: this.stringService.authGetAllUsers, - data: allUsers, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getAllUsersController")); - } - }; -} - -export default AuthController; diff --git a/server/controllers/checkController.js b/server/controllers/checkController.js deleted file mode 100755 index e50d5b7fc..000000000 --- a/server/controllers/checkController.js +++ /dev/null @@ -1,241 +0,0 @@ -import { - createCheckParamValidation, - createCheckBodyValidation, - getChecksParamValidation, - getChecksQueryValidation, - getTeamChecksParamValidation, - getTeamChecksQueryValidation, - deleteChecksParamValidation, - deleteChecksByTeamIdParamValidation, - updateChecksTTLBodyValidation, - ackCheckBodyValidation, - 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; - this.settingsService = settingsService; - this.stringService = stringService; - } - - 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 { - 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, - }); - - return res.success({ - msg: this.stringService.checkGet, - data: result, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getChecks")); - } - }; - - getChecksByTeam = async (req, res, next) => { - try { - await getTeamChecksParamValidation.validateAsync(req.params); - 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, - }); - return res.success({ - msg: this.stringService.checkGet, - data: checkData, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getTeamChecks")); - } - }; - - getChecksSummaryByTeamId = async (req, res, next) => { - try { - const { teamId } = req.user; - const summary = await this.db.getChecksSummaryByTeamId({ teamId }); - return res.success({ - msg: this.stringService.checkGetSummary, - data: summary, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getChecksSummaryByTeamId")); - } - }; - - ackCheck = async (req, res, next) => { - try { - 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); - - return res.success({ - msg: this.stringService.checkUpdateStatus, - data: updatedCheck, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "ackCheck")); - } - }; - - ackAllChecks = async (req, res, next) => { - try { - 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 updatedChecks = await this.db.ackAllChecks(monitorId, teamId, ack, path); - - return res.success({ - msg: this.stringService.checkUpdateStatus, - data: updatedChecks, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "ackAllChecks")); - } - }; - - deleteChecks = async (req, res, next) => { - try { - await deleteChecksParamValidation.validateAsync(req.params); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - - try { - const deletedCount = await this.db.deleteChecks(req.params.monitorId); - - return res.success({ - msg: this.stringService.checkDelete, - data: { deletedCount }, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "deleteChecks")); - } - }; - - deleteChecksByTeamId = async (req, res, next) => { - try { - 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); - - return res.success({ - msg: this.stringService.checkDelete, - data: { deletedCount }, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "deleteChecksByTeamId")); - } - }; - - updateChecksTTL = async (req, res, next) => { - const SECONDS_PER_DAY = 86400; - - try { - 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); - - return res.success({ - msg: this.stringService.checkUpdateTTL, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "updateTTL")); - } - }; -} -export default CheckController; diff --git a/server/controllers/controllerUtils.js b/server/controllers/controllerUtils.js deleted file mode 100755 index ec7f1122f..000000000 --- a/server/controllers/controllerUtils.js +++ /dev/null @@ -1,26 +0,0 @@ -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; - const cert = await sslChecker(hostname); - // Throw an error if no cert or if cert.validTo is not present - if (cert?.validTo === null || cert?.validTo === undefined) { - throw new Error("Certificate not found"); - } - return cert; -}; - -export { handleValidationError, handleError, fetchMonitorCertificate }; diff --git a/server/controllers/diagnosticController.js b/server/controllers/diagnosticController.js deleted file mode 100755 index 8fa5fa42b..000000000 --- a/server/controllers/diagnosticController.js +++ /dev/null @@ -1,48 +0,0 @@ -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); - } - - async getMonitorsByTeamIdExecutionStats(req, res, next) { - try { - const data = await this.db.getMonitorsByTeamIdExecutionStats(req); - return res.success({ - msg: "OK", - data, - }); - } 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")); - } - } -} -export default DiagnosticController; diff --git a/server/controllers/inviteController.js b/server/controllers/inviteController.js deleted file mode 100755 index 3799c58b9..000000000 --- a/server/controllers/inviteController.js +++ /dev/null @@ -1,129 +0,0 @@ -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"; - -const SERVICE_NAME = "inviteController"; - -class InviteController { - constructor(db, settingsService, emailService, stringService) { - this.db = db; - this.settingsService = settingsService; - this.emailService = emailService; - this.stringService = stringService; - } - - /** - * 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). - */ - 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 }); - return res.success({ - msg: this.stringService.inviteIssued, - data: inviteToken, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "inviteController")); - } - }; - - 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, - }); - } - - return res.success({ - msg: this.stringService.inviteIssued, - data: inviteToken, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "inviteController")); - } - }; - - inviteVerifyController = async (req, res, next) => { - try { - await inviteVerificationBodyValidation.validateAsync(req.body); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - - try { - const invite = await this.db.getInviteToken(req.body.token); - - return res.success({ - msg: this.stringService.inviteVerified, - data: invite, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "inviteVerifyController")); - } - }; -} - -export default InviteController; diff --git a/server/controllers/logController.js b/server/controllers/logController.js deleted file mode 100644 index 5381fd3fa..000000000 --- a/server/controllers/logController.js +++ /dev/null @@ -1,23 +0,0 @@ -import { handleError } from "./controllerUtils.js"; - -const SERVICE_NAME = "JobQueueController"; - -class LogController { - constructor(logger) { - this.logger = logger; - } - - getLogs = async (req, res, next) => { - try { - const logs = await this.logger.getLogs(); - res.success({ - msg: "Logs fetched successfully", - data: logs, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getLogs")); - return; - } - }; -} -export default LogController; diff --git a/server/controllers/maintenanceWindowController.js b/server/controllers/maintenanceWindowController.js deleted file mode 100755 index 20077cc65..000000000 --- a/server/controllers/maintenanceWindowController.js +++ /dev/null @@ -1,157 +0,0 @@ -import { - createMaintenanceWindowBodyValidation, - editMaintenanceWindowByIdParamValidation, - editMaintenanceByIdWindowBodyValidation, - getMaintenanceWindowByIdParamValidation, - getMaintenanceWindowsByMonitorIdParamValidation, - getMaintenanceWindowsByTeamIdQueryValidation, - deleteMaintenanceWindowByIdParamValidation, -} from "../validation/joi.js"; -import { handleValidationError, handleError } from "./controllerUtils.js"; - -const SERVICE_NAME = "maintenanceWindowController"; - -class MaintenanceWindowController { - constructor(db, settingsService, stringService) { - this.db = db; - this.settingsService = settingsService; - this.stringService = stringService; - } - - createMaintenanceWindows = async (req, res, next) => { - try { - 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); - - return res.success({ - msg: this.stringService.maintenanceWindowCreate, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "createMaintenanceWindow")); - } - }; - - getMaintenanceWindowById = async (req, res, next) => { - try { - await getMaintenanceWindowByIdParamValidation.validateAsync(req.params); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { - const maintenanceWindow = await this.db.getMaintenanceWindowById(req.params.id); - - return res.success({ - msg: this.stringService.maintenanceWindowGetById, - data: maintenanceWindow, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getMaintenanceWindowById")); - } - }; - - getMaintenanceWindowsByTeamId = async (req, res, next) => { - try { - 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 - ); - - return res.success({ - msg: this.stringService.maintenanceWindowGetByTeam, - data: maintenanceWindows, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getMaintenanceWindowsByUserId")); - } - }; - - getMaintenanceWindowsByMonitorId = async (req, res, next) => { - try { - await getMaintenanceWindowsByMonitorIdParamValidation.validateAsync(req.params); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - - try { - const maintenanceWindows = await this.db.getMaintenanceWindowsByMonitorId( - req.params.monitorId - ); - - return res.success({ - msg: this.stringService.maintenanceWindowGetByUser, - data: maintenanceWindows, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getMaintenanceWindowsByMonitorId")); - } - }; - - deleteMaintenanceWindow = async (req, res, next) => { - try { - await deleteMaintenanceWindowByIdParamValidation.validateAsync(req.params); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { - await this.db.deleteMaintenanceWindowById(req.params.id); - return res.success({ - msg: this.stringService.maintenanceWindowDelete, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "deleteMaintenanceWindow")); - } - }; - - editMaintenanceWindow = async (req, res, next) => { - try { - 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 - ); - return res.success({ - msg: this.stringService.maintenanceWindowEdit, - data: editedMaintenanceWindow, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "editMaintenanceWindow")); - } - }; -} - -export default MaintenanceWindowController; diff --git a/server/controllers/monitorController.js b/server/controllers/monitorController.js deleted file mode 100755 index d32ef463e..000000000 --- a/server/controllers/monitorController.js +++ /dev/null @@ -1,739 +0,0 @@ -import { - getMonitorByIdParamValidation, - getMonitorByIdQueryValidation, - getMonitorsByTeamIdParamValidation, - getMonitorsByTeamIdQueryValidation, - createMonitorBodyValidation, - createMonitorsBodyValidation, - getMonitorURLByQueryValidation, - editMonitorBodyValidation, - pauseMonitorParamValidation, - getMonitorStatsByIdParamValidation, - getMonitorStatsByIdQueryValidation, - getCertificateParamValidation, - getHardwareDetailsByIdParamValidation, - 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"; - -class MonitorController { - constructor(db, settingsService, jobQueue, stringService, emailService) { - this.db = db; - this.settingsService = settingsService; - this.jobQueue = jobQueue; - this.stringService = stringService; - this.emailService = emailService; - } - - /** - * 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(); - return res.success({ - msg: this.stringService.monitorGetAll, - data: monitors, - }); - } catch (error) { - next(handleError(error, 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 = async (req, res, next) => { - try { - const { monitorId } = req.params; - const { dateRange, normalize } = req.query; - - const data = await this.db.getUptimeDetailsById({ - monitorId, - dateRange, - normalize, - }); - return res.success({ - msg: this.stringService.monitorGetByIdSuccess, - data, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getMonitorDetailsById")); - } - }; - - /** - * 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 { - 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 monitorStats = await this.db.getMonitorStatsById({ - monitorId, - limit, - sortOrder, - dateRange, - numToDisplay, - normalize, - }); - return res.success({ - msg: this.stringService.monitorStatsById, - data: monitorStats, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getMonitorStatsById")); - } - }; - - /** - * Get hardware details for a specific monitor by ID - * @async - * @param {Express.Request} req - Express request object containing monitorId in params - * @param {Express.Response} res - Express response object - * @param {Express.NextFunction} next - Express next middleware function - * @returns {Promise} - * @throws {Error} - Throws error if monitor not found or other database errors - */ - getHardwareDetailsById = async (req, res, next) => { - try { - 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 }); - return res.success({ - msg: this.stringService.monitorGetByIdSuccess, - data: monitor, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getHardwareDetailsById")); - } - }; - - getMonitorCertificate = async (req, res, next, fetchMonitorCertificate) => { - try { - 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); - - return res.success({ - msg: this.stringService.monitorCertificate, - data: { - certificateDate: new Date(certificate.validTo), - }, - }); - } catch (error) { - next(handleError(error, 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 { - 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); - return res.success({ - msg: this.stringService.monitorGetByIdSuccess, - data: monitor, - }); - } catch (error) { - next(handleError(error, 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 { - 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, - }); - - // 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")); - } - }; - - /** - * 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 - if (!req.file) { - throw new Error("No file uploaded"); - } - - // Check if the file is a CSV - if (!req.file.mimetype.includes("csv")) { - throw new Error("File is not a CSV"); - } - - // Validate if the file is empty - if (req.file.size === 0) { - throw new Error("File is empty"); - } - - const { _id, teamId } = req.user; - - if (!_id || !teamId) { - throw new Error("Missing userId or teamId"); - } - - // Get file buffer from memory and convert to string - const fileData = req.file.buffer.toString("utf-8"); - - // Parse the CSV data - 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 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, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "checkEndpointResolution")); - } - }; - - /** - * 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 { - 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")); - } - }; - - /** - * 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")); - } - }; - - /** - * 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 { - await getMonitorByIdParamValidation.validateAsync(req.params); - await editMonitorBodyValidation.validateAsync(req.body); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - - try { - const { monitorId } = req.params; - - const editedMonitor = await this.db.editMonitor(monitorId, req.body); - - await this.jobQueue.updateJob(editedMonitor); - - return res.success({ - msg: this.stringService.monitorEdit, - data: editedMonitor, - }); - } catch (error) { - next(handleError(error, 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 { - 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); - - return res.success({ - msg: monitor.isActive - ? this.stringService.monitorResume - : this.stringService.monitorPause, - data: monitor, - }); - } catch (error) { - next(handleError(error, 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 { - 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)) - ); - - return res.success({ - msg: this.stringService.monitorDemoAdded, - data: demoMonitors.length, - }); - } catch (error) { - next(handleError(error, 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 { - 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.", - }); - } - - return res.success({ - msg: this.stringService.sendTestEmail, - data: { messageId }, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "sendTestEmail")); - } - }; - - getMonitorsByTeamId = async (req, res, next) => { - try { - 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 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")); - } - }; - - getMonitorsAndSummaryByTeamId = async (req, res, next) => { - try { - 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 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")); - } - }; - - getMonitorsWithChecksByTeamId = async (req, res, next) => { - try { - await getMonitorsByTeamIdParamValidation.validateAsync(req.params); - await getMonitorsByTeamIdQueryValidation.validateAsync(req.query); - } catch (error) { - return next(handleValidationError(error, SERVICE_NAME)); - } - - try { - const { explain } = req; - let { limit, type, page, rowsPerPage, filter, field, order } = req.query; - const { teamId } = req.user; - - const result = await this.db.getMonitorsWithChecksByTeamId({ - limit, - type, - page, - rowsPerPage, - filter, - field, - order, - teamId, - explain, - }); - return res.success({ - msg: "OK", - data: result, - }); - } catch (error) { - return next(handleError(error, 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, - }); - } - 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); - - return res.file({ - data: csv, - headers: { - "Content-Type": "text/csv", - "Content-Disposition": "attachment; filename=monitors.csv", - }, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "exportMonitorsToCSV")); - } - }; -} - -export default MonitorController; diff --git a/server/controllers/notificationController.js b/server/controllers/notificationController.js deleted file mode 100755 index e7e2a1f2a..000000000 --- a/server/controllers/notificationController.js +++ /dev/null @@ -1,147 +0,0 @@ -import { - triggerNotificationBodyValidation, - createNotificationBodyValidation, -} from "../validation/joi.js"; -import { handleError, handleValidationError } from "./controllerUtils.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 }) { - this.notificationService = notificationService; - this.stringService = stringService; - this.statusService = statusService; - this.db = db; - } - - testNotification = async (req, res, next) => { - try { - const notification = req.body; - - const success = await this.notificationService.sendTestNotification(notification); - - if (!success) { - return res.error({ - msg: "Sending notification failed", - status: 400, - }); - } - - return res.success({ - msg: "Notification sent successfully", - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "testWebhook")); - } - }; - - createNotification = async (req, res, next) => { - try { - 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; - 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")); - } - }; - - 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")); - } - }; - - 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")); - } - }; - - getNotificationById = async (req, res, next) => { - try { - const notification = await this.db.getNotificationById(req.params.id); - return res.success({ - msg: "Notification fetched successfully", - data: notification, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getNotificationById")); - } - }; - - editNotification = async (req, res, next) => { - try { - 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); - return res.success({ - msg: "Notification updated successfully", - data: notification, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "editNotification")); - } - }; - - testAllNotifications = async (req, res, next) => { - try { - const { monitorId } = req.body; - const monitor = await this.db.getMonitorById(monitorId); - const notifications = monitor.notifications; - if (notifications.length === 0) throw new Error("No notifications"); - const result = await this.notificationService.testAllNotifications(notifications); - if (!result) throw new Error("Failed to send all notifications"); - return res.success({ - msg: "All notifications sent successfully", - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "testAllNotifications")); - } - }; -} - -export default NotificationController; diff --git a/server/controllers/statusPageController.js b/server/controllers/statusPageController.js deleted file mode 100755 index 577cf4997..000000000 --- a/server/controllers/statusPageController.js +++ /dev/null @@ -1,126 +0,0 @@ -import { handleError, handleValidationError } from "./controllerUtils.js"; -import { - createStatusPageBodyValidation, - getStatusPageParamValidation, - getStatusPageQueryValidation, - imageValidation, -} from "../validation/joi.js"; - -const SERVICE_NAME = "statusPageController"; - -class StatusPageController { - constructor(db, stringService) { - this.db = db; - this.stringService = stringService; - } - - createStatusPage = async (req, res, next) => { - try { - 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, - image: req.file, - userId: _id, - teamId, - }); - return res.success({ - msg: this.stringService.statusPageCreate, - data: statusPage, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "createStatusPage")); - } - }; - - updateStatusPage = async (req, res, next) => { - try { - 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; - } - return res.success({ - msg: this.stringService.statusPageUpdate, - data: statusPage, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "updateStatusPage")); - } - }; - - getStatusPage = async (req, res, next) => { - try { - const statusPage = await this.db.getStatusPage(); - return res.success({ - msg: this.stringService.statusPageByUrl, - data: statusPage, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getStatusPage")); - } - }; - - getStatusPageByUrl = async (req, res, next) => { - try { - 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")); - } - }; - - getStatusPagesByTeamId = async (req, res, next) => { - try { - const teamId = req.user.teamId; - const statusPages = await this.db.getStatusPagesByTeamId(teamId); - - return res.success({ - msg: this.stringService.statusPageByTeamId, - data: statusPages, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getStatusPageByTeamId")); - } - }; - - deleteStatusPage = async (req, res, next) => { - try { - await this.db.deleteStatusPage(req.params.url); - return res.success({ - msg: this.stringService.statusPageDelete, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "deleteStatusPage")); - } - }; -} - -export default StatusPageController; diff --git a/server/db/mongo/MongoDB.js b/server/db/mongo/MongoDB.js deleted file mode 100755 index 5e5e7aec7..000000000 --- a/server/db/mongo/MongoDB.js +++ /dev/null @@ -1,151 +0,0 @@ -import mongoose from "mongoose"; -import UserModel from "../models/User.js"; -import AppSettings from "../models/AppSettings.js"; -import logger from "../../utils/logger.js"; - -//**************************************** -// User Operations -//**************************************** - -import * as userModule from "./modules/userModule.js"; - -//**************************************** -// Invite Token Operations -//**************************************** - -import * as inviteModule from "./modules/inviteModule.js"; - -//**************************************** -// Recovery Operations -//**************************************** -import * as recoveryModule from "./modules/recoveryModule.js"; - -//**************************************** -// Monitors -//**************************************** - -import * as monitorModule from "./modules/monitorModule.js"; - -//**************************************** -// Page Speed Checks -//**************************************** - -import * as pageSpeedCheckModule from "./modules/pageSpeedCheckModule.js"; - -//**************************************** -// Hardware Checks -//**************************************** -import * as hardwareCheckModule from "./modules/hardwareCheckModule.js"; - -//**************************************** -// Checks -//**************************************** - -import * as checkModule from "./modules/checkModule.js"; - -//**************************************** -// Maintenance Window -//**************************************** -import * as maintenanceWindowModule from "./modules/maintenanceWindowModule.js"; - -//**************************************** -// Notifications -//**************************************** -import * as notificationModule from "./modules/notificationModule.js"; - -//**************************************** -// AppSettings -//**************************************** -import * as settingsModule from "./modules/settingsModule.js"; - -//**************************************** -// Status Page -//**************************************** -import * as statusPageModule from "./modules/statusPageModule.js"; - -//**************************************** -// Diagnostic -//**************************************** -import * as diagnosticModule from "./modules/diagnosticModule.js"; - -class MongoDB { - static SERVICE_NAME = "MongoDB"; - - constructor({ appSettings }) { - this.appSettings = appSettings; - Object.assign(this, userModule); - Object.assign(this, inviteModule); - Object.assign(this, recoveryModule); - Object.assign(this, monitorModule); - Object.assign(this, pageSpeedCheckModule); - Object.assign(this, hardwareCheckModule); - Object.assign(this, checkModule); - Object.assign(this, maintenanceWindowModule); - Object.assign(this, notificationModule); - Object.assign(this, settingsModule); - Object.assign(this, statusPageModule); - Object.assign(this, diagnosticModule); - } - - connect = async () => { - try { - const connectionString = - this.appSettings.dbConnectionString || "mongodb://localhost:27017/uptime_db"; - await mongoose.connect(connectionString); - // If there are no AppSettings, create one - await AppSettings.findOneAndUpdate( - {}, // empty filter to match any document - {}, // empty update - { - new: true, - setDefaultsOnInsert: true, - } - ); - // Sync indexes - const models = mongoose.modelNames(); - for (const modelName of models) { - const model = mongoose.model(modelName); - await model.syncIndexes(); - } - - logger.info({ - message: "Connected to MongoDB", - service: this.SERVICE_NAME, - method: "connect", - }); - } catch (error) { - logger.error({ - message: error.message, - service: this.SERVICE_NAME, - method: "connect", - stack: error.stack, - }); - throw error; - } - }; - - disconnect = async () => { - try { - logger.info({ message: "Disconnecting from MongoDB" }); - await mongoose.disconnect(); - logger.info({ message: "Disconnected from MongoDB" }); - return; - } catch (error) { - logger.error({ - message: error.message, - service: this.SERVICE_NAME, - method: "disconnect", - stack: error.stack, - }); - } - }; - 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 deleted file mode 100755 index fb0cc539e..000000000 --- a/server/db/mongo/modules/checkModule.js +++ /dev/null @@ -1,418 +0,0 @@ -import Check from "../../models/Check.js"; -import Monitor from "../../models/Monitor.js"; -import HardwareCheck from "../../models/HardwareCheck.js"; -import PageSpeedCheck from "../../models/PageSpeedCheck.js"; -import User from "../../models/User.js"; -import logger from "../../../utils/logger.js"; -import { ObjectId } from "mongodb"; -import { buildChecksSummaryByTeamIdPipeline } from "./checkModuleQueries.js"; - -const SERVICE_NAME = "checkModule"; -const dateRangeLookup = { - recent: new Date(new Date().setDate(new Date().getDate() - 2)), - hour: new Date(new Date().setHours(new Date().getHours() - 1)), - day: new Date(new Date().setDate(new Date().getDate() - 1)), - week: new Date(new Date().setDate(new Date().getDate() - 7)), - month: new Date(new Date().setMonth(new Date().getMonth() - 1)), - all: undefined, -}; - -/** - * Create a check for a monitor - * @async - * @param {Object} checkData - * @param {string} checkData.monitorId - * @param {boolean} checkData.status - * @param {number} checkData.responseTime - * @param {number} checkData.statusCode - * @param {string} checkData.message - * @returns {Promise} - * @throws {Error} - */ - -const createCheck = async (checkData) => { - try { - const check = await new Check({ ...checkData }).save(); - return check; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "createCheck"; - throw error; - } -}; - -const createChecks = async (checks) => { - try { - await Check.insertMany(checks, { ordered: false }); - } catch (error) { - error.service = SERVICE_NAME; - error.method = "createCheck"; - throw error; - } -}; - -/** - * Get all checks for a monitor - * @async - * @param {string} monitorId - * @returns {Promise>} - * @throws {Error} - */ -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 } }] }; - - // Match - const matchStage = { - monitorId: new ObjectId(monitorId), - ...(typeof status !== "undefined" && { status }), - ...(typeof ack !== "undefined" && ackStage), - ...(dateRangeLookup[dateRange] && { - createdAt: { - $gte: dateRangeLookup[dateRange], - }, - }), - }; - - if (filter !== undefined) { - switch (filter) { - case "all": - break; - case "down": - break; - case "resolve": - matchStage.statusCode = 5000; - break; - default: - logger.warn({ - message: "invalid filter", - service: SERVICE_NAME, - method: "getChecks", - }); - break; - } - } - - //Sort - sortOrder = sortOrder === "asc" ? 1 : -1; - - // Pagination - let skip = 0; - if (page && rowsPerPage) { - skip = page * rowsPerPage; - } - - const checkModels = { - http: Check, - ping: Check, - docker: Check, - port: Check, - pagespeed: PageSpeedCheck, - hardware: HardwareCheck, - }; - - const Model = checkModels[type]; - - const checks = await Model.aggregate([ - { $match: matchStage }, - { $sort: { createdAt: sortOrder } }, - { - $facet: { - summary: [{ $count: "checksCount" }], - checks: [{ $skip: skip }, { $limit: rowsPerPage }], - }, - }, - { - $project: { - checksCount: { - $ifNull: [{ $arrayElemAt: ["$summary.checksCount", 0] }, 0], - }, - checks: { - $ifNull: ["$checks", []], - }, - }, - }, - ]); - return checks[0]; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getChecks"; - throw error; - } -}; - -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 matchStage = { - teamId: new ObjectId(teamId), - status: false, - ...(typeof ack !== "undefined" && ackStage), - ...(dateRangeLookup[dateRange] && { - createdAt: { - $gte: dateRangeLookup[dateRange], - }, - }), - }; - // Add filter to match stage - if (filter !== undefined) { - switch (filter) { - case "all": - break; - case "down": - break; - case "resolve": - matchStage.statusCode = 5000; - break; - default: - logger.warn({ - message: "invalid filter", - service: SERVICE_NAME, - method: "getChecksByTeam", - }); - break; - } - } - - sortOrder = sortOrder === "asc" ? 1 : -1; - - // pagination - let skip = 0; - if (page && rowsPerPage) { - skip = page * rowsPerPage; - } - - const aggregatePipeline = [ - { $match: matchStage }, - { - $unionWith: { - coll: "hardwarechecks", - pipeline: [{ $match: matchStage }], - }, - }, - { - $unionWith: { - coll: "pagespeedchecks", - pipeline: [{ $match: matchStage }], - }, - }, - - { $sort: { createdAt: sortOrder } }, - { - $facet: { - summary: [{ $count: "checksCount" }], - checks: [{ $skip: skip }, { $limit: rowsPerPage }], - }, - }, - { - $project: { - checksCount: { $arrayElemAt: ["$summary.checksCount", 0] }, - checks: "$checks", - }, - }, - ]; - - const checks = await Check.aggregate(aggregatePipeline); - return checks[0]; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getChecksByTeam"; - throw error; - } -}; - -/** - * Update the acknowledgment status of a check - * @async - * @param {string} checkId - The ID of the check to update - * @param {string} teamId - The ID of the team - * @param {boolean} ack - The acknowledgment status to set - * @returns {Promise} - * @throws {Error} - */ -const ackCheck = async (checkId, teamId, ack) => { - try { - const updatedCheck = await Check.findOneAndUpdate( - { _id: checkId, teamId: teamId }, - { $set: { ack, ackAt: new Date() } }, - { new: true } - ); - - if (!updatedCheck) { - throw new Error("Check not found"); - } - - return updatedCheck; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "ackCheck"; - throw error; - } -}; - -/** - * Update the acknowledgment status of all checks for a monitor or team - * @async - * @param {string} id - The monitor ID or team ID - * @param {boolean} ack - The acknowledgment status to set - * @param {string} path - The path type ('monitor' or 'team') - * @returns {Promise} - * @throws {Error} - */ -const ackAllChecks = async (monitorId, teamId, ack, path) => { - try { - const updatedChecks = await Check.updateMany( - path === "monitor" ? { monitorId } : { teamId }, - { $set: { ack, ackAt: new Date() } } - ); - return updatedChecks.modifiedCount; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "ackAllChecks"; - throw error; - } -}; - -/** - * Get checks and summary by team ID - * @async - * @param {string} teamId - * @returns {Promise} - * @throws {Error} - */ -const getChecksSummaryByTeamId = async ({ teamId }) => { - try { - const matchStage = { - teamId: new ObjectId(teamId), - }; - const checks = await Check.aggregate( - buildChecksSummaryByTeamIdPipeline({ matchStage }) - ); - return checks[0].summary; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getChecksSummaryByTeamId"; - throw error; - } -}; - -/** - * Delete all checks for a monitor - * @async - * @param {string} monitorId - * @returns {number} - * @throws {Error} - */ - -const deleteChecks = async (monitorId) => { - try { - const result = await Check.deleteMany({ monitorId }); - return result.deletedCount; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteChecks"; - throw error; - } -}; - -/** - * Delete all checks for a team - * @async - * @param {string} monitorId - * @returns {number} - * @throws {Error} - */ - -const deleteChecksByTeamId = async (teamId) => { - try { - // Find all monitor IDs for this team (only get _id field for efficiency) - const teamMonitors = await Monitor.find({ teamId }, { _id: 1 }); - const monitorIds = teamMonitors.map((monitor) => monitor._id); - - // Delete all checks for these monitors in one operation - const deleteResult = await Check.deleteMany({ monitorId: { $in: monitorIds } }); - - return deleteResult.deletedCount; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteChecksByTeamId"; - throw error; - } -}; - -const updateChecksTTL = async (teamId, ttl) => { - try { - await Check.collection.dropIndex("expiry_1"); - } catch (error) { - logger.error({ - message: error.message, - service: SERVICE_NAME, - method: "updateChecksTTL", - stack: error.stack, - }); - } - - try { - await Check.collection.createIndex( - { expiry: 1 }, - { expireAfterSeconds: ttl } // TTL in seconds, adjust as necessary - ); - } catch (error) { - error.service = SERVICE_NAME; - error.method = "updateChecksTTL"; - throw error; - } - // Update user - try { - await User.updateMany({ teamId: teamId }, { checkTTL: ttl }); - } catch (error) { - error.service = SERVICE_NAME; - error.method = "updateChecksTTL"; - throw error; - } -}; - -export { - createCheck, - createChecks, - getChecksByMonitor, - getChecksByTeam, - ackCheck, - ackAllChecks, - getChecksSummaryByTeamId, - deleteChecks, - deleteChecksByTeamId, - updateChecksTTL, -}; diff --git a/server/db/mongo/modules/diagnosticModule.js b/server/db/mongo/modules/diagnosticModule.js deleted file mode 100755 index e59830518..000000000 --- a/server/db/mongo/modules/diagnosticModule.js +++ /dev/null @@ -1,56 +0,0 @@ -import Monitor from "../../models/Monitor.js"; -import { ObjectId } from "mongodb"; - -const SERVICE_NAME = "diagnosticModule"; -import { - buildMonitorSummaryByTeamIdPipeline, - buildMonitorsByTeamIdPipeline, - buildFilteredMonitorsByTeamIdPipeline, -} from "./monitorModuleQueries.js"; - -const getMonitorsByTeamIdExecutionStats = async (req) => { - try { - let { limit, type, page, rowsPerPage, filter, field, order } = req.query; - limit = parseInt(limit); - page = parseInt(page); - rowsPerPage = parseInt(rowsPerPage); - if (field === undefined) { - field = "name"; - order = "asc"; - } - // Build match stage - const matchStage = { teamId: new ObjectId(req.params.teamId) }; - if (type !== undefined) { - matchStage.type = Array.isArray(type) ? { $in: type } : type; - } - - const summary = await Monitor.aggregate( - buildMonitorSummaryByTeamIdPipeline({ matchStage }) - ).explain("executionStats"); - - const monitors = await Monitor.aggregate( - buildMonitorsByTeamIdPipeline({ matchStage, field, order }) - ).explain("executionStats"); - - const filteredMonitors = await Monitor.aggregate( - buildFilteredMonitorsByTeamIdPipeline({ - matchStage, - filter, - page, - rowsPerPage, - field, - order, - limit, - type, - }) - ).explain("executionStats"); - - return { summary, monitors, filteredMonitors }; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getMonitorSummaryByTeamIdExecutionStats"; - throw error; - } -}; - -export { getMonitorsByTeamIdExecutionStats }; diff --git a/server/db/mongo/modules/hardwareCheckModule.js b/server/db/mongo/modules/hardwareCheckModule.js deleted file mode 100755 index 868c83958..000000000 --- a/server/db/mongo/modules/hardwareCheckModule.js +++ /dev/null @@ -1,74 +0,0 @@ -import HardwareCheck from "../../models/HardwareCheck.js"; -import Monitor from "../../models/Monitor.js"; -import logger from "../../../utils/logger.js"; - -const SERVICE_NAME = "hardwareCheckModule"; -const createHardwareCheck = async (hardwareCheckData) => { - try { - const { monitorId, status } = hardwareCheckData; - const n = (await HardwareCheck.countDocuments({ monitorId })) + 1; - const monitor = await Monitor.findById(monitorId); - - if (!monitor) { - logger.error({ - message: "Monitor not found", - service: SERVICE_NAME, - method: "createHardwareCheck", - details: `monitor ID: ${monitorId}`, - }); - return null; - } - - let newUptimePercentage; - if (monitor.uptimePercentage === undefined) { - newUptimePercentage = status === true ? 1 : 0; - } else { - newUptimePercentage = - (monitor.uptimePercentage * (n - 1) + (status === true ? 1 : 0)) / n; - } - - await Monitor.findOneAndUpdate( - { _id: monitorId }, - { uptimePercentage: newUptimePercentage } - ); - - const hardwareCheck = await new HardwareCheck({ - ...hardwareCheckData, - }).save(); - return hardwareCheck; - } catch (error) { - logger.error({ - message: "Error creating hardware check", - service: SERVICE_NAME, - method: "createHardwareCheck", - stack: error.stack, - }); - error.service = SERVICE_NAME; - error.method = "createHardwareCheck"; - throw error; - } -}; - -const createHardwareChecks = async (hardwareChecks) => { - try { - await HardwareCheck.insertMany(hardwareChecks, { ordered: false }); - return true; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "createHardwareChecks"; - throw error; - } -}; - -const deleteHardwareChecksByMonitorId = async (monitorId) => { - try { - const result = await HardwareCheck.deleteMany({ monitorId }); - return result.deletedCount; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteHardwareChecksByMonitorId"; - throw error; - } -}; - -export { createHardwareCheck, createHardwareChecks, deleteHardwareChecksByMonitorId }; diff --git a/server/db/mongo/modules/inviteModule.js b/server/db/mongo/modules/inviteModule.js deleted file mode 100755 index f5c960697..000000000 --- a/server/db/mongo/modules/inviteModule.js +++ /dev/null @@ -1,89 +0,0 @@ -import InviteToken from "../../models/InviteToken.js"; -import crypto from "crypto"; -import ServiceRegistry from "../../../service/serviceRegistry.js"; -import StringService from "../../../service/stringService.js"; - -const SERVICE_NAME = "inviteModule"; -/** - * Request an invite token for a user. - * - * This function deletes any existing invite tokens for the user's email, - * generates a new token, saves it, and then returns the new token. - * - * @param {Object} userData - The user data. - * @param {string} userData.email - The user's email. - * @param {mongoose.Schema.Types.ObjectId} userData.teamId - The ID of the team. - * @param {Array} userData.role - The user's role(s). - * @param {Date} [userData.expiry=Date.now] - The expiry date of the token. Defaults to the current date and time. - * @returns {Promise} The invite token. - * @throws {Error} If there is an error. - */ -const requestInviteToken = async (userData) => { - try { - await InviteToken.deleteMany({ email: userData.email }); - userData.token = crypto.randomBytes(32).toString("hex"); - let inviteToken = new InviteToken(userData); - await inviteToken.save(); - return inviteToken; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "requestInviteToken"; - throw error; - } -}; - -/** - * Retrieves an invite token - * - * This function searches for an invite token in the database and deletes it. - * If the invite token is not found, it throws an error. - * - * @param {string} token - The invite token to search for. - * @returns {Promise} The invite token data. - * @throws {Error} If the invite token is not found or there is another error. - */ -const getInviteToken = async (token) => { - const stringService = ServiceRegistry.get(StringService.SERVICE_NAME); - try { - const invite = await InviteToken.findOne({ - token, - }); - if (invite === null) { - throw new Error(stringService.authInviteNotFound); - } - return invite; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getInviteToken"; - throw error; - } -}; - -/** - * Retrieves and deletes an invite token - * - * This function searches for an invite token in the database and deletes it. - * If the invite token is not found, it throws an error. - * - * @param {string} token - The invite token to search for. - * @returns {Promise} The invite token data. - * @throws {Error} If the invite token is not found or there is another error. - */ -const getInviteTokenAndDelete = async (token) => { - const stringService = ServiceRegistry.get(StringService.SERVICE_NAME); - try { - const invite = await InviteToken.findOneAndDelete({ - token, - }); - if (invite === null) { - throw new Error(stringService.authInviteNotFound); - } - return invite; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getInviteTokenAndDelete"; - throw error; - } -}; - -export { requestInviteToken, getInviteToken, getInviteTokenAndDelete }; diff --git a/server/db/mongo/modules/maintenanceWindowModule.js b/server/db/mongo/modules/maintenanceWindowModule.js deleted file mode 100755 index 959f3f961..000000000 --- a/server/db/mongo/modules/maintenanceWindowModule.js +++ /dev/null @@ -1,199 +0,0 @@ -import MaintenanceWindow from "../../models/MaintenanceWindow.js"; -const SERVICE_NAME = "maintenanceWindowModule"; - -/** - * Asynchronously creates a new MaintenanceWindow document and saves it to the database. - * If the maintenance window is a one-time event, the expiry field is set to the same value as the end field. - * @async - * @function createMaintenanceWindow - * @param {Object} maintenanceWindowData - The data for the new MaintenanceWindow document. - * @param {mongoose.Schema.Types.ObjectId} maintenanceWindowData.monitorId - The ID of the monitor. - * @param {Boolean} maintenanceWindowData.active - Indicates whether the maintenance window is active. - * @param {Boolean} maintenanceWindowData.oneTime - Indicates whether the maintenance window is a one-time event. - * @param {Date} maintenanceWindowData.start - The start date and time of the maintenance window. - * @param {Date} maintenanceWindowData.end - The end date and time of the maintenance window. - * @returns {Promise} The saved MaintenanceWindow document. - * @throws {Error} If there is an error saving the document. - */ -const createMaintenanceWindow = async (maintenanceWindowData) => { - try { - const maintenanceWindow = new MaintenanceWindow({ - ...maintenanceWindowData, - }); - - // If the maintenance window is a one time window, set the expiry to the end date - if (maintenanceWindowData.oneTime) { - maintenanceWindow.expiry = maintenanceWindowData.end; - } - const result = await maintenanceWindow.save(); - return result; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "createMaintenanceWindow"; - throw error; - } -}; - -const getMaintenanceWindowById = async (maintenanceWindowId) => { - try { - const maintenanceWindow = await MaintenanceWindow.findById({ - _id: maintenanceWindowId, - }); - return maintenanceWindow; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getMaintenanceWindowById"; - throw error; - } -}; - -/** - * Asynchronously retrieves all MaintenanceWindow documents associated with a specific team ID. - * @async - * @function getMaintenanceWindowByUserId - * @param {String} teamId - The ID of the team. - * @param {Object} query - The request body. - * @returns {Promise>} An array of MaintenanceWindow documents. - * @throws {Error} If there is an error retrieving the documents. - */ -const getMaintenanceWindowsByTeamId = async (teamId, query) => { - try { - let { active, page, rowsPerPage, field, order } = query || {}; - const maintenanceQuery = { teamId }; - - if (active !== undefined) maintenanceQuery.active = active; - - const maintenanceWindowCount = - await MaintenanceWindow.countDocuments(maintenanceQuery); - - // Pagination - let skip = 0; - if (page && rowsPerPage) { - skip = page * rowsPerPage; - } - - // Sorting - let sort = {}; - if (field !== undefined && order !== undefined) { - sort[field] = order === "asc" ? 1 : -1; - } - - const maintenanceWindows = await MaintenanceWindow.find(maintenanceQuery) - .skip(skip) - .limit(rowsPerPage) - .sort(sort); - - return { maintenanceWindows, maintenanceWindowCount }; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getMaintenanceWindowByUserId"; - throw error; - } -}; - -/** - * Asynchronously retrieves all MaintenanceWindow documents associated with a specific monitor ID. - * @async - * @function getMaintenanceWindowsByMonitorId - * @param {mongoose.Schema.Types.ObjectId} monitorId - The ID of the monitor. - * @returns {Promise>} An array of MaintenanceWindow documents. - * @throws {Error} If there is an error retrieving the documents. - */ -const getMaintenanceWindowsByMonitorId = async (monitorId) => { - try { - const maintenanceWindows = await MaintenanceWindow.find({ - monitorId: monitorId, - }); - return maintenanceWindows; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getMaintenanceWindowsByMonitorId"; - throw error; - } -}; - -/** - * Asynchronously deletes a MaintenanceWindow document by its ID. - * @async - * @function deleteMaintenanceWindowById - * @param {mongoose.Schema.Types.ObjectId} maintenanceWindowId - The ID of the MaintenanceWindow document to delete. - * @returns {Promise} The deleted MaintenanceWindow document. - * @throws {Error} If there is an error deleting the document. - */ -const deleteMaintenanceWindowById = async (maintenanceWindowId) => { - try { - const maintenanceWindow = - await MaintenanceWindow.findByIdAndDelete(maintenanceWindowId); - return maintenanceWindow; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteMaintenanceWindowById"; - throw error; - } -}; - -/** - * Asynchronously deletes all MaintenanceWindow documents associated with a specific monitor ID. - * @async - * @function deleteMaintenanceWindowByMonitorId - * @param {mongoose.Schema.Types.ObjectId} monitorId - The ID of the monitor. - * @returns {Promise} The result of the delete operation. This object contains information about the operation, such as the number of documents deleted. - * @throws {Error} If there is an error deleting the documents. - * @example - */ -const deleteMaintenanceWindowByMonitorId = async (monitorId) => { - try { - const result = await MaintenanceWindow.deleteMany({ monitorId: monitorId }); - return result; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteMaintenanceWindowByMonitorId"; - throw error; - } -}; - -/** - * Asynchronously deletes all MaintenanceWindow documents associated with a specific user ID. - * @async - * @function deleteMaintenanceWindowByUserId - * @param {String} userId - The ID of the user. - * @returns {Promise} The result of the delete operation. This object contains information about the operation, such as the number of documents deleted. - * @throws {Error} If there is an error deleting the documents. - * @example - */ -const deleteMaintenanceWindowByUserId = async (userId) => { - try { - const result = await MaintenanceWindow.deleteMany({ userId: userId }); - return result; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteMaintenanceWindowByUserId"; - throw error; - } -}; - -const editMaintenanceWindowById = async (maintenanceWindowId, maintenanceWindowData) => { - try { - const editedMaintenanceWindow = await MaintenanceWindow.findByIdAndUpdate( - maintenanceWindowId, - maintenanceWindowData, - { new: true } - ); - return editedMaintenanceWindow; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "editMaintenanceWindowById"; - throw error; - } -}; - -export { - createMaintenanceWindow, - getMaintenanceWindowById, - getMaintenanceWindowsByTeamId, - getMaintenanceWindowsByMonitorId, - deleteMaintenanceWindowById, - deleteMaintenanceWindowByMonitorId, - deleteMaintenanceWindowByUserId, - editMaintenanceWindowById, -}; diff --git a/server/db/mongo/modules/monitorModule.js b/server/db/mongo/modules/monitorModule.js deleted file mode 100755 index bc31915c2..000000000 --- a/server/db/mongo/modules/monitorModule.js +++ /dev/null @@ -1,876 +0,0 @@ -import Monitor from "../../models/Monitor.js"; -import MonitorStats from "../../models/MonitorStats.js"; -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 fs from "fs"; -import path from "path"; -import { fileURLToPath } from "url"; -import { ObjectId } from "mongodb"; - -import { - buildUptimeDetailsPipeline, - buildHardwareDetailsPipeline, - buildMonitorSummaryByTeamIdPipeline, - buildMonitorsByTeamIdPipeline, - buildMonitorsAndSummaryByTeamIdPipeline, - buildMonitorsWithChecksByTeamIdPipeline, - buildFilteredMonitorsByTeamIdPipeline, -} from "./monitorModuleQueries.js"; -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -const demoMonitorsPath = path.resolve(__dirname, "../../../utils/demoMonitors.json"); -const demoMonitors = JSON.parse(fs.readFileSync(demoMonitorsPath, "utf8")); - -const SERVICE_NAME = "monitorModule"; - -const CHECK_MODEL_LOOKUP = { - http: Check, - ping: Check, - docker: Check, - port: Check, - pagespeed: PageSpeedCheck, - hardware: HardwareCheck, -}; - -/** - * Get all monitors - * @async - * @param {Express.Request} req - * @param {Express.Response} res - * @returns {Promise>} - * @throws {Error} - */ -const getAllMonitors = async (req, res) => { - try { - const monitors = await Monitor.find(); - return monitors; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getAllMonitors"; - throw error; - } -}; - -/** - * 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. - * @returns {number} Uptime duration in ms. - */ -const calculateUptimeDuration = (checks) => { - if (!checks || checks.length === 0) { - return 0; - } - const latestCheck = new Date(checks[0].createdAt); - let latestDownCheck = 0; - - for (let i = checks.length - 1; i >= 0; i--) { - if (checks[i].status === false) { - latestDownCheck = new Date(checks[i].createdAt); - break; - } - } - - // If no down check is found, uptime is from the last check to now - if (latestDownCheck === 0) { - return Date.now() - new Date(checks[checks.length - 1].createdAt); - } - - // Otherwise the uptime is from the last check to the last down check - return latestCheck - latestDownCheck; -}; - -/** - * Helper function to get duration since last check - * @param {Array} checks Array of check objects. - * @returns {number} Timestamp of the most recent check. - */ -const getLastChecked = (checks) => { - if (!checks || checks.length === 0) { - return 0; // Handle case when no checks are available - } - // Data is sorted newest->oldest, so last check is the most recent - return new Date() - new Date(checks[0].createdAt); -}; - -/** - * Helper function to get latestResponseTime - * @param {Array} checks Array of check objects. - * @returns {number} Timestamp of the most recent check. - */ -const getLatestResponseTime = (checks) => { - if (!checks || checks.length === 0) { - return 0; - } - - return checks[0]?.responseTime ?? 0; -}; - -/** - * Helper function to get average response time - * @param {Array} checks Array of check objects. - * @returns {number} Timestamp of the most recent check. - */ -const getAverageResponseTime = (checks) => { - if (!checks || checks.length === 0) { - return 0; - } - - const validChecks = checks.filter((check) => typeof check.responseTime === "number"); - if (validChecks.length === 0) { - return 0; - } - const aggResponseTime = validChecks.reduce((sum, check) => { - return sum + check.responseTime; - }, 0); - return aggResponseTime / validChecks.length; -}; - -/** - * Helper function to get percentage 24h uptime - * @param {Array} checks Array of check objects. - * @returns {number} Timestamp of the most recent check. - */ - -const getUptimePercentage = (checks) => { - if (!checks || checks.length === 0) { - return 0; - } - const upCount = checks.reduce((count, check) => { - return check.status === true ? count + 1 : count; - }, 0); - return (upCount / checks.length) * 100; -}; - -/** - * Helper function to get all incidents - * @param {Array} checks Array of check objects. - * @returns {number} Timestamp of the most recent check. - */ - -const getIncidents = (checks) => { - if (!checks || checks.length === 0) { - return 0; // Handle case when no checks are available - } - return checks.reduce((acc, check) => { - return check.status === false ? (acc += 1) : acc; - }, 0); -}; - -/** - * Get date range parameters - * @param {string} dateRange - 'day' | 'week' | 'month' | 'all' - * @returns {Object} Start and end dates - */ -const getDateRange = (dateRange) => { - const startDates = { - recent: new Date(new Date().setHours(new Date().getHours() - 2)), - day: new Date(new Date().setDate(new Date().getDate() - 1)), - week: new Date(new Date().setDate(new Date().getDate() - 7)), - month: new Date(new Date().setMonth(new Date().getMonth() - 1)), - all: new Date(0), - }; - return { - start: startDates[dateRange], - end: new Date(), - }; -}; - -/** - * Get checks for a monitor - * @param {string} monitorId - Monitor ID - * @param {Object} model - Check model to use - * @param {Object} dateRange - Date range parameters - * @param {number} sortOrder - Sort order (1 for ascending, -1 for descending) - * @returns {Promise} All checks and date-ranged checks - */ -const getMonitorChecks = async (monitorId, model, dateRange, sortOrder) => { - const indexSpec = { - monitorId: 1, - createdAt: sortOrder, // This will be 1 or -1 - }; - - const [checksAll, checksForDateRange] = await Promise.all([ - model.find({ monitorId }).sort({ createdAt: sortOrder }).hint(indexSpec).lean(), - model - .find({ - monitorId, - createdAt: { $gte: dateRange.start, $lte: dateRange.end }, - }) - .hint(indexSpec) - .lean(), - ]); - - return { checksAll, checksForDateRange }; -}; - -/** - * Process checks for display - * @param {Array} checks - Checks to process - * @param {number} numToDisplay - Number of checks to display - * @param {boolean} normalize - Whether to normalize the data - * @returns {Array} Processed checks - */ -const processChecksForDisplay = (normalizeData, checks, numToDisplay, normalize) => { - let processedChecks = checks; - if (numToDisplay && checks.length > numToDisplay) { - const n = Math.ceil(checks.length / numToDisplay); - processedChecks = checks.filter((_, index) => index % n === 0); - } - return normalize ? normalizeData(processedChecks, 1, 100) : processedChecks; -}; - -/** - * Get time-grouped checks based on date range - * @param {Array} checks Array of check objects - * @param {string} dateRange 'day' | 'week' | 'month' - * @returns {Object} Grouped checks by time period - */ -const groupChecksByTime = (checks, dateRange) => { - return checks.reduce((acc, check) => { - // Validate the date - const checkDate = new Date(check.createdAt); - if (Number.isNaN(checkDate.getTime()) || checkDate.getTime() === 0) { - return acc; - } - - const time = - dateRange === "day" - ? checkDate.setMinutes(0, 0, 0) - : checkDate.toISOString().split("T")[0]; - - if (!acc[time]) { - acc[time] = { time, checks: [] }; - } - acc[time].checks.push(check); - return acc; - }, {}); -}; - -/** - * Calculate aggregate stats for a group of checks - * @param {Object} group Group of checks - * @returns {Object} Stats for the group - */ -const calculateGroupStats = (group) => { - const totalChecks = group.checks.length; - - const checksWithResponseTime = group.checks.filter( - (check) => typeof check.responseTime === "number" && !Number.isNaN(check.responseTime) - ); - - return { - time: group.time, - uptimePercentage: getUptimePercentage(group.checks), - totalChecks, - totalIncidents: group.checks.filter((check) => !check.status).length, - avgResponseTime: - checksWithResponseTime.length > 0 - ? checksWithResponseTime.reduce((sum, check) => sum + check.responseTime, 0) / - checksWithResponseTime.length - : 0, - }; -}; - -/** - * Get uptime details by monitor ID - * @async - * @param {Express.Request} req - * @param {Express.Response} res - * @returns {Promise} - * @throws {Error} - */ -const getUptimeDetailsById = async ({ monitorId, dateRange, normalize }) => { - const stringService = ServiceRegistry.get(StringService.SERVICE_NAME); - try { - const dates = getDateRange(dateRange); - const formatLookup = { - recent: "%Y-%m-%dT%H:%M:00Z", - day: "%Y-%m-%dT%H:00:00Z", - week: "%Y-%m-%dT%H:00:00Z", - month: "%Y-%m-%dT00:00:00Z", - }; - - const dateString = formatLookup[dateRange]; - - const results = await Check.aggregate( - buildUptimeDetailsPipeline(monitorId, dates, dateString) - ); - - const monitorData = results[0]; - - monitorData.groupedUpChecks = NormalizeDataUptimeDetails( - monitorData.groupedUpChecks, - 10, - 100 - ); - - monitorData.groupedDownChecks = NormalizeDataUptimeDetails( - monitorData.groupedDownChecks, - 10, - 100 - ); - - const normalizedGroupChecks = NormalizeDataUptimeDetails( - monitorData.groupedChecks, - 10, - 100 - ); - - monitorData.groupedChecks = normalizedGroupChecks; - const monitorStats = await MonitorStats.findOne({ monitorId }); - return { monitorData, monitorStats }; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getUptimeDetailsById"; - throw error; - } -}; - -/** - * Get stats by monitor ID - * @async - * @param {Express.Request} req - * @param {Express.Response} res - * @returns {Promise} - * @throws {Error} - */ -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 - const monitor = await Monitor.findById(monitorId); - if (monitor === null || monitor === undefined) { - throw new Error(stringService.getDbFindMonitorById(monitorId)); - } - - // Get query params - const sort = sortOrder === "asc" ? 1 : -1; - - // 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 - ); - - // Build monitor stats - const monitorStats = { - ...monitor.toObject(), - uptimeDuration: calculateUptimeDuration(checksAll), - lastChecked: getLastChecked(checksAll), - latestResponseTime: getLatestResponseTime(checksAll), - periodIncidents: getIncidents(checksForDateRange), - periodTotalChecks: checksForDateRange.length, - checks: processChecksForDisplay( - NormalizeData, - checksForDateRange, - numToDisplay, - normalize - ), - }; - - 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); - const groupedChecks = groupChecksByTime(checksForDateRange, dateRange); - monitorStats.aggregateData = Object.values(groupedChecks).map(calculateGroupStats); - } - - return monitorStats; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getMonitorStatsById"; - throw error; - } -}; - -const getHardwareDetailsById = async ({ monitorId, dateRange }) => { - try { - const monitor = await Monitor.findById(monitorId); - const dates = getDateRange(dateRange); - const formatLookup = { - recent: "%Y-%m-%dT%H:%M:00Z", - day: "%Y-%m-%dT%H:00:00Z", - week: "%Y-%m-%dT%H:00:00Z", - month: "%Y-%m-%dT00:00:00Z", - }; - const dateString = formatLookup[dateRange]; - const hardwareStats = await HardwareCheck.aggregate( - buildHardwareDetailsPipeline(monitor, dates, dateString) - ); - - const monitorStats = { - ...monitor.toObject(), - stats: hardwareStats[0], - }; - return monitorStats; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getHardwareDetailsById"; - throw error; - } -}; - -/** - * Get a monitor by ID - * @async - * @param {Express.Request} req - * @param {Express.Response} res - * @returns {Promise} - * @throws {Error} - */ -const getMonitorById = async (monitorId) => { - const stringService = ServiceRegistry.get(StringService.SERVICE_NAME); - try { - const monitor = await Monitor.findById(monitorId); - if (monitor === null || monitor === undefined) { - const error = new Error(stringService.getDbFindMonitorById(monitorId)); - error.status = 404; - throw error; - } - - return monitor; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getMonitorById"; - throw error; - } -}; - -const getMonitorsByTeamId = async ({ - limit, - type, - page, - rowsPerPage, - filter, - field, - order, - teamId, -}) => { - limit = parseInt(limit); - page = parseInt(page); - rowsPerPage = parseInt(rowsPerPage); - if (field === undefined) { - field = "name"; - order = "asc"; - } - // Build match stage - const matchStage = { teamId: new ObjectId(teamId) }; - if (type !== undefined) { - matchStage.type = Array.isArray(type) ? { $in: type } : type; - } - - const summaryResult = await Monitor.aggregate( - buildMonitorSummaryByTeamIdPipeline({ matchStage }) - ); - const summary = summaryResult[0]; - - const monitors = await Monitor.aggregate( - buildMonitorsByTeamIdPipeline({ matchStage, field, order }) - ); - - const filteredMonitors = await Monitor.aggregate( - buildFilteredMonitorsByTeamIdPipeline({ - matchStage, - filter, - page, - rowsPerPage, - field, - order, - limit, - type, - }) - ); - - const normalizedFilteredMonitors = filteredMonitors.map((monitor) => { - if (!monitor.checks) { - return monitor; - } - monitor.checks = NormalizeData(monitor.checks, 10, 100); - return monitor; - }); - - return { summary, monitors, filteredMonitors: normalizedFilteredMonitors }; -}; - -const getMonitorsAndSummaryByTeamId = async ({ type, explain, teamId }) => { - try { - const matchStage = { teamId: new ObjectId(teamId) }; - if (type !== undefined) { - matchStage.type = Array.isArray(type) ? { $in: type } : type; - } - - if (explain === true) { - return Monitor.aggregate( - buildMonitorsAndSummaryByTeamIdPipeline({ matchStage }) - ).explain("executionStats"); - } - - const queryResult = await Monitor.aggregate( - buildMonitorsAndSummaryByTeamIdPipeline({ matchStage }) - ); - const { monitors, summary } = queryResult?.[0] ?? {}; - return { monitors, summary }; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getMonitorsAndSummaryByTeamId"; - throw error; - } -}; - -const getMonitorsWithChecksByTeamId = async ({ - limit, - type, - page, - rowsPerPage, - filter, - field, - order, - teamId, - explain, -}) => { - try { - limit = parseInt(limit); - page = parseInt(page); - rowsPerPage = parseInt(rowsPerPage); - if (field === undefined) { - field = "name"; - order = "asc"; - } - // Build match stage - const matchStage = { teamId: new ObjectId(teamId) }; - if (type !== undefined) { - matchStage.type = Array.isArray(type) ? { $in: type } : type; - } - - if (explain === true) { - return Monitor.aggregate( - buildMonitorsWithChecksByTeamIdPipeline({ - matchStage, - filter, - page, - rowsPerPage, - field, - order, - limit, - type, - }) - ).explain("executionStats"); - } - - const queryResult = await Monitor.aggregate( - buildMonitorsWithChecksByTeamIdPipeline({ - matchStage, - filter, - page, - rowsPerPage, - field, - order, - limit, - type, - }) - ); - const monitors = queryResult[0]?.monitors; - const count = queryResult[0]?.count; - const normalizedFilteredMonitors = monitors.map((monitor) => { - if (!monitor.checks) { - return monitor; - } - monitor.checks = NormalizeData(monitor.checks, 10, 100); - return monitor; - }); - return { count, monitors: normalizedFilteredMonitors }; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getMonitorsWithChecksByTeamId"; - throw error; - } -}; - -/** - * Create a monitor - * @async - * @param {Express.Request} req - * @param {Express.Response} res - * @returns {Promise} - * @throws {Error} - */ -const createMonitor = async ({ body, teamId, userId }) => { - try { - const monitor = new Monitor({ ...body, teamId, userId }); - const saved = await monitor.save(); - return saved; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "createMonitor"; - throw error; - } -}; - -/** - * Create bulk monitors - * @async - * @param {Express.Request} req - * @returns {Promise} - * @throws {Error} - */ -const createBulkMonitors = async (req) => { - try { - const monitors = req.map( - (item) => new Monitor({ ...item, notifications: undefined }) - ); - await Monitor.bulkSave(monitors); - return monitors; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "createBulkMonitors"; - throw error; - } -}; - -/** - * Delete a monitor by ID - * @async - * @param {Express.Request} req - * @param {Express.Response} res - * @returns {Promise} - * @throws {Error} - */ -const deleteMonitor = async ({ monitorId }) => { - const stringService = ServiceRegistry.get(StringService.SERVICE_NAME); - try { - const monitor = await Monitor.findByIdAndDelete(monitorId); - - if (!monitor) { - throw new Error(stringService.getDbFindMonitorById(monitorId)); - } - - return monitor; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteMonitor"; - throw error; - } -}; - -/** - * DELETE ALL MONITORS (TEMP) - */ - -const deleteAllMonitors = async (teamId) => { - try { - const monitors = await Monitor.find({ teamId }); - const { deletedCount } = await Monitor.deleteMany({ teamId }); - - return { monitors, deletedCount }; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteAllMonitors"; - throw error; - } -}; - -/** - * Delete all monitors associated with a user ID - * @async - * @param {string} userId - The ID of the user whose monitors are to be deleted. - * @returns {Promise} A promise that resolves when the operation is complete. - */ -const deleteMonitorsByUserId = async (userId) => { - try { - const result = await Monitor.deleteMany({ userId: userId }); - return result; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteMonitorsByUserId"; - throw error; - } -}; - -/** - * Edit a monitor by ID - * @async - * @param {Express.Request} req - * @param {Express.Response} res - * @returns {Promise} - * @throws {Error} - */ -const editMonitor = async (candidateId, candidateMonitor) => { - try { - const editedMonitor = await Monitor.findByIdAndUpdate(candidateId, candidateMonitor, { - new: true, - }); - return editedMonitor; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "editMonitor"; - throw error; - } -}; - -const addDemoMonitors = async (userId, teamId) => { - try { - const demoMonitorsToInsert = demoMonitors.map((monitor) => { - return { - userId, - teamId, - name: monitor.name, - description: monitor.name, - type: "http", - url: monitor.url, - interval: 60000, - }; - }); - const insertedMonitors = await Monitor.insertMany(demoMonitorsToInsert); - return insertedMonitors; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "addDemoMonitors"; - throw error; - } -}; - -const pauseMonitor = async ({ monitorId }) => { - try { - const monitor = await Monitor.findOneAndUpdate( - { _id: monitorId }, - [ - { - $set: { - isActive: { $not: "$isActive" }, - status: "$$REMOVE", - }, - }, - ], - { new: true } - ); - - return monitor; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "pauseMonitor"; - throw error; - } -}; - -export { - getAllMonitors, - getAllMonitorsWithUptimeStats, - getMonitorStatsById, - getMonitorById, - getMonitorsByTeamId, - getMonitorsAndSummaryByTeamId, - getMonitorsWithChecksByTeamId, - getUptimeDetailsById, - createMonitor, - createBulkMonitors, - deleteMonitor, - deleteAllMonitors, - deleteMonitorsByUserId, - editMonitor, - addDemoMonitors, - getHardwareDetailsById, - pauseMonitor, -}; - -// Helper functions -export { - calculateUptimeDuration, - getLastChecked, - getLatestResponseTime, - getAverageResponseTime, - getUptimePercentage, - getIncidents, - getDateRange, - getMonitorChecks, - processChecksForDisplay, - groupChecksByTime, - calculateGroupStats, -}; - -// limit 25 -// page 1 -// rowsPerPage 25 -// filter undefined -// field name -// order asc -// skip 25 -// sort { name: 1 } -// filteredMonitors [] - -// limit 25 -// page NaN -// rowsPerPage 25 -// filter undefined -// field name -// order asc -// skip 0 -// sort { name: 1 } diff --git a/server/db/mongo/modules/networkCheckModule.js b/server/db/mongo/modules/networkCheckModule.js deleted file mode 100644 index 53c55c7e3..000000000 --- a/server/db/mongo/modules/networkCheckModule.js +++ /dev/null @@ -1,46 +0,0 @@ -import NetworkCheck from "../../models/NetworkCheck.js"; - -const SERVICE_NAME = "networkCheckModule"; - -/** - * Creates and saves a new network check document to the database. - * @async - * @param {object} networkCheckData - The data for the new network check. This should conform to the NetworkCheckSchema. - * @param {string} networkCheckData.monitorId - The ID of the monitor associated with this check. - * @returns {Promise} A promise that resolves to the newly created network check document. - * @throws {Error} Throws an error if the database operation fails. - */ -const createNetworkCheck = async (networkCheckData) => { - try { - const networkCheck = await new NetworkCheck(networkCheckData); - await networkCheck.save(); - return networkCheck; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "createNetworkCheck"; - throw error; - } -}; - -/** - * Retrieves a list of network checks for a specific monitor, sorted by most recent. - * @async - * @param {string} monitorId - The ID of the monitor to retrieve checks for. - * @param {number} [limit=100] - The maximum number of checks to return. Defaults to 100. - * @returns {Promise>} A promise that resolves to an array of network check documents. - * @throws {Error} Throws an error if the database operation fails. - */ -const getNetworkChecksByMonitorId = async (monitorId, limit = 100) => { - try { - const networkChecks = await NetworkCheck.find({ monitorId }) - .sort({ createdAt: -1 }) - .limit(limit); - return networkChecks; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getNetworkChecksByMonitorId"; - throw error; - } -}; - -export { createNetworkCheck, getNetworkChecksByMonitorId }; diff --git a/server/db/mongo/modules/notificationModule.js b/server/db/mongo/modules/notificationModule.js deleted file mode 100755 index 08d57b1a9..000000000 --- a/server/db/mongo/modules/notificationModule.js +++ /dev/null @@ -1,120 +0,0 @@ -import Notification from "../../models/Notification.js"; -import Monitor from "../../models/Monitor.js"; -const SERVICE_NAME = "notificationModule"; -/** - * Creates a new notification. - * @param {Object} notificationData - The data for the new notification. - * @param {mongoose.Types.ObjectId} notificationData.monitorId - The ID of the monitor. - * @param {string} notificationData.type - The type of the notification (e.g., "email", "sms"). - * @param {string} [notificationData.address] - The address for the notification (if applicable). - * @param {string} [notificationData.phone] - The phone number for the notification (if applicable). - * @returns {Promise} The created notification. - * @throws Will throw an error if the notification cannot be created. - */ -const createNotification = async (notificationData) => { - try { - const notification = await new Notification({ ...notificationData }).save(); - return notification; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "createNotification"; - throw error; - } -}; - -const getNotificationsByTeamId = async (teamId) => { - try { - const notifications = await Notification.find({ teamId }); - return notifications; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getNotificationsByTeamId"; - throw error; - } -}; - -const getNotificationsByIds = async (notificationIds) => { - try { - const notifications = await Notification.find({ _id: { $in: notificationIds } }); - return notifications; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getNotificationsByIds"; - throw error; - } -}; - -/** - * Retrieves notifications by monitor ID. - * @param {mongoose.Types.ObjectId} monitorId - The ID of the monitor. - * @returns {Promise>} An array of notifications. - * @throws Will throw an error if the notifications cannot be retrieved. - */ -const getNotificationsByMonitorId = async (monitorId) => { - try { - const notifications = await Notification.find({ monitorId }); - return notifications; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getNotificationsByMonitorId"; - throw error; - } -}; - -const deleteNotificationsByMonitorId = async (monitorId) => { - try { - const result = await Notification.deleteMany({ monitorId }); - return result.deletedCount; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteNotificationsByMonitorId"; - throw error; - } -}; - -const deleteNotificationById = async (id) => { - try { - const result = await Notification.findByIdAndDelete(id); - await Monitor.updateMany({ notifications: id }, { $pull: { notifications: id } }); - return result; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteNotificationById"; - throw error; - } -}; - -const getNotificationById = async (id) => { - try { - const notification = await Notification.findById(id); - return notification; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getNotificationById"; - throw error; - } -}; - -const editNotification = async (id, notificationData) => { - try { - const notification = await Notification.findByIdAndUpdate(id, notificationData, { - new: true, - }); - return notification; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "editNotification"; - throw error; - } -}; - -export { - createNotification, - getNotificationsByTeamId, - getNotificationsByIds, - getNotificationsByMonitorId, - deleteNotificationsByMonitorId, - deleteNotificationById, - getNotificationById, - editNotification, -}; diff --git a/server/db/mongo/modules/pageSpeedCheckModule.js b/server/db/mongo/modules/pageSpeedCheckModule.js deleted file mode 100755 index 9169a0836..000000000 --- a/server/db/mongo/modules/pageSpeedCheckModule.js +++ /dev/null @@ -1,57 +0,0 @@ -import PageSpeedCheck from "../../models/PageSpeedCheck.js"; -const SERVICE_NAME = "pageSpeedCheckModule"; -/** - * Create a PageSpeed check for a monitor - * @async - * @param {Object} pageSpeedCheckData - * @param {string} pageSpeedCheckData.monitorId - * @param {number} pageSpeedCheckData.accessibility - * @param {number} pageSpeedCheckData.bestPractices - * @param {number} pageSpeedCheckData.seo - * @param {number} pageSpeedCheckData.performance - * @returns {Promise} - * @throws {Error} - */ -const createPageSpeedCheck = async (pageSpeedCheckData) => { - try { - const pageSpeedCheck = await new PageSpeedCheck({ - ...pageSpeedCheckData, - }).save(); - return pageSpeedCheck; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "createPageSpeedCheck"; - throw error; - } -}; -const createPageSpeedChecks = async (pageSpeedChecks) => { - try { - await PageSpeedCheck.insertMany(pageSpeedChecks, { ordered: false }); - return true; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "createPageSpeedCheck"; - throw error; - } -}; - -/** - * Delete all PageSpeed checks for a monitor - * @async - * @param {string} monitorId - * @returns {number} - * @throws {Error} - */ - -const deletePageSpeedChecksByMonitorId = async (monitorId) => { - try { - const result = await PageSpeedCheck.deleteMany({ monitorId }); - return result.deletedCount; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deletePageSpeedChecksByMonitorId"; - throw error; - } -}; - -export { createPageSpeedCheck, createPageSpeedChecks, deletePageSpeedChecksByMonitorId }; diff --git a/server/db/mongo/modules/recoveryModule.js b/server/db/mongo/modules/recoveryModule.js deleted file mode 100755 index 3b39e847c..000000000 --- a/server/db/mongo/modules/recoveryModule.js +++ /dev/null @@ -1,88 +0,0 @@ -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"; - -const SERVICE_NAME = "recoveryModule"; - -/** - * Request a recovery token - * @async - * @param {Express.Request} req - * @param {Express.Response} res - * @returns {Promise} - * @throws {Error} - */ -const requestRecoveryToken = async (req, res) => { - try { - // Delete any existing tokens - await RecoveryToken.deleteMany({ email: req.body.email }); - let recoveryToken = new RecoveryToken({ - email: req.body.email, - token: crypto.randomBytes(32).toString("hex"), - }); - await recoveryToken.save(); - return recoveryToken; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "requestRecoveryToken"; - throw error; - } -}; - -const validateRecoveryToken = async (req, res) => { - const stringService = serviceRegistry.get(StringService.SERVICE_NAME); - try { - const candidateToken = req.body.recoveryToken; - const recoveryToken = await RecoveryToken.findOne({ - token: candidateToken, - }); - if (recoveryToken !== null) { - return recoveryToken; - } else { - throw new Error(stringService.dbTokenNotFound); - } - } catch (error) { - error.service = SERVICE_NAME; - error.method = "validateRecoveryToken"; - throw error; - } -}; - -const resetPassword = async (req, res) => { - const stringService = serviceRegistry.get(StringService.SERVICE_NAME); - try { - const newPassword = req.body.password; - - // Validate token again - const recoveryToken = await validateRecoveryToken(req, res); - const user = await UserModel.findOne({ email: recoveryToken.email }); - - if (user === null) { - throw new Error(stringService.dbUserNotFound); - } - - const match = await user.comparePassword(newPassword); - if (match === true) { - throw new Error(stringService.dbResetPasswordBadMatch); - } - - user.password = newPassword; - await user.save(); - await RecoveryToken.deleteMany({ email: recoveryToken.email }); - // Fetch the user again without the password - const userWithoutPassword = await UserModel.findOne({ - email: recoveryToken.email, - }) - .select("-password") - .select("-profileImage"); - return userWithoutPassword; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "resetPassword"; - throw error; - } -}; - -export { requestRecoveryToken, validateRecoveryToken, resetPassword }; diff --git a/server/db/mongo/modules/settingsModule.js b/server/db/mongo/modules/settingsModule.js deleted file mode 100755 index 4fd0d1634..000000000 --- a/server/db/mongo/modules/settingsModule.js +++ /dev/null @@ -1,43 +0,0 @@ -import AppSettings from "../../models/AppSettings.js"; -const SERVICE_NAME = "SettingsModule"; - -const getAppSettings = async () => { - try { - const settings = AppSettings.findOne(); - return settings; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getSettings"; - throw error; - } -}; - -const updateAppSettings = async (newSettings) => { - try { - const update = { $set: { ...newSettings } }; - - if (newSettings.pagespeedApiKey === "") { - update.$unset = { pagespeedApiKey: "" }; - delete update.$set.pagespeedApiKey; - } - - if (newSettings.systemEmailPassword === "") { - update.$unset = { systemEmailPassword: "" }; - delete update.$set.systemEmailPassword; - } - - await AppSettings.findOneAndUpdate({}, update, { - upsert: true, - }); - const settings = await AppSettings.findOne() - .select("-__v -_id -createdAt -updatedAt -singleton") - .lean(); - return settings; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "updateAppSettings"; - throw error; - } -}; - -export { getAppSettings, updateAppSettings }; diff --git a/server/db/mongo/modules/statusPageModule.js b/server/db/mongo/modules/statusPageModule.js deleted file mode 100755 index 8a682d567..000000000 --- a/server/db/mongo/modules/statusPageModule.js +++ /dev/null @@ -1,256 +0,0 @@ -import StatusPage from "../../models/StatusPage.js"; -import { NormalizeData } from "../../../utils/dataUtils.js"; -import ServiceRegistry from "../../../service/serviceRegistry.js"; -import StringService from "../../../service/stringService.js"; - -const SERVICE_NAME = "statusPageModule"; - -const createStatusPage = async ({ statusPageData, image, userId, teamId }) => { - const stringService = ServiceRegistry.get(StringService.SERVICE_NAME); - - try { - const statusPage = new StatusPage({ - ...statusPageData, - userId, - teamId, - }); - if (image) { - statusPage.logo = { - data: image.buffer, - contentType: image.mimetype, - }; - } - await statusPage.save(); - return statusPage; - } catch (error) { - if (error?.code === 11000) { - // Handle duplicate URL errors - error.status = 400; - error.message = stringService.statusPageUrlNotUnique; - } - error.service = SERVICE_NAME; - error.method = "createStatusPage"; - throw error; - } -}; - -const updateStatusPage = async (statusPageData, image) => { - try { - if (image) { - statusPageData.logo = { - data: image.buffer, - contentType: image.mimetype, - }; - } else { - statusPageData.logo = null; - } - - if (statusPageData.deleteSubmonitors === "true") { - statusPageData.subMonitors = []; - } - const statusPage = await StatusPage.findOneAndUpdate( - { url: statusPageData.url }, - statusPageData, - { - new: true, - } - ); - - return statusPage; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "updateStatusPage"; - throw error; - } -}; - -const getStatusPageByUrl = async (url, type) => { - // TODO This is deprecated, can remove and have controller call getStatusPage - try { - return getStatusPage(url); - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getStatusPageByUrl"; - throw error; - } -}; - -const getStatusPagesByTeamId = async (teamId) => { - try { - const statusPages = await StatusPage.find({ teamId }); - return statusPages; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getStatusPagesByTeamId"; - throw error; - } -}; - -const getStatusPage = async (url) => { - const stringService = ServiceRegistry.get(StringService.SERVICE_NAME); - - try { - const preliminaryStatusPage = await StatusPage.findOne({ url }); - if (!preliminaryStatusPage) { - const error = new Error(stringService.statusPageNotFound); - error.status = 404; - throw error; - } - - if (!preliminaryStatusPage.monitors || preliminaryStatusPage.monitors.length === 0) { - const { - _id, - color, - companyName, - isPublished, - logo, - originalMonitors, - showCharts, - showUptimePercentage, - timezone, - showAdminLoginLink, - url, - } = preliminaryStatusPage; - return { - statusPage: { - _id, - color, - companyName, - isPublished, - logo, - originalMonitors, - showCharts, - showUptimePercentage, - timezone, - showAdminLoginLink, - url, - }, - monitors: [], - }; - } - - const statusPageQuery = await StatusPage.aggregate([ - { $match: { url: url } }, - { - $set: { - originalMonitors: "$monitors", - }, - }, - { - $lookup: { - from: "monitors", - localField: "monitors", - foreignField: "_id", - as: "monitors", - }, - }, - { - $unwind: { - path: "$monitors", - preserveNullAndEmptyArrays: true, - }, - }, - { - $lookup: { - from: "checks", - let: { monitorId: "$monitors._id" }, - pipeline: [ - { - $match: { - $expr: { $eq: ["$monitorId", "$$monitorId"] }, - }, - }, - { $sort: { createdAt: -1 } }, - { $limit: 25 }, - ], - as: "monitors.checks", - }, - }, - { - $addFields: { - "monitors.orderIndex": { - $indexOfArray: ["$originalMonitors", "$monitors._id"], - }, - }, - }, - { $match: { "monitors.orderIndex": { $ne: -1 } } }, - { $sort: { "monitors.orderIndex": 1 } }, - - { - $group: { - _id: "$_id", - statusPage: { $first: "$$ROOT" }, - monitors: { $push: "$monitors" }, - }, - }, - { - $project: { - statusPage: { - _id: 1, - color: 1, - companyName: 1, - isPublished: 1, - logo: 1, - originalMonitors: 1, - showCharts: 1, - showUptimePercentage: 1, - timezone: 1, - showAdminLoginLink: 1, - url: 1, - }, - monitors: 1, - }, - }, - ]); - if (!statusPageQuery.length) { - const error = new Error(stringService.statusPageNotFound); - error.status = 404; - throw error; - } - - const { statusPage, monitors } = statusPageQuery[0]; - - const normalizedMonitors = monitors.map((monitor) => { - return { - ...monitor, - checks: NormalizeData(monitor.checks, 10, 100), - }; - }); - - return { statusPage, monitors: normalizedMonitors }; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getStatusPageByUrl"; - throw error; - } -}; - -const deleteStatusPage = async (url) => { - try { - await StatusPage.deleteOne({ url }); - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteStatusPage"; - throw error; - } -}; - -const deleteStatusPagesByMonitorId = async (monitorId) => { - try { - await StatusPage.deleteMany({ monitors: { $in: [monitorId] } }); - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteStatusPageByMonitorId"; - throw error; - } -}; - -export { - createStatusPage, - updateStatusPage, - getStatusPagesByTeamId, - getStatusPage, - getStatusPageByUrl, - deleteStatusPage, - deleteStatusPagesByMonitorId, -}; diff --git a/server/db/mongo/modules/userModule.js b/server/db/mongo/modules/userModule.js deleted file mode 100755 index 4069be8c6..000000000 --- a/server/db/mongo/modules/userModule.js +++ /dev/null @@ -1,232 +0,0 @@ -import UserModel from "../../models/User.js"; -import TeamModel from "../../models/Team.js"; -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"; -const SERVICE_NAME = "userModule"; - -/** - * Insert a User - * @async - * @param {Express.Request} req - * @param {Express.Response} res - * @returns {Promise} - * @throws {Error} - */ -const insertUser = async ( - userData, - imageFile, - generateAvatarImage = GenerateAvatarImage -) => { - const stringService = ServiceRegistry.get(StringService.SERVICE_NAME); - try { - if (imageFile) { - // 1. Save the full size image - userData.profileImage = { - data: imageFile.buffer, - contentType: imageFile.mimetype, - }; - - // 2. Get the avatar sized image - const avatar = await generateAvatarImage(imageFile); - userData.avatarImage = avatar; - } - - // Handle creating team if superadmin - if (userData.role.includes("superadmin")) { - const team = new TeamModel({ - email: userData.email, - }); - userData.teamId = team._id; - userData.checkTTL = 60 * 60 * 24 * 30; - await team.save(); - } - - 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 - } catch (error) { - if (error.code === DUPLICATE_KEY_CODE) { - error.message = stringService.dbUserExists; - } - error.service = SERVICE_NAME; - error.method = "insertUser"; - throw error; - } -}; - -/** - * Get User by Email - * Gets a user by Email. Not sure if we'll ever need this except for login. - * If not needed except for login, we can move password comparison here - * Throws error if user not found - * @async - * @param {Express.Request} req - * @param {Express.Response} res - * @returns {Promise} - * @throws {Error} - */ -const getUserByEmail = async (email) => { - const stringService = ServiceRegistry.get(StringService.SERVICE_NAME); - - try { - // Need the password to be able to compare, removed .select() - // We can strip the hash before returning the user - const user = await UserModel.findOne({ email: email }).select("-profileImage"); - if (!user) { - throw new Error(stringService.dbUserNotFound); - } - return user; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getUserByEmail"; - throw error; - } -}; - -/** - * Update a user by ID - * @async - * @param {Express.Request} req - * @param {Express.Response} res - * @returns {Promise} - * @throws {Error} - */ - -const updateUser = async ( - req, - res, - parseBoolean = ParseBoolean, - generateAvatarImage = GenerateAvatarImage -) => { - const candidateUserId = req.params.userId; - try { - const candidateUser = { ...req.body }; - // ****************************************** - // Handle profile image - // ****************************************** - - if (parseBoolean(candidateUser.deleteProfileImage) === true) { - candidateUser.profileImage = null; - candidateUser.avatarImage = null; - } else if (req.file) { - // 1. Save the full size image - candidateUser.profileImage = { - data: req.file.buffer, - contentType: req.file.mimetype, - }; - - // 2. Get the avatar sized image - const avatar = await generateAvatarImage(req.file); - candidateUser.avatarImage = avatar; - } - - // ****************************************** - // End handling profile image - // ****************************************** - - const updatedUser = await UserModel.findByIdAndUpdate( - candidateUserId, - candidateUser, - { new: true } // Returns updated user instead of pre-update user - ) - .select("-password") - .select("-profileImage"); - return updatedUser; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "updateUser"; - throw error; - } -}; - -/** - * Delete a user by ID - * @async - * @param {Express.Request} req - * @param {Express.Response} res - * @returns {Promise} - * @throws {Error} - */ -const deleteUser = async (userId) => { - const stringService = ServiceRegistry.get(StringService.SERVICE_NAME); - - try { - const deletedUser = await UserModel.findByIdAndDelete(userId); - if (!deletedUser) { - throw new Error(stringService.dbUserNotFound); - } - return deletedUser; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteUser"; - throw error; - } -}; - -/** - * Delete a user by ID - * @async - * @param {string} teamId - * @returns {void} - * @throws {Error} - */ -const deleteTeam = async (teamId) => { - try { - await TeamModel.findByIdAndDelete(teamId); - return true; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteTeam"; - throw error; - } -}; - -const deleteAllOtherUsers = async () => { - try { - await UserModel.deleteMany({ role: { $ne: "superadmin" } }); - return true; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteAllOtherUsers"; - throw error; - } -}; - -const getAllUsers = async (req, res) => { - try { - const users = await UserModel.find().select("-password").select("-profileImage"); - return users; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getAllUsers"; - throw error; - } -}; - -const logoutUser = async (userId) => { - try { - await UserModel.updateOne({ _id: userId }, { $unset: { authToken: null } }); - return true; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "logoutUser"; - throw error; - } -}; - -export { - insertUser, - getUserByEmail, - updateUser, - deleteUser, - deleteTeam, - deleteAllOtherUsers, - getAllUsers, - logoutUser, -}; diff --git a/server/index.js b/server/index.js deleted file mode 100755 index 4b74ef46c..000000000 --- a/server/index.js +++ /dev/null @@ -1,405 +0,0 @@ -import path from "path"; -import fs from "fs"; -import swaggerUi from "swagger-ui-express"; - -import express from "express"; -import helmet from "helmet"; -import cors from "cors"; -import compression from "compression"; -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 AuthRoutes from "./routes/authRoute.js"; -import AuthController from "./controllers/authController.js"; - -import InviteRoutes from "./routes/inviteRoute.js"; -import InviteController from "./controllers/inviteController.js"; - -import MonitorRoutes from "./routes/monitorRoute.js"; -import MonitorController from "./controllers/monitorController.js"; - -import CheckRoutes from "./routes/checkRoute.js"; -import CheckController from "./controllers/checkController.js"; - -import MaintenanceWindowRoutes from "./routes/maintenanceWindowRoute.js"; -import MaintenanceWindowController from "./controllers/maintenanceWindowController.js"; - -import SettingsRoutes from "./routes/settingsRoute.js"; -import SettingsController from "./controllers/settingsController.js"; - -import StatusPageRoutes from "./routes/statusPageRoute.js"; -import StatusPageController from "./controllers/statusPageController.js"; - -import QueueRoutes from "./routes/queueRoute.js"; -import QueueController from "./controllers/queueController.js"; - -import LogRoutes from "./routes/logRoutes.js"; -import LogController from "./controllers/logController.js"; - -import NotificationRoutes from "./routes/notificationRoute.js"; -import NotificationController from "./controllers/notificationController.js"; - -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 { Queue, Worker } from "bullmq"; - -import PulseQueue from "./service/PulseQueue/PulseQueue.js"; -import PulseQueueHelper from "./service/PulseQueue/PulseQueueHelper.js"; - -//Network service and dependencies -import NetworkService from "./service/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 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 AppSettings from "./db/models/AppSettings.js"; - -// Status Service and dependencies -import StatusService from "./service/statusService.js"; - -// Notification Service and dependencies -import NotificationService from "./service/notificationService.js"; -import NotificationUtils from "./service/notificationUtils.js"; - -// Buffer Service and dependencies -import BufferService from "./service/bufferService.js"; - -// Service Registry -import ServiceRegistry from "./service/serviceRegistry.js"; - -import MongoDB from "./db/mongo/MongoDB.js"; - -// Redis Service and dependencies -import IORedis from "ioredis"; -import RedisService from "./service/redisService.js"; - -import TranslationService from "./service/translationService.js"; -import languageMiddleware from "./middleware/languageMiddleware.js"; -import StringService from "./service/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 frontendPath = path.join(__dirname, "public"); - -let server; - -const shutdown = async () => { - if (isShuttingDown) { - return; - } - isShuttingDown = true; - logger.info({ message: "Attempting graceful shutdown" }); - setTimeout(async () => { - logger.error({ - message: "Could not shut down in time, forcing shutdown", - service: SERVICE_NAME, - method: "shutdown", - }); - await ServiceRegistry.get(RedisService.SERVICE_NAME).flushRedis(); - process.exit(1); - }, SHUTDOWN_TIMEOUT); - try { - server.close(); - await ServiceRegistry.get(JobQueue.SERVICE_NAME).shutdown(); - await ServiceRegistry.get(MongoDB.SERVICE_NAME).disconnect(); - logger.info({ message: "Graceful shutdown complete" }); - process.exit(0); - } catch (error) { - logger.error({ - message: error.message, - service: SERVICE_NAME, - method: "shutdown", - stack: error.stack, - }); - } -}; -// Need to wrap server setup in a function to handle async nature of JobQueue -const startApp = async () => { - const app = express(); - // Create and Register Primary services - const translationService = new TranslationService(logger); - const stringService = new StringService(translationService); - ServiceRegistry.register(StringService.SERVICE_NAME, stringService); - - // Create services - const settingsService = new SettingsService(AppSettings); - const appSettings = settingsService.loadSettings(); - - // Create DB - const db = new MongoDB({ appSettings }); - await db.connect(); - - // 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 bufferService = new BufferService({ db, logger }); - const statusService = new StatusService({ db, logger, buffer: bufferService }); - const notificationUtils = new NotificationUtils({ - stringService, - emailService, - }); - - const notificationService = new NotificationService({ - emailService, - db, - logger, - networkService, - stringService, - notificationUtils, - }); - - const redisService = new RedisService({ Redis: IORedis, logger }); - - // const jobQueueHelper = new JobQueueHelper({ - // redisService, - // Queue, - // Worker, - // logger, - // db, - // networkService, - // statusService, - // notificationService, - // }); - // const jobQueue = await JobQueue.create({ - // db, - // jobQueueHelper, - // logger, - // stringService, - // }); - - const pulseQueueHelper = new PulseQueueHelper({ - db, - logger, - networkService, - statusService, - notificationService, - }); - const pulseQueue = await PulseQueue.create({ - appSettings, - db, - pulseQueueHelper, - logger, - }); - - // Register services - // ServiceRegistry.register(JobQueue.SERVICE_NAME, jobQueue); - ServiceRegistry.register(JobQueue.SERVICE_NAME, pulseQueue); - ServiceRegistry.register(MongoDB.SERVICE_NAME, db); - ServiceRegistry.register(SettingsService.SERVICE_NAME, settingsService); - ServiceRegistry.register(EmailService.SERVICE_NAME, emailService); - ServiceRegistry.register(NetworkService.SERVICE_NAME, networkService); - ServiceRegistry.register(BufferService.SERVICE_NAME, bufferService); - ServiceRegistry.register(StatusService.SERVICE_NAME, statusService); - ServiceRegistry.register(NotificationService.SERVICE_NAME, notificationService); - ServiceRegistry.register(TranslationService.SERVICE_NAME, translationService); - ServiceRegistry.register(RedisService.SERVICE_NAME, redisService); - - await translationService.initialize(); - - const port = appSettings.port || 52345; - server = app.listen(port, () => { - logger.info({ message: `Server started on port:${port}` }); - }); - - process.on("SIGUSR2", shutdown); - process.on("SIGINT", shutdown); - process.on("SIGTERM", shutdown); - - //Create controllers - const authController = new AuthController({ - db: ServiceRegistry.get(MongoDB.SERVICE_NAME), - 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, - }); - - 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), - 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 diagnosticController = new DiagnosticController( - ServiceRegistry.get(MongoDB.SERVICE_NAME) - ); - - //Create routes - const authRoutes = new AuthRoutes(authController); - const monitorRoutes = new MonitorRoutes(monitorController); - const settingsRoutes = new SettingsRoutes(settingsController); - const checkRoutes = new CheckRoutes(checkController); - const inviteRoutes = new InviteRoutes(inviteController); - 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); - app.use( - cors({ - origin: allowedOrigin, - methods: "GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS", - allowedHeaders: "*", - credentials: true, - }) - ); - app.use(express.json()); - app.use( - helmet({ - hsts: false, - contentSecurityPolicy: { - useDefaults: true, - directives: { - upgradeInsecureRequests: null, - }, - }, - }) - ); - app.use( - compression({ - level: 6, - threshold: 1024, - filter: (req, res) => { - if (req.headers["x-no-compression"]) { - return false; - } - return compression.filter(req, res); - }, - }) - ); - - app.use(languageMiddleware(stringService, translationService, settingsService)); - // Swagger UI - app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(openApiSpec)); - - //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/checks", verifyJWT, checkRoutes.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", - }); - }); - // FE routes - app.get("*", (req, res) => { - res.sendFile(path.join(frontendPath, "index.html")); - }); - app.use(handleErrors); -}; - -startApp().catch((error) => { - logger.error({ - message: error.message, - service: SERVICE_NAME, - method: "startApp", - stack: error.stack, - }); - process.exit(1); -}); diff --git a/server/middleware/languageMiddleware.js b/server/middleware/languageMiddleware.js deleted file mode 100755 index 1449e203f..000000000 --- a/server/middleware/languageMiddleware.js +++ /dev/null @@ -1,28 +0,0 @@ -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(); - - 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(); - - translationService.setLanguage(language); - stringService.setLanguage(language); - - next(); - } - }; - -export default languageMiddleware; diff --git a/server/nodemon.json b/server/nodemon.json index 928d1abc2..88d6ac686 100755 --- a/server/nodemon.json +++ b/server/nodemon.json @@ -1,5 +1,5 @@ { - "ignore": ["locales/*", "*.log", "node_modules/*"], - "watch": ["*.js", "*.json"], + "ignore": ["src/locales/*", "*.log", "node_modules/*"], + "watch": ["src/**/*.js", "*.json"], "ext": "js,json" } diff --git a/server/openapi.json b/server/openapi.json old mode 100755 new mode 100644 index 7b7bb3cb3..6cc2e7de0 --- a/server/openapi.json +++ b/server/openapi.json @@ -1,7 +1,7 @@ { "openapi": "3.1.0", "info": { - "title": "Checkmate", + "title": "Checkmate API", "summary": "Checkmate OpenAPI Specifications", "description": "Checkmate is an open source monitoring tool used to track the operational status and performance of servers and websites. It regularly checks whether a server/website is accessible and performs optimally, providing real-time alerts and reports on the monitored services' availability, downtime, and response time.", "contact": { @@ -13,7 +13,7 @@ "name": "AGPLv3", "url": "https://github.com/bluewave-labs/checkmate/tree/HEAD/LICENSE" }, - "version": "1.0" + "version": "2.3" }, "servers": [ { @@ -22,8 +22,8 @@ "variables": { "PORT": { "description": "API Port", - "enum": ["5000"], - "default": "5000" + "enum": ["52345"], + "default": "52345" }, "API_PATH": { "description": "API Base Path", @@ -32,26 +32,10 @@ } } }, - { - "url": "http://localhost/{API_PATH}", - "description": "Distribution Local Development Server", - "variables": { - "API_PATH": { - "description": "API Base Path", - "enum": ["api/v1"], - "default": "api/v1" - } - } - }, { "url": "https://checkmate-demo.bluewavelabs.ca/{API_PATH}", "description": "Checkmate Demo Server", "variables": { - "PORT": { - "description": "API Port", - "enum": ["5000"], - "default": "5000" - }, "API_PATH": { "description": "API Base Path", "enum": ["api/v1"], @@ -63,114 +47,81 @@ "tags": [ { "name": "auth", - "description": "Authentication" + "description": "Authentication and user management" }, { "name": "invite", - "description": "Invite" + "description": "Team invitation management" }, { "name": "monitors", - "description": "Monitors" + "description": "Monitor management (uptime, page speed, hardware)" }, { "name": "checks", - "description": "Checks" + "description": "Monitor check results and history" }, { "name": "maintenance-window", - "description": "Maintenance window" + "description": "Scheduled maintenance windows" }, { "name": "queue", - "description": "Queue" + "description": "Job queue management" }, { "name": "status-page", - "description": "Status Page" + "description": "Public status page management" + }, + { + "name": "settings", + "description": "Application configuration settings" + }, + { + "name": "logs", + "description": "Application logs and diagnostics" + }, + { + "name": "notifications", + "description": "Notification integrations (email, slack, discord, etc.)" + }, + { + "name": "diagnostic", + "description": "System health and performance diagnostics" } ], "paths": { "/auth/register": { "post": { "tags": ["auth"], - "description": "Register a new user", + "summary": "Register new user", + "description": "Register a new user account with profile information", "requestBody": { + "required": true, "content": { "application/json": { "schema": { - "type": "object", - "required": [ - "firstName", - "lastName", - "email", - "password", - "role", - "teamId" - ], - "properties": { - "firstName": { - "type": "string" - }, - "lastName": { - "type": "string" - }, - "email": { - "type": "string", - "format": "email" - }, - "password": { - "type": "string", - "format": "password" - }, - "profileImage": { - "type": "file", - "format": "file" - }, - "role": { - "type": "array", - "enum": [["user"], ["admin"], ["superadmin"], ["Demo"]], - "default": ["superadmin"] - }, - "teamId": { - "type": "string", - "format": "uuid" - } - } + "$ref": "#/components/schemas/RegisterRequest" } } } }, "responses": { "200": { - "description": "OK", + "description": "User registered successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SuccessResponse" + "$ref": "#/components/schemas/AuthResponse" } } } }, "422": { - "description": "Unprocessable Content", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "$ref": "#/components/responses/ValidationError" }, "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "$ref": "#/components/responses/InternalServerError" } } } @@ -178,57 +129,37 @@ "/auth/login": { "post": { "tags": ["auth"], - "description": "Login with credentials", + "summary": "User login", + "description": "Authenticate user with email and password", "requestBody": { + "required": true, "content": { "application/json": { "schema": { - "type": "object", - "required": ["email", "password"], - "properties": { - "email": { - "type": "string", - "format": "email" - }, - "password": { - "type": "string", - "format": "password" - } - } + "$ref": "#/components/schemas/LoginRequest" } } } }, "responses": { "200": { - "description": "OK", + "description": "Login successful", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SuccessResponse" + "$ref": "#/components/schemas/AuthResponse" } } } }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, "422": { - "description": "Unprocessable Content", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "$ref": "#/components/responses/ValidationError" }, "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "$ref": "#/components/responses/InternalServerError" } } } @@ -236,23 +167,13 @@ "/auth/refresh": { "post": { "tags": ["auth"], - "description": "Generates a new auth token if the refresh token is valid.", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": {} - } - } - }, - "required": false - }, + "summary": "Refresh access token", + "description": "Generate new access token using refresh token", "parameters": [ { "name": "x-refresh-token", "in": "header", - "description": "Refresh token required to generate a new auth token.", + "description": "Refresh token", "required": true, "schema": { "type": "string" @@ -261,7 +182,7 @@ { "name": "authorization", "in": "header", - "description": "Old access token, used to extract payload).", + "description": "Bearer token (old access token)", "required": true, "schema": { "type": "string" @@ -270,34 +191,20 @@ ], "responses": { "200": { - "description": "New access token generated.", + "description": "Token refreshed successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SuccessResponse" + "$ref": "#/components/schemas/AuthResponse" } } } }, "401": { - "description": "Unauthorized or invalid refresh token.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "$ref": "#/components/responses/Unauthorized" }, "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "$ref": "#/components/responses/InternalServerError" } } } @@ -305,7 +212,1472 @@ "/auth/user/{userId}": { "put": { "tags": ["auth"], - "description": "Change user information", + "summary": "Update user profile", + "description": "Update user information including profile image", + "parameters": [ + { + "name": "userId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/UserUpdateRequest" + } + } + } + }, + "responses": { + "200": { + "$ref": "#/components/responses/Success" + }, + "422": { + "$ref": "#/components/responses/ValidationError" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "delete": { + "tags": ["auth"], + "summary": "Delete user account", + "description": "Permanently delete user account and associated data", + "parameters": [ + { + "name": "userId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/Success" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/auth/users": { + "get": { + "tags": ["auth"], + "summary": "Get all users", + "description": "Retrieve all users (admin/superadmin only)", + "responses": { + "200": { + "description": "Users retrieved successfully", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + } + } + } + ] + } + } + } + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/auth/users/superadmin": { + "get": { + "tags": ["auth"], + "summary": "Check superadmin exists", + "description": "Check if a superadmin account exists in the system", + "responses": { + "200": { + "description": "Check completed", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "exists": { + "type": "boolean" + } + } + } + } + } + ] + } + } + } + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + }, + "/auth/recovery/request": { + "post": { + "tags": ["auth"], + "summary": "Request password reset", + "description": "Send password reset email to user", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["email"], + "properties": { + "email": { + "type": "string", + "format": "email", + "description": "User email address" + } + } + } + } + } + }, + "responses": { + "200": { + "$ref": "#/components/responses/Success" + }, + "404": { + "description": "User not found" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + }, + "/auth/recovery/validate": { + "post": { + "tags": ["auth"], + "summary": "Validate recovery token", + "description": "Validate password reset token", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["recoveryToken"], + "properties": { + "recoveryToken": { + "type": "string", + "description": "Password reset token" + } + } + } + } + } + }, + "responses": { + "200": { + "$ref": "#/components/responses/Success" + }, + "400": { + "description": "Invalid or expired token" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + }, + "/auth/recovery/reset": { + "post": { + "tags": ["auth"], + "summary": "Reset password", + "description": "Reset user password with recovery token", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["recoveryToken", "password"], + "properties": { + "recoveryToken": { + "type": "string", + "description": "Password reset token" + }, + "password": { + "type": "string", + "format": "password", + "description": "New password" + } + } + } + } + } + }, + "responses": { + "200": { + "$ref": "#/components/responses/Success" + }, + "400": { + "description": "Invalid token or password requirements not met" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + }, + "/monitors": { + "get": { + "tags": ["monitors"], + "summary": "Get all monitors", + "description": "Retrieve all monitors for the authenticated user", + "responses": { + "200": { + "description": "Monitors retrieved successfully", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Monitor" + } + } + } + } + ] + } + } + } + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "post": { + "tags": ["monitors"], + "summary": "Create monitor", + "description": "Create a new monitoring endpoint", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateMonitorRequest" + } + } + } + }, + "responses": { + "201": { + "description": "Monitor created successfully", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Monitor" + } + } + } + ] + } + } + } + }, + "422": { + "$ref": "#/components/responses/ValidationError" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "delete": { + "tags": ["monitors"], + "summary": "Delete all monitors", + "description": "Delete all monitors (superadmin only)", + "responses": { + "200": { + "$ref": "#/components/responses/Success" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/monitors/team": { + "get": { + "tags": ["monitors"], + "summary": "Get monitors by team", + "description": "Get monitors filtered by team with optional parameters", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter by monitor status (up/down)", + "schema": { + "type": "boolean" + } + }, + { + "name": "type", + "in": "query", + "description": "Filter by monitor type", + "schema": { + "type": "string", + "enum": ["http", "ping", "pagespeed", "hardware", "docker", "port"] + } + }, + { + "name": "page", + "in": "query", + "description": "Page number for pagination", + "schema": { + "type": "integer", + "minimum": 1, + "default": 1 + } + }, + { + "name": "rowsPerPage", + "in": "query", + "description": "Number of monitors per page", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 10 + } + }, + { + "name": "filter", + "in": "query", + "description": "Search filter value", + "schema": { + "type": "string" + } + }, + { + "name": "field", + "in": "query", + "description": "Field to filter on", + "schema": { + "type": "string", + "enum": ["name", "url", "description"] + } + } + ], + "responses": { + "200": { + "description": "Team monitors retrieved successfully", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "monitors": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Monitor" + } + }, + "totalCount": { + "type": "integer" + } + } + } + } + } + ] + } + } + } + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/monitors/team/with-checks": { + "get": { + "tags": ["monitors"], + "summary": "Get monitors with recent checks", + "description": "Get team monitors with their most recent check results", + "parameters": [ + { + "name": "limit", + "in": "query", + "description": "Number of recent checks to include per monitor", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 50, + "default": 10 + } + } + ], + "responses": { + "200": { + "description": "Monitors with checks retrieved successfully" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/monitors/team/summary": { + "get": { + "tags": ["monitors"], + "summary": "Get monitors summary", + "description": "Get team monitors with summary statistics", + "parameters": [ + { + "name": "type", + "in": "query", + "description": "Filter by monitor type", + "schema": { + "type": "array", + "items": { + "type": "string", + "enum": ["http", "ping", "pagespeed", "hardware", "docker", "port"] + } + } + } + ], + "responses": { + "200": { + "description": "Monitor summary retrieved successfully" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/monitors/{monitorId}": { + "get": { + "tags": ["monitors"], + "summary": "Get monitor by ID", + "description": "Retrieve a specific monitor by its ID", + "parameters": [ + { + "name": "monitorId", + "in": "path", + "required": true, + "description": "Monitor ID", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Monitor retrieved successfully", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Monitor" + } + } + } + ] + } + } + } + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "put": { + "tags": ["monitors"], + "summary": "Update monitor", + "description": "Update an existing monitor", + "parameters": [ + { + "name": "monitorId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateMonitorRequest" + } + } + } + }, + "responses": { + "200": { + "$ref": "#/components/responses/Success" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "422": { + "$ref": "#/components/responses/ValidationError" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "delete": { + "tags": ["monitors"], + "summary": "Delete monitor", + "description": "Delete a specific monitor", + "parameters": [ + { + "name": "monitorId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/Success" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/monitors/uptime/details/{monitorId}": { + "get": { + "tags": ["monitors"], + "summary": "Get uptime details", + "description": "Get detailed uptime statistics for a monitor", + "parameters": [ + { + "name": "monitorId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Uptime details retrieved successfully" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/monitors/hardware/details/{monitorId}": { + "get": { + "tags": ["monitors"], + "summary": "Get hardware monitoring details", + "description": "Get hardware performance metrics for a monitor", + "parameters": [ + { + "name": "monitorId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Hardware details retrieved successfully" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/monitors/pause/{monitorId}": { + "post": { + "tags": ["monitors"], + "summary": "Pause/unpause monitor", + "description": "Toggle monitor active status", + "parameters": [ + { + "name": "monitorId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/Success" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/monitors/stats/{monitorId}": { + "get": { + "tags": ["monitors"], + "summary": "Get monitor statistics", + "description": "Get comprehensive statistics for a monitor", + "parameters": [ + { + "name": "monitorId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Monitor statistics retrieved successfully" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/monitors/certificate/{monitorId}": { + "get": { + "tags": ["monitors"], + "summary": "Get SSL certificate info", + "description": "Get SSL certificate information for a monitor", + "parameters": [ + { + "name": "monitorId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Certificate information retrieved successfully" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/monitors/demo": { + "post": { + "tags": ["monitors"], + "summary": "Add demo monitors", + "description": "Add preconfigured demo monitors for testing", + "responses": { + "200": { + "$ref": "#/components/responses/Success" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/monitors/export": { + "get": { + "tags": ["monitors"], + "summary": "Export monitors to CSV", + "description": "Export all monitors to CSV format", + "responses": { + "200": { + "description": "CSV file generated successfully", + "content": { + "text/csv": { + "schema": { + "type": "string" + } + } + } + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/monitors/bulk": { + "post": { + "tags": ["monitors"], + "summary": "Bulk import monitors", + "description": "Import monitors from CSV file", + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "csvFile": { + "type": "string", + "format": "binary", + "description": "CSV file containing monitor data" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Monitors imported successfully" + }, + "422": { + "$ref": "#/components/responses/ValidationError" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/monitors/test-email": { + "post": { + "tags": ["monitors"], + "summary": "Send test email", + "description": "Send a test email notification", + "responses": { + "200": { + "$ref": "#/components/responses/Success" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/settings": { + "get": { + "tags": ["settings"], + "summary": "Get application settings", + "description": "Retrieve current application configuration", + "responses": { + "200": { + "description": "Settings retrieved successfully", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/AppSettings" + } + } + } + ] + } + } + } + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "put": { + "tags": ["settings"], + "summary": "Update application settings", + "description": "Update application configuration (admin/superadmin only)", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AppSettings" + } + } + } + }, + "responses": { + "200": { + "$ref": "#/components/responses/Success" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "422": { + "$ref": "#/components/responses/ValidationError" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/settings/test-email": { + "post": { + "tags": ["settings"], + "summary": "Send test email", + "description": "Send test email to verify email configuration", + "responses": { + "200": { + "$ref": "#/components/responses/Success" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/notifications": { + "get": { + "tags": ["notifications"], + "summary": "Get team notifications", + "description": "Get all notification configurations for the team", + "responses": { + "200": { + "description": "Notifications retrieved successfully", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Notification" + } + } + } + } + ] + } + } + } + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "post": { + "tags": ["notifications"], + "summary": "Create notification", + "description": "Create a new notification integration", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateNotificationRequest" + } + } + } + }, + "responses": { + "201": { + "description": "Notification created successfully" + }, + "422": { + "$ref": "#/components/responses/ValidationError" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/notifications/{id}": { + "get": { + "tags": ["notifications"], + "summary": "Get notification by ID", + "description": "Retrieve a specific notification configuration", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Notification retrieved successfully" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "put": { + "tags": ["notifications"], + "summary": "Update notification", + "description": "Update an existing notification configuration", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateNotificationRequest" + } + } + } + }, + "responses": { + "200": { + "$ref": "#/components/responses/Success" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "422": { + "$ref": "#/components/responses/ValidationError" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "delete": { + "tags": ["notifications"], + "summary": "Delete notification", + "description": "Delete a notification configuration", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/Success" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/notifications/test": { + "post": { + "tags": ["notifications"], + "summary": "Test notification", + "description": "Send a test notification to verify configuration", + "responses": { + "200": { + "$ref": "#/components/responses/Success" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/notifications/test/all": { + "post": { + "tags": ["notifications"], + "summary": "Test all notifications", + "description": "Send test notifications to all configured integrations", + "responses": { + "200": { + "$ref": "#/components/responses/Success" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/logs": { + "get": { + "tags": ["logs"], + "summary": "Get application logs", + "description": "Retrieve application logs (admin/superadmin only)", + "parameters": [ + { + "name": "level", + "in": "query", + "description": "Log level filter", + "schema": { + "type": "string", + "enum": ["error", "warn", "info", "debug"] + } + }, + { + "name": "limit", + "in": "query", + "description": "Number of log entries to return", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 1000, + "default": 100 + } + } + ], + "responses": { + "200": { + "description": "Logs retrieved successfully" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/diagnostic/system": { + "get": { + "tags": ["diagnostic"], + "summary": "Get system diagnostics", + "description": "Get system health and performance metrics (admin/superadmin only)", + "responses": { + "200": { + "description": "System diagnostics retrieved successfully", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/SystemDiagnostics" + } + } + } + ] + } + } + } + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/auth/user": { + "put": { + "tags": ["auth"], + "summary": "Update current user profile", + "description": "Update authenticated user's profile information", + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/UserUpdateRequest" + } + } + } + }, + "responses": { + "200": { + "$ref": "#/components/responses/Success" + }, + "422": { + "$ref": "#/components/responses/ValidationError" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "delete": { + "tags": ["auth"], + "summary": "Delete current user account", + "description": "Delete authenticated user's account and associated data", + "responses": { + "200": { + "$ref": "#/components/responses/Success" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/auth/users/{userId}": { + "get": { + "tags": ["auth"], + "summary": "Get user by ID", + "description": "Get a specific user by ID (superadmin only)", + "parameters": [ + { + "name": "userId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "User retrieved successfully", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/User" + } + } + } + ] + } + } + } + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "put": { + "tags": ["auth"], + "summary": "Update user by ID", + "description": "Update a specific user by ID (superadmin only)", "parameters": [ { "name": "userId", @@ -328,85 +1700,19 @@ }, "responses": { "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuccessResponse" - } - } - } + "$ref": "#/components/responses/Success" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "403": { + "$ref": "#/components/responses/Forbidden" }, "422": { - "description": "Unprocessable Content", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "$ref": "#/components/responses/ValidationError" }, "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - }, - "delete": { - "tags": ["auth"], - "description": "Delete user", - "parameters": [ - { - "name": "userId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuccessResponse" - } - } - } - }, - "422": { - "description": "Unprocessable Content", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "$ref": "#/components/responses/InternalServerError" } }, "security": [ @@ -416,97 +1722,63 @@ ] } }, - "/auth/users/superadmin": { - "get": { - "tags": ["auth"], - "description": "Checks to see if an admin account exists", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuccessResponse" - } - } - } - }, - "422": { - "description": "Unprocessable Content", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/auth/users": { - "get": { - "tags": ["auth"], - "description": "Get all users", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuccessResponse" - } - } - } - }, - "422": { - "description": "Unprocessable Content", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/auth/recovery/request": { + "/invite": { "post": { - "tags": ["auth"], - "description": "Request a recovery token", + "tags": ["invite"], + "summary": "Create invite token", + "description": "Create a new invitation token", "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["email", "role"], + "properties": { + "email": { + "type": "string", + "format": "email" + }, + "role": { + "type": "array", + "items": { + "type": "string", + "enum": ["user", "admin"] + } + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Invite token created successfully" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "422": { + "$ref": "#/components/responses/ValidationError" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/invite/send": { + "post": { + "tags": ["invite"], + "summary": "Send invitation email", + "description": "Send invitation email to user", + "requestBody": { + "required": true, "content": { "application/json": { "schema": { @@ -524,199 +1796,13 @@ }, "responses": { "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuccessResponse" - } - } - } + "$ref": "#/components/responses/Success" }, - "422": { - "description": "Unprocessable Content", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "403": { + "$ref": "#/components/responses/Forbidden" }, "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - } - } - }, - "/auth/recovery/validate": { - "post": { - "tags": ["auth"], - "description": "Validate recovery token", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["recoveryToken"], - "properties": { - "recoveryToken": { - "type": "string" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuccessResponse" - } - } - } - }, - "422": { - "description": "Unprocessable Content", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - } - } - }, - "/auth/recovery/reset": { - "post": { - "tags": ["auth"], - "description": "Password reset", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["recoveryToken", "password"], - "properties": { - "recoveryToken": { - "type": "string" - }, - "password": { - "type": "string" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuccessResponse" - } - } - } - }, - "422": { - "description": "Unprocessable Content", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - } - } - }, - "/invite": { - "post": { - "tags": ["invite"], - "description": "Request an invitation", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["email", "role"], - "properties": { - "email": { - "type": "string" - }, - "role": { - "type": "array" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuccessResponse" - } - } - } - }, - "422": { - "description": "Unprocessable Content", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "$ref": "#/components/responses/InternalServerError" } }, "security": [ @@ -729,8 +1815,10 @@ "/invite/verify": { "post": { "tags": ["invite"], - "description": "Request an invitation", + "summary": "Verify invitation token", + "description": "Verify an invitation token", "requestBody": { + "required": true, "content": { "application/json": { "schema": { @@ -747,851 +1835,22 @@ }, "responses": { "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuccessResponse" - } - } - } - }, - "422": { - "description": "Unprocessable Content", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/monitors": { - "get": { - "tags": ["monitors"], - "description": "Get all monitors", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuccessResponse" - } - } - } - }, - "422": { - "description": "Unprocessable Content", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - }, - "post": { - "tags": ["monitors"], - "description": "Create a new monitor", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateMonitorBody" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuccessResponse" - } - } - } - }, - "422": { - "description": "Unprocessable Content", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - }, - "delete": { - "tags": ["monitors"], - "description": "Delete all monitors", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuccessResponse" - } - } - } - }, - "422": { - "description": "Unprocessable Content", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/monitors/uptime": { - "get": { - "tags": ["monitors"], - "description": "Get all monitors with uptime stats for 1, 7, 30, and 90 days", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuccessResponse" - } - } - } - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/monitors/resolution/url": { - "get": { - "tags": ["monitors"], - "description": "Check DNS resolution for a given URL", - "parameters": [ - { - "name": "monitorURL", - "in": "query", - "required": true, - "schema": { - "type": "string", - "example": "https://example.com" - }, - "description": "The URL to check DNS resolution for" - } - ], - "responses": { - "200": { - "description": "URL resolved successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuccessResponse" - } - } - } + "$ref": "#/components/responses/Success" }, "400": { - "description": "DNS resolution failed", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "422": { - "description": "Unprocessable Content", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "description": "Invalid or expired token" }, "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "$ref": "#/components/responses/InternalServerError" } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/monitors/{monitorId}": { - "get": { - "tags": ["monitors"], - "description": "Get monitor by id", - "parameters": [ - { - "name": "monitorId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuccessResponse" - } - } - } - }, - "422": { - "description": "Unprocessable Content", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - }, - "put": { - "tags": ["monitors"], - "description": "Update monitor by id", - "parameters": [ - { - "name": "monitorId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateMonitorBody" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuccessResponse" - } - } - } - }, - "422": { - "description": "Unprocessable Content", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - }, - "delete": { - "tags": ["monitors"], - "description": "Delete monitor by id", - "parameters": [ - { - "name": "monitorId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuccessResponse" - } - } - } - }, - "422": { - "description": "Unprocessable Content", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/monitors/stats/{monitorId}": { - "get": { - "tags": ["monitors"], - "description": "Get monitor stats", - "parameters": [ - { - "name": "monitorId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuccessResponse" - } - } - } - }, - "422": { - "description": "Unprocessable Content", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/monitors/certificate/{monitorId}": { - "get": { - "tags": ["monitors"], - "description": "Get monitor certificate", - "parameters": [ - { - "name": "monitorId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuccessResponse" - } - } - } - }, - "422": { - "description": "Unprocessable Content", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/monitors/team/summary/{teamId}": { - "get": { - "tags": ["monitors"], - "description": "Get monitors and summary by teamId", - "parameters": [ - { - "name": "teamId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "type", - "in": "query", - "required": false, - "schema": { - "type": "array", - "enum": ["http", "ping", "pagespeed"] - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuccessResponse" - } - } - } - }, - "422": { - "description": "Unprocessable Content", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/monitors/team/{teamId}": { - "get": { - "tags": ["monitors"], - "description": "Get monitors by teamId", - "parameters": [ - { - "name": "teamId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "status", - "description": "Status of monitor, true for up, false for down", - "in": "query", - "required": false, - "schema": { - "type": "boolean" - } - }, - { - "name": "checkOrder", - "description": "Order of checks", - "in": "query", - "required": false, - "schema": { - "type": "string", - "enum": ["asc", "desc"] - } - }, - { - "name": "limit", - "description": "Number of checks to return with monitor", - "in": "query", - "required": false, - "schema": { - "type": "integer" - } - }, - { - "name": "type", - "description": "Type of monitor", - "in": "query", - "required": false, - "schema": { - "type": "string", - "enum": ["http", "ping", "pagespeed"] - } - }, - { - "name": "page", - "in": "query", - "required": false, - "schema": { - "type": "integer" - } - }, - { - "name": "rowsPerPage", - "in": "query", - "required": false, - "schema": { - "type": "integer" - } - }, - { - "name": "filter", - "description": "Value to filter by", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "field", - "description": "Field to filter on", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "order", - "description": "Sort order of results", - "in": "query", - "required": false, - "schema": { - "type": "string", - "enum": ["http", "ping", "pagespeed"] - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuccessResponse" - } - } - } - }, - "422": { - "description": "Unprocessable Content", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/monitors/pause/{monitorId}": { - "post": { - "tags": ["monitors"], - "description": "Pause monitor", - "parameters": [ - { - "name": "monitorId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuccessResponse" - } - } - } - }, - "422": { - "description": "Unprocessable Content", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/monitors/demo": { - "post": { - "tags": ["monitors"], - "description": "Create a demo monitor", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateMonitorBody" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuccessResponse" - } - } - } - }, - "422": { - "description": "Unprocessable Content", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - }, - "security": [ - { - "bearerAuth": [] - } - ] + } } }, "/checks/{monitorId}": { "get": { "tags": ["checks"], - "description": "Get all checks for a monitor", + "summary": "Get checks by monitor", + "description": "Get all checks for a specific monitor", "parameters": [ { "name": "monitorId", @@ -1604,94 +1863,13 @@ ], "responses": { "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuccessResponse" - } - } - } + "description": "Checks retrieved successfully" }, - "422": { - "description": "Unprocessable Content", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "404": { + "$ref": "#/components/responses/NotFound" }, "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - }, - "post": { - "tags": ["checks"], - "description": "Create a new check", - "parameters": [ - { - "name": "monitorId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateCheckBody" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuccessResponse" - } - } - } - }, - "422": { - "description": "Unprocessable Content", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "$ref": "#/components/responses/InternalServerError" } }, "security": [ @@ -1702,7 +1880,8 @@ }, "delete": { "tags": ["checks"], - "description": "Delete all checks for a monitor", + "summary": "Delete checks by monitor", + "description": "Delete all checks for a specific monitor", "parameters": [ { "name": "monitorId", @@ -1715,34 +1894,13 @@ ], "responses": { "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuccessResponse" - } - } - } + "$ref": "#/components/responses/Success" }, - "422": { - "description": "Unprocessable Content", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "404": { + "$ref": "#/components/responses/NotFound" }, "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "$ref": "#/components/responses/InternalServerError" } }, "security": [ @@ -1752,50 +1910,17 @@ ] } }, - "/checks/team/{teamId}": { + "/checks/team": { "get": { "tags": ["checks"], - "description": "Get all checks for a team", - "parameters": [ - { - "name": "teamId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], + "summary": "Get checks by team", + "description": "Get all checks for team monitors", "responses": { "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuccessResponse" - } - } - } - }, - "422": { - "description": "Unprocessable Content", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "description": "Team checks retrieved successfully" }, "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "$ref": "#/components/responses/InternalServerError" } }, "security": [ @@ -1806,47 +1931,37 @@ }, "delete": { "tags": ["checks"], - "description": "Delete all checks for a team", - "parameters": [ - { - "name": "teamId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], + "summary": "Delete team checks", + "description": "Delete all checks for team (admin/superadmin only)", "responses": { "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuccessResponse" - } - } - } + "$ref": "#/components/responses/Success" }, - "422": { - "description": "Unprocessable Content", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "403": { + "$ref": "#/components/responses/Forbidden" }, "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/checks/team/summary": { + "get": { + "tags": ["checks"], + "summary": "Get team checks summary", + "description": "Get summary statistics for team checks", + "responses": { + "200": { + "description": "Team checks summary retrieved successfully" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" } }, "security": [ @@ -1859,48 +1974,151 @@ "/checks/team/ttl": { "put": { "tags": ["checks"], - "description": "Update check TTL", + "summary": "Update checks TTL", + "description": "Update time-to-live for checks (admin/superadmin only)", "requestBody": { + "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateCheckTTLBody" + "type": "object", + "required": ["ttl"], + "properties": { + "ttl": { + "type": "integer", + "minimum": 1, + "description": "Time to live in days" + } + } } } } }, "responses": { "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuccessResponse" - } - } - } + "$ref": "#/components/responses/Success" + }, + "403": { + "$ref": "#/components/responses/Forbidden" }, "422": { - "description": "Unprocessable Content", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "$ref": "#/components/responses/ValidationError" }, "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/checks/check/{checkId}": { + "put": { + "tags": ["checks"], + "summary": "Acknowledge check", + "description": "Acknowledge a specific check", + "parameters": [ + { + "name": "checkId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/Success" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/maintenance-window": { + "post": { + "tags": ["maintenance-window"], + "summary": "Create maintenance window", + "description": "Create a new maintenance window", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["name", "startTime", "endTime"], + "properties": { + "name": { + "type": "string", + "maxLength": 100 + }, + "description": { + "type": "string", + "maxLength": 500 + }, + "startTime": { + "type": "string", + "format": "date-time" + }, + "endTime": { + "type": "string", + "format": "date-time" + }, + "monitors": { + "type": "array", + "items": { + "type": "string" + } + } } } } } }, + "responses": { + "201": { + "description": "Maintenance window created successfully" + }, + "422": { + "$ref": "#/components/responses/ValidationError" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/maintenance-window/team": { + "get": { + "tags": ["maintenance-window"], + "summary": "Get team maintenance windows", + "description": "Get all maintenance windows for the team", + "responses": { + "200": { + "description": "Maintenance windows retrieved successfully" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, "security": [ { "bearerAuth": [] @@ -1911,7 +2129,8 @@ "/maintenance-window/monitor/{monitorId}": { "get": { "tags": ["maintenance-window"], - "description": "Get maintenance window for monitor", + "summary": "Get maintenance windows by monitor", + "description": "Get all maintenance windows for a specific monitor", "parameters": [ { "name": "monitorId", @@ -1924,35 +2143,47 @@ ], "responses": { "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuccessResponse" - } - } - } + "description": "Monitor maintenance windows retrieved successfully" }, - "422": { - "description": "Unprocessable Content", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "404": { + "$ref": "#/components/responses/NotFound" }, "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/maintenance-window/{id}": { + "get": { + "tags": ["maintenance-window"], + "summary": "Get maintenance window by ID", + "description": "Get a specific maintenance window", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" } } + ], + "responses": { + "200": { + "description": "Maintenance window retrieved successfully" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } }, "security": [ { @@ -1960,12 +2191,13 @@ } ] }, - "post": { + "put": { "tags": ["maintenance-window"], - "description": "Create maintenance window for monitor", + "summary": "Update maintenance window", + "description": "Update an existing maintenance window", "parameters": [ { - "name": "monitorId", + "name": "id", "in": "path", "required": true, "schema": { @@ -1974,44 +2206,51 @@ } ], "requestBody": { + "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateMaintenanceWindowBody" + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 100 + }, + "description": { + "type": "string", + "maxLength": 500 + }, + "startTime": { + "type": "string", + "format": "date-time" + }, + "endTime": { + "type": "string", + "format": "date-time" + }, + "monitors": { + "type": "array", + "items": { + "type": "string" + } + } + } } } } }, "responses": { "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuccessResponse" - } - } - } + "$ref": "#/components/responses/Success" + }, + "404": { + "$ref": "#/components/responses/NotFound" }, "422": { - "description": "Unprocessable Content", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "$ref": "#/components/responses/ValidationError" }, "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "$ref": "#/components/responses/InternalServerError" } }, "security": [ @@ -2019,15 +2258,14 @@ "bearerAuth": [] } ] - } - }, - "/maintenance-window/user/{userId}": { - "get": { + }, + "delete": { "tags": ["maintenance-window"], - "description": "Get maintenance window for user", + "summary": "Delete maintenance window", + "description": "Delete a specific maintenance window", "parameters": [ { - "name": "userId", + "name": "id", "in": "path", "required": true, "schema": { @@ -2037,34 +2275,13 @@ ], "responses": { "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuccessResponse" - } - } - } + "$ref": "#/components/responses/Success" }, - "422": { - "description": "Unprocessable Content", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "404": { + "$ref": "#/components/responses/NotFound" }, "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "$ref": "#/components/responses/InternalServerError" } }, "security": [ @@ -2074,81 +2291,238 @@ ] } }, - "/queue/jobs": { + "/queue/health": { "get": { "tags": ["queue"], - "description": "Get all jobs in queue", + "summary": "Check queue health", + "description": "Check the health status of the job queue (admin/superadmin only)", "responses": { "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuccessResponse" - } - } - } + "description": "Queue health status retrieved successfully" }, - "422": { - "description": "Unprocessable Content", + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/queue/all-metrics": { + "get": { + "tags": ["queue"], + "summary": "Get all queue metrics", + "description": "Get comprehensive queue metrics (admin/superadmin only)", + "responses": { + "200": { + "description": "All queue metrics retrieved successfully" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/queue/flush": { + "post": { + "tags": ["queue"], + "summary": "Flush queue", + "description": "Clear all jobs from the queue (admin/superadmin only)", + "responses": { + "200": { + "$ref": "#/components/responses/Success" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/monitors/games": { + "get": { + "tags": ["monitors"], + "summary": "Get game server list", + "description": "Get available game servers for monitoring", + "responses": { + "200": { + "description": "Game servers retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "allOf": [ + { + "$ref": "#/components/schemas/SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + } + } + } + } + } + ] } } } }, "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/status-page": { + "get": { + "tags": ["status-page"], + "summary": "Get status page", + "description": "Get default status page information", + "responses": { + "200": { + "$ref": "#/components/responses/Success" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + }, + "post": { + "tags": ["status-page"], + "summary": "Create status page", + "description": "Create a new status page with optional logo upload", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "logo": { + "type": "string", + "format": "binary", + "description": "Logo file for the status page" + }, + "title": { + "type": "string", + "description": "Status page title" + }, + "description": { + "type": "string", + "description": "Status page description" + }, + "url": { + "type": "string", + "description": "Custom URL for the status page" + } } } } } }, + "responses": { + "201": { + "$ref": "#/components/responses/Success" + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, "security": [ { "bearerAuth": [] } ] }, - "post": { - "tags": ["queue"], - "description": "Create a new job. Useful for testing scaling workers", + "put": { + "tags": ["status-page"], + "summary": "Update status page", + "description": "Update an existing status page with optional logo upload", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "logo": { + "type": "string", + "format": "binary", + "description": "Logo file for the status page" + }, + "title": { + "type": "string", + "description": "Status page title" + }, + "description": { + "type": "string", + "description": "Status page description" + }, + "url": { + "type": "string", + "description": "Custom URL for the status page" + } + } + } + } + } + }, "responses": { "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuccessResponse" - } - } - } + "$ref": "#/components/responses/Success" }, - "422": { - "description": "Unprocessable Content", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "404": { + "$ref": "#/components/responses/NotFound" }, "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "$ref": "#/components/responses/InternalServerError" } }, "security": [ @@ -2158,83 +2532,20 @@ ] } }, - "/queue/metrics": { + "/status-page/team": { "get": { - "tags": ["queue"], - "description": "Get queue metrics", + "tags": ["status-page"], + "summary": "Get status pages by team", + "description": "Get all status pages for the authenticated user's team", "responses": { "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuccessResponse" - } - } - } + "$ref": "#/components/responses/Success" }, - "422": { - "description": "Unprocessable Content", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "401": { + "$ref": "#/components/responses/Unauthorized" }, "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/queue/obliterate": { - "post": { - "tags": ["queue"], - "description": "Obliterate job queue", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuccessResponse" - } - } - } - }, - "422": { - "description": "Unprocessable Content", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "$ref": "#/components/responses/InternalServerError" } }, "security": [ @@ -2247,106 +2558,58 @@ "/status-page/{url}": { "get": { "tags": ["status-page"], - "description": "Get a status page by URL", + "summary": "Get status page by URL", + "description": "Get a specific status page by its custom URL", "parameters": [ { "name": "url", "in": "path", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + }, + "description": "Custom URL of the status page" } ], "responses": { "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/SuccessResponse" } - } - } - }, - "422": { - "description": "Unprocessable Content", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "$ref": "#/components/responses/Success" }, "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "$ref": "#/components/responses/NotFound" }, "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/ErrorResponse" } - } - } + "$ref": "#/components/responses/InternalServerError" } } }, - "post": { + "delete": { "tags": ["status-page"], - "description": "Create a status page", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateStatusPageBody" - } - } + "summary": "Delete status page", + "description": "Delete a status page by its custom URL", + "parameters": [ + { + "name": "url", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Custom URL of the status page (supports wildcards)" } - }, + ], "responses": { "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuccessResponse" - } - } - } + "$ref": "#/components/responses/Success" }, - "422": { - "description": "Unprocessable Content", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "401": { + "$ref": "#/components/responses/Unauthorized" }, - "400": { - "description": "Duplicate URL", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "404": { + "$ref": "#/components/responses/NotFound" }, "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "$ref": "#/components/responses/InternalServerError" } }, "security": [ @@ -2362,72 +2625,597 @@ "bearerAuth": { "type": "http", "scheme": "bearer", - "bearerFormat": "JWT" + "bearerFormat": "JWT", + "description": "JWT token obtained from login endpoint" + } + }, + "responses": { + "Success": { + "description": "Operation successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse" + } + } + } + }, + "NotFound": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "ValidationError": { + "description": "Validation failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "Unauthorized": { + "description": "Authentication required or token invalid", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "Forbidden": { + "description": "Insufficient permissions", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "InternalServerError": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } }, "schemas": { - "ErrorResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "default": false - }, - "msg": { - "type": "string" - } - } - }, "SuccessResponse": { "type": "object", + "required": ["success", "msg"], "properties": { "success": { "type": "boolean", - "default": true + "example": true }, "msg": { - "type": "string" + "type": "string", + "example": "Operation completed successfully" }, "data": { - "type": "object" + "type": "object", + "description": "Response payload (varies by endpoint)" } } }, - "UserUpdateRequest": { + "ErrorResponse": { "type": "object", - "required": ["firstName", "lastName", "email", "password", "role", "teamId"], + "required": ["success", "msg"], + "properties": { + "success": { + "type": "boolean", + "example": false + }, + "msg": { + "type": "string", + "example": "An error occurred" + }, + "details": { + "type": "object", + "description": "Additional error details" + } + } + }, + "RegisterRequest": { + "type": "object", + "required": ["firstName", "lastName", "email", "password"], "properties": { "firstName": { - "type": "string" + "type": "string", + "minLength": 1, + "maxLength": 50, + "example": "John" }, "lastName": { - "type": "string" + "type": "string", + "minLength": 1, + "maxLength": 50, + "example": "Doe" + }, + "email": { + "type": "string", + "format": "email", + "example": "john@example.com" }, "password": { "type": "string", - "format": "password" - }, - "newPassword": { - "type": "string", - "format": "password" + "minLength": 8, + "format": "password", + "example": "SecurePass123!" }, "profileImage": { - "type": "file", - "format": "file" + "type": "string", + "format": "binary", + "description": "Optional profile image file" }, "role": { "type": "array", - "enum": [["user"], ["admin"], ["superadmin"], ["Demo"]], - "default": ["superadmin"] - }, - "deleteProfileImage": { - "type": "boolean" + "items": { + "type": "string", + "enum": ["user", "admin", "superadmin", "Demo"] + }, + "default": ["user"] } } }, - "CreateMonitorBody": { + "LoginRequest": { + "type": "object", + "required": ["email", "password"], + "properties": { + "email": { + "type": "string", + "format": "email", + "example": "john@example.com" + }, + "password": { + "type": "string", + "format": "password", + "example": "SecurePass123!" + } + } + }, + "AuthResponse": { + "allOf": [ + { + "$ref": "#/components/schemas/SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "token": { + "type": "string", + "description": "JWT access token" + }, + "user": { + "$ref": "#/components/schemas/User" + } + } + } + } + } + ] + }, + "UserUpdateRequest": { + "type": "object", + "properties": { + "firstName": { + "type": "string", + "minLength": 1, + "maxLength": 50 + }, + "lastName": { + "type": "string", + "minLength": 1, + "maxLength": 50 + }, + "password": { + "type": "string", + "format": "password", + "description": "Current password for verification" + }, + "newPassword": { + "type": "string", + "minLength": 8, + "format": "password", + "description": "New password (if changing)" + }, + "profileImage": { + "type": "string", + "format": "binary", + "description": "New profile image file" + }, + "deleteProfileImage": { + "type": "boolean", + "description": "Flag to delete current profile image" + } + } + }, + "User": { + "type": "object", + "properties": { + "_id": { + "type": "string", + "example": "64f123a456b789012c345def" + }, + "firstName": { + "type": "string", + "example": "John" + }, + "lastName": { + "type": "string", + "example": "Doe" + }, + "email": { + "type": "string", + "format": "email", + "example": "john@example.com" + }, + "role": { + "type": "array", + "items": { + "type": "string", + "enum": ["user", "admin", "superadmin", "Demo"] + } + }, + "profileImage": { + "type": "string", + "description": "URL or path to profile image" + }, + "isActive": { + "type": "boolean" + }, + "teamId": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + } + }, + "CreateMonitorRequest": { + "type": "object", + "required": ["name", "description", "type", "url", "interval"], + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "example": "My Website Monitor" + }, + "description": { + "type": "string", + "maxLength": 500, + "example": "Monitors the main website homepage" + }, + "type": { + "type": "string", + "enum": ["http", "ping", "pagespeed", "hardware", "docker", "port"], + "example": "http" + }, + "url": { + "type": "string", + "format": "uri", + "example": "https://example.com" + }, + "interval": { + "type": "integer", + "minimum": 30, + "maximum": 3600, + "example": 300, + "description": "Check interval in seconds" + }, + "isActive": { + "type": "boolean", + "default": true + }, + "notifications": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Array of notification IDs to associate with this monitor" + }, + "httpOptions": { + "type": "object", + "properties": { + "method": { + "type": "string", + "enum": ["GET", "POST", "PUT", "DELETE", "HEAD"], + "default": "GET" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "body": { + "type": "string", + "description": "Request body for POST/PUT requests" + }, + "timeout": { + "type": "integer", + "minimum": 1000, + "maximum": 30000, + "default": 5000, + "description": "Request timeout in milliseconds" + } + } + }, + "assertions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["status-code", "response-time", "body-contains", "header-contains"] + }, + "comparison": { + "type": "string", + "enum": ["equals", "not-equals", "greater-than", "less-than", "contains", "not-contains"] + }, + "value": { + "type": "string" + } + } + } + } + } + }, + "UpdateMonitorRequest": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "description": { + "type": "string", + "maxLength": 500 + }, + "interval": { + "type": "integer", + "minimum": 30, + "maximum": 3600 + }, + "isActive": { + "type": "boolean" + }, + "notifications": { + "type": "array", + "items": { + "type": "string" + } + }, + "httpOptions": { + "type": "object", + "properties": { + "method": { + "type": "string", + "enum": ["GET", "POST", "PUT", "DELETE", "HEAD"] + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "body": { + "type": "string" + }, + "timeout": { + "type": "integer", + "minimum": 1000, + "maximum": 30000 + } + } + }, + "assertions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["status-code", "response-time", "body-contains", "header-contains"] + }, + "comparison": { + "type": "string", + "enum": ["equals", "not-equals", "greater-than", "less-than", "contains", "not-contains"] + }, + "value": { + "type": "string" + } + } + } + } + } + }, + "Monitor": { + "type": "object", + "properties": { + "_id": { + "type": "string", + "example": "64f123a456b789012c345def" + }, + "userId": { + "type": "string" + }, + "teamId": { + "type": "string" + }, + "name": { + "type": "string", + "example": "My Website Monitor" + }, + "description": { + "type": "string", + "example": "Monitors the main website homepage" + }, + "type": { + "type": "string", + "enum": ["http", "ping", "pagespeed", "hardware", "docker", "port"] + }, + "url": { + "type": "string", + "format": "uri", + "example": "https://example.com" + }, + "interval": { + "type": "integer", + "example": 300 + }, + "isActive": { + "type": "boolean", + "example": true + }, + "status": { + "type": "boolean", + "description": "Current monitor status (up/down)" + }, + "lastChecked": { + "type": "string", + "format": "date-time", + "description": "Timestamp of last check" + }, + "notifications": { + "type": "array", + "items": { + "type": "string" + } + }, + "httpOptions": { + "type": "object", + "properties": { + "method": { + "type": "string" + }, + "headers": { + "type": "object" + }, + "body": { + "type": "string" + }, + "timeout": { + "type": "integer" + } + } + }, + "assertions": { + "type": "array", + "items": { + "type": "object" + } + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + } + }, + "CreateNotificationRequest": { + "type": "object", + "required": ["type", "name"], + "properties": { + "type": { + "type": "string", + "enum": ["email", "webhook", "slack", "discord", "telegram", "zapier"], + "example": "email" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "example": "Admin Email Notifications" + }, + "config": { + "type": "object", + "description": "Configuration specific to notification type", + "oneOf": [ + { + "title": "Email Configuration", + "properties": { + "to": { + "type": "array", + "items": { + "type": "string", + "format": "email" + } + } + } + }, + { + "title": "Webhook Configuration", + "properties": { + "url": { + "type": "string", + "format": "uri" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + { + "title": "Slack Configuration", + "properties": { + "webhookUrl": { + "type": "string", + "format": "uri" + }, + "channel": { + "type": "string" + } + } + }, + { + "title": "Discord Configuration", + "properties": { + "webhookUrl": { + "type": "string", + "format": "uri" + } + } + } + ] + }, + "isActive": { + "type": "boolean", + "default": true + } + } + }, + "Notification": { "type": "object", - "required": ["userId", "teamId", "name", "description", "type", "url"], "properties": { "_id": { "type": "string" @@ -2438,122 +3226,153 @@ "teamId": { "type": "string" }, + "type": { + "type": "string", + "enum": ["email", "webhook", "slack", "discord", "telegram", "zapier"] + }, "name": { "type": "string" }, - "description": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["http", "ping", "pagespeed"] - }, - "url": { - "type": "string" + "config": { + "type": "object", + "description": "Type-specific configuration" }, "isActive": { "type": "boolean" }, - "interval": { - "type": "integer" + "createdAt": { + "type": "string", + "format": "date-time" }, - "notifications": { - "type": "array", - "items": { - "type": "string" + "updatedAt": { + "type": "string", + "format": "date-time" + } + } + }, + "AppSettings": { + "type": "object", + "properties": { + "appName": { + "type": "string", + "example": "Checkmate" + }, + "appUrl": { + "type": "string", + "format": "uri", + "example": "https://checkmate.example.com" + }, + "logLevel": { + "type": "string", + "enum": ["error", "warn", "info", "debug"], + "default": "info" + }, + "emailConfig": { + "type": "object", + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "integer" + }, + "secure": { + "type": "boolean" + }, + "user": { + "type": "string" + }, + "pass": { + "type": "string", + "format": "password" + }, + "from": { + "type": "string", + "format": "email" + } } + }, + "webhookRetries": { + "type": "integer", + "minimum": 0, + "maximum": 10, + "default": 3 + }, + "checksRetention": { + "type": "integer", + "minimum": 1, + "maximum": 365, + "default": 90, + "description": "Days to retain check results" } } }, - "UpdateMonitorBody": { + "SystemDiagnostics": { "type": "object", "properties": { - "name": { - "type": "string" + "uptime": { + "type": "number", + "description": "System uptime in seconds" }, - "description": { - "type": "string" - }, - "interval": { - "type": "integer" - }, - "notifications": { - "type": "array", - "items": { - "type": "string" + "memory": { + "type": "object", + "properties": { + "total": { + "type": "number", + "description": "Total memory in bytes" + }, + "used": { + "type": "number", + "description": "Used memory in bytes" + }, + "free": { + "type": "number", + "description": "Free memory in bytes" + } } - } - } - }, - "CreateCheckBody": { - "type": "object", - "required": ["monitorId", "status", "responseTime", "statusCode", "message"], - "properties": { - "monitorId": { - "type": "string" }, - "status": { - "type": "boolean" + "cpu": { + "type": "object", + "properties": { + "usage": { + "type": "number", + "description": "CPU usage percentage" + }, + "cores": { + "type": "integer", + "description": "Number of CPU cores" + } + } }, - "responseTime": { - "type": "integer" + "database": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["connected", "disconnected", "error"] + }, + "responseTime": { + "type": "number", + "description": "Database response time in milliseconds" + } + } }, - "statusCode": { - "type": "integer" - }, - "message": { - "type": "string" - } - } - }, - "UpdateCheckTTLBody": { - "type": "object", - "required": ["ttl"], - "properties": { - "ttl": { - "type": "integer" - } - } - }, - "CreateMaintenanceWindowBody": { - "type": "object", - "required": ["userId", "active", "oneTime", "start", "end"], - "properties": { - "userId": { - "type": "string" - }, - "active": { - "type": "boolean" - }, - "oneTime": { - "type": "boolean" - }, - "start": { - "type": "string", - "format": "date-time" - }, - "end": { - "type": "string", - "format": "date-time" - }, - "expiry": { - "type": "string", - "format": "date-time" - } - } - }, - "CreateStatusPageBody": { - "type": "object", - "required": ["companyName", "url", "timezone", "color", "theme", "monitors"], - "properties": { - "companyName": { "type": "string" }, - "url": { "type": "string" }, - "timezone": { "type": "string" }, - "color": { "type": "string" }, - "theme": { "type": "string" }, - "monitors": { - "type": "array", - "items": { "type": "string" } + "queue": { + "type": "object", + "properties": { + "active": { + "type": "integer" + }, + "waiting": { + "type": "integer" + }, + "completed": { + "type": "integer" + }, + "failed": { + "type": "integer" + } + } } } } diff --git a/server/package-lock.json b/server/package-lock.json old mode 100755 new mode 100644 index 3fd675af3..3084a5361 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -13,16 +13,21 @@ "axios": "^1.7.2", "bcryptjs": "3.0.2", "bullmq": "5.41.2", - "compression": "1.8.0", + "compression": "1.8.1", + "cookie-parser": "^1.4.7", "cors": "^2.8.5", "dockerode": "4.0.6", "dotenv": "^16.4.5", "express": "^4.19.2", + "express-rate-limit": "8.0.1", + "gamedig": "^5.3.1", "handlebars": "^4.7.8", "helmet": "^8.0.0", "ioredis": "^5.4.2", + "isomorphic-dompurify": "^2.26.0", "jmespath": "^0.16.0", "joi": "^17.13.1", + "jsdom": "^26.1.0", "jsonwebtoken": "9.0.2", "mailersend": "^2.2.0", "mjml": "^5.0.0-alpha.4", @@ -33,6 +38,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" }, @@ -50,37 +56,47 @@ "sinon": "19.0.2" } }, - "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==", + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@babel/code-frame": { + "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.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 +108,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": { @@ -110,6 +126,116 @@ "node": ">=0.1.90" } }, + "node_modules/@csstools/color-helpers": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", + "integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.10.tgz", + "integrity": "sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.0.2", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@dabh/diagnostics": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", @@ -122,9 +248,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 +258,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 +281,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 +294,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 +314,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 +338,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 +361,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 +374,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 +412,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 +425,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 +462,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 +472,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 +486,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 +500,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 +510,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 +928,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 +956,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 +1130,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", @@ -1072,6 +1158,18 @@ "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", "license": "BSD-3-Clause" }, + "node_modules/@sindresorhus/is": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", + "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, "node_modules/@sinonjs/commons": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", @@ -1121,20 +1219,30 @@ "dev": true, "license": "(Unlicense OR Apache-2.0)" }, - "node_modules/@trysound/sax": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", - "license": "ISC", + "node_modules/@szmarczak/http-timer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", + "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.1" + }, "engines": { - "node": ">=10.13.0" + "node": ">=14.16" } }, "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/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "license": "MIT" }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", @@ -1147,15 +1255,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": { @@ -1164,6 +1273,13 @@ "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", "license": "MIT" }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/webidl-conversions": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", @@ -1192,11 +1308,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 +1335,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 +1354,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", @@ -1277,6 +1402,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "license": "MIT" + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -1340,13 +1471,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" } }, @@ -1356,6 +1487,39 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/barse": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/barse/-/barse-0.4.3.tgz", + "integrity": "sha512-UEpvriJqAn8zuVinYICuKoPttZy3XxXEoqX/V2uYAL4zzJRuNzCK3+20nAu3YUIa2U7G53kf90wfBIp9/A+Odw==", + "license": "MIT", + "dependencies": { + "readable-stream": "~1.0.2" + } + }, + "node_modules/barse/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "license": "MIT" + }, + "node_modules/barse/node_modules/readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/barse/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "license": "MIT" + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -1463,12 +1627,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 +1657,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 +1676,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 +1689,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" @@ -1645,10 +1811,37 @@ } } }, + "node_modules/cacheable-lookup": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", + "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, + "node_modules/cacheable-request": { + "version": "10.2.14", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz", + "integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==", + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "^4.0.2", + "get-stream": "^6.0.1", + "http-cache-semantics": "^4.1.1", + "keyv": "^4.5.3", + "mimic-response": "^4.0.0", + "normalize-url": "^8.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + } + }, "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 +1852,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 +1902,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 +2027,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", @@ -2031,16 +2242,16 @@ } }, "node_modules/compression": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.0.tgz", - "integrity": "sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", "license": "MIT", "dependencies": { "bytes": "3.1.2", "compressible": "~2.0.18", "debug": "2.6.9", "negotiator": "~0.6.4", - "on-headers": "~1.0.2", + "on-headers": "~1.1.0", "safe-buffer": "5.2.1", "vary": "~1.1.2" }, @@ -2063,15 +2274,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", @@ -2161,6 +2363,28 @@ "node": ">= 0.6" } }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", @@ -2265,9 +2489,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", @@ -2280,23 +2504,10 @@ "url": "https://github.com/sponsors/fb55" } }, - "node_modules/css-tree": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", - "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", - "license": "MIT", - "dependencies": { - "mdn-data": "2.0.30", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, "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 +2529,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 +2545,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 +2637,41 @@ "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", "license": "CC0-1.0" }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "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/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/date.js": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/date.js/-/date.js-0.3.3.tgz", @@ -2451,9 +2697,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" @@ -2480,6 +2726,39 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "license": "MIT" + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -2494,7 +2773,17 @@ "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/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "license": "MIT", + "engines": { + "node": ">=10" + } }, "node_modules/delayed-stream": { "version": "1.0.0", @@ -2534,9 +2823,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" @@ -2645,10 +2934,19 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz", + "integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "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 +2958,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,11 +3005,19 @@ "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/emitter-component": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/emitter-component/-/emitter-component-1.1.2.tgz", + "integrity": "sha512-QdXO3nXOzZB4pAjM0n6ZE+R9/+kPpECA/XSELIcc54NeYVnBqIk+4DFiBgK+8QbV3mdvTG6nedl7dTYgO+5wDw==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -2734,9 +3040,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 +3065,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 +3122,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 +3133,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 +3189,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 +3213,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 +3254,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 +3272,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 +3284,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 +3305,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 +3324,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 +3342,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 +3353,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 +3375,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 +3388,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 +3401,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 +3411,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" } @@ -3142,6 +3425,13 @@ "node": ">= 0.6" } }, + "node_modules/event-to-promise": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/event-to-promise/-/event-to-promise-0.7.0.tgz", + "integrity": "sha512-VOBBfyaADfe378ZzG0tgkzmsvzUyeU5arehrFzNRt5yaASUDshgctTwSrPI17ocAwR3+YftsxRClHF+GBKFByQ==", + "deprecated": "Use promise-toolbox/fromEvent instead", + "license": "MIT" + }, "node_modules/express": { "version": "4.21.2", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", @@ -3188,6 +3478,24 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.0.1.tgz", + "integrity": "sha512-aZVCnybn7TVmxO4BtlmnvX+nuz8qHW124KKJ8dumsBsmv5ZLxE0pYu7S2nwyRBGHHCAzdmnGyrc5U/rksSPO7Q==", + "license": "MIT", + "dependencies": { + "ip-address": "10.0.1" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/express/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -3213,19 +3521,40 @@ "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/fast-xml-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.3.tgz", + "integrity": "sha512-OdCYfRqfpuLUFonTNjvd30rCBZUneHpSQkCqfaeWQ9qrKcl6XlWeDBNVwGb+INAIxRshuN2jF+BE0L6gbBO2mw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^2.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } }, "node_modules/fecha": { "version": "4.2.3", @@ -3233,11 +3562,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 +3675,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 +3685,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 +3718,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 +3734,42 @@ } }, "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/form-data-encoder": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", + "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", + "license": "MIT", + "engines": { + "node": ">= 14.17" + } + }, + "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", @@ -3439,19 +3817,70 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gamedig": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/gamedig/-/gamedig-5.3.1.tgz", + "integrity": "sha512-RM/eCR8bAKEX+5dA9sxg4D6oQjS5t0MPsIxGquerBLSIu0f1hAVaDGC58Lp1srYwF6A1C2wYY2p9PzOrPTKf4Q==", + "license": "MIT", + "dependencies": { + "fast-xml-parser": "5.2.3", + "gbxremote": "0.2.1", + "got": "13.0.0", + "iconv-lite": "0.6.3", + "long": "5.3.2", + "minimist": "1.2.8", + "seek-bzip": "2.0.0", + "telnet-client": "2.2.5", + "varint": "6.0.0" + }, + "bin": { + "gamedig": "bin/gamedig.js" + }, + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/gamedig/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "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/gbxremote": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/gbxremote/-/gbxremote-0.2.1.tgz", + "integrity": "sha512-SMehu6Y6ndq2Qgp9VxAb8Np3f+UUD+RWoW2SAMaxzGS96rWXyr4T1GGkecO0HHtxeH1m7pEh4FJWB8a/6aM2XQ==", + "dependencies": { + "any-promise": "^1.1.0", + "barse": "~0.4.2", + "event-to-promise": "^0.7.0", + "string-to-stream": "^1.0.1", + "xmlrpc": "^1.3.1" + }, + "engines": { + "node": ">=0.10" } }, "node_modules/get-caller-file": { @@ -3464,21 +3893,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 +3916,31 @@ "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/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -3508,15 +3962,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": { @@ -3544,6 +4023,31 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/got": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/got/-/got-13.0.0.tgz", + "integrity": "sha512-XfBk1CxOOScDcMr9O1yKkNaQyy865NbYs+F7dr4H0LZMVgCj2Le59k6PqbNHoL5ToeaEQUYh6c6yMfVcc6SJxA==", + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^5.2.0", + "@szmarczak/http-timer": "^5.0.1", + "cacheable-lookup": "^7.0.0", + "cacheable-request": "^10.2.8", + "decompress-response": "^6.0.0", + "form-data-encoder": "^2.1.2", + "get-stream": "^6.0.1", + "http2-wrapper": "^2.1.10", + "lowercase-keys": "^3.0.0", + "p-cancelable": "^3.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, "node_modules/handlebars": { "version": "4.7.8", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", @@ -3587,6 +4091,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,14 +4129,26 @@ } }, "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" } }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -3626,19 +4157,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", @@ -3691,6 +4221,12 @@ "entities": "^4.4.0" } }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause" + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -3707,17 +4243,43 @@ "node": ">= 0.8" } }, - "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==", + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "license": "MIT", "dependencies": { - "agent-base": "6", + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http2-wrapper": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", + "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.2.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "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": "^7.1.2", "debug": "4" }, "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/human-interval": { @@ -3766,6 +4328,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 +4341,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 +4361,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 +4373,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", @@ -3832,6 +4396,15 @@ "url": "https://opencollective.com/ioredis" } }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -3914,6 +4487,12 @@ "node": ">=8" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "license": "MIT" + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -3951,14 +4530,45 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "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==", + "node_modules/isomorphic-dompurify": { + "version": "2.26.0", + "resolved": "https://registry.npmjs.org/isomorphic-dompurify/-/isomorphic-dompurify-2.26.0.tgz", + "integrity": "sha512-nZmoK4wKdzPs5USq4JHBiimjdKSVAOm2T1KyDoadtMPNXYHxiENd19ou4iU/V4juFM6LVgYQnpxCYmxqNP4Obw==", "license": "MIT", "dependencies": { - "node-fetch": "^2.6.1", - "unfetch": "^4.2.0" + "dompurify": "^3.2.6", + "jsdom": "^26.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/isomorphic-unfetch": { + "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": "^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 +4596,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", @@ -4071,11 +4665,50 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/json-buffer": { "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 + "license": "MIT" }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", @@ -4087,13 +4720,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 +4753,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 +4773,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 +4812,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 +4836,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" } @@ -4221,7 +4869,7 @@ "version": "4.5.4", "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 +4885,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 +4950,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 +5006,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,18 +5056,30 @@ } }, "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" }, + "node_modules/lowercase-keys": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", + "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -4424,25 +5087,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", @@ -4452,12 +5131,6 @@ "node": ">= 0.4" } }, - "node_modules/mdn-data": { - "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" - }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -4510,9 +5183,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 +5203,38 @@ "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==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, + "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": ">=16 || 14 >=14.17" + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", + "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/minimist": { @@ -4643,6 +5335,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 +5738,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 +5799,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 +5845,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 +5876,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 +5950,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 +5981,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 +6000,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 +6028,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" @@ -5272,6 +6046,12 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "license": "MIT" }, + "node_modules/net": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/net/-/net-1.0.2.tgz", + "integrity": "sha512-kbhcj2SVVR4caaVnGLJKmlk2+f+oLkjqdKeQlmUtz6nGzOpbcobwVIeSURNgraV/v3tlmGIX82OcPCl0K6RbHQ==", + "license": "MIT" + }, "node_modules/nise": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/nise/-/nise-6.1.1.tgz", @@ -5302,6 +6082,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 +6166,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 +6203,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 +6213,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", @@ -5459,6 +6235,18 @@ "node": ">=0.10.0" } }, + "node_modules/normalize-url": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.2.tgz", + "integrity": "sha512-Ee/R3SyN4BuynXcnTaekmaVdbDAEiNrHqjQIA37mHU8G9pf7aaAD4ZX3XjBLo6rsdcxA/gtkcNYZLt30ACgynw==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -5477,6 +6265,12 @@ "integrity": "sha512-pv/ue2Odr7IfYOO0byC1KgBI10wo5YDauLhxY6/saNzAdAs0r1SotGCPzzCLNPL0xtrAwWRialLu23AAu9xO1g==", "license": "MIT" }, + "node_modules/nwsapi": { + "version": "2.2.21", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.21.tgz", + "integrity": "sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==", + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -5487,9 +6281,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" @@ -5511,9 +6305,9 @@ } }, "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -5542,6 +6336,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", @@ -5554,6 +6349,15 @@ "node": ">= 0.8.0" } }, + "node_modules/p-cancelable": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", + "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -5593,9 +6397,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 +6433,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 +6469,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 +6532,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 +6569,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 +6588,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -5781,12 +6597,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 +6613,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 +6627,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 +6743,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 +6820,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 +6835,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 +6850,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 +6865,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 +6880,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 +6911,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 +6926,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 +6973,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 +6990,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 +7204,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 +7231,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 +7281,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", @@ -6444,11 +7314,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/rambda": { "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 +7414,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", @@ -6546,6 +7423,12 @@ "node": ">=0.10.0" } }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "license": "MIT" + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -6555,6 +7438,27 @@ "node": ">=4" } }, + "node_modules/responselike": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", + "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", + "license": "MIT", + "dependencies": { + "lowercase-keys": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "license": "MIT" + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -6590,10 +7494,50 @@ "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/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/seek-bzip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-2.0.0.tgz", + "integrity": "sha512-SMguiTnYrhpLdk3PwfzHeotrcwi8bNV4iemL9tx9poR/yeaMYwB9VzR1w7b57DuWpuqR8n6oZboi0hj3AxZxQg==", + "license": "MIT", + "dependencies": { + "commander": "^6.0.0" + }, + "bin": { + "seek-bunzip": "bin/seek-bunzip", + "seek-table": "bin/seek-bzip-table" + } + }, + "node_modules/seek-bzip/node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "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" @@ -6977,6 +7921,15 @@ "node": ">= 0.8" } }, + "node_modules/stream": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stream/-/stream-0.0.2.tgz", + "integrity": "sha512-gCq3NDI2P35B2n6t76YJuOp7d6cN/C7Rt0577l91wllh0sY9ZBuw9KaSGqH/b0hzn3CWWJbpbW0W0WvQ1H/Q7g==", + "license": "MIT", + "dependencies": { + "emitter-component": "^1.1.1" + } + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -6994,6 +7947,46 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-to-stream": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string-to-stream/-/string-to-stream-1.1.1.tgz", + "integrity": "sha512-QySF2+3Rwq0SdO3s7BAp4x+c3qsClpPQ6abAmb0DGViiSBAkT5kL6JT2iyzEVP+T1SmzHrQD1TwlP9QAHCc+Sw==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "^2.1.0" + } + }, + "node_modules/string-to-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/string-to-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/string-to-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -7103,20 +8096,126 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strnum": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "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": { @@ -7132,44 +8231,10 @@ "node": ">=8" } }, - "node_modules/svgo": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", - "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", - "license": "MIT", - "dependencies": { - "@trysound/sax": "0.2.0", - "commander": "^7.2.0", - "css-select": "^5.1.0", - "css-tree": "^2.3.1", - "css-what": "^6.1.0", - "csso": "^5.0.5", - "picocolors": "^1.0.0" - }, - "bin": { - "svgo": "bin/svgo" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/svgo" - } - }, - "node_modules/svgo/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "license": "MIT", - "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" @@ -7190,6 +8255,12 @@ "express": ">=4.0.0 || >=5.0.0-beta" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "license": "MIT" + }, "node_modules/tar-fs": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", @@ -7202,12 +8273,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", @@ -7224,6 +8289,20 @@ "node": ">=6" } }, + "node_modules/telnet-client": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/telnet-client/-/telnet-client-2.2.5.tgz", + "integrity": "sha512-X5xEkmKHgpCpngnH7QnkFX87UyBErauHsjzlCGVp87MbhnmHoaAeacuALGfqovHh3MXAfrpPs+g30PgyBpy8Jw==", + "license": "MIT", + "dependencies": { + "net": "^1.0.2", + "stream": "^0.0.2" + }, + "funding": { + "type": "paypal", + "url": "https://paypal.me/kozjak" + } + }, "node_modules/test-exclude": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", @@ -7239,16 +8318,54 @@ "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==", + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", "license": "MIT" }, "node_modules/to-regex-range": { @@ -7282,16 +8399,28 @@ "nodetouch": "bin/nodetouch.js" } }, - "node_modules/tr46": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", - "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", - "license": "MIT", + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "license": "BSD-3-Clause", "dependencies": { - "punycode": "^2.3.0" + "tldts": "^6.1.32" }, "engines": { - "node": ">=14" + "node": ">=16" + } + }, + "node_modules/tr46": { + "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.1" + }, + "engines": { + "node": ">=18" } }, "node_modules/triple-beam": { @@ -7320,6 +8449,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 +8472,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 +8520,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 +8553,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 +8573,7 @@ "license": "MIT", "dependencies": { "escalade": "^3.2.0", - "picocolors": "^1.1.0" + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -7453,6 +8587,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" } @@ -7509,6 +8644,12 @@ "node": ">=10" } }, + "node_modules/varint": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz", + "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==", + "license": "MIT" + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -7518,6 +8659,18 @@ "node": ">= 0.8" } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/web-resource-inliner": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/web-resource-inliner/-/web-resource-inliner-7.0.0.tgz", @@ -7643,6 +8796,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 +8848,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 +8916,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" } @@ -7865,6 +9028,71 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlbuilder": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz", + "integrity": "sha512-eKRAFz04jghooy8muekqzo8uCSVNeyRedbuJrp0fovbLIi7wlsYtdUn3vBAAPq2Y3/0xMz2WMEUQ8yhVVO9Stw==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "license": "MIT" + }, + "node_modules/xmlrpc": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/xmlrpc/-/xmlrpc-1.3.2.tgz", + "integrity": "sha512-jQf5gbrP6wvzN71fgkcPPkF4bF/Wyovd7Xdff8d6/ihxYmgETQYSuTc+Hl+tsh/jmgPLro/Aro48LMFlIyEKKQ==", + "license": "MIT", + "dependencies": { + "sax": "1.2.x", + "xmlbuilder": "8.2.x" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.0.0" + } + }, + "node_modules/xmlrpc/node_modules/sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "license": "ISC" + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/server/package.json b/server/package.json index 51e7cd218..929fab58a 100755 --- a/server/package.json +++ b/server/package.json @@ -6,7 +6,7 @@ "type": "module", "scripts": { "test": "c8 mocha", - "dev": "nodemon index.js", + "dev": "nodemon src/index.js", "lint": "eslint .", "lint-fix": "eslint --fix .", "format": "prettier --write .", @@ -20,16 +20,21 @@ "axios": "^1.7.2", "bcryptjs": "3.0.2", "bullmq": "5.41.2", - "compression": "1.8.0", + "compression": "1.8.1", + "cookie-parser": "^1.4.7", "cors": "^2.8.5", "dockerode": "4.0.6", "dotenv": "^16.4.5", "express": "^4.19.2", + "express-rate-limit": "8.0.1", + "gamedig": "^5.3.1", "handlebars": "^4.7.8", "helmet": "^8.0.0", "ioredis": "^5.4.2", + "isomorphic-dompurify": "^2.26.0", "jmespath": "^0.16.0", "joi": "^17.13.1", + "jsdom": "^26.1.0", "jsonwebtoken": "9.0.2", "mailersend": "^2.2.0", "mjml": "^5.0.0-alpha.4", @@ -40,6 +45,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/maintenanceWindowRoute.js b/server/routes/maintenanceWindowRoute.js deleted file mode 100755 index 36f982e25..000000000 --- a/server/routes/maintenanceWindowRoute.js +++ /dev/null @@ -1,46 +0,0 @@ -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.initRoutes(); - } - initRoutes() { - this.router.post("/", this.maintenanceWindowController.createMaintenanceWindows); - - this.router.get( - "/monitor/:monitorId", - verifyOwnership(Monitor, "monitorId"), - this.maintenanceWindowController.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 - ); - } - - getRouter() { - return this.router; - } -} - -export default MaintenanceWindowRoutes; diff --git a/server/routes/monitorRoute.js b/server/routes/monitorRoute.js deleted file mode 100755 index 168f8b88d..000000000 --- a/server/routes/monitorRoute.js +++ /dev/null @@ -1,124 +0,0 @@ -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 -}); - -class MonitorRoutes { - constructor(monitorController) { - this.router = Router(); - this.monitorController = monitorController; - this.initRoutes(); - } - - 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 - ); - }); - this.router.get("/team", this.monitorController.getMonitorsByTeamId); - - 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 - ); - } - - getRouter() { - return this.router; - } -} - -export default MonitorRoutes; diff --git a/server/routes/queueRoute.js b/server/routes/queueRoute.js deleted file mode 100755 index c40d21528..000000000 --- a/server/routes/queueRoute.js +++ /dev/null @@ -1,52 +0,0 @@ -import { Router } from "express"; -import { isAllowed } from "../middleware/isAllowed.js"; -class QueueRoutes { - constructor(queueController) { - this.router = Router(); - this.queueController = queueController; - 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.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 - ); - } - - getRouter() { - return this.router; - } -} - -export default QueueRoutes; diff --git a/server/service/JobQueue/JobQueue.js b/server/service/JobQueue/JobQueue.js deleted file mode 100644 index 38cca7d4d..000000000 --- a/server/service/JobQueue/JobQueue.js +++ /dev/null @@ -1,321 +0,0 @@ -const QUEUE_NAMES = ["uptime", "pagespeed", "hardware"]; -const SERVICE_NAME = "JobQueue"; -const HEALTH_CHECK_INTERVAL = 10 * 60 * 1000; // 10 minutes -const QUEUE_LOOKUP = { - hardware: "hardware", - http: "uptime", - ping: "uptime", - port: "uptime", - docker: "uptime", - pagespeed: "pagespeed", -}; -const getSchedulerId = (monitor) => `scheduler:${monitor.type}:${monitor._id}`; - -class JobQueue { - static SERVICE_NAME = SERVICE_NAME; - - constructor({ db, jobQueueHelper, logger, stringService }) { - this.db = db; - this.jobQueueHelper = jobQueueHelper; - this.stringService = stringService; - this.logger = logger; - this.queues = {}; - this.workers = []; - } - - static async create({ db, jobQueueHelper, logger, stringService }) { - const instance = new JobQueue({ db, jobQueueHelper, logger, stringService }); - await instance.init(); - return instance; - } - - async init() { - try { - await this.initQueues(); - await this.initWorkers(); - const monitors = await this.db.getAllMonitors(); - await Promise.all( - monitors - .filter((monitor) => monitor.isActive) - .map(async (monitor) => { - try { - await this.addJob(monitor._id, monitor); - } catch (error) { - this.logger.error({ - message: error.message, - service: SERVICE_NAME, - method: "initJobQueue", - stack: error.stack, - }); - } - }) - ); - this.healthCheckInterval = setInterval(async () => { - try { - const queueHealthChecks = await this.checkQueueHealth(); - const queueIsStuck = queueHealthChecks.some((healthCheck) => healthCheck.stuck); - if (queueIsStuck) { - this.logger.warn({ - message: "Queue is stuck", - service: SERVICE_NAME, - method: "periodicHealthCheck", - details: queueHealthChecks, - }); - await this.flushQueues(); - } - } catch (error) { - this.logger.error({ - message: error.message, - service: SERVICE_NAME, - method: "periodicHealthCheck", - stack: error.stack, - }); - } - }, HEALTH_CHECK_INTERVAL); - } catch (error) { - this.logger.error({ - message: error.message, - service: SERVICE_NAME, - method: "initJobQueue", - stack: error.stack, - }); - } - } - - async initQueues() { - const readyPromises = []; - - for (const queueName of QUEUE_NAMES) { - const q = this.jobQueueHelper.createQueue(queueName); - this.queues[queueName] = q; - readyPromises.push(q.waitUntilReady()); - } - await Promise.all(readyPromises); - this.logger.info({ - message: "Queues ready", - service: SERVICE_NAME, - method: "initQueues", - }); - } - - async initWorkers() { - const workerReadyPromises = []; - - for (const queueName of QUEUE_NAMES) { - const worker = this.jobQueueHelper.createWorker(queueName, this.queues[queueName]); - this.workers.push(worker); - workerReadyPromises.push(worker.waitUntilReady()); - } - await Promise.all(workerReadyPromises); - this.logger.info({ - message: "Workers ready", - service: SERVICE_NAME, - method: "initWorkers", - }); - } - - pauseJob = async (monitor) => { - this.deleteJob(monitor); - }; - - resumeJob = async (monitor) => { - this.addJob(monitor._id, monitor); - }; - - async addJob(jobName, monitor) { - this.logger.info({ - message: `Adding job ${monitor?.url ?? "No URL"}`, - service: SERVICE_NAME, - method: "addJob", - }); - - const queueName = QUEUE_LOOKUP[monitor.type]; - const queue = this.queues[queueName]; - if (typeof queue === "undefined") { - throw new Error(`Queue for ${monitor.type} not found`); - } - const jobTemplate = { - name: jobName, - data: monitor, - opts: { - attempts: 1, - backoff: { - type: "exponential", - delay: 1000, - }, - removeOnComplete: true, - removeOnFail: false, - timeout: 1 * 60 * 1000, - }, - }; - - const schedulerId = getSchedulerId(monitor); - await queue.upsertJobScheduler( - schedulerId, - { every: monitor?.interval ?? 60000 }, - jobTemplate - ); - } - - async deleteJob(monitor) { - try { - const queue = this.queues[QUEUE_LOOKUP[monitor.type]]; - const schedulerId = getSchedulerId(monitor); - const wasDeleted = await queue.removeJobScheduler(schedulerId); - - if (wasDeleted === true) { - this.logger.info({ - message: this.stringService.jobQueueDeleteJob, - service: SERVICE_NAME, - method: "deleteJob", - details: `Deleted job ${monitor._id}`, - }); - return true; - } else { - this.logger.error({ - message: this.stringService.jobQueueDeleteJob, - service: SERVICE_NAME, - method: "deleteJob", - details: `Failed to delete job ${monitor._id}`, - }); - return false; - } - } catch (error) { - error.service === undefined ? (error.service = SERVICE_NAME) : null; - error.method === undefined ? (error.method = "deleteJob") : null; - throw error; - } - } - - async updateJob(monitor) { - await this.deleteJob(monitor); - await this.addJob(monitor._id, monitor); - } - - async getJobs() { - try { - let stats = {}; - await Promise.all( - QUEUE_NAMES.map(async (name) => { - const queue = this.queues[name]; - const jobs = await queue.getJobs(); - const ret = await Promise.all( - jobs.map(async (job) => { - const state = await job.getState(); - return { url: job.data.url, state, progress: job.progress }; - }) - ); - stats[name] = { jobs: ret }; - }) - ); - return stats; - } catch (error) { - error.service === undefined ? (error.service = SERVICE_NAME) : null; - error.method === undefined ? (error.method = "getJobStats") : null; - throw error; - } - } - - async getMetrics() { - try { - let metrics = {}; - - await Promise.all( - QUEUE_NAMES.map(async (name) => { - const queue = this.queues[name]; - const [waiting, active, failed, delayed, repeatableJobs] = await Promise.all([ - queue.getWaitingCount(), - queue.getActiveCount(), - queue.getFailedCount(), - queue.getDelayedCount(), - queue.getRepeatableJobs(), - ]); - - metrics[name] = { - waiting, - active, - failed, - delayed, - repeatableJobs: repeatableJobs.length, - }; - }) - ); - - return metrics; - } catch (error) { - this.logger.error({ - message: error.message, - service: SERVICE_NAME, - method: "getMetrics", - stack: error.stack, - }); - } - } - - async checkQueueHealth() { - const res = []; - for (const queueName of QUEUE_NAMES) { - const q = this.queues[queueName]; - await q.waitUntilReady(); - - const lastJobProcessedTime = q.lastJobProcessedTime; - const currentTime = Date.now(); - const timeDiff = currentTime - lastJobProcessedTime; - - // Check for jobs - const jobCounts = await q.getJobCounts(); - const hasJobs = Object.values(jobCounts).some((count) => count > 0); - - res.push({ - queueName, - timeSinceLastJob: timeDiff, - stuck: hasJobs && timeDiff > 10000, - jobCounts, - }); - } - return res; - } - - async flushQueues() { - try { - this.logger.warn({ - message: "Flushing queues", - method: "flushQueues", - service: SERVICE_NAME, - }); - for (const worker of this.workers) { - await worker.close(); - } - this.workers = []; - - for (const queue of Object.values(this.queues)) { - await queue.obliterate(); - } - this.queue = {}; - await this.init(); - return true; - } catch (error) { - this.logger.warn({ - message: `${error.message} - Flushing redis manually`, - service: SERVICE_NAME, - method: "flushQueues", - }); - return await this.jobQueueHelper.flushRedis(); - } - } - - async shutdown() { - if (this.healthCheckInterval) { - clearInterval(this.healthCheckInterval); - this.healthCheckInterval = null; - } - for (const worker of this.workers) { - await worker.close(); - } - for (const queue of Object.values(this.queues)) { - await queue.obliterate(); - } - } -} - -export default JobQueue; diff --git a/server/service/JobQueue/JobQueueHelper.js b/server/service/JobQueue/JobQueueHelper.js deleted file mode 100644 index d286b7b14..000000000 --- a/server/service/JobQueue/JobQueueHelper.js +++ /dev/null @@ -1,315 +0,0 @@ -const SERVICE_NAME = "JobQueueHelper"; - -class JobQueueHelper { - constructor({ - redisService, - Queue, - Worker, - logger, - db, - networkService, - statusService, - notificationService, - }) { - this.db = db; - this.redisService = redisService; - this.Queue = Queue; - this.Worker = Worker; - this.logger = logger; - this.networkService = networkService; - this.statusService = statusService; - this.notificationService = notificationService; - } - - createQueue(queueName) { - const connection = this.redisService.getNewConnection(); - const q = new this.Queue(queueName, { - connection, - }); - q.lastJobProcessedTime = Date.now(); - q.on("cleaned", (jobs, type) => { - this.logger.debug({ - message: `Queue ${queueName} is cleaned with jobs: ${jobs} and type: ${type}`, - service: SERVICE_NAME, - method: "createQueue:cleaned", - }); - }); - q.on("error", (err) => { - this.logger.error({ - message: `Queue ${queueName} is error with msg: ${err}`, - service: SERVICE_NAME, - method: "createQueue:error", - }); - }); - q.on("ioredis:close", () => { - this.logger.debug({ - message: `Queue ${queueName} is ioredis:close`, - service: SERVICE_NAME, - method: "createQueue:ioredis:close", - }); - }); - q.on("paused", () => { - this.logger.debug({ - message: `Queue ${queueName} is paused`, - service: SERVICE_NAME, - method: "createQueue:paused", - }); - }); - q.on("progress", (job, progress) => { - this.logger.debug({ - message: `Queue ${queueName} is progress with msg: ${progress}`, - service: SERVICE_NAME, - method: "createQueue:progress", - }); - }); - q.on("removed", (job) => { - this.logger.debug({ - message: `Queue ${queueName} is removed with msg: ${job}`, - service: SERVICE_NAME, - method: "createQueue:removed", - }); - }); - q.on("resumed", () => { - this.logger.debug({ - message: `Queue ${queueName} is resumed`, - service: SERVICE_NAME, - method: "createQueue:resumed", - }); - }); - q.on("waiting", () => { - this.logger.debug({ - message: `Queue ${queueName} is waiting`, - service: SERVICE_NAME, - method: "createQueue:waiting", - }); - }); - return q; - } - - createWorker(queueName, queue) { - const connection = this.redisService.getNewConnection({ - maxRetriesPerRequest: null, - }); - const worker = new this.Worker(queueName, this.createJobHandler(queue), { - connection, - concurrency: 50, - }); - worker.on("active", (job) => { - this.logger.debug({ - message: `Worker ${queueName} is active`, - service: SERVICE_NAME, - method: "createWorker:active", - }); - }); - - worker.on("closed", () => { - this.logger.debug({ - message: `Worker ${queueName} is closed`, - service: SERVICE_NAME, - method: "createWorker:closed", - }); - }); - - worker.on("closing", (msg) => { - this.logger.debug({ - message: `Worker ${queueName} is closing with msg: ${msg}`, - service: SERVICE_NAME, - method: "createWorker:closing", - }); - }); - - worker.on("completed", (job) => { - this.logger.debug({ - message: `Worker ${queueName} is completed`, - service: SERVICE_NAME, - method: "createWorker:completed", - }); - }); - - worker.on("drained", () => { - this.logger.debug({ - message: `Worker ${queueName} is drained`, - service: SERVICE_NAME, - method: "createWorker:drained", - }); - }); - - worker.on("error", (failedReason) => { - this.logger.error({ - message: `Worker ${queueName} is error with msg: ${failedReason}`, - service: SERVICE_NAME, - method: "createWorker:error", - }); - }); - - worker.on("failed", (job, error, prev) => { - this.logger.error({ - message: `Worker ${queueName} is failed with msg: ${error.message}`, - service: error?.service ?? SERVICE_NAME, - method: error?.method ?? "createWorker:failed", - stack: error?.stack, - }); - }); - - worker.on("ioredis:close", () => { - this.logger.debug({ - message: `Worker ${queueName} is ioredis:close`, - service: SERVICE_NAME, - method: "createWorker:ioredis:close", - }); - }); - - worker.on("paused", () => { - this.logger.debug({ - message: `Worker ${queueName} is paused`, - service: SERVICE_NAME, - method: "createWorker:paused", - }); - }); - - worker.on("progress", (job, progress) => { - this.logger.debug({ - message: `Worker ${queueName} is progress with msg: ${progress}`, - service: SERVICE_NAME, - method: "createWorker:progress", - }); - }); - - worker.on("ready", () => { - this.logger.debug({ - message: `Worker ${queueName} is ready`, - service: SERVICE_NAME, - method: "createWorker:ready", - }); - }); - - worker.on("resumed", () => { - this.logger.debug({ - message: `Worker ${queueName} is resumed`, - service: SERVICE_NAME, - method: "createWorker:resumed", - }); - }); - - worker.on("stalled", () => { - this.logger.warn({ - message: `Worker ${queueName} is stalled`, - service: SERVICE_NAME, - method: "createWorker:stalled", - }); - }); - return worker; - } - - 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; - } - - createJobHandler(q) { - return async (job) => { - try { - // Update the last job processed time for this queue - q.lastJobProcessedTime = Date.now(); - // Get all maintenance windows for this monitor - await job.updateProgress(0); - const monitorId = job.data._id; - const maintenanceWindowActive = await this.isInMaintenanceWindow(monitorId); - // If a maintenance window is active, we're done - - if (maintenanceWindowActive) { - await job.updateProgress(100); - this.logger.info({ - message: `Monitor ${monitorId} is in maintenance window`, - service: SERVICE_NAME, - method: "createWorker", - }); - return false; - } - - // Get the current status - await job.updateProgress(30); - const monitor = job.data; - const networkResponse = await this.networkService.getStatus(monitor); - - // If the network response is not found, we're done - if (!networkResponse) { - await job.updateProgress(100); - return false; - } - - // Handle status change - await job.updateProgress(60); - const { - monitor: updatedMonitor, - statusChanged, - prevStatus, - } = await this.statusService.updateStatus(networkResponse); - // Handle notifications - await job.updateProgress(80); - this.notificationService - .handleNotifications({ - ...networkResponse, - monitor: updatedMonitor, - prevStatus, - statusChanged, - }) - .catch((error) => { - this.logger.error({ - message: error.message, - service: SERVICE_NAME, - method: "createJobHandler", - details: `Error sending notifications for job ${job.id}: ${error.message}`, - stack: error.stack, - }); - }); - await job.updateProgress(100); - return true; - } catch (error) { - await job.updateProgress(100); - throw error; - } - }; - } - async flushRedis() { - try { - const connection = this.redisService.getNewConnection(); - const flushResult = await connection.flushall(); - return flushResult; - } catch (error) { - this.logger.warn({ - message: error.message, - service: SERVICE_NAME, - method: "flushRedis", - }); - return false; - } - } -} - -export default JobQueueHelper; diff --git a/server/service/PulseQueue/PulseQueue.js b/server/service/PulseQueue/PulseQueue.js deleted file mode 100644 index 50a959559..000000000 --- a/server/service/PulseQueue/PulseQueue.js +++ /dev/null @@ -1,212 +0,0 @@ -import { Pulse } from "@pulsecron/pulse"; - -const SERVICE_NAME = "PulseQueue"; -class PulseQueue { - static SERVICE_NAME = SERVICE_NAME; - - constructor({ appSettings, db, pulseQueueHelper, logger }) { - this.db = db; - this.appSettings = appSettings; - this.pulseQueueHelper = pulseQueueHelper; - this.logger = logger; - } - - static async create({ appSettings, db, pulseQueueHelper, logger }) { - const instance = new PulseQueue({ appSettings, db, pulseQueueHelper, logger }); - await instance.init(); - return instance; - } - - // **************************************** - // Core methods - // **************************************** - init = async () => { - try { - 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(), {}); - - 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 PulseQueue", - service: SERVICE_NAME, - method: "init", - details: error, - }); - return false; - } - }; - - addJob = async (monitorId, monitor) => { - this.logger.debug({ - message: `Adding job ${monitor?.url ?? "No URL"}`, - service: SERVICE_NAME, - method: "addJob", - }); - const intervalInSeconds = monitor.interval / 1000; - const job = this.pulse.create("monitor-job", { - monitor, - }); - job.unique({ "data.monitor._id": monitor._id }); - job.attrs.jobId = monitorId.toString(); - job.repeatEvery(`${intervalInSeconds} seconds`); - if (monitor.isActive === false) { - job.disable(); - } - await job.save(); - }; - - deleteJob = async (monitor) => { - this.logger.debug({ - message: `Deleting job ${monitor?.url ?? "No URL"}`, - service: SERVICE_NAME, - method: "deleteJob", - }); - await this.pulse.cancel({ - "data.monitor._id": monitor._id, - }); - }; - - pauseJob = async (monitor) => { - const result = await this.pulse.disable({ - "data.monitor._id": monitor._id, - }); - - if (result.length < 1) { - throw new Error("Failed to pause monitor"); - } - - this.logger.debug({ - message: `Paused monitor ${monitor._id}`, - service: SERVICE_NAME, - method: "pauseJob", - }); - }; - - resumeJob = async (monitor) => { - const result = await this.pulse.enable({ - "data.monitor._id": monitor._id, - }); - - if (result.length < 1) { - throw new Error("Failed to resume monitor"); - } - - this.logger.debug({ - message: `Resumed monitor ${monitor._id}`, - service: SERVICE_NAME, - method: "resumeJob", - }); - }; - - updateJob = async (monitor) => { - const jobs = await this.pulse.jobs({ - "data.monitor._id": monitor._id, - }); - - const job = jobs[0]; - if (!job) { - throw new Error("Job not found"); - } - - const intervalInSeconds = monitor.interval / 1000; - job.repeatEvery(`${intervalInSeconds} seconds`); - job.attrs.data.monitor = monitor; - await job.save(); - }; - - shutdown = async () => { - this.logger.info({ - message: "Shutting down JobQueue", - service: SERVICE_NAME, - method: "shutdown", - }); - await this.pulse.stop(); - }; - - // **************************************** - // Diagnostic methods - // **************************************** - - getMetrics = async () => { - const jobs = await this.pulse.jobs(); - const metrics = jobs.reduce( - (acc, job) => { - acc.totalRuns += job.attrs.runCount || 0; - acc.totalFailures += job.attrs.failCount || 0; - acc.jobs++; - if (job.attrs.failCount > 0 && job.attrs.failedAt >= job.attrs.lastFinishedAt) { - acc.failingJobs++; - } - if (job.attrs.lockedAt) { - acc.activeJobs++; - } - if (job.attrs.failCount > 0) { - acc.jobsWithFailures.push({ - monitorId: job.attrs.data.monitor._id, - monitorUrl: job.attrs.data.monitor.url, - monitorType: job.attrs.data.monitor.type, - failedAt: job.attrs.failedAt, - failCount: job.attrs.failCount, - failReason: job.attrs.failReason, - }); - } - return acc; - }, - { - jobs: 0, - activeJobs: 0, - failingJobs: 0, - jobsWithFailures: [], - totalRuns: 0, - totalFailures: 0, - } - ); - return metrics; - }; - - getJobs = async () => { - const jobs = await this.pulse.jobs(); - return jobs.map((job) => { - return { - monitorId: job.attrs.data.monitor._id, - monitorUrl: job.attrs.data.monitor.url, - monitorType: job.attrs.data.monitor.type, - active: !job.attrs.disabled, - lockedAt: job.attrs.lockedAt, - runCount: job.attrs.runCount || 0, - failCount: job.attrs.failCount || 0, - failReason: job.attrs.failReason, - lastRunAt: job.attrs.lastRunAt, - lastFinishedAt: job.attrs.lastFinishedAt, - lastRunTook: job.attrs.lockedAt - ? null - : job.attrs.lastFinishedAt - job.attrs.lastRunAt, - lastFailedAt: job.attrs.failedAt, - }; - }); - }; - - flushQueues = async () => { - const cancelRes = await this.pulse.cancel(); - await this.pulse.stop(); - const initRes = await this.init(); - return { - flushedJobs: cancelRes, - initSuccess: initRes, - }; - }; - - obliterate = async () => { - await this.flushQueues(); - }; -} - -export default PulseQueue; diff --git a/server/service/redisService.js b/server/service/redisService.js deleted file mode 100644 index a89c21fab..000000000 --- a/server/service/redisService.js +++ /dev/null @@ -1,59 +0,0 @@ -const SERVICE_NAME = "RedisService"; - -class RedisService { - static SERVICE_NAME = SERVICE_NAME; - constructor({ Redis, logger }) { - this.Redis = Redis; - this.connections = new Set(); - this.logger = logger; - } - - getNewConnection(options = {}) { - const connection = new this.Redis(process.env.REDIS_URL, { - retryStrategy: (times) => { - return null; - }, - ...options, - }); - this.connections.add(connection); - return connection; - } - - async closeAllConnections() { - const closePromises = Array.from(this.connections).map((conn) => - conn.quit().catch((err) => { - this.logger.error({ - message: "Error closing Redis connection", - service: SERVICE_NAME, - method: "closeAllConnections", - details: { error: err }, - }); - }) - ); - - await Promise.all(closePromises); - this.connections.clear(); - this.logger.info({ - message: "All Redis connections closed", - service: SERVICE_NAME, - method: "closeAllConnections", - }); - } - - async flushRedis() { - this.logger.info({ - message: "Flushing Redis", - service: SERVICE_NAME, - method: "flushRedis", - }); - const flushPromises = Array.from(this.connections).map((conn) => conn.flushall()); - await Promise.all(flushPromises); - this.logger.info({ - message: "Redis flushed", - service: SERVICE_NAME, - method: "flushRedis", - }); - } -} - -export default RedisService; diff --git a/server/service/serviceRegistry.js b/server/service/serviceRegistry.js deleted file mode 100755 index 69a3b8be5..000000000 --- a/server/service/serviceRegistry.js +++ /dev/null @@ -1,35 +0,0 @@ -const SERVICE_NAME = "ServiceRegistry"; -import logger from "../utils/logger.js"; -class ServiceRegistry { - static SERVICE_NAME = SERVICE_NAME; - constructor() { - this.services = {}; - } - - register(name, service) { - logger.info({ - message: `Registering service ${name}`, - service: SERVICE_NAME, - method: "register", - }); - this.services[name] = service; - } - - get(name) { - if (!this.services[name]) { - logger.error({ - message: `Service ${name} is not registered`, - service: SERVICE_NAME, - method: "get", - }); - throw new Error(`Service ${name} is not registered`); - } - return this.services[name]; - } - - listServices() { - return Object.keys(this.services); - } -} - -export default new ServiceRegistry(); diff --git a/server/src/app.js b/server/src/app.js new file mode 100644 index 000000000..9cabda6b6 --- /dev/null +++ b/server/src/app.js @@ -0,0 +1,85 @@ +import express from "express"; +import path from "path"; +import { responseHandler } from "./middleware/responseHandler.js"; +import cors from "cors"; +import helmet from "helmet"; +import compression from "compression"; +import cookieParser from "cookie-parser"; +import languageMiddleware from "./middleware/languageMiddleware.js"; +import swaggerUi from "swagger-ui-express"; +import { handleErrors } from "./middleware/handleErrors.js"; +import { setupRoutes } from "./config/routes.js"; +import { generalApiLimiter } from "./middleware/rateLimiter.js"; +import { sanitizeBody, sanitizeQuery } from "./middleware/sanitization.js"; + +export const createApp = ({ services, controllers, envSettings, frontendPath, openApiSpec }) => { + const allowedOrigin = envSettings.clientHost; + + const app = express(); + app.use(generalApiLimiter); + // Static files + app.use(express.static(frontendPath)); + + // Response handler + app.use(responseHandler); + + app.use( + cors({ + origin: allowedOrigin, + methods: "GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS", + allowedHeaders: "*", + credentials: true, + }) + ); + app.use(express.json()); + app.use(cookieParser()); + + app.use(sanitizeBody()); + app.use(sanitizeQuery()); + + app.use( + helmet({ + hsts: false, + contentSecurityPolicy: { + useDefaults: true, + directives: { + upgradeInsecureRequests: null, + "script-src": ["'self'", "'unsafe-inline'", "'unsafe-eval'"], + "object-src": ["'none'"], + "base-uri": ["'self'"], + }, + }, + }) + ); + app.use( + compression({ + level: 6, + threshold: 1024, + filter: (req, res) => { + if (req.headers["x-no-compression"]) { + return false; + } + return compression.filter(req, res); + }, + }) + ); + app.use(languageMiddleware(services.stringService, services.translationService, services.settingsService)); + // Swagger UI + app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(openApiSpec)); + + app.use("/api/v1/health", (req, res) => { + res.json({ + status: "OK", + }); + }); + + // Main app routes + setupRoutes(app, controllers); + + // FE routes + app.get("*", (req, res) => { + res.sendFile(path.join(frontendPath, "index.html")); + }); + app.use(handleErrors); + return app; +}; diff --git a/server/src/config/controllers.js b/server/src/config/controllers.js new file mode 100644 index 000000000..693e35ab8 --- /dev/null +++ b/server/src/config/controllers.js @@ -0,0 +1,66 @@ +import { createCommonDependencies } from "../controllers/baseController.js"; + +// Services + +// Controllers +import MonitorController from "../controllers/monitorController.js"; +import AuthController from "../controllers/authController.js"; +import SettingsController from "../controllers/settingsController.js"; +import CheckController from "../controllers/checkController.js"; +import InviteController from "../controllers/inviteController.js"; +import MaintenanceWindowController from "../controllers/maintenanceWindowController.js"; +import QueueController from "../controllers/queueController.js"; +import LogController from "../controllers/logController.js"; +import StatusPageController from "../controllers/statusPageController.js"; +import NotificationController from "../controllers/notificationController.js"; +import DiagnosticController from "../controllers/diagnosticController.js"; + +export const initializeControllers = (services) => { + const controllers = {}; + const commonDependencies = createCommonDependencies(services.db, services.errorService, services.logger, services.stringService); + + controllers.authController = new AuthController(commonDependencies, { + settingsService: services.settingsService, + emailService: services.emailService, + jobQueue: services.jobQueue, + userService: services.userService, + }); + + controllers.monitorController = new MonitorController(commonDependencies, { + settingsService: services.settingsService, + jobQueue: services.jobQueue, + emailService: services.emailService, + monitorService: services.monitorService, + }); + + controllers.settingsController = new SettingsController(commonDependencies, { + settingsService: services.settingsService, + emailService: services.emailService, + }); + controllers.checkController = new CheckController(commonDependencies, { + settingsService: services.settingsService, + checkService: services.checkService, + }); + controllers.inviteController = new InviteController(commonDependencies, { + inviteService: services.inviteService, + }); + + controllers.maintenanceWindowController = new MaintenanceWindowController(commonDependencies, { + settingsService: services.settingsService, + maintenanceWindowService: services.maintenanceWindowService, + }); + controllers.queueController = new QueueController(commonDependencies, { + jobQueue: services.jobQueue, + }); + controllers.logController = new LogController(commonDependencies); + controllers.statusPageController = new StatusPageController(commonDependencies); + controllers.notificationController = new NotificationController(commonDependencies, { + notificationService: services.notificationService, + statusService: services.statusService, + }); + controllers.diagnosticController = new DiagnosticController(commonDependencies, { + diagnosticService: services.diagnosticService, + }); + + return controllers; +}; diff --git a/server/src/config/database.js b/server/src/config/database.js new file mode 100644 index 000000000..e69de29bb diff --git a/server/src/config/routes.js b/server/src/config/routes.js new file mode 100644 index 000000000..7056393f9 --- /dev/null +++ b/server/src/config/routes.js @@ -0,0 +1,40 @@ +import { verifyJWT } from "../middleware/verifyJWT.js"; +import { authApiLimiter } from "../middleware/rateLimiter.js"; + +import AuthRoutes from "../routes/authRoute.js"; +import InviteRoutes from "../routes/inviteRoute.js"; +import MonitorRoutes from "../routes/monitorRoute.js"; +import CheckRoutes from "../routes/checkRoute.js"; +import SettingsRoutes from "../routes/settingsRoute.js"; +import MaintenanceWindowRoutes from "../routes/maintenanceWindowRoute.js"; +import StatusPageRoutes from "../routes/statusPageRoute.js"; +import QueueRoutes from "../routes/queueRoute.js"; +import LogRoutes from "../routes/logRoutes.js"; +import DiagnosticRoutes from "../routes/diagnosticRoute.js"; +import NotificationRoutes from "../routes/notificationRoute.js"; + +export const setupRoutes = (app, controllers) => { + const authRoutes = new AuthRoutes(controllers.authController); + const monitorRoutes = new MonitorRoutes(controllers.monitorController); + const settingsRoutes = new SettingsRoutes(controllers.settingsController); + const checkRoutes = new CheckRoutes(controllers.checkController); + const inviteRoutes = new InviteRoutes(controllers.inviteController); + const maintenanceWindowRoutes = new MaintenanceWindowRoutes(controllers.maintenanceWindowController); + const queueRoutes = new QueueRoutes(controllers.queueController); + const logRoutes = new LogRoutes(controllers.logController); + const statusPageRoutes = new StatusPageRoutes(controllers.statusPageController); + const notificationRoutes = new NotificationRoutes(controllers.notificationController); + const diagnosticRoutes = new DiagnosticRoutes(controllers.diagnosticController); + + app.use("/api/v1/auth", authApiLimiter, authRoutes.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()); +}; diff --git a/server/src/config/services.js b/server/src/config/services.js new file mode 100644 index 000000000..73220128f --- /dev/null +++ b/server/src/config/services.js @@ -0,0 +1,234 @@ +import ServiceRegistry from "../service/system/serviceRegistry.js"; +import TranslationService from "../service/system/translationService.js"; +import StringService from "../service/system/stringService.js"; +import MongoDB from "../db/mongo/MongoDB.js"; +import NetworkService from "../service/infrastructure/networkService.js"; +import EmailService from "../service/infrastructure/emailService.js"; +import BufferService from "../service/infrastructure/bufferService.js"; +import StatusService from "../service/infrastructure/statusService.js"; +import NotificationUtils from "../service/infrastructure/notificationUtils.js"; +import NotificationService from "../service/infrastructure/notificationService.js"; +import ErrorService from "../service/infrastructure/errorService.js"; +import SuperSimpleQueueHelper from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; +import SuperSimpleQueue from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; +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"; +import papaparse from "papaparse"; +import axios from "axios"; +import ping from "ping"; +import http from "http"; +import Docker from "dockerode"; +import net from "net"; +import fs from "fs"; +import path from "path"; +import nodemailer from "nodemailer"; +import pkg from "handlebars"; +const { compile } = pkg; +import mjml2html from "mjml"; +import jwt from "jsonwebtoken"; +import crypto from "crypto"; +import { games } from "gamedig"; + +import { fileURLToPath } from "url"; +import { ObjectId } from "mongodb"; + +// DB Modules +import { NormalizeData, NormalizeDataUptimeDetails } from "../utils/dataUtils.js"; +import { GenerateAvatarImage } from "../utils/imageProcessing.js"; +import { ParseBoolean } from "../utils/utils.js"; + +// Models +import Check from "../db/models/Check.js"; +import HardwareCheck from "../db/models/HardwareCheck.js"; +import PageSpeedCheck from "../db/models/PageSpeedCheck.js"; +import Monitor from "../db/models/Monitor.js"; +import User from "../db/models/User.js"; +import InviteToken from "../db/models/InviteToken.js"; +import StatusPage from "../db/models/StatusPage.js"; +import Team from "../db/models/Team.js"; +import MaintenanceWindow from "../db/models/MaintenanceWindow.js"; +import MonitorStats from "../db/models/MonitorStats.js"; +import NetworkCheck from "../db/models/NetworkCheck.js"; +import Notification from "../db/models/Notification.js"; +import RecoveryToken from "../db/models/RecoveryToken.js"; +import AppSettings from "../db/models/AppSettings.js"; + +import InviteModule from "../db/mongo/modules/inviteModule.js"; +import CheckModule from "../db/mongo/modules/checkModule.js"; +import StatusPageModule from "../db/mongo/modules/statusPageModule.js"; +import UserModule from "../db/mongo/modules/userModule.js"; +import HardwareCheckModule from "../db/mongo/modules/hardwareCheckModule.js"; +import MaintenanceWindowModule from "../db/mongo/modules/maintenanceWindowModule.js"; +import MonitorModule from "../db/mongo/modules/monitorModule.js"; +import NetworkCheckModule from "../db/mongo/modules/networkCheckModule.js"; +import NotificationModule from "../db/mongo/modules/notificationModule.js"; +import PageSpeedCheckModule from "../db/mongo/modules/pageSpeedCheckModule.js"; +import RecoveryModule from "../db/mongo/modules/recoveryModule.js"; +import SettingsModule from "../db/mongo/modules/settingsModule.js"; + +export const initializeServices = async ({ logger, envSettings, settingsService }) => { + const serviceRegistry = new ServiceRegistry({ logger }); + ServiceRegistry.instance = serviceRegistry; + + const translationService = new TranslationService(logger); + await translationService.initialize(); + + const stringService = new StringService(translationService); + + // Create DB + const checkModule = new CheckModule({ logger, Check, HardwareCheck, PageSpeedCheck, Monitor, User }); + const inviteModule = new InviteModule({ InviteToken, crypto, stringService }); + const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, stringService }); + const userModule = new UserModule({ User, Team, GenerateAvatarImage, ParseBoolean, stringService }); + const hardwareCheckModule = new HardwareCheckModule({ HardwareCheck, Monitor, logger }); + const maintenanceWindowModule = new MaintenanceWindowModule({ MaintenanceWindow }); + const monitorModule = new MonitorModule({ + Monitor, + MonitorStats, + Check, + PageSpeedCheck, + HardwareCheck, + stringService, + fs, + path, + fileURLToPath, + ObjectId, + NormalizeData, + NormalizeDataUptimeDetails, + }); + const networkCheckModule = new NetworkCheckModule({ NetworkCheck }); + const notificationModule = new NotificationModule({ Notification, Monitor }); + const pageSpeedCheckModule = new PageSpeedCheckModule({ PageSpeedCheck }); + const recoveryModule = new RecoveryModule({ User, RecoveryToken, crypto, stringService }); + const settingsModule = new SettingsModule({ AppSettings }); + + const db = new MongoDB({ + logger, + envSettings, + checkModule, + inviteModule, + statusPageModule, + userModule, + hardwareCheckModule, + maintenanceWindowModule, + monitorModule, + networkCheckModule, + notificationModule, + pageSpeedCheckModule, + recoveryModule, + settingsModule, + }); + + await db.connect(); + + 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, envSettings }); + const statusService = new StatusService({ db, logger, buffer: bufferService }); + + const notificationUtils = new NotificationUtils({ + stringService, + emailService, + }); + + const notificationService = new NotificationService({ + emailService, + db, + logger, + networkService, + stringService, + notificationUtils, + }); + + const errorService = new ErrorService(); + + const superSimpleQueueHelper = new SuperSimpleQueueHelper({ + db, + logger, + networkService, + statusService, + notificationService, + }); + + const superSimpleQueue = await SuperSimpleQueue.create({ + envSettings, + db, + logger, + helper: superSimpleQueueHelper, + }); + + // Business services + const userService = new UserService({ + crypto, + db, + emailService, + settingsService, + logger, + stringService, + jwt, + errorService, + jobQueue: superSimpleQueue, + }); + 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, + games, + }); + + const services = { + settingsService, + translationService, + stringService, + db, + networkService, + emailService, + bufferService, + statusService, + notificationService, + jobQueue: superSimpleQueue, + userService, + checkService, + diagnosticService, + inviteService, + maintenanceWindowService, + monitorService, + errorService, + logger, + }; + + Object.values(services).forEach((service) => { + ServiceRegistry.register(service.serviceName, service); + }); + + return services; +}; diff --git a/server/controllers/announcementsController.js b/server/src/controllers/announcementsController.js similarity index 74% rename from server/controllers/announcementsController.js rename to server/src/controllers/announcementsController.js index 024ed95a5..430859791 100755 --- a/server/controllers/announcementsController.js +++ b/server/src/controllers/announcementsController.js @@ -1,5 +1,5 @@ import { createAnnouncementValidation } from "../validation/joi.js"; -import { handleError } from "./controllerUtils.js"; +import BaseController from "./baseController.js"; const SERVICE_NAME = "announcementController"; @@ -10,14 +10,18 @@ const SERVICE_NAME = "announcementController"; * @class AnnouncementController */ -class AnnouncementController { - constructor(db, stringService) { - this.db = db; - this.stringService = stringService; +class AnnouncementController extends BaseController { + static SERVICE_NAME = SERVICE_NAME; + constructor(commonDependencies) { + super(commonDependencies); this.createAnnouncement = this.createAnnouncement.bind(this); this.getAnnouncement = this.getAnnouncement.bind(this); } + get serviceName() { + return AnnouncementController.SERVICE_NAME; + } + /** * Handles the creation of a new announcement. * @@ -28,16 +32,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 +47,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 +61,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/src/controllers/authController.js b/server/src/controllers/authController.js new file mode 100755 index 000000000..c186e8423 --- /dev/null +++ b/server/src/controllers/authController.js @@ -0,0 +1,461 @@ +import BaseController from "./baseController.js"; +import { + registrationBodyValidation, + loginValidation, + editUserBodyValidation, + recoveryValidation, + recoveryTokenBodyValidation, + newPasswordValidation, + getUserByIdParamValidation, + editUserByIdParamValidation, + editUserByIdBodyValidation, + editSuperadminUserByIdBodyValidation, +} from "../validation/joi.js"; + +const SERVICE_NAME = "authController"; + +/** + * 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 { + static SERVICE_NAME = SERVICE_NAME; + /** + * 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.userService = userService; + } + + get serviceName() { + return AuthController.SERVICE_NAME; + } + + /** + * Registers a new user in the system. + * + * @async + * @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 = this.asyncHandler( + async (req, res) => { + if (req.body?.email) { + req.body.email = req.body.email?.toLowerCase(); + } + await registrationBodyValidation.validateAsync(req.body); + const { user, token } = await this.userService.registerUser(req.body, req.file); + res.success({ + msg: this.stringService.authCreateUser, + data: { user, token }, + }); + }, + SERVICE_NAME, + "registerUser" + ); + + /** + * Authenticates a user and returns a JWT token. + * + * @async + * @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 = this.asyncHandler( + async (req, res) => { + if (req.body?.email) { + req.body.email = req.body.email?.toLowerCase(); + } + await loginValidation.validateAsync(req.body); + const { user, token } = await this.userService.loginUser(req.body.email, req.body.password); + + return res.success({ + msg: this.stringService.authLoginUser, + data: { + user, + token, + }, + }); + }, + SERVICE_NAME, + "loginUser" + ); + + /** + * Updates the current user's profile information. + * + * @async + * @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 = this.asyncHandler( + async (req, res) => { + await editUserBodyValidation.validateAsync(req.body); + + const updatedUser = await this.userService.editUser(req.body, req.file, req.user); + + res.success({ + msg: this.stringService.authUpdateUser, + data: updatedUser, + }); + }, + SERVICE_NAME, + "editUser" + ); + + /** + * Checks if a superadmin account exists in the system. + * + * @async + * @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 = this.asyncHandler( + async (req, res) => { + const superAdminExists = await this.userService.checkSuperadminExists(); + return res.success({ + msg: this.stringService.authAdminExists, + data: superAdminExists, + }); + }, + SERVICE_NAME, + "checkSuperadminExists" + ); + + /** + * Initiates password recovery process by sending a recovery email. + * + * @async + * @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 = this.asyncHandler( + async (req, res) => { + await recoveryValidation.validateAsync(req.body); + const email = req?.body?.email; + const msgId = await this.userService.requestRecovery(email); + return res.success({ + msg: this.stringService.authCreateRecoveryToken, + data: msgId, + }); + }, + SERVICE_NAME, + "requestRecovery" + ); + + /** + * Validates a password recovery token. + * + * @async + * @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 = this.asyncHandler( + async (req, res) => { + await recoveryTokenBodyValidation.validateAsync(req.body); + await this.userService.validateRecovery(req.body.recoveryToken); + return res.success({ + msg: this.stringService.authVerifyRecoveryToken, + }); + }, + SERVICE_NAME, + "validateRecovery" + ); + + /** + * Resets user password using a valid recovery token. + * + * @async + * @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 = this.asyncHandler( + async (req, res) => { + await newPasswordValidation.validateAsync(req.body); + const { user, token } = await this.userService.resetPassword(req.body.password, req.body.recoveryToken); + return res.success({ + msg: this.stringService.authResetPassword, + data: { user, token }, + }); + }, + SERVICE_NAME, + "resetPassword" + ); + + /** + * Deletes the current user's account and associated data. + * + * @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 = this.asyncHandler( + async (req, res) => { + await this.userService.deleteUser(req.user); + return res.success({ + msg: this.stringService.authDeleteUser, + }); + }, + 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, + }); + }, + 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/src/controllers/baseController.js b/server/src/controllers/baseController.js new file mode 100644 index 000000000..910cf57b4 --- /dev/null +++ b/server/src/controllers/baseController.js @@ -0,0 +1,83 @@ +import { AppError } from "../service/infrastructure/errorService.js"; + +export const createCommonDependencies = (db, errorService, logger, stringService) => { + return { + db, + errorService, + logger, + stringService, + }; +}; + +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/src/controllers/checkController.js b/server/src/controllers/checkController.js new file mode 100755 index 000000000..4069df3ff --- /dev/null +++ b/server/src/controllers/checkController.js @@ -0,0 +1,360 @@ +import BaseController from "./baseController.js"; +import { + getChecksParamValidation, + getChecksQueryValidation, + getTeamChecksQueryValidation, + deleteChecksParamValidation, + deleteChecksByTeamIdParamValidation, + updateChecksTTLBodyValidation, + ackCheckBodyValidation, + ackAllChecksParamValidation, + ackAllChecksBodyValidation, +} from "../validation/joi.js"; + +const SERVICE_NAME = "checkController"; + +/** + * 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 { + static SERVICE_NAME = SERVICE_NAME; + /** + * 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.checkService = checkService; + } + + get serviceName() { + return CheckController.SERVICE_NAME; + } + + /** + * 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); + + 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, + }); + }, + SERVICE_NAME, + "getChecksByMonitor" + ); + + /** + * 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); + const checkData = await this.checkService.getChecksByTeam({ + teamId: req?.user?.teamId, + query: req?.query, + }); + return res.success({ + msg: this.stringService.checkGet, + data: checkData, + }); + }, + SERVICE_NAME, + "getChecksByTeam" + ); + + /** + * 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, + }); + }, + SERVICE_NAME, + "getChecksSummaryByTeamId" + ); + + /** + * 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); + + 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, + }); + }, + SERVICE_NAME, + "ackCheck" + ); + + /** + * 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); + + const teamId = req?.user?.teamId; + if (!teamId) { + throw new Error("No team ID in request"); + } + + 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, + }); + }, + SERVICE_NAME, + "ackAllChecks" + ); + + /** + * 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); + + const deletedCount = await this.checkService.deleteChecks({ + monitorId: req.params.monitorId, + teamId: req?.user?.teamId, + }); + + return res.success({ + msg: this.stringService.checkDelete, + data: { deletedCount }, + }); + }, + SERVICE_NAME, + "deleteChecks" + ); + + /** + * 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); + + const deletedCount = await this.checkService.deleteChecksByTeamId({ teamId: req?.user?.teamId }); + + return res.success({ + msg: this.stringService.checkDelete, + data: { deletedCount }, + }); + }, + SERVICE_NAME, + "deleteChecksByTeamId" + ); + + /** + * 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); + + await this.checkService.updateChecksTTL({ + teamId: req?.user?.teamId, + ttl: req?.body?.ttl, + }); + + return res.success({ + msg: this.stringService.checkUpdateTTL, + }); + }, + SERVICE_NAME, + "updateChecksTtl" + ); +} + +export default CheckController; diff --git a/server/src/controllers/controllerUtils.js b/server/src/controllers/controllerUtils.js new file mode 100755 index 000000000..f21ba154b --- /dev/null +++ b/server/src/controllers/controllerUtils.js @@ -0,0 +1,12 @@ +const fetchMonitorCertificate = async (sslChecker, monitor) => { + const monitorUrl = new URL(monitor.url); + const hostname = monitorUrl.hostname; + const cert = await sslChecker(hostname); + // Throw an error if no cert or if cert.validTo is not present + if (cert?.validTo === null || cert?.validTo === undefined) { + throw new Error("Certificate not found"); + } + return cert; +}; + +export { fetchMonitorCertificate }; diff --git a/server/src/controllers/diagnosticController.js b/server/src/controllers/diagnosticController.js new file mode 100755 index 000000000..0723a9f6c --- /dev/null +++ b/server/src/controllers/diagnosticController.js @@ -0,0 +1,61 @@ +const SERVICE_NAME = "diagnosticController"; +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 { + static SERVICE_NAME = SERVICE_NAME; + /** + * 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; + } + + get serviceName() { + return DiagnosticController.SERVICE_NAME; + } + /** + * 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: diagnostics, + }); + }, + SERVICE_NAME, + "getSystemStats" + ); +} + +export default DiagnosticController; diff --git a/server/src/controllers/inviteController.js b/server/src/controllers/inviteController.js new file mode 100755 index 000000000..a377234c0 --- /dev/null +++ b/server/src/controllers/inviteController.js @@ -0,0 +1,101 @@ +import { inviteBodyValidation, inviteVerificationBodyValidation } from "../validation/joi.js"; +import BaseController from "./baseController.js"; +const SERVICE_NAME = "inviteController"; + +/** + * Controller for handling user invitation operations + * Manages invite token generation, email sending, and token verification + */ +class InviteController extends BaseController { + static SERVICE_NAME = SERVICE_NAME; + /** + * 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; + } + + get serviceName() { + return InviteController.SERVICE_NAME; + } + + /** + * 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 = 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, + }); + }, + SERVICE_NAME, + "getInviteToken" + ); + + /** + * 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, + }); + }, + SERVICE_NAME, + "sendInviteEmail" + ); + + /** + * 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); + const invite = await this.inviteService.verifyInviteToken({ inviteToken: req?.body?.token }); + return res.success({ + msg: this.stringService.inviteVerified, + data: invite, + }); + }, + SERVICE_NAME, + "verifyInviteToken" + ); +} + +export default InviteController; diff --git a/server/src/controllers/logController.js b/server/src/controllers/logController.js new file mode 100644 index 000000000..3537246bb --- /dev/null +++ b/server/src/controllers/logController.js @@ -0,0 +1,26 @@ +import BaseController from "./baseController.js"; +const SERVICE_NAME = "LogController"; + +class LogController extends BaseController { + static SERVICE_NAME = SERVICE_NAME; + constructor(commonDependencies) { + super(commonDependencies); + } + + get serviceName() { + return LogController.SERVICE_NAME; + } + + getLogs = this.asyncHandler( + async (req, res) => { + const logs = await this.logger.getLogs(); + res.success({ + msg: "Logs fetched successfully", + data: logs, + }); + }, + SERVICE_NAME, + "getLogs" + ); +} +export default LogController; diff --git a/server/src/controllers/maintenanceWindowController.js b/server/src/controllers/maintenanceWindowController.js new file mode 100755 index 000000000..2fdb1ee15 --- /dev/null +++ b/server/src/controllers/maintenanceWindowController.js @@ -0,0 +1,146 @@ +import { + createMaintenanceWindowBodyValidation, + editMaintenanceWindowByIdParamValidation, + editMaintenanceByIdWindowBodyValidation, + getMaintenanceWindowByIdParamValidation, + getMaintenanceWindowsByMonitorIdParamValidation, + getMaintenanceWindowsByTeamIdQueryValidation, + deleteMaintenanceWindowByIdParamValidation, +} from "../validation/joi.js"; +import BaseController from "./baseController.js"; + +const SERVICE_NAME = "maintenanceWindowController"; + +class MaintenanceWindowController extends BaseController { + static SERVICE_NAME = SERVICE_NAME; + constructor(commonDependencies, { settingsService, maintenanceWindowService }) { + super(commonDependencies); + this.settingsService = settingsService; + this.maintenanceWindowService = maintenanceWindowService; + } + + get serviceName() { + return MaintenanceWindowController.SERVICE_NAME; + } + + createMaintenanceWindows = this.asyncHandler( + async (req, res) => { + await createMaintenanceWindowBodyValidation.validateAsync(req.body); + + 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, + }); + }, + SERVICE_NAME, + "createMaintenanceWindows" + ); + + getMaintenanceWindowById = this.asyncHandler( + async (req, res) => { + await getMaintenanceWindowByIdParamValidation.validateAsync(req.params); + + 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, + }); + }, + SERVICE_NAME, + "getMaintenanceWindowById" + ); + + getMaintenanceWindowsByTeamId = this.asyncHandler( + async (req, res) => { + await getMaintenanceWindowsByTeamIdQueryValidation.validateAsync(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, + }); + }, + SERVICE_NAME, + "getMaintenanceWindowsByTeamId" + ); + + getMaintenanceWindowsByMonitorId = this.asyncHandler( + async (req, res) => { + await getMaintenanceWindowsByMonitorIdParamValidation.validateAsync(req.params); + + 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, + }); + }, + SERVICE_NAME, + "getMaintenanceWindowsByMonitorId" + ); + + deleteMaintenanceWindow = this.asyncHandler( + async (req, res) => { + await deleteMaintenanceWindowByIdParamValidation.validateAsync(req.params); + + 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, + }); + }, + SERVICE_NAME, + "deleteMaintenanceWindow" + ); + + editMaintenanceWindow = this.asyncHandler( + async (req, res) => { + await editMaintenanceWindowByIdParamValidation.validateAsync(req.params); + await editMaintenanceByIdWindowBodyValidation.validateAsync(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, + }); + }, + SERVICE_NAME, + "editMaintenanceWindow" + ); +} + +export default MaintenanceWindowController; diff --git a/server/src/controllers/monitorController.js b/server/src/controllers/monitorController.js new file mode 100755 index 000000000..0f40a4a01 --- /dev/null +++ b/server/src/controllers/monitorController.js @@ -0,0 +1,457 @@ +import { + getMonitorByIdParamValidation, + getMonitorByIdQueryValidation, + getMonitorsByTeamIdParamValidation, + getMonitorsByTeamIdQueryValidation, + createMonitorBodyValidation, + editMonitorBodyValidation, + pauseMonitorParamValidation, + getMonitorStatsByIdParamValidation, + getMonitorStatsByIdQueryValidation, + getCertificateParamValidation, + getHardwareDetailsByIdParamValidation, + getHardwareDetailsByIdQueryValidation, +} from "../validation/joi.js"; +import sslChecker from "ssl-checker"; +import { fetchMonitorCertificate } from "./controllerUtils.js"; +import BaseController from "./baseController.js"; + +const SERVICE_NAME = "monitorController"; +class MonitorController extends BaseController { + static SERVICE_NAME = SERVICE_NAME; + constructor(commonDependencies, { settingsService, jobQueue, emailService, monitorService }) { + super(commonDependencies); + this.settingsService = settingsService; + this.jobQueue = jobQueue; + this.emailService = emailService; + this.monitorService = monitorService; + } + + get serviceName() { + return MonitorController.SERVICE_NAME; + } + + async verifyTeamAccess(teamId, monitorId) { + const monitor = await this.db.monitorModule.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, + }); + }, + SERVICE_NAME, + "getAllMonitors" + ); + + getUptimeDetailsById = this.asyncHandler( + async (req, res) => { + const monitorId = req?.params?.monitorId; + const dateRange = req?.query?.dateRange; + const normalize = req?.query?.normalize; + + const teamId = req?.user?.teamId; + + 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, + }); + }, + SERVICE_NAME, + "getUptimeDetailsById" + ); + + getMonitorStatsById = this.asyncHandler( + async (req, res) => { + await getMonitorStatsByIdParamValidation.validateAsync(req.params); + await getMonitorStatsByIdQueryValidation.validateAsync(req.query); + + let { limit, sortOrder, dateRange, numToDisplay, normalize } = req.query; + const monitorId = req?.params?.monitorId; + + 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, + dateRange, + numToDisplay, + normalize, + }); + + return res.success({ + msg: this.stringService.monitorStatsById, + data: monitorStats, + }); + }, + SERVICE_NAME, + "getMonitorStatsById" + ); + + /** + * Get hardware details for a specific monitor by ID + * @async + * @param {Express.Request} req - Express request object containing monitorId in params + * @param {Express.Response} res - Express response object + * @param {Express.NextFunction} next - Express next middleware function + * @returns {Promise} + * @throws {Error} - Throws error if monitor not found or other database errors + */ + getHardwareDetailsById = this.asyncHandler( + async (req, res) => { + await getHardwareDetailsByIdParamValidation.validateAsync(req.params); + await getHardwareDetailsByIdQueryValidation.validateAsync(req.query); + + 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, + }); + }, + SERVICE_NAME, + "getHardwareDetailsById" + ); + + getMonitorCertificate = this.asyncHandler( + async (req, res) => { + await getCertificateParamValidation.validateAsync(req.params); + + const { monitorId } = req.params; + const monitor = await this.db.monitorModule.getMonitorById(monitorId); + const certificate = await fetchMonitorCertificate(sslChecker, monitor); + + return res.success({ + msg: this.stringService.monitorCertificate, + data: { + certificateDate: new Date(certificate.validTo), + }, + }); + }, + SERVICE_NAME, + "getMonitorCertificate" + ); + + getMonitorById = this.asyncHandler( + async (req, res) => { + await getMonitorByIdParamValidation.validateAsync(req.params); + await getMonitorByIdQueryValidation.validateAsync(req.query); + + 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, + }); + }, + SERVICE_NAME, + "getMonitorById" + ); + + createMonitor = this.asyncHandler( + async (req, res) => { + await createMonitorBodyValidation.validateAsync(req.body); + + const userId = req?.user?._id; + const teamId = req?.user?.teamId; + + const monitor = await this.monitorService.createMonitor({ teamId, userId, body: req.body }); + + return res.success({ + msg: this.stringService.monitorCreate, + data: monitor, + }); + }, + SERVICE_NAME, + "createMonitor" + ); + + createBulkMonitors = this.asyncHandler( + async (req, res) => { + if (!req.file) { + throw this.errorService.createBadRequestError("No file uploaded"); + } + + if (!req.file.mimetype.includes("csv")) { + throw this.errorService.createBadRequestError("File is not a CSV"); + } + + if (req.file.size === 0) { + throw this.errorService.createBadRequestError("File is empty"); + } + + const userId = req?.user?._id; + const teamId = req?.user?.teamId; + + if (!userId || !teamId) { + throw this.errorService.createBadRequestError("Missing userId or teamId"); + } + + const fileData = req?.file?.buffer?.toString("utf-8"); + if (!fileData) { + throw this.errorService.createBadRequestError("Cannot get file from buffer"); + } + + const monitors = await this.monitorService.createBulkMonitors({ fileData, userId, teamId }); + + return res.success({ + msg: this.stringService.bulkMonitorsCreate, + data: monitors, + }); + }, + SERVICE_NAME, + "createBulkMonitors" + ); + + deleteMonitor = this.asyncHandler( + async (req, res) => { + await getMonitorByIdParamValidation.validateAsync(req.params); + const monitorId = req.params.monitorId; + 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 }); + + return res.success({ msg: `Deleted ${deletedCount} monitors` }); + }, + SERVICE_NAME, + "deleteAllMonitors" + ); + + editMonitor = this.asyncHandler( + async (req, res) => { + await getMonitorByIdParamValidation.validateAsync(req.params); + await editMonitorBodyValidation.validateAsync(req.body); + const monitorId = req?.params?.monitorId; + + const teamId = req?.user?.teamId; + if (!teamId) { + throw this.errorService.createBadRequestError("Team ID is required"); + } + + const editedMonitor = await this.monitorService.editMonitor({ teamId, monitorId, body: req.body }); + + return res.success({ + msg: this.stringService.monitorEdit, + data: editedMonitor, + }); + }, + SERVICE_NAME, + "editMonitor" + ); + + pauseMonitor = this.asyncHandler( + async (req, res) => { + await pauseMonitorParamValidation.validateAsync(req.params); + + const monitorId = req.params.monitorId; + 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, + data: monitor, + }); + }, + SERVICE_NAME, + "pauseMonitor" + ); + + addDemoMonitors = this.asyncHandler( + async (req, res) => { + const { _id, teamId } = req.user; + const demoMonitors = await this.monitorService.addDemoMonitors({ userId: _id, teamId }); + + return res.success({ + msg: this.stringService.monitorDemoAdded, + data: demoMonitors?.length ?? 0, + }); + }, + SERVICE_NAME, + "addDemoMonitors" + ); + + sendTestEmail = this.asyncHandler( + async (req, res) => { + const { to } = req.body; + if (!to || typeof to !== "string") { + throw this.errorService.createBadRequestError(this.stringService.errorForValidEmailAddress); + } + + const messageId = await this.monitorService.sendTestEmail({ to }); + return res.success({ + msg: this.stringService.sendTestEmail, + data: { messageId }, + }); + }, + SERVICE_NAME, + "sendTestEmail" + ); + + getMonitorsByTeamId = this.asyncHandler( + async (req, res) => { + await getMonitorsByTeamIdParamValidation.validateAsync(req.params); + await getMonitorsByTeamIdQueryValidation.validateAsync(req.query); + + let { limit, type, page, rowsPerPage, filter, field, order } = req.query; + const teamId = req?.user?.teamId; + + const monitors = await this.monitorService.getMonitorsByTeamId({ teamId, limit, type, page, rowsPerPage, filter, field, order }); + + return res.success({ + msg: this.stringService.monitorGetByTeamId, + data: monitors, + }); + }, + SERVICE_NAME, + "getMonitorsByTeamId" + ); + + getMonitorsAndSummaryByTeamId = this.asyncHandler( + async (req, res) => { + await getMonitorsByTeamIdParamValidation.validateAsync(req.params); + await getMonitorsByTeamIdQueryValidation.validateAsync(req.query); + + 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 }); + + return res.success({ + msg: "OK", // TODO + data: result, + }); + }, + SERVICE_NAME, + "getMonitorsAndSummaryByTeamId" + ); + + getMonitorsWithChecksByTeamId = this.asyncHandler( + async (req, res) => { + await getMonitorsByTeamIdParamValidation.validateAsync(req.params); + await getMonitorsByTeamIdQueryValidation.validateAsync(req.query); + + const explain = req?.query?.explain; + let { limit, type, page, rowsPerPage, filter, field, order } = req.query; + const teamId = req?.user?.teamId; + if (!teamId) { + throw this.errorService.createBadRequestError("Team ID is required"); + } + + const monitors = await this.monitorService.getMonitorsWithChecksByTeamId({ + teamId, + limit, + type, + page, + rowsPerPage, + filter, + field, + order, + explain, + }); + + return res.success({ + msg: "OK", + data: monitors, + }); + }, + SERVICE_NAME, + "getMonitorsWithChecksByTeamId" + ); + + exportMonitorsToCSV = this.asyncHandler( + async (req, res) => { + const teamId = req?.user?.teamId; + if (!teamId) { + throw this.errorService.createBadRequestError("Team ID is required"); + } + + const csv = await this.monitorService.exportMonitorsToCSV({ teamId }); + + return res.file({ + data: csv, + headers: { + "Content-Type": "text/csv", + "Content-Disposition": "attachment; filename=monitors.csv", + }, + }); + }, + SERVICE_NAME, + "exportMonitorsToCSV" + ); + + getAllGames = this.asyncHandler( + async (req, res) => { + return res.success({ + msg: "OK", + data: this.monitorService.getAllGames(), + }); + }, + SERVICE_NAME, + "getAllGames" + ); +} + +export default MonitorController; diff --git a/server/src/controllers/notificationController.js b/server/src/controllers/notificationController.js new file mode 100755 index 000000000..e00dd5b6f --- /dev/null +++ b/server/src/controllers/notificationController.js @@ -0,0 +1,180 @@ +import { createNotificationBodyValidation } from "../validation/joi.js"; +import BaseController from "./baseController.js"; + +const SERVICE_NAME = "NotificationController"; + +class NotificationController extends BaseController { + static SERVICE_NAME = SERVICE_NAME; + constructor(commonDependencies, { notificationService, statusService }) { + super(commonDependencies); + this.notificationService = notificationService; + this.statusService = statusService; + } + + get serviceName() { + return NotificationController.SERVICE_NAME; + } + + testNotification = this.asyncHandler( + async (req, res) => { + const notification = req.body; + + const success = await this.notificationService.sendTestNotification(notification); + + if (!success) { + throw this.errorService.createServerError("Sending notification failed"); + } + + return res.success({ + msg: "Notification sent successfully", + }); + }, + SERVICE_NAME, + "testNotification" + ); + + createNotification = this.asyncHandler( + async (req, res) => { + await createNotificationBodyValidation.validateAsync(req.body, { + abortEarly: false, + }); + + const body = req.body; + + 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.notificationModule.createNotification(body); + return res.success({ + msg: "Notification created successfully", + data: notification, + }); + }, + 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.notificationModule.getNotificationsByTeamId(teamId); + + return res.success({ + msg: "Notifications fetched successfully", + data: notifications, + }); + }, + 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.notificationModule.getNotificationById(req.params.id); + if (!notification.teamId.equals(teamId)) { + throw this.errorService.createAuthorizationError(); + } + + await this.db.notificationModule.deleteNotificationById(req.params.id); + return res.success({ + msg: "Notification deleted successfully", + }); + }, + SERVICE_NAME, + "deleteNotification" + ); + + getNotificationById = this.asyncHandler( + async (req, res) => { + const notification = await this.db.notificationModule.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, + }); + }, + SERVICE_NAME, + "getNotificationById" + ); + + editNotification = this.asyncHandler( + async (req, res) => { + await createNotificationBodyValidation.validateAsync(req.body, { + abortEarly: false, + }); + + const teamId = req?.user?.teamId; + if (!teamId) { + throw this.errorService.createBadRequestError("Team ID is required"); + } + + const notification = await this.db.notificationModule.getNotificationById(req.params.id); + + if (!notification.teamId.equals(teamId)) { + throw this.errorService.createAuthorizationError(); + } + + const editedNotification = await this.db.notificationModule.editNotification(req.params.id, req.body); + return res.success({ + msg: "Notification updated successfully", + data: editedNotification, + }); + }, + 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"); + } + + const monitor = await this.db.monitorModule.getMonitorById(monitorId); + + if (!monitor.teamId.equals(teamId)) { + throw this.errorService.createAuthorizationError(); + } + + const notifications = monitor.notifications; + if (notifications.length === 0) throw this.errorService.createBadRequestError("No notifications"); + const result = await this.notificationService.testAllNotifications(notifications); + if (!result) throw this.errorService.createServerError("Failed to send all notifications"); + return res.success({ + msg: "All notifications sent successfully", + }); + }, + SERVICE_NAME, + "testAllNotifications" + ); +} + +export default NotificationController; diff --git a/server/controllers/queueController.js b/server/src/controllers/queueController.js similarity index 50% rename from server/controllers/queueController.js rename to server/src/controllers/queueController.js index 968106a5a..d2c3fd06f 100755 --- a/server/controllers/queueController.js +++ b/server/src/controllers/queueController.js @@ -1,89 +1,87 @@ -import { handleError } from "./controllerUtils.js"; - +import BaseController from "./baseController.js"; const SERVICE_NAME = "JobQueueController"; -class JobQueueController { - constructor(jobQueue, stringService) { +class JobQueueController extends BaseController { + static SERVICE_NAME = SERVICE_NAME; + constructor(commonDependencies, { jobQueue }) { + super(commonDependencies); this.jobQueue = jobQueue; - this.stringService = stringService; } - getMetrics = async (req, res, next) => { - try { + get serviceName() { + return JobQueueController.SERVICE_NAME; + } + + 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/src/controllers/settingsController.js similarity index 67% rename from server/controllers/settingsController.js rename to server/src/controllers/settingsController.js index ef1d689d6..7da724d23 100755 --- a/server/controllers/settingsController.js +++ b/server/src/controllers/settingsController.js @@ -1,16 +1,21 @@ 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 { + static SERVICE_NAME = SERVICE_NAME; + constructor(commonDependencies, { settingsService, emailService }) { + super(commonDependencies); this.settingsService = settingsService; - this.stringService = stringService; this.emailService = emailService; } + get serviceName() { + return SettingsController.SERVICE_NAME; + } + buildAppSettings = (dbSettings) => { const sanitizedSettings = { ...dbSettings }; delete sanitizedSettings.version; @@ -32,45 +37,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 updatedSettings = await this.db.settingsModule.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 +106,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/src/controllers/statusPageController.js b/server/src/controllers/statusPageController.js new file mode 100755 index 000000000..1cbcb49a2 --- /dev/null +++ b/server/src/controllers/statusPageController.js @@ -0,0 +1,108 @@ +import { createStatusPageBodyValidation, getStatusPageParamValidation, getStatusPageQueryValidation, imageValidation } from "../validation/joi.js"; +import BaseController from "./baseController.js"; + +const SERVICE_NAME = "statusPageController"; + +class StatusPageController extends BaseController { + static SERVICE_NAME = SERVICE_NAME; + constructor(commonDependencies) { + super(commonDependencies); + } + + get serviceName() { + return StatusPageController.SERVICE_NAME; + } + + createStatusPage = this.asyncHandler( + async (req, res) => { + await createStatusPageBodyValidation.validateAsync(req.body); + await imageValidation.validateAsync(req.file); + + const { _id, teamId } = req.user; + const statusPage = await this.db.statusPageModule.createStatusPage({ + statusPageData: req.body, + image: req.file, + userId: _id, + teamId, + }); + return res.success({ + msg: this.stringService.statusPageCreate, + data: statusPage, + }); + }, + SERVICE_NAME, + "createStatusPage" + ); + + updateStatusPage = this.asyncHandler( + async (req, res) => { + await createStatusPageBodyValidation.validateAsync(req.body); + await imageValidation.validateAsync(req.file); + + const statusPage = await this.db.statusPageModule.updateStatusPage(req.body, req.file); + if (statusPage === null) { + throw this.errorService.createNotFoundError(this.stringService.statusPageNotFound); + } + return res.success({ + msg: this.stringService.statusPageUpdate, + data: statusPage, + }); + }, + SERVICE_NAME, + "updateStatusPage" + ); + + getStatusPage = this.asyncHandler( + async (req, res) => { + const statusPage = await this.db.statusPageModule.getStatusPage(); + return res.success({ + msg: this.stringService.statusPageByUrl, + data: statusPage, + }); + }, + SERVICE_NAME, + "getStatusPage" + ); + + getStatusPageByUrl = this.asyncHandler( + async (req, res) => { + await getStatusPageParamValidation.validateAsync(req.params); + await getStatusPageQueryValidation.validateAsync(req.query); + + const statusPage = await this.db.statusPageModule.getStatusPageByUrl(req.params.url); + return res.success({ + msg: this.stringService.statusPageByUrl, + data: statusPage, + }); + }, + SERVICE_NAME, + "getStatusPageByUrl" + ); + + getStatusPagesByTeamId = this.asyncHandler( + async (req, res) => { + const teamId = req.user.teamId; + const statusPages = await this.db.statusPageModule.getStatusPagesByTeamId(teamId); + + return res.success({ + msg: this.stringService.statusPageByTeamId, + data: statusPages, + }); + }, + SERVICE_NAME, + "getStatusPagesByTeamId" + ); + + deleteStatusPage = this.asyncHandler( + async (req, res) => { + await this.db.deleteStatusPage(req.params.url); + return res.success({ + msg: this.stringService.statusPageDelete, + }); + }, + SERVICE_NAME, + "deleteStatusPage" + ); +} + +export default StatusPageController; diff --git a/server/db/models/AppSettings.js b/server/src/db/models/AppSettings.js similarity index 89% rename from server/db/models/AppSettings.js rename to server/src/db/models/AppSettings.js index cd63118e9..2313d638c 100755 --- a/server/db/models/AppSettings.js +++ b/server/src/db/models/AppSettings.js @@ -65,6 +65,12 @@ const AppSettingsSchema = mongoose.Schema( type: Number, default: 1, }, + globalThresholds: { + cpu: { type: Number }, + memory: { type: Number }, + disk: { type: Number }, + temperature: { type: Number }, + }, }, { timestamps: true, diff --git a/server/db/models/Check.js b/server/src/db/models/Check.js similarity index 100% rename from server/db/models/Check.js rename to server/src/db/models/Check.js diff --git a/server/db/models/HardwareCheck.js b/server/src/db/models/HardwareCheck.js similarity index 77% rename from server/db/models/HardwareCheck.js rename to server/src/db/models/HardwareCheck.js index bda9f693c..5b7d56213 100755 --- a/server/db/models/HardwareCheck.js +++ b/server/src/db/models/HardwareCheck.js @@ -40,6 +40,20 @@ const captureSchema = mongoose.Schema({ mode: { type: String, default: "" }, }); +const networkInterfaceSchema = mongoose.Schema({ + name: { type: String }, + bytes_sent: { type: Number, default: 0 }, + bytes_recv: { type: Number, default: 0 }, + packets_sent: { type: Number, default: 0 }, + packets_recv: { type: Number, default: 0 }, + err_in: { type: Number, default: 0 }, + err_out: { type: Number, default: 0 }, + drop_in: { type: Number, default: 0 }, + drop_out: { type: Number, default: 0 }, + fifo_in: { type: Number, default: 0 }, + fifo_out: { type: Number, default: 0 }, +}); + const HardwareCheckSchema = mongoose.Schema( { ...BaseCheckSchema.obj, @@ -69,6 +83,12 @@ const HardwareCheckSchema = mongoose.Schema( type: captureSchema, default: () => ({}), }, + + net: { + type: [networkInterfaceSchema], + default: () => [], + required: false, + }, }, { timestamps: true } ); diff --git a/server/db/models/InviteToken.js b/server/src/db/models/InviteToken.js similarity index 100% rename from server/db/models/InviteToken.js rename to server/src/db/models/InviteToken.js diff --git a/server/db/models/MaintenanceWindow.js b/server/src/db/models/MaintenanceWindow.js similarity index 100% rename from server/db/models/MaintenanceWindow.js rename to server/src/db/models/MaintenanceWindow.js diff --git a/server/db/models/Monitor.js b/server/src/db/models/Monitor.js similarity index 90% rename from server/db/models/Monitor.js rename to server/src/db/models/Monitor.js index c71ea5f18..1ee467999 100755 --- a/server/db/models/Monitor.js +++ b/server/src/db/models/Monitor.js @@ -33,7 +33,7 @@ const MonitorSchema = mongoose.Schema( type: { type: String, required: true, - enum: ["http", "ping", "pagespeed", "hardware", "docker", "port"], + enum: ["http", "ping", "pagespeed", "hardware", "docker", "port", "game"], }, ignoreTlsErrors: { type: Boolean, @@ -115,6 +115,9 @@ const MonitorSchema = mongoose.Schema( return this.alertThreshold; }, }, + gameId: { + type: String, + }, }, { timestamps: true, @@ -126,18 +129,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 +163,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/models/MonitorStats.js b/server/src/db/models/MonitorStats.js similarity index 100% rename from server/db/models/MonitorStats.js rename to server/src/db/models/MonitorStats.js diff --git a/server/db/models/NetworkCheck.js b/server/src/db/models/NetworkCheck.js similarity index 100% rename from server/db/models/NetworkCheck.js rename to server/src/db/models/NetworkCheck.js diff --git a/server/db/models/Notification.js b/server/src/db/models/Notification.js similarity index 100% rename from server/db/models/Notification.js rename to server/src/db/models/Notification.js diff --git a/server/db/models/PageSpeedCheck.js b/server/src/db/models/PageSpeedCheck.js similarity index 96% rename from server/db/models/PageSpeedCheck.js rename to server/src/db/models/PageSpeedCheck.js index 513dd3039..67c7c375e 100755 --- a/server/db/models/PageSpeedCheck.js +++ b/server/src/db/models/PageSpeedCheck.js @@ -1,7 +1,5 @@ import mongoose from "mongoose"; import { BaseCheckSchema } from "./Check.js"; -import logger from "../../utils/logger.js"; -import { time } from "console"; const AuditSchema = mongoose.Schema({ id: { type: String, required: true }, title: { type: String, required: true }, diff --git a/server/db/models/RecoveryToken.js b/server/src/db/models/RecoveryToken.js similarity index 100% rename from server/db/models/RecoveryToken.js rename to server/src/db/models/RecoveryToken.js diff --git a/server/db/models/StatusPage.js b/server/src/db/models/StatusPage.js similarity index 96% rename from server/db/models/StatusPage.js rename to server/src/db/models/StatusPage.js index 92cbab1cd..621749fb8 100755 --- a/server/db/models/StatusPage.js +++ b/server/src/db/models/StatusPage.js @@ -74,6 +74,10 @@ const StatusPageSchema = mongoose.Schema( type: Boolean, default: false, }, + customCSS: { + type: String, + default: "", + }, }, { timestamps: true } ); diff --git a/server/db/models/Team.js b/server/src/db/models/Team.js similarity index 100% rename from server/db/models/Team.js rename to server/src/db/models/Team.js diff --git a/server/db/models/User.js b/server/src/db/models/User.js similarity index 94% rename from server/db/models/User.js rename to server/src/db/models/User.js index f180cfb86..9368d98b1 100755 --- a/server/db/models/User.js +++ b/server/src/db/models/User.js @@ -103,12 +103,4 @@ UserSchema.methods.comparePassword = async function (submittedPassword) { const User = mongoose.model("User", UserSchema); -User.init().then(() => { - logger.info({ - message: "User model initialized", - service: "UserModel", - method: "init", - }); -}); - export default User; diff --git a/server/src/db/mongo/MongoDB.js b/server/src/db/mongo/MongoDB.js new file mode 100755 index 000000000..0c6274320 --- /dev/null +++ b/server/src/db/mongo/MongoDB.js @@ -0,0 +1,96 @@ +import mongoose from "mongoose"; +import AppSettings from "../models/AppSettings.js"; + +class MongoDB { + static SERVICE_NAME = "MongoDB"; + + constructor({ + logger, + envSettings, + checkModule, + inviteModule, + statusPageModule, + userModule, + hardwareCheckModule, + maintenanceWindowModule, + monitorModule, + networkCheckModule, + notificationModule, + pageSpeedCheckModule, + recoveryModule, + settingsModule, + }) { + this.logger = logger; + this.envSettings = envSettings; + this.userModule = userModule; + this.inviteModule = inviteModule; + this.recoveryModule = recoveryModule; + this.pageSpeedCheckModule = pageSpeedCheckModule; + this.hardwareCheckModule = hardwareCheckModule; + this.checkModule = checkModule; + this.maintenanceWindowModule = maintenanceWindowModule; + this.monitorModule = monitorModule; + this.notificationModule = notificationModule; + this.settingsModule = settingsModule; + this.statusPageModule = statusPageModule; + this.networkCheckModule = networkCheckModule; + } + + get serviceName() { + return MongoDB.SERVICE_NAME; + } + + connect = async () => { + try { + const connectionString = this.envSettings.dbConnectionString || "mongodb://localhost:27017/uptime_db"; + await mongoose.connect(connectionString); + // If there are no AppSettings, create one // TODO why is this here? + await AppSettings.findOneAndUpdate( + {}, // empty filter to match any document + {}, // empty update + { + new: true, + setDefaultsOnInsert: true, + } + ); + // Sync indexes + const models = mongoose.modelNames(); + for (const modelName of models) { + const model = mongoose.model(modelName); + await model.syncIndexes(); + } + + this.logger.info({ + message: "Connected to MongoDB", + service: this.SERVICE_NAME, + method: "connect", + }); + } catch (error) { + this.logger.error({ + message: error.message, + service: this.SERVICE_NAME, + method: "connect", + stack: error.stack, + }); + throw error; + } + }; + + disconnect = async () => { + try { + this.logger.info({ message: "Disconnecting from MongoDB" }); + await mongoose.disconnect(); + this.logger.info({ message: "Disconnected from MongoDB" }); + return; + } catch (error) { + this.logger.error({ + message: error.message, + service: this.SERVICE_NAME, + method: "disconnect", + stack: error.stack, + }); + } + }; +} + +export default MongoDB; diff --git a/server/src/db/mongo/modules/checkModule.js b/server/src/db/mongo/modules/checkModule.js new file mode 100755 index 000000000..a606a593b --- /dev/null +++ b/server/src/db/mongo/modules/checkModule.js @@ -0,0 +1,307 @@ +import { ObjectId } from "mongodb"; +import { buildChecksSummaryByTeamIdPipeline } from "./checkModuleQueries.js"; + +const SERVICE_NAME = "checkModule"; +const dateRangeLookup = { + recent: new Date(new Date().setDate(new Date().getDate() - 2)), + hour: new Date(new Date().setHours(new Date().getHours() - 1)), + day: new Date(new Date().setDate(new Date().getDate() - 1)), + week: new Date(new Date().setDate(new Date().getDate() - 7)), + month: new Date(new Date().setMonth(new Date().getMonth() - 1)), + all: undefined, +}; + +class CheckModule { + constructor({ logger, Check, HardwareCheck, PageSpeedCheck, Monitor, User }) { + this.logger = logger; + this.Check = Check; + this.HardwareCheck = HardwareCheck; + this.PageSpeedCheck = PageSpeedCheck; + this.Monitor = Monitor; + this.User = User; + } + + createChecks = async (checks) => { + try { + await this.Check.insertMany(checks, { ordered: false }); + } catch (error) { + error.service = SERVICE_NAME; + error.method = "createCheck"; + throw error; + } + }; + + 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 } }] }; + + // Match + const matchStage = { + monitorId: new ObjectId(monitorId), + ...(typeof status !== "undefined" && { status }), + ...(typeof ack !== "undefined" && ackStage), + ...(dateRangeLookup[dateRange] && { + createdAt: { + $gte: dateRangeLookup[dateRange], + }, + }), + }; + + if (filter !== undefined) { + switch (filter) { + case "all": + break; + case "down": + break; + case "resolve": + matchStage.statusCode = 5000; + break; + default: + this.logger.warn({ + message: "invalid filter", + service: SERVICE_NAME, + method: "getChecks", + }); + break; + } + } + + //Sort + sortOrder = sortOrder === "asc" ? 1 : -1; + + // Pagination + let skip = 0; + if (page && rowsPerPage) { + skip = page * rowsPerPage; + } + + const checkModels = { + http: this.Check, + ping: this.Check, + docker: this.Check, + port: this.Check, + pagespeed: this.PageSpeedCheck, + hardware: this.HardwareCheck, + game: this.Check, + }; + + const Model = checkModels[type]; + + const checks = await Model.aggregate([ + { $match: matchStage }, + { $sort: { createdAt: sortOrder } }, + { + $facet: { + summary: [{ $count: "checksCount" }], + checks: [{ $skip: skip }, { $limit: rowsPerPage }], + }, + }, + { + $project: { + checksCount: { + $ifNull: [{ $arrayElemAt: ["$summary.checksCount", 0] }, 0], + }, + checks: { + $ifNull: ["$checks", []], + }, + }, + }, + ]); + return checks[0]; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getChecks"; + throw error; + } + }; + + 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 matchStage = { + teamId: new ObjectId(teamId), + status: false, + ...(typeof ack !== "undefined" && ackStage), + ...(dateRangeLookup[dateRange] && { + createdAt: { + $gte: dateRangeLookup[dateRange], + }, + }), + }; + // Add filter to match stage + if (filter !== undefined) { + switch (filter) { + case "all": + break; + case "down": + break; + case "resolve": + matchStage.statusCode = 5000; + break; + default: + this.logger.warn({ + message: "invalid filter", + service: SERVICE_NAME, + method: "getChecksByTeam", + }); + break; + } + } + + sortOrder = sortOrder === "asc" ? 1 : -1; + + // pagination + let skip = 0; + if (page && rowsPerPage) { + skip = page * rowsPerPage; + } + + const aggregatePipeline = [ + { $match: matchStage }, + { + $unionWith: { + coll: "hardwarechecks", + pipeline: [{ $match: matchStage }], + }, + }, + { + $unionWith: { + coll: "pagespeedchecks", + pipeline: [{ $match: matchStage }], + }, + }, + + { $sort: { createdAt: sortOrder } }, + { + $facet: { + summary: [{ $count: "checksCount" }], + checks: [{ $skip: skip }, { $limit: rowsPerPage }], + }, + }, + { + $project: { + checksCount: { $arrayElemAt: ["$summary.checksCount", 0] }, + checks: "$checks", + }, + }, + ]; + + const checks = await this.Check.aggregate(aggregatePipeline); + return checks[0]; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getChecksByTeam"; + throw error; + } + }; + + ackCheck = async (checkId, teamId, ack) => { + try { + const updatedCheck = await this.Check.findOneAndUpdate({ _id: checkId, teamId: teamId }, { $set: { ack, ackAt: new Date() } }, { new: true }); + + if (!updatedCheck) { + throw new Error("Check not found"); + } + + return updatedCheck; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "ackCheck"; + throw error; + } + }; + + ackAllChecks = async (monitorId, teamId, ack, path) => { + try { + const updatedChecks = await this.Check.updateMany(path === "monitor" ? { monitorId } : { teamId }, { $set: { ack, ackAt: new Date() } }); + return updatedChecks.modifiedCount; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "ackAllChecks"; + throw error; + } + }; + + getChecksSummaryByTeamId = async ({ teamId }) => { + try { + const matchStage = { + teamId: new ObjectId(teamId), + }; + const checks = await this.Check.aggregate(buildChecksSummaryByTeamIdPipeline({ matchStage })); + return checks[0].summary; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getChecksSummaryByTeamId"; + throw error; + } + }; + deleteChecks = async (monitorId) => { + try { + const result = await this.Check.deleteMany({ monitorId }); + return result.deletedCount; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "deleteChecks"; + throw error; + } + }; + + deleteChecksByTeamId = async (teamId) => { + try { + // Find all monitor IDs for this team (only get _id field for efficiency) + const teamMonitors = await this.Monitor.find({ teamId }, { _id: 1 }); + const monitorIds = teamMonitors.map((monitor) => monitor._id); + + // Delete all checks for these monitors in one operation + const deleteResult = await this.Check.deleteMany({ monitorId: { $in: monitorIds } }); + + return deleteResult.deletedCount; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "deleteChecksByTeamId"; + throw error; + } + }; + + updateChecksTTL = async (teamId, ttl) => { + try { + await this.Check.collection.dropIndex("expiry_1"); + } catch (error) { + this.logger.error({ + message: error.message, + service: SERVICE_NAME, + method: "updateChecksTTL", + stack: error.stack, + }); + } + + try { + await this.Check.collection.createIndex( + { expiry: 1 }, + { expireAfterSeconds: ttl } // TTL in seconds, adjust as necessary + ); + } catch (error) { + error.service = SERVICE_NAME; + error.method = "updateChecksTTL"; + throw error; + } + // Update user + try { + await this.User.updateMany({ teamId: teamId }, { checkTTL: ttl }); + } catch (error) { + error.service = SERVICE_NAME; + error.method = "updateChecksTTL"; + throw error; + } + }; +} + +export default CheckModule; diff --git a/server/db/mongo/modules/checkModuleQueries.js b/server/src/db/mongo/modules/checkModuleQueries.js similarity index 73% rename from server/db/mongo/modules/checkModuleQueries.js rename to server/src/db/mongo/modules/checkModuleQueries.js index ffea43ba5..f2ad2a922 100644 --- a/server/db/mongo/modules/checkModuleQueries.js +++ b/server/src/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/src/db/mongo/modules/hardwareCheckModule.js b/server/src/db/mongo/modules/hardwareCheckModule.js new file mode 100755 index 000000000..448e72fed --- /dev/null +++ b/server/src/db/mongo/modules/hardwareCheckModule.js @@ -0,0 +1,22 @@ +const SERVICE_NAME = "hardwareCheckModule"; + +class HardwareCheckModule { + constructor({ HardwareCheck, Monitor, logger }) { + this.HardwareCheck = HardwareCheck; + this.Monitor = Monitor; + this.logger = logger; + } + + createHardwareChecks = async (hardwareChecks) => { + try { + await this.HardwareCheck.insertMany(hardwareChecks, { ordered: false }); + return true; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "createHardwareChecks"; + throw error; + } + }; +} + +export default HardwareCheckModule; diff --git a/server/src/db/mongo/modules/inviteModule.js b/server/src/db/mongo/modules/inviteModule.js new file mode 100755 index 000000000..444e51efd --- /dev/null +++ b/server/src/db/mongo/modules/inviteModule.js @@ -0,0 +1,56 @@ +const SERVICE_NAME = "inviteModule"; + +class InviteModule { + constructor({ InviteToken, crypto, stringService }) { + this.InviteToken = InviteToken; + this.crypto = crypto; + this.stringService = stringService; + } + + requestInviteToken = async (userData) => { + try { + await this.InviteToken.deleteMany({ email: userData.email }); + userData.token = this.crypto.randomBytes(32).toString("hex"); + let inviteToken = new this.InviteToken(userData); + await inviteToken.save(); + return inviteToken; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "requestInviteToken"; + throw error; + } + }; + + getInviteToken = async (token) => { + try { + const invite = await this.InviteToken.findOne({ + token, + }); + if (invite === null) { + throw new Error(this.stringService.authInviteNotFound); + } + return invite; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getInviteToken"; + throw error; + } + }; + getInviteTokenAndDelete = async (token) => { + try { + const invite = await this.InviteToken.findOneAndDelete({ + token, + }); + if (invite === null) { + throw new Error(this.stringService.authInviteNotFound); + } + return invite; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getInviteTokenAndDelete"; + throw error; + } + }; +} + +export default InviteModule; diff --git a/server/src/db/mongo/modules/maintenanceWindowModule.js b/server/src/db/mongo/modules/maintenanceWindowModule.js new file mode 100755 index 000000000..61523c9f0 --- /dev/null +++ b/server/src/db/mongo/modules/maintenanceWindowModule.js @@ -0,0 +1,106 @@ +const SERVICE_NAME = "maintenanceWindowModule"; + +class MaintenanceWindowModule { + constructor({ MaintenanceWindow }) { + this.MaintenanceWindow = MaintenanceWindow; + } + createMaintenanceWindow = async (maintenanceWindowData) => { + try { + const maintenanceWindow = new this.MaintenanceWindow({ + ...maintenanceWindowData, + }); + + // If the maintenance window is a one time window, set the expiry to the end date + if (maintenanceWindowData.oneTime) { + maintenanceWindow.expiry = maintenanceWindowData.end; + } + const result = await maintenanceWindow.save(); + return result; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "createMaintenanceWindow"; + throw error; + } + }; + getMaintenanceWindowById = async ({ id, teamId }) => { + try { + const maintenanceWindow = await this.MaintenanceWindow.findOne({ + _id: id, + teamId: teamId, + }); + return maintenanceWindow; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getMaintenanceWindowById"; + throw error; + } + }; + + getMaintenanceWindowsByTeamId = async (teamId, query) => { + try { + let { active, page, rowsPerPage, field, order } = query || {}; + const maintenanceQuery = { teamId }; + + if (active !== undefined) maintenanceQuery.active = active; + + const maintenanceWindowCount = await this.MaintenanceWindow.countDocuments(maintenanceQuery); + + // Pagination + let skip = 0; + if (page && rowsPerPage) { + skip = page * rowsPerPage; + } + + // Sorting + let sort = {}; + if (field !== undefined && order !== undefined) { + sort[field] = order === "asc" ? 1 : -1; + } + + const maintenanceWindows = await this.MaintenanceWindow.find(maintenanceQuery).skip(skip).limit(rowsPerPage).sort(sort); + + return { maintenanceWindows, maintenanceWindowCount }; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getMaintenanceWindowByUserId"; + throw error; + } + }; + getMaintenanceWindowsByMonitorId = async ({ monitorId, teamId }) => { + try { + const maintenanceWindows = await this.MaintenanceWindow.find({ + monitorId: monitorId, + teamId: teamId, + }); + return maintenanceWindows; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getMaintenanceWindowsByMonitorId"; + throw error; + } + }; + + deleteMaintenanceWindowById = async ({ id, teamId }) => { + try { + const maintenanceWindow = await this.MaintenanceWindow.findOneAndDelete({ _id: id, teamId }); + return maintenanceWindow; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "deleteMaintenanceWindowById"; + throw error; + } + }; + + editMaintenanceWindowById = async ({ id, body }) => { + try { + const editedMaintenanceWindow = await this.MaintenanceWindow.findByIdAndUpdate(id, body, { new: true }); + return editedMaintenanceWindow; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "editMaintenanceWindowById"; + throw error; + } + }; +} + +export default MaintenanceWindowModule; diff --git a/server/src/db/mongo/modules/monitorModule.js b/server/src/db/mongo/modules/monitorModule.js new file mode 100755 index 000000000..0e301cdcf --- /dev/null +++ b/server/src/db/mongo/modules/monitorModule.js @@ -0,0 +1,604 @@ +import { + buildUptimeDetailsPipeline, + buildHardwareDetailsPipeline, + buildMonitorSummaryByTeamIdPipeline, + buildMonitorsByTeamIdPipeline, + buildMonitorsAndSummaryByTeamIdPipeline, + buildMonitorsWithChecksByTeamIdPipeline, + buildFilteredMonitorsByTeamIdPipeline, +} from "./monitorModuleQueries.js"; + +const SERVICE_NAME = "monitorModule"; + +class MonitorModule { + constructor({ + Monitor, + MonitorStats, + Check, + PageSpeedCheck, + HardwareCheck, + stringService, + fs, + path, + fileURLToPath, + ObjectId, + NormalizeData, + NormalizeDataUptimeDetails, + }) { + this.Monitor = Monitor; + this.MonitorStats = MonitorStats; + this.Check = Check; + this.PageSpeedCheck = PageSpeedCheck; + this.HardwareCheck = HardwareCheck; + this.stringService = stringService; + this.fs = fs; + this.path = path; + this.fileURLToPath = fileURLToPath; + this.ObjectId = ObjectId; + this.NormalizeData = NormalizeData; + this.NormalizeDataUptimeDetails = NormalizeDataUptimeDetails; + + this.CHECK_MODEL_LOOKUP = { + http: Check, + ping: Check, + docker: Check, + port: Check, + pagespeed: PageSpeedCheck, + hardware: HardwareCheck, + }; + + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + + this.demoMonitorsPath = path.resolve(__dirname, "../../../utils/demoMonitors.json"); + } + + // Helper + calculateUptimeDuration = (checks) => { + if (!checks || checks.length === 0) { + return 0; + } + const latestCheck = new Date(checks[0].createdAt); + let latestDownCheck = 0; + + for (let i = checks.length - 1; i >= 0; i--) { + if (checks[i].status === false) { + latestDownCheck = new Date(checks[i].createdAt); + break; + } + } + + // If no down check is found, uptime is from the last check to now + if (latestDownCheck === 0) { + return Date.now() - new Date(checks[checks.length - 1].createdAt); + } + + // Otherwise the uptime is from the last check to the last down check + return latestCheck - latestDownCheck; + }; + + // Helper + getLastChecked = (checks) => { + if (!checks || checks.length === 0) { + return 0; // Handle case when no checks are available + } + // Data is sorted newest->oldest, so last check is the most recent + return new Date() - new Date(checks[0].createdAt); + }; + getLatestResponseTime = (checks) => { + if (!checks || checks.length === 0) { + return 0; + } + + return checks[0]?.responseTime ?? 0; + }; + + // Helper + getAverageResponseTime = (checks) => { + if (!checks || checks.length === 0) { + return 0; + } + + const validChecks = checks.filter((check) => typeof check.responseTime === "number"); + if (validChecks.length === 0) { + return 0; + } + const aggResponseTime = validChecks.reduce((sum, check) => { + return sum + check.responseTime; + }, 0); + return aggResponseTime / validChecks.length; + }; + + // Helper + getUptimePercentage = (checks) => { + if (!checks || checks.length === 0) { + return 0; + } + const upCount = checks.reduce((count, check) => { + return check.status === true ? count + 1 : count; + }, 0); + return (upCount / checks.length) * 100; + }; + + // Helper + getIncidents = (checks) => { + if (!checks || checks.length === 0) { + return 0; // Handle case when no checks are available + } + return checks.reduce((acc, check) => { + return check.status === false ? (acc += 1) : acc; + }, 0); + }; + + // Helper + getDateRange = (dateRange) => { + const startDates = { + recent: new Date(new Date().setHours(new Date().getHours() - 2)), + day: new Date(new Date().setDate(new Date().getDate() - 1)), + week: new Date(new Date().setDate(new Date().getDate() - 7)), + month: new Date(new Date().setMonth(new Date().getMonth() - 1)), + all: new Date(0), + }; + return { + start: startDates[dateRange], + end: new Date(), + }; + }; + + //Helper + getMonitorChecks = async (monitorId, model, dateRange, sortOrder) => { + const indexSpec = { + monitorId: 1, + createdAt: sortOrder, // This will be 1 or -1 + }; + + const [checksAll, checksForDateRange] = await Promise.all([ + model.find({ monitorId }).sort({ createdAt: sortOrder }).hint(indexSpec).lean(), + model + .find({ + monitorId, + createdAt: { $gte: dateRange.start, $lte: dateRange.end }, + }) + .hint(indexSpec) + .lean(), + ]); + + return { checksAll, checksForDateRange }; + }; + + // Helper + processChecksForDisplay = (normalizeData, checks, numToDisplay, normalize) => { + let processedChecks = checks; + if (numToDisplay && checks.length > numToDisplay) { + const n = Math.ceil(checks.length / numToDisplay); + processedChecks = checks.filter((_, index) => index % n === 0); + } + return normalize ? normalizeData(processedChecks, 1, 100) : processedChecks; + }; + + // Helper + groupChecksByTime = (checks, dateRange) => { + return checks.reduce((acc, check) => { + // Validate the date + const checkDate = new Date(check.createdAt); + if (Number.isNaN(checkDate.getTime()) || checkDate.getTime() === 0) { + return acc; + } + + const time = dateRange === "day" ? checkDate.setMinutes(0, 0, 0) : checkDate.toISOString().split("T")[0]; + + if (!acc[time]) { + acc[time] = { time, checks: [] }; + } + acc[time].checks.push(check); + return acc; + }, {}); + }; + + // Helper + calculateGroupStats = (group) => { + const totalChecks = group.checks.length; + + const checksWithResponseTime = group.checks.filter((check) => typeof check.responseTime === "number" && !Number.isNaN(check.responseTime)); + + return { + time: group.time, + uptimePercentage: this.getUptimePercentage(group.checks), + totalChecks, + totalIncidents: group.checks.filter((check) => !check.status).length, + avgResponseTime: + checksWithResponseTime.length > 0 + ? checksWithResponseTime.reduce((sum, check) => sum + check.responseTime, 0) / checksWithResponseTime.length + : 0, + }; + }; + + getAllMonitors = async () => { + try { + const monitors = await this.Monitor.find(); + return monitors; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getAllMonitors"; + throw error; + } + }; + + getMonitorById = async (monitorId) => { + try { + const monitor = await this.Monitor.findById(monitorId); + if (monitor === null || monitor === undefined) { + const error = new Error(this.stringService.getDbFindMonitorById(monitorId)); + error.status = 404; + throw error; + } + + return monitor; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getMonitorById"; + throw error; + } + }; + + getMonitorsByIds = async (monitorIds) => { + try { + const objectIds = monitorIds.map((id) => new this.ObjectId(id)); + return await this.Monitor.find({ _id: { $in: objectIds } }, { _id: 1, teamId: 1 }).lean(); + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getMonitorsByIds"; + throw error; + } + }; + getUptimeDetailsById = async ({ monitorId, dateRange }) => { + try { + const dates = this.getDateRange(dateRange); + const formatLookup = { + recent: "%Y-%m-%dT%H:%M:00Z", + day: "%Y-%m-%dT%H:00:00Z", + week: "%Y-%m-%dT%H:00:00Z", + month: "%Y-%m-%dT00:00:00Z", + }; + + const dateString = formatLookup[dateRange]; + + const results = await this.Check.aggregate(buildUptimeDetailsPipeline(monitorId, dates, dateString)); + + const monitorData = results[0]; + + monitorData.groupedUpChecks = this.NormalizeDataUptimeDetails(monitorData.groupedUpChecks, 10, 100); + + monitorData.groupedDownChecks = this.NormalizeDataUptimeDetails(monitorData.groupedDownChecks, 10, 100); + + const normalizedGroupChecks = this.NormalizeDataUptimeDetails(monitorData.groupedChecks, 10, 100); + + monitorData.groupedChecks = normalizedGroupChecks; + const monitorStats = await this.MonitorStats.findOne({ monitorId }); + return { monitorData, monitorStats }; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getUptimeDetailsById"; + throw error; + } + }; + + getMonitorStatsById = async ({ monitorId, sortOrder, dateRange, numToDisplay, normalize }) => { + try { + // Get monitor, if we can't find it, abort with error + const monitor = await this.Monitor.findById(monitorId); + if (monitor === null || monitor === undefined) { + throw new Error(this.stringService.getDbFindMonitorById(monitorId)); + } + + // Get query params + const sort = sortOrder === "asc" ? 1 : -1; + + // Get Checks for monitor in date range requested + const model = this.CHECK_MODEL_LOOKUP[monitor.type]; + const dates = this.getDateRange(dateRange); + const { checksAll, checksForDateRange } = await this.getMonitorChecks(monitorId, model, dates, sort); + + // Build monitor stats + const monitorStats = { + ...monitor.toObject(), + uptimeDuration: this.calculateUptimeDuration(checksAll), + lastChecked: this.getLastChecked(checksAll), + latestResponseTime: this.getLatestResponseTime(checksAll), + periodIncidents: this.getIncidents(checksForDateRange), + periodTotalChecks: checksForDateRange.length, + checks: this.processChecksForDisplay(this.NormalizeData, checksForDateRange, numToDisplay, normalize), + }; + + if (monitor.type === "http" || monitor.type === "ping" || monitor.type === "docker" || monitor.type === "port" || monitor.type === "game") { + // HTTP/PING Specific stats + monitorStats.periodAvgResponseTime = this.getAverageResponseTime(checksForDateRange); + monitorStats.periodUptime = this.getUptimePercentage(checksForDateRange); + const groupedChecks = this.groupChecksByTime(checksForDateRange, dateRange); + monitorStats.aggregateData = Object.values(groupedChecks).map(this.calculateGroupStats); + } + + return monitorStats; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getMonitorStatsById"; + throw error; + } + }; + + getHardwareDetailsById = async ({ monitorId, dateRange }) => { + try { + const monitor = await this.Monitor.findById(monitorId); + const dates = this.getDateRange(dateRange); + + const formatLookup = { + recent: "%Y-%m-%dT%H:%M:00Z", + day: "%Y-%m-%dT%H:00:00Z", + week: "%Y-%m-%dT%H:00:00Z", + month: "%Y-%m-%dT00:00:00Z", + }; + const dateString = formatLookup[dateRange]; + + const hardwareStats = await this.HardwareCheck.aggregate(buildHardwareDetailsPipeline(monitor, dates, dateString)); + + const stats = hardwareStats[0]; + + return { + ...monitor.toObject(), + stats, + }; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getHardwareDetailsById"; + throw error; + } + }; + + getMonitorsByTeamId = async ({ limit, type, page, rowsPerPage, filter, field, order, teamId }) => { + limit = parseInt(limit); + page = parseInt(page); + rowsPerPage = parseInt(rowsPerPage); + if (field === undefined) { + field = "name"; + order = "asc"; + } + // Build match stage + const matchStage = { teamId: new this.ObjectId(teamId) }; + if (type !== undefined) { + matchStage.type = Array.isArray(type) ? { $in: type } : type; + } + + const summaryResult = await this.Monitor.aggregate(buildMonitorSummaryByTeamIdPipeline({ matchStage })); + const summary = summaryResult[0]; + + const monitors = await this.Monitor.aggregate(buildMonitorsByTeamIdPipeline({ matchStage, field, order })); + + const filteredMonitors = await this.Monitor.aggregate( + buildFilteredMonitorsByTeamIdPipeline({ + matchStage, + filter, + page, + rowsPerPage, + field, + order, + limit, + type, + }) + ); + + const normalizedFilteredMonitors = filteredMonitors.map((monitor) => { + if (!monitor.checks) { + return monitor; + } + monitor.checks = this.NormalizeData(monitor.checks, 10, 100); + return monitor; + }); + + return { summary, monitors, filteredMonitors: normalizedFilteredMonitors }; + }; + + getMonitorsAndSummaryByTeamId = async ({ type, explain, teamId }) => { + try { + const matchStage = { teamId: new this.ObjectId(teamId) }; + if (type !== undefined) { + matchStage.type = Array.isArray(type) ? { $in: type } : type; + } + + if (explain === true) { + return this.Monitor.aggregate(buildMonitorsAndSummaryByTeamIdPipeline({ matchStage })).explain("executionStats"); + } + + const queryResult = await this.Monitor.aggregate(buildMonitorsAndSummaryByTeamIdPipeline({ matchStage })); + const { monitors, summary } = queryResult?.[0] ?? {}; + return { monitors, summary }; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getMonitorsAndSummaryByTeamId"; + throw error; + } + }; + + getMonitorsWithChecksByTeamId = async ({ limit, type, page, rowsPerPage, filter, field, order, teamId, explain }) => { + try { + limit = parseInt(limit); + page = parseInt(page); + rowsPerPage = parseInt(rowsPerPage); + if (field === undefined) { + field = "name"; + order = "asc"; + } + // Build match stage + const matchStage = { teamId: new this.ObjectId(teamId) }; + if (type !== undefined) { + matchStage.type = Array.isArray(type) ? { $in: type } : type; + } + + if (explain === true) { + return this.Monitor.aggregate( + buildMonitorsWithChecksByTeamIdPipeline({ + matchStage, + filter, + page, + rowsPerPage, + field, + order, + limit, + type, + }) + ).explain("executionStats"); + } + + const queryResult = await this.Monitor.aggregate( + buildMonitorsWithChecksByTeamIdPipeline({ + matchStage, + filter, + page, + rowsPerPage, + field, + order, + limit, + type, + }) + ); + const monitors = queryResult[0]?.monitors; + const count = queryResult[0]?.count; + const normalizedFilteredMonitors = monitors.map((monitor) => { + if (!monitor.checks) { + return monitor; + } + monitor.checks = this.NormalizeData(monitor.checks, 10, 100); + return monitor; + }); + return { count, monitors: normalizedFilteredMonitors }; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getMonitorsWithChecksByTeamId"; + throw error; + } + }; + createMonitor = async ({ body, teamId, userId }) => { + try { + const monitor = new this.Monitor({ ...body, teamId, userId }); + const saved = await monitor.save(); + return saved; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "createMonitor"; + throw error; + } + }; + + createBulkMonitors = async (req) => { + try { + const monitors = req.map((item) => new this.Monitor({ ...item, notifications: undefined })); + await this.Monitor.bulkSave(monitors); + return monitors; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "createBulkMonitors"; + throw error; + } + }; + + deleteMonitor = async ({ teamId, monitorId }) => { + try { + const deletedMonitor = await this.Monitor.findOneAndDelete({ _id: monitorId, teamId }); + + if (!deletedMonitor) { + throw new Error(this.stringService.getDbFindMonitorById(monitorId)); + } + + return deletedMonitor; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "deleteMonitor"; + throw error; + } + }; + + deleteAllMonitors = async (teamId) => { + try { + const monitors = await this.Monitor.find({ teamId }); + const { deletedCount } = await this.Monitor.deleteMany({ teamId }); + + return { monitors, deletedCount }; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "deleteAllMonitors"; + throw error; + } + }; + + deleteMonitorsByUserId = async (userId) => { + try { + const result = await this.Monitor.deleteMany({ userId: userId }); + return result; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "deleteMonitorsByUserId"; + throw error; + } + }; + + editMonitor = async ({ monitorId, body }) => { + try { + const editedMonitor = await this.Monitor.findByIdAndUpdate(monitorId, body, { + new: true, + }); + return editedMonitor; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "editMonitor"; + throw error; + } + }; + + addDemoMonitors = async (userId, teamId) => { + try { + const demoMonitors = JSON.parse(this.fs.readFileSync(this.demoMonitorsPath, "utf8")); + + const demoMonitorsToInsert = demoMonitors.map((monitor) => { + return { + userId, + teamId, + name: monitor.name, + description: monitor.name, + type: "http", + url: monitor.url, + interval: 60000, + }; + }); + const insertedMonitors = await this.Monitor.insertMany(demoMonitorsToInsert); + return insertedMonitors; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "addDemoMonitors"; + throw error; + } + }; + + pauseMonitor = async ({ monitorId }) => { + try { + const monitor = await this.Monitor.findOneAndUpdate( + { _id: monitorId }, + [ + { + $set: { + isActive: { $not: "$isActive" }, + status: "$$REMOVE", + }, + }, + ], + { new: true } + ); + + return monitor; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "pauseMonitor"; + throw error; + } + }; +} + +export default MonitorModule; diff --git a/server/db/mongo/modules/monitorModuleQueries.js b/server/src/db/mongo/modules/monitorModuleQueries.js similarity index 55% rename from server/db/mongo/modules/monitorModuleQueries.js rename to server/src/db/mongo/modules/monitorModuleQueries.js index ab45eebb6..7753066fc 100755 --- a/server/db/mongo/modules/monitorModuleQueries.js +++ b/server/src/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, @@ -223,6 +220,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { diskCount: { $size: "$disk", }, + netCount: { $size: "$net" }, }, }, { @@ -230,16 +228,13 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { from: "hardwarechecks", let: { diskCount: "$diskCount", + netCount: "$netCount", }, pipeline: [ { $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] }], }, }, }, @@ -265,6 +260,12 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { disks: { $push: "$disk", }, + net: { + $push: "$net", + }, + updatedAts: { + $push: "$updatedAt", + }, }, }, { @@ -326,10 +327,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 +338,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 +349,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { input: "$disks", as: "diskArray", in: { - $arrayElemAt: [ - "$$diskArray.total_bytes", - "$$diskIndex", - ], + $arrayElemAt: ["$$diskArray.total_bytes", "$$diskIndex"], }, }, }, @@ -379,10 +371,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { input: "$disks", as: "diskArray", in: { - $arrayElemAt: [ - "$$diskArray.usage_percent", - "$$diskIndex", - ], + $arrayElemAt: ["$$diskArray.usage_percent", "$$diskIndex"], }, }, }, @@ -390,6 +379,385 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { }, }, }, + net: { + $map: { + input: { $range: [0, { $size: { $arrayElemAt: ["$net", 0] } }] }, + as: "netIndex", + in: { + name: { + $arrayElemAt: [ + { + $map: { + input: { $arrayElemAt: ["$net", 0] }, + as: "iface", + in: "$$iface.name", + }, + }, + "$$netIndex", + ], + }, + bytesSentPerSecond: { + $let: { + vars: { + first: { + $arrayElemAt: [ + { $map: { input: { $arrayElemAt: ["$net", 0] }, as: "iface", in: "$$iface.bytes_sent" } }, + "$$netIndex", + ], + }, + last: { + $arrayElemAt: [ + { + $map: { + input: { $arrayElemAt: ["$net", { $subtract: [{ $size: "$net" }, 1] }] }, + as: "iface", + in: "$$iface.bytes_sent", + }, + }, + "$$netIndex", + ], + }, + tFirst: { $arrayElemAt: ["$updatedAts", 0] }, + tLast: { $arrayElemAt: ["$updatedAts", { $subtract: [{ $size: "$updatedAts" }, 1] }] }, + }, + in: { + $cond: [ + { $gt: [{ $subtract: ["$$tLast", "$$tFirst"] }, 0] }, + { + $divide: [{ $subtract: ["$$last", "$$first"] }, { $divide: [{ $subtract: ["$$tLast", "$$tFirst"] }, 1000] }], + }, + 0, + ], + }, + }, + }, + deltaBytesRecv: { + $let: { + vars: { + first: { + $arrayElemAt: [ + { $map: { input: { $arrayElemAt: ["$net", 0] }, as: "iface", in: "$$iface.bytes_recv" } }, + "$$netIndex", + ], + }, + last: { + $arrayElemAt: [ + { + $map: { + input: { $arrayElemAt: ["$net", { $subtract: [{ $size: "$net" }, 1] }] }, + as: "iface", + in: "$$iface.bytes_recv", + }, + }, + "$$netIndex", + ], + }, + tFirst: { $arrayElemAt: ["$updatedAts", 0] }, + tLast: { $arrayElemAt: ["$updatedAts", { $subtract: [{ $size: "$updatedAts" }, 1] }] }, + }, + in: { + $cond: [ + { $gt: [{ $subtract: ["$$tLast", "$$tFirst"] }, 0] }, + { + $divide: [{ $subtract: ["$$last", "$$first"] }, { $divide: [{ $subtract: ["$$tLast", "$$tFirst"] }, 1000] }], + }, + 0, + ], + }, + }, + }, + deltaPacketsSent: { + $let: { + vars: { + first: { + $arrayElemAt: [ + { $map: { input: { $arrayElemAt: ["$net", 0] }, as: "iface", in: "$$iface.packets_sent" } }, + "$$netIndex", + ], + }, + last: { + $arrayElemAt: [ + { + $map: { + input: { $arrayElemAt: ["$net", { $subtract: [{ $size: "$net" }, 1] }] }, + as: "iface", + in: "$$iface.packets_sent", + }, + }, + "$$netIndex", + ], + }, + tFirst: { $arrayElemAt: ["$updatedAts", 0] }, + tLast: { $arrayElemAt: ["$updatedAts", { $subtract: [{ $size: "$updatedAts" }, 1] }] }, + }, + in: { + $cond: [ + { $gt: [{ $subtract: ["$$tLast", "$$tFirst"] }, 0] }, + { + $divide: [{ $subtract: ["$$last", "$$first"] }, { $divide: [{ $subtract: ["$$tLast", "$$tFirst"] }, 1000] }], + }, + 0, + ], + }, + }, + }, + deltaPacketsRecv: { + $let: { + vars: { + first: { + $arrayElemAt: [ + { + $map: { + input: { $arrayElemAt: ["$net", 0] }, + as: "iface", + in: "$$iface.packets_recv", + }, + }, + "$$netIndex", + ], + }, + last: { + $arrayElemAt: [ + { + $map: { + input: { + $arrayElemAt: [ + "$net", + { + $subtract: [{ $size: "$net" }, 1], + }, + ], + }, + as: "iface", + in: "$$iface.packets_recv", + }, + }, + "$$netIndex", + ], + }, + tFirst: { $arrayElemAt: ["$updatedAts", 0] }, + tLast: { + $arrayElemAt: [ + "$updatedAts", + { + $subtract: [{ $size: "$updatedAts" }, 1], + }, + ], + }, + }, + in: { + $cond: [ + { $gt: [{ $subtract: ["$$tLast", "$$tFirst"] }, 0] }, + { + $divide: [{ $subtract: ["$$last", "$$first"] }, { $divide: [{ $subtract: ["$$tLast", "$$tFirst"] }, 1000] }], + }, + 0, + ], + }, + }, + }, + deltaErrIn: { + $let: { + vars: { + first: { + $arrayElemAt: [{ $map: { input: { $arrayElemAt: ["$net", 0] }, as: "iface", in: "$$iface.err_in" } }, "$$netIndex"], + }, + last: { + $arrayElemAt: [ + { + $map: { + input: { $arrayElemAt: ["$net", { $subtract: [{ $size: "$net" }, 1] }] }, + as: "iface", + in: "$$iface.err_in", + }, + }, + "$$netIndex", + ], + }, + tFirst: { $arrayElemAt: ["$updatedAts", 0] }, + tLast: { $arrayElemAt: ["$updatedAts", { $subtract: [{ $size: "$updatedAts" }, 1] }] }, + }, + in: { + $cond: [ + { $gt: [{ $subtract: ["$$tLast", "$$tFirst"] }, 0] }, + { + $divide: [{ $subtract: ["$$last", "$$first"] }, { $divide: [{ $subtract: ["$$tLast", "$$tFirst"] }, 1000] }], + }, + 0, + ], + }, + }, + }, + deltaErrOut: { + $let: { + vars: { + first: { + $arrayElemAt: [ + { + $map: { + input: { $arrayElemAt: ["$net", 0] }, + as: "iface", + in: "$$iface.err_out", + }, + }, + "$$netIndex", + ], + }, + last: { + $arrayElemAt: [ + { + $map: { + input: { + $arrayElemAt: [ + "$net", + { + $subtract: [{ $size: "$net" }, 1], + }, + ], + }, + as: "iface", + in: "$$iface.err_out", + }, + }, + "$$netIndex", + ], + }, + tFirst: { $arrayElemAt: ["$updatedAts", 0] }, + tLast: { + $arrayElemAt: [ + "$updatedAts", + { + $subtract: [{ $size: "$updatedAts" }, 1], + }, + ], + }, + }, + in: { + $cond: [ + { $gt: [{ $subtract: ["$$tLast", "$$tFirst"] }, 0] }, + { + $divide: [{ $subtract: ["$$last", "$$first"] }, { $divide: [{ $subtract: ["$$tLast", "$$tFirst"] }, 1000] }], + }, + 0, + ], + }, + }, + }, + deltaDropIn: { + $let: { + vars: { + first: { + $arrayElemAt: [ + { + $map: { + input: { $arrayElemAt: ["$net", 0] }, + as: "iface", + in: "$$iface.drop_in", + }, + }, + "$$netIndex", + ], + }, + last: { + $arrayElemAt: [ + { + $map: { + input: { + $arrayElemAt: [ + "$net", + { + $subtract: [{ $size: "$net" }, 1], + }, + ], + }, + as: "iface", + in: "$$iface.drop_in", + }, + }, + "$$netIndex", + ], + }, + tFirst: { $arrayElemAt: ["$updatedAts", 0] }, + tLast: { + $arrayElemAt: [ + "$updatedAts", + { + $subtract: [{ $size: "$updatedAts" }, 1], + }, + ], + }, + }, + in: { + $cond: [ + { $gt: [{ $subtract: ["$$tLast", "$$tFirst"] }, 0] }, + { + $divide: [{ $subtract: ["$$last", "$$first"] }, { $divide: [{ $subtract: ["$$tLast", "$$tFirst"] }, 1000] }], + }, + 0, + ], + }, + }, + }, + deltaDropOut: { + $let: { + vars: { + first: { + $arrayElemAt: [ + { + $map: { + input: { $arrayElemAt: ["$net", 0] }, + as: "iface", + in: "$$iface.drop_out", + }, + }, + "$$netIndex", + ], + }, + last: { + $arrayElemAt: [ + { + $map: { + input: { + $arrayElemAt: [ + "$net", + { + $subtract: [{ $size: "$net" }, 1], + }, + ], + }, + as: "iface", + in: "$$iface.drop_out", + }, + }, + "$$netIndex", + ], + }, + tFirst: { $arrayElemAt: ["$updatedAts", 0] }, + tLast: { + $arrayElemAt: [ + "$updatedAts", + { + $subtract: [{ $size: "$updatedAts" }, 1], + }, + ], + }, + }, + in: { + $cond: [ + { $gt: [{ $subtract: ["$$tLast", "$$tFirst"] }, 0] }, + { + $divide: [{ $subtract: ["$$last", "$$first"] }, { $divide: [{ $subtract: ["$$tLast", "$$tFirst"] }, 1000] }], + }, + 0, + ], + }, + }, + }, + }, + }, + }, + firstUpdatedAt: 1, + lastUpdatedAt: 1, }, }, ], @@ -556,26 +924,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 +1023,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 +1131,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" } }], }, }, ] @@ -870,7 +1206,7 @@ const buildGetMonitorsByTeamIdPipeline = (req) => { $switch: { branches: [ { - case: { $in: ["$type", ["http", "ping", "docker", "port"]] }, + case: { $in: ["$type", ["http", "ping", "docker", "port", "game"]] }, then: "$standardchecks", }, { diff --git a/server/src/db/mongo/modules/networkCheckModule.js b/server/src/db/mongo/modules/networkCheckModule.js new file mode 100644 index 000000000..917e68440 --- /dev/null +++ b/server/src/db/mongo/modules/networkCheckModule.js @@ -0,0 +1,30 @@ +const SERVICE_NAME = "networkCheckModule"; + +class NetworkCheckModule { + constructor({ NetworkCheck }) { + this.NetworkCheck = NetworkCheck; + } + createNetworkCheck = async (networkCheckData) => { + try { + const networkCheck = await new this.NetworkCheck(networkCheckData); + await networkCheck.save(); + return networkCheck; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "createNetworkCheck"; + throw error; + } + }; + getNetworkChecksByMonitorId = async (monitorId, limit = 100) => { + try { + const networkChecks = await this.NetworkCheck.find({ monitorId }).sort({ createdAt: -1 }).limit(limit); + return networkChecks; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getNetworkChecksByMonitorId"; + throw error; + } + }; +} + +export default NetworkCheckModule; diff --git a/server/src/db/mongo/modules/notificationModule.js b/server/src/db/mongo/modules/notificationModule.js new file mode 100755 index 000000000..3c6edb54a --- /dev/null +++ b/server/src/db/mongo/modules/notificationModule.js @@ -0,0 +1,100 @@ +// import Notification from "../../models/Notification.js"; +// import Monitor from "../../models/Monitor.js"; +const SERVICE_NAME = "notificationModule"; + +class NotificationModule { + constructor({ Notification, Monitor }) { + this.Notification = Notification; + this.Monitor = Monitor; + } + createNotification = async (notificationData) => { + try { + const notification = await new this.Notification({ ...notificationData }).save(); + return notification; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "createNotification"; + throw error; + } + }; + getNotificationsByTeamId = async (teamId) => { + try { + const notifications = await this.Notification.find({ teamId }); + return notifications; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getNotificationsByTeamId"; + throw error; + } + }; + getNotificationsByIds = async (notificationIds) => { + try { + const notifications = await this.Notification.find({ _id: { $in: notificationIds } }); + return notifications; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getNotificationsByIds"; + throw error; + } + }; + getNotificationsByMonitorId = async (monitorId) => { + try { + const notifications = await this.Notification.find({ monitorId }); + return notifications; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getNotificationsByMonitorId"; + throw error; + } + }; + deleteNotificationsByMonitorId = async (monitorId) => { + try { + const result = await this.Notification.deleteMany({ monitorId }); + return result.deletedCount; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "deleteNotificationsByMonitorId"; + throw error; + } + }; + deleteNotificationById = async (id) => { + try { + const notification = await this.Notification.findById(id); + if (!notification) { + throw new Error("Notification not found"); + } + + const result = await this.Notification.findByIdAndDelete(id); + await this.Monitor.updateMany({ notifications: id }, { $pull: { notifications: id } }); + return result; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "deleteNotificationById"; + throw error; + } + }; + getNotificationById = async (id) => { + try { + const notification = await this.Notification.findById(id); + return notification; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getNotificationById"; + throw error; + } + }; + editNotification = async (id, notificationData) => { + try { + const notification = await this.Notification.findByIdAndUpdate(id, notificationData, { + new: true, + }); + return notification; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "editNotification"; + throw error; + } + }; +} + +export default NotificationModule; diff --git a/server/src/db/mongo/modules/pageSpeedCheckModule.js b/server/src/db/mongo/modules/pageSpeedCheckModule.js new file mode 100755 index 000000000..b2a3eb71d --- /dev/null +++ b/server/src/db/mongo/modules/pageSpeedCheckModule.js @@ -0,0 +1,31 @@ +// import PageSpeedCheck from "../../models/PageSpeedCheck.js"; +const SERVICE_NAME = "pageSpeedCheckModule"; + +class PageSpeedCheckModule { + constructor({ PageSpeedCheck }) { + this.PageSpeedCheck = PageSpeedCheck; + } + + createPageSpeedChecks = async (pageSpeedChecks) => { + try { + await this.PageSpeedCheck.insertMany(pageSpeedChecks, { ordered: false }); + return true; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "createPageSpeedCheck"; + throw error; + } + }; + + deletePageSpeedChecksByMonitorId = async (monitorId) => { + try { + const result = await this.PageSpeedCheck.deleteMany({ monitorId }); + return result.deletedCount; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "deletePageSpeedChecksByMonitorId"; + throw error; + } + }; +} +export default PageSpeedCheckModule; diff --git a/server/src/db/mongo/modules/recoveryModule.js b/server/src/db/mongo/modules/recoveryModule.js new file mode 100755 index 000000000..cc22306a3 --- /dev/null +++ b/server/src/db/mongo/modules/recoveryModule.js @@ -0,0 +1,80 @@ +const SERVICE_NAME = "recoveryModule"; + +class RecoveryModule { + constructor({ User, RecoveryToken, crypto, stringService }) { + this.User = User; + this.RecoveryToken = RecoveryToken; + this.crypto = crypto; + this.stringService = stringService; + } + + requestRecoveryToken = async (email) => { + try { + // Delete any existing tokens + await this.RecoveryToken.deleteMany({ email }); + let recoveryToken = new this.RecoveryToken({ + email, + token: this.crypto.randomBytes(32).toString("hex"), + }); + await recoveryToken.save(); + return recoveryToken; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "requestRecoveryToken"; + throw error; + } + }; + validateRecoveryToken = async (candidateToken) => { + try { + const recoveryToken = await this.RecoveryToken.findOne({ + token: candidateToken, + }); + if (recoveryToken !== null) { + return recoveryToken; + } else { + throw new Error(this.stringService.dbTokenNotFound); + } + } catch (error) { + error.service = SERVICE_NAME; + error.method = "validateRecoveryToken"; + throw error; + } + }; + + resetPassword = async (password, candidateToken) => { + try { + const newPassword = password; + + // Validate token again + const recoveryToken = await this.validateRecoveryToken(candidateToken); + const user = await this.User.findOne({ email: recoveryToken.email }); + + if (user === null) { + throw new Error(this.stringService.dbUserNotFound); + } + + const match = await user.comparePassword(newPassword); + + if (match === true) { + throw new Error("Password cannot be the same as the old password"); + } + + user.password = newPassword; + await user.save(); + await this.RecoveryToken.deleteMany({ email: recoveryToken.email }); + // Fetch the user again without the password + const userWithoutPassword = await this.User.findOne({ + email: recoveryToken.email, + }) + .select("-password") + .select("-profileImage"); + return userWithoutPassword; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "resetPassword"; + throw error; + } + }; +} + +export default RecoveryModule; diff --git a/server/src/db/mongo/modules/settingsModule.js b/server/src/db/mongo/modules/settingsModule.js new file mode 100755 index 000000000..37b851bf2 --- /dev/null +++ b/server/src/db/mongo/modules/settingsModule.js @@ -0,0 +1,36 @@ +// import AppSettings from "../../models/AppSettings.js"; +const SERVICE_NAME = "SettingsModule"; + +class SettingsModule { + constructor({ AppSettings }) { + this.AppSettings = AppSettings; + } + + updateAppSettings = async (newSettings) => { + try { + const update = { $set: { ...newSettings } }; + + if (newSettings.pagespeedApiKey === "") { + update.$unset = { pagespeedApiKey: "" }; + delete update.$set.pagespeedApiKey; + } + + if (newSettings.systemEmailPassword === "") { + update.$unset = { systemEmailPassword: "" }; + delete update.$set.systemEmailPassword; + } + + await this.AppSettings.findOneAndUpdate({}, update, { + upsert: true, + }); + const settings = await this.AppSettings.findOne().select("-__v -_id -createdAt -updatedAt -singleton").lean(); + return settings; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "updateAppSettings"; + throw error; + } + }; +} + +export default SettingsModule; diff --git a/server/src/db/mongo/modules/statusPageModule.js b/server/src/db/mongo/modules/statusPageModule.js new file mode 100755 index 000000000..10710d0f7 --- /dev/null +++ b/server/src/db/mongo/modules/statusPageModule.js @@ -0,0 +1,287 @@ +// import StatusPage from "../../models/StatusPage.js"; +// import { NormalizeData } from "../../../utils/dataUtils.js"; +// import ServiceRegistry from "../../../service/system/serviceRegistry.js"; +// import StringService from "../../../service/system/stringService.js"; + +const SERVICE_NAME = "statusPageModule"; + +class StatusPageModule { + constructor({ StatusPage, NormalizeData, stringService }) { + this.StatusPage = StatusPage; + this.NormalizeData = NormalizeData; + this.stringService = stringService; + } + + createStatusPage = async ({ statusPageData, image, userId, teamId }) => { + try { + const statusPage = new this.StatusPage({ + ...statusPageData, + userId, + teamId, + }); + if (image) { + statusPage.logo = { + data: image.buffer, + contentType: image.mimetype, + }; + } + await statusPage.save(); + return statusPage; + } catch (error) { + if (error?.code === 11000) { + // Handle duplicate URL errors + error.status = 400; + error.message = this.stringService.statusPageUrlNotUnique; + } + error.service = SERVICE_NAME; + error.method = "createStatusPage"; + throw error; + } + }; + + updateStatusPage = async (statusPageData, image) => { + try { + if (image) { + statusPageData.logo = { + data: image.buffer, + contentType: image.mimetype, + }; + } else { + statusPageData.logo = null; + } + + if (statusPageData.deleteSubmonitors === "true") { + statusPageData.subMonitors = []; + } + const statusPage = await this.StatusPage.findOneAndUpdate({ url: statusPageData.url }, statusPageData, { + new: true, + }); + + return statusPage; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "updateStatusPage"; + throw error; + } + }; + + getStatusPageByUrl = async (url) => { + // TODO This is deprecated, can remove and have controller call getStatusPage + try { + return this.getStatusPage(url); + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getStatusPageByUrl"; + throw error; + } + }; + + getStatusPagesByTeamId = async (teamId) => { + try { + const statusPages = await this.StatusPage.find({ teamId }); + return statusPages; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getStatusPagesByTeamId"; + throw error; + } + }; + + getStatusPage = async (url) => { + try { + const preliminaryStatusPage = await this.StatusPage.findOne({ url }); + if (!preliminaryStatusPage) { + const error = new Error(this.stringService.statusPageNotFound); + error.status = 404; + throw error; + } + + if (!preliminaryStatusPage.monitors || preliminaryStatusPage.monitors.length === 0) { + const { _id, color, companyName, isPublished, logo, originalMonitors, showCharts, showUptimePercentage, timezone, showAdminLoginLink, url } = + preliminaryStatusPage; + return { + statusPage: { + _id, + color, + companyName, + isPublished, + logo, + originalMonitors, + showCharts, + showUptimePercentage, + timezone, + showAdminLoginLink, + url, + }, + monitors: [], + }; + } + + const statusPageQuery = await this.StatusPage.aggregate([ + { $match: { url: url } }, + { + $set: { + originalMonitors: "$monitors", + }, + }, + { + $lookup: { + from: "monitors", + localField: "monitors", + foreignField: "_id", + as: "monitors", + }, + }, + { + $unwind: { + path: "$monitors", + preserveNullAndEmptyArrays: true, + }, + }, + { + $lookup: { + from: "maintenancewindows", + let: { monitorId: "$monitors._id" }, + pipeline: [{ $match: { $expr: { $eq: ["$monitorId", "$$monitorId"] } } }], + as: "monitors.maintenanceWindows", + }, + }, + { + $lookup: { + from: "checks", + let: { monitorId: "$monitors._id" }, + pipeline: [ + { + $match: { + $expr: { $eq: ["$monitorId", "$$monitorId"] }, + }, + }, + { $sort: { createdAt: -1 } }, + { $limit: 25 }, + ], + as: "monitors.checks", + }, + }, + { + $addFields: { + "monitors.orderIndex": { + $indexOfArray: ["$originalMonitors", "$monitors._id"], + }, + "monitors.isMaintenance": { + $reduce: { + input: "$monitors.maintenanceWindows", + initialValue: false, + in: { + $or: [ + "$$value", + { + $and: [{ $eq: ["$$this.active", true] }, { $lte: ["$$this.start", "$$NOW"] }, { $gte: ["$$this.end", "$$NOW"] }], + }, + ], + }, + }, + }, + }, + }, + { $match: { "monitors.orderIndex": { $ne: -1 } } }, + { $sort: { "monitors.orderIndex": 1 } }, + + { + $group: { + _id: "$_id", + statusPage: { $first: "$$ROOT" }, + monitors: { $push: "$monitors" }, + }, + }, + { + $project: { + statusPage: { + _id: 1, + color: 1, + companyName: 1, + isPublished: 1, + logo: 1, + originalMonitors: 1, + showCharts: 1, + showUptimePercentage: 1, + timezone: 1, + showAdminLoginLink: 1, + url: 1, + }, + monitors: { + _id: 1, + userId: 1, + teamId: 1, + name: 1, + description: 1, + status: 1, + type: 1, + ignoreTlsErrors: 1, + jsonPath: 1, + expectedValue: 1, + matchMethod: 1, + url: 1, + port: 1, + isActive: 1, + interval: 1, + uptimePercentage: 1, + notifications: 1, + secret: 1, + thresholds: 1, + alertThreshold: 1, + cpuAlertThreshold: 1, + memoryAlertThreshold: 1, + diskAlertThreshold: 1, + tempAlertThreshold: 1, + checks: 1, + isMaintenance: 1, + createdAt: 1, + updatedAt: 1, + }, + }, + }, + ]); + if (!statusPageQuery.length) { + const error = new Error(this.stringService.statusPageNotFound); + error.status = 404; + throw error; + } + + const { statusPage, monitors } = statusPageQuery[0]; + + const normalizedMonitors = monitors.map((monitor) => { + return { + ...monitor, + checks: this.NormalizeData(monitor.checks, 10, 100), + }; + }); + + return { statusPage, monitors: normalizedMonitors }; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getStatusPageByUrl"; + throw error; + } + }; + + deleteStatusPage = async (url) => { + try { + await this.StatusPage.deleteOne({ url }); + } catch (error) { + error.service = SERVICE_NAME; + error.method = "deleteStatusPage"; + throw error; + } + }; + deleteStatusPagesByMonitorId = async (monitorId) => { + try { + await this.StatusPage.deleteMany({ monitors: { $in: [monitorId] } }); + } catch (error) { + error.service = SERVICE_NAME; + error.method = "deleteStatusPageByMonitorId"; + throw error; + } + }; +} + +export default StatusPageModule; diff --git a/server/src/db/mongo/modules/userModule.js b/server/src/db/mongo/modules/userModule.js new file mode 100755 index 000000000..83b3b15f9 --- /dev/null +++ b/server/src/db/mongo/modules/userModule.js @@ -0,0 +1,171 @@ +const SERVICE_NAME = "userModule"; +const DUPLICATE_KEY_CODE = 11000; // MongoDB error code for duplicate key + +class UserModule { + constructor({ User, Team, GenerateAvatarImage, ParseBoolean, stringService }) { + this.User = User; + this.Team = Team; + this.GenerateAvatarImage = GenerateAvatarImage; + this.ParseBoolean = ParseBoolean; + this.stringService = stringService; + } + + checkSuperadmin = async () => { + try { + const superAdmin = await this.User.findOne({ role: "superadmin" }); + if (superAdmin !== null) { + return true; + } + return false; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "checkSuperadmin"; + throw error; + } + }; + + insertUser = async (userData, imageFile) => { + try { + if (imageFile) { + // 1. Save the full size image + userData.profileImage = { + data: imageFile.buffer, + contentType: imageFile.mimetype, + }; + + // 2. Get the avatar sized image + const avatar = await this.GenerateAvatarImage(imageFile); + userData.avatarImage = avatar; + } + + // Handle creating team if superadmin + if (userData.role.includes("superadmin")) { + const team = new this.Team({ + email: userData.email, + }); + userData.teamId = team._id; + userData.checkTTL = 60 * 60 * 24 * 30; + await team.save(); + } + + const newUser = new this.User(userData); + await newUser.save(); + return await this.User.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 = this.stringService.dbUserExists; + } + error.service = SERVICE_NAME; + error.method = "insertUser"; + throw error; + } + }; + getUserByEmail = async (email) => { + try { + // Need the password to be able to compare, removed .select() + // We can strip the hash before returning the user + const user = await this.User.findOne({ email: email }).select("-profileImage"); + if (!user) { + throw new Error(this.stringService.dbUserNotFound); + } + return user; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getUserByEmail"; + throw error; + } + }; + + updateUser = async ({ userId, user, file }) => { + if (!userId) { + throw new Error("No user in request"); + } + + try { + const candidateUser = { ...user }; + + if (this.ParseBoolean(candidateUser.deleteProfileImage) === true) { + candidateUser.profileImage = null; + candidateUser.avatarImage = null; + } else if (file) { + // 1. Save the full size image + candidateUser.profileImage = { + data: file.buffer, + contentType: file.mimetype, + }; + + // 2. Get the avatar sized image + const avatar = await this.GenerateAvatarImage(file); + candidateUser.avatarImage = avatar; + } + + const updatedUser = await this.User.findByIdAndUpdate( + userId, + candidateUser, + { new: true } // Returns updated user instead of pre-update user + ) + .select("-password") + .select("-profileImage"); + return updatedUser; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "updateUser"; + throw error; + } + }; + deleteUser = async (userId) => { + try { + const deletedUser = await this.User.findByIdAndDelete(userId); + if (!deletedUser) { + throw new Error(this.stringService.dbUserNotFound); + } + return deletedUser; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "deleteUser"; + throw error; + } + }; + + getAllUsers = async () => { + try { + const users = await this.User.find().select("-password").select("-profileImage"); + return users; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getAllUsers"; + throw error; + } + }; + + getUserById = async (roles, userId) => { + try { + if (!roles.includes("superadmin")) { + throw new Error("User is not a superadmin"); + } + + const user = await this.User.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; + } + }; + + editUserById = async (userId, user) => { + try { + await this.User.findByIdAndUpdate(userId, user, { new: true }).select("-password").select("-profileImage"); + } catch (error) { + error.service = SERVICE_NAME; + error.method = "editUserById"; + throw error; + } + }; +} + +export default UserModule; diff --git a/server/db/mongo/utils/seedDb.js b/server/src/db/mongo/utils/seedDb.js similarity index 97% rename from server/db/mongo/utils/seedDb.js rename to server/src/db/mongo/utils/seedDb.js index 64f23178c..224744a51 100755 --- a/server/db/mongo/utils/seedDb.js +++ b/server/src/db/mongo/utils/seedDb.js @@ -1,6 +1,6 @@ import Monitor from "../../models/Monitor.js"; import Check from "../../models/Check.js"; -import logger from "../../../utils/logger.js"; +import { logger } from "../../../utils/logger.js"; const generateRandomUrl = () => { const domains = ["example.com", "test.org", "demo.net", "sample.io", "mock.dev"]; diff --git a/server/src/index.js b/server/src/index.js new file mode 100755 index 000000000..6b12eb25d --- /dev/null +++ b/server/src/index.js @@ -0,0 +1,60 @@ +import { initializeServices } from "./config/services.js"; +import { initializeControllers } from "./config/controllers.js"; +import { createApp } from "./app.js"; +import { initShutdownListener } from "./shutdown.js"; +import { fileURLToPath } from "url"; +import path from "path"; +import fs from "fs"; + +import Logger from "./utils/logger.js"; +import SettingsService from "./service/system/settingsService.js"; +import AppSettings from "./db/models/AppSettings.js"; + +const SERVICE_NAME = "Server"; +let logger; + +const startApp = async () => { + // FE path + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + const openApiSpec = JSON.parse(fs.readFileSync(path.join(__dirname, "../openapi.json"), "utf8")); + const frontendPath = path.join(__dirname, "public"); + + // Create services + const settingsService = new SettingsService(AppSettings); + const envSettings = settingsService.loadSettings(); + + // Create logger + logger = new Logger({ envSettings }); + + // Initialize services + const services = await initializeServices({ logger, envSettings, settingsService }); + + // Initialize controllers + const controllers = initializeControllers(services); + + const app = createApp({ + services, + controllers, + envSettings, + frontendPath, + openApiSpec, + }); + + const port = envSettings.port || 52345; + const server = app.listen(port, () => { + logger.info({ message: `Server started on port:${port}` }); + }); + + initShutdownListener(server, services); +}; + +startApp().catch((error) => { + logger.error({ + message: error.message, + service: SERVICE_NAME, + method: "startApp", + stack: error.stack, + }); + process.exit(1); +}); diff --git a/server/locales/en.json b/server/src/locales/en.json similarity index 100% rename from server/locales/en.json rename to server/src/locales/en.json diff --git a/server/locales/en.json.bak b/server/src/locales/en.json.bak similarity index 100% rename from server/locales/en.json.bak rename to server/src/locales/en.json.bak diff --git a/server/locales/tr.json b/server/src/locales/tr.json similarity index 100% rename from server/locales/tr.json rename to server/src/locales/tr.json diff --git a/server/middleware/handleErrors.js b/server/src/middleware/handleErrors.js similarity index 72% rename from server/middleware/handleErrors.js rename to server/src/middleware/handleErrors.js index 87895fcf2..b0ded5ab7 100755 --- a/server/middleware/handleErrors.js +++ b/server/src/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 { logger } from "../utils/logger.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/src/middleware/isAllowed.js similarity index 82% rename from server/middleware/isAllowed.js rename to server/src/middleware/isAllowed.js index 4c6f89aa3..5d61974be 100755 --- a/server/middleware/isAllowed.js +++ b/server/src/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/src/middleware/languageMiddleware.js b/server/src/middleware/languageMiddleware.js new file mode 100755 index 000000000..6293dc837 --- /dev/null +++ b/server/src/middleware/languageMiddleware.js @@ -0,0 +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(); + + 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(); + + translationService.setLanguage(language); + stringService.setLanguage(language); + + next(); + } +}; + +export default languageMiddleware; diff --git a/server/src/middleware/rateLimiter.js b/server/src/middleware/rateLimiter.js new file mode 100644 index 000000000..70b080d23 --- /dev/null +++ b/server/src/middleware/rateLimiter.js @@ -0,0 +1,17 @@ +import rateLimit from "express-rate-limit"; + +export const generalApiLimiter = rateLimit({ + window: 60 * 1000, + limit: 600, + standardHeaders: true, + legacyHeaders: false, + ipv6Subnet: 64, +}); + +export const authApiLimiter = rateLimit({ + window: 60 * 1000, + limit: 15, + standardHeaders: true, + legacyHeaders: false, + ipv6Subnet: 64, +}); diff --git a/server/middleware/responseHandler.js b/server/src/middleware/responseHandler.js similarity index 100% rename from server/middleware/responseHandler.js rename to server/src/middleware/responseHandler.js diff --git a/server/src/middleware/sanitization.js b/server/src/middleware/sanitization.js new file mode 100644 index 000000000..a89325de1 --- /dev/null +++ b/server/src/middleware/sanitization.js @@ -0,0 +1,85 @@ +import { JSDOM } from "jsdom"; +import DOMPurify from "isomorphic-dompurify"; + +// Initialize DOMPurify with jsdom +const window = new JSDOM("").window; +const purify = DOMPurify(window); + +/** + * Sanitizes user input to prevent XSS attacks + * @param {string} input - The input string to sanitize + * @param {Object} options - Sanitization options + * @returns {string} The sanitized string + */ +export const sanitizeInput = (input, options = {}) => { + if (typeof input !== "string") { + return input; + } + + // Default configuration - remove all HTML tags and attributes + const defaultConfig = { + ALLOWED_TAGS: [], + ALLOWED_ATTR: [], + KEEP_CONTENT: true, + ...options, + }; + + return purify.sanitize(input, defaultConfig); +}; + +/** + * Sanitizes an object recursively + * @param {Object} obj - The object to sanitize + * @param {Object} options - Sanitization options + * @returns {Object} The sanitized object + */ +export const sanitizeObject = (obj, options = {}) => { + if (typeof obj !== "object" || obj === null) { + return obj; + } + + if (Array.isArray(obj)) { + return obj.map((item) => sanitizeObject(item, options)); + } + + const sanitized = {}; + for (const [key, value] of Object.entries(obj)) { + if (typeof value === "string") { + sanitized[key] = sanitizeInput(value, options); + } else if (typeof value === "object" && value !== null) { + sanitized[key] = sanitizeObject(value, options); + } else { + sanitized[key] = value; + } + } + + return sanitized; +}; + +/** + * Express middleware for sanitizing request body + * @param {Object} options - Sanitization options + * @returns {Function} Express middleware function + */ +export const sanitizeBody = (options = {}) => { + return (req, res, next) => { + if (req.body && typeof req.body === "object") { + req.body = sanitizeObject(req.body, options); + } + next(); + }; +}; + +/** + * Express middleware for sanitizing query parameters + * @param {Object} options - Sanitization options + * @returns {Function} Express middleware function + */ +export const sanitizeQuery = (options = {}) => { + return (req, res, next) => { + if (req.query && typeof req.query === "object") { + req.query = sanitizeObject(req.query, options); + } + next(); + }; +}; diff --git a/server/middleware/verifyJWT.js b/server/src/middleware/verifyJWT.js similarity index 73% rename from server/middleware/verifyJWT.js rename to server/src/middleware/verifyJWT.js index bd17edcfd..d01db0a97 100755 --- a/server/middleware/verifyJWT.js +++ b/server/src/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/src/middleware/verifyOwnership.js similarity index 89% rename from server/middleware/verifyOwnership.js rename to server/src/middleware/verifyOwnership.js index 2040d54ee..4431b0c26 100755 --- a/server/middleware/verifyOwnership.js +++ b/server/src/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 { logger } from "../utils/logger.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/src/middleware/verifySuperAdmin.js similarity index 95% rename from server/middleware/verifySuperAdmin.js rename to server/src/middleware/verifySuperAdmin.js index 98c336fad..a5a93eede 100755 --- a/server/middleware/verifySuperAdmin.js +++ b/server/src/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/middleware/verifyTeamAccess.js b/server/src/middleware/verifyTeamAccess.js similarity index 100% rename from server/middleware/verifyTeamAccess.js rename to server/src/middleware/verifyTeamAccess.js diff --git a/server/routes/announcementsRoute.js b/server/src/routes/announcementsRoute.js similarity index 83% rename from server/routes/announcementsRoute.js rename to server/src/routes/announcementsRoute.js index 0b234cb96..011dfac4a 100755 --- a/server/routes/announcementsRoute.js +++ b/server/src/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/src/routes/authRoute.js similarity index 60% rename from server/routes/authRoute.js rename to server/src/routes/authRoute.js index aabd8de89..e790c60b0 100755 --- a/server/routes/authRoute.js +++ b/server/src/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/src/routes/checkRoute.js similarity index 58% rename from server/routes/checkRoute.js rename to server/src/routes/checkRoute.js index f215cbe37..8bdb2d4fb 100755 --- a/server/routes/checkRoute.js +++ b/server/src/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/src/routes/diagnosticRoute.js similarity index 56% rename from server/routes/diagnosticRoute.js rename to server/src/routes/diagnosticRoute.js index 616d01ccf..a15f5a038 100755 --- a/server/routes/diagnosticRoute.js +++ b/server/src/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/src/routes/inviteRoute.js similarity index 56% rename from server/routes/inviteRoute.js rename to server/src/routes/inviteRoute.js index 301959389..d495c5476 100755 --- a/server/routes/inviteRoute.js +++ b/server/src/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/logRoutes.js b/server/src/routes/logRoutes.js similarity index 100% rename from server/routes/logRoutes.js rename to server/src/routes/logRoutes.js diff --git a/server/src/routes/maintenanceWindowRoute.js b/server/src/routes/maintenanceWindowRoute.js new file mode 100755 index 000000000..774434ed6 --- /dev/null +++ b/server/src/routes/maintenanceWindowRoute.js @@ -0,0 +1,25 @@ +import { Router } from "express"; +import MaintenanceWindow from "../db/models/MaintenanceWindow.js"; +class MaintenanceWindowRoutes { + constructor(maintenanceWindowController) { + this.router = Router(); + this.mwController = maintenanceWindowController; + this.initRoutes(); + } + initRoutes() { + this.router.post("/", this.mwController.createMaintenanceWindows); + this.router.get("/team/", this.mwController.getMaintenanceWindowsByTeamId); + + this.router.get("/monitor/:monitorId", this.mwController.getMaintenanceWindowsByMonitorId); + + this.router.get("/:id", this.mwController.getMaintenanceWindowById); + this.router.put("/:id", this.mwController.editMaintenanceWindow); + this.router.delete("/:id", this.mwController.deleteMaintenanceWindow); + } + + getRouter() { + return this.router; + } +} + +export default MaintenanceWindowRoutes; diff --git a/server/src/routes/monitorRoute.js b/server/src/routes/monitorRoute.js new file mode 100755 index 000000000..043d361d1 --- /dev/null +++ b/server/src/routes/monitorRoute.js @@ -0,0 +1,60 @@ +import { Router } from "express"; +import { isAllowed } from "../middleware/isAllowed.js"; +import multer from "multer"; +import { fetchMonitorCertificate } from "../controllers/controllerUtils.js"; +const upload = multer({ + storage: multer.memoryStorage(), // Store file in memory as Buffer +}); + +class MonitorRoutes { + constructor(monitorController) { + this.router = Router(); + this.monitorController = monitorController; + this.initRoutes(); + } + + initRoutes() { + // 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); + this.router.get("/games", this.monitorController.getAllGames); + + // Individual monitor CRUD routes + this.router.get("/:monitorId", this.monitorController.getMonitorById); + this.router.put("/:monitorId", isAllowed(["admin", "superadmin"]), this.monitorController.editMonitor); + this.router.delete("/:monitorId", isAllowed(["admin", "superadmin"]), this.monitorController.deleteMonitor); + } + + getRouter() { + return this.router; + } +} + +export default MonitorRoutes; diff --git a/server/routes/notificationRoute.js b/server/src/routes/notificationRoute.js similarity index 56% rename from server/routes/notificationRoute.js rename to server/src/routes/notificationRoute.js index 09becd16c..f88b0b96c 100755 --- a/server/routes/notificationRoute.js +++ b/server/src/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/src/routes/queueRoute.js b/server/src/routes/queueRoute.js new file mode 100755 index 000000000..55e829b91 --- /dev/null +++ b/server/src/routes/queueRoute.js @@ -0,0 +1,24 @@ +import { Router } from "express"; +import { isAllowed } from "../middleware/isAllowed.js"; +class QueueRoutes { + constructor(queueController) { + this.router = Router(); + this.queueController = queueController; + this.initRoutes(); + } + initRoutes() { + this.router.get("/jobs", isAllowed(["admin", "superadmin"]), this.queueController.getJobs); + this.router.post("/jobs", isAllowed(["admin", "superadmin"]), this.queueController.addJob); + + 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() { + return this.router; + } +} + +export default QueueRoutes; diff --git a/server/routes/settingsRoute.js b/server/src/routes/settingsRoute.js similarity index 62% rename from server/routes/settingsRoute.js rename to server/src/routes/settingsRoute.js index f293fd660..88eb67e65 100755 --- a/server/routes/settingsRoute.js +++ b/server/src/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/src/routes/statusPageRoute.js similarity index 75% rename from server/routes/statusPageRoute.js rename to server/src/routes/statusPageRoute.js index 98b3a3433..53eb21fba 100755 --- a/server/routes/statusPageRoute.js +++ b/server/src/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/src/service/business/checkService.js b/server/src/service/business/checkService.js new file mode 100644 index 000000000..7966fdbb4 --- /dev/null +++ b/server/src/service/business/checkService.js @@ -0,0 +1,150 @@ +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; + } + + get serviceName() { + return CheckService.SERVICE_NAME; + } + + 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.monitorModule.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.checkModule.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.checkModule.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.checkModule.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.checkModule.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.monitorModule.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.checkModule.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.monitorModule.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.checkModule.deleteChecks(monitorId); + return deletedCount; + }; + deleteChecksByTeamId = async ({ teamId }) => { + if (!teamId) { + throw this.errorService.createBadRequestError("No team ID in request"); + } + + const deletedCount = await this.db.checkModule.deleteChecksByTeamId(teamId); + return deletedCount; + }; + + updateChecksTTL = async ({ teamId, ttl }) => { + const SECONDS_PER_DAY = 86400; + const newTTL = parseInt(ttl, 10) * SECONDS_PER_DAY; + await this.db.checkModule.updateChecksTTL(teamId, newTTL); + }; +} + +export default CheckService; diff --git a/server/src/service/business/diagnosticService.js b/server/src/service/business/diagnosticService.js new file mode 100644 index 000000000..353604932 --- /dev/null +++ b/server/src/service/business/diagnosticService.js @@ -0,0 +1,94 @@ +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"] }); + } + + get serviceName() { + return DiagnosticService.SERVICE_NAME; + } + + 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/src/service/business/inviteService.js b/server/src/service/business/inviteService.js new file mode 100644 index 000000000..369ed195a --- /dev/null +++ b/server/src/service/business/inviteService.js @@ -0,0 +1,44 @@ +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; + } + + get serviceName() { + return InviteService.SERVICE_NAME; + } + + getInviteToken = async ({ invite, teamId }) => { + invite.teamId = teamId; + const inviteToken = await this.db.inviteModule.requestInviteToken(invite); + return inviteToken; + }; + + sendInviteEmail = async ({ inviteRequest, firstName }) => { + const inviteToken = await this.db.inviteModule.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.inviteModule.getInviteToken(inviteToken); + return invite; + }; +} + +export default InviteService; diff --git a/server/src/service/business/maintenanceWindowService.js b/server/src/service/business/maintenanceWindowService.js new file mode 100644 index 000000000..9d52702e9 --- /dev/null +++ b/server/src/service/business/maintenanceWindowService.js @@ -0,0 +1,66 @@ +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; + } + + get serviceName() { + return MaintenanceWindowService.SERVICE_NAME; + } + + createMaintenanceWindow = async ({ teamId, body }) => { + const monitorIds = body.monitors; + const monitors = await this.db.monitorModule.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.maintenanceWindowModule.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.maintenanceWindowModule.getMaintenanceWindowById({ id, teamId }); + return maintenanceWindow; + }; + + getMaintenanceWindowsByTeamId = async ({ teamId, query }) => { + const maintenanceWindows = await this.db.maintenanceWindowModule.getMaintenanceWindowsByTeamId(teamId, query); + return maintenanceWindows; + }; + + getMaintenanceWindowsByMonitorId = async ({ monitorId, teamId }) => { + const maintenanceWindows = await this.db.maintenanceWindowModule.getMaintenanceWindowsByMonitorId({ monitorId, teamId }); + return maintenanceWindows; + }; + + deleteMaintenanceWindow = async ({ id, teamId }) => { + await this.db.maintenanceWindowModule.deleteMaintenanceWindowById({ id, teamId }); + }; + + editMaintenanceWindow = async ({ id, teamId, body }) => { + const editedMaintenanceWindow = await this.db.maintenanceWindowModule.editMaintenanceWindowById({ id, body, teamId }); + return editedMaintenanceWindow; + }; +} + +export default MaintenanceWindowService; diff --git a/server/src/service/business/monitorService.js b/server/src/service/business/monitorService.js new file mode 100644 index 000000000..637870b71 --- /dev/null +++ b/server/src/service/business/monitorService.js @@ -0,0 +1,272 @@ +import { createMonitorsBodyValidation } from "../../validation/joi.js"; + +const SERVICE_NAME = "MonitorService"; +class MonitorService { + static SERVICE_NAME = SERVICE_NAME; + + constructor({ db, settingsService, jobQueue, stringService, emailService, papaparse, logger, errorService, games }) { + this.db = db; + this.settingsService = settingsService; + this.jobQueue = jobQueue; + this.stringService = stringService; + this.emailService = emailService; + this.papaparse = papaparse; + this.logger = logger; + this.errorService = errorService; + this.games = games; + } + + get serviceName() { + return MonitorService.SERVICE_NAME; + } + + verifyTeamAccess = async ({ teamId, monitorId }) => { + const monitor = await this.db.monitorModule.getMonitorById(monitorId); + if (!monitor?.teamId?.equals(teamId)) { + throw this.errorService.createAuthorizationError(); + } + }; + + getAllMonitors = async () => { + const monitors = await this.db.monitorModule.getAllMonitors(); + return monitors; + }; + + getUptimeDetailsById = async ({ teamId, monitorId, dateRange, normalize }) => { + await this.verifyTeamAccess({ teamId, monitorId }); + const data = await this.db.monitorModule.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.monitorModule.getMonitorStatsById({ + monitorId, + limit, + sortOrder, + dateRange, + numToDisplay, + normalize, + }); + + return monitorStats; + }; + + getHardwareDetailsById = async ({ teamId, monitorId, dateRange }) => { + await this.verifyTeamAccess({ teamId, monitorId }); + const monitor = await this.db.monitorModule.getHardwareDetailsById({ monitorId, dateRange }); + + return monitor; + }; + + getMonitorById = async ({ teamId, monitorId }) => { + await this.verifyTeamAccess({ teamId, monitorId }); + const monitor = await this.db.monitorModule.getMonitorById(monitorId); + + return monitor; + }; + + createMonitor = async ({ teamId, userId, body }) => { + const monitor = await this.db.monitorModule.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.monitorModule.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.monitorModule.deleteMonitor({ teamId, monitorId }); + await this.jobQueue.deleteJob(monitor); + await this.db.statusPageModule.deleteStatusPagesByMonitorId(monitor._id); + return monitor; + }; + + deleteAllMonitors = async ({ teamId }) => { + const { monitors, deletedCount } = await this.db.monitorModule.deleteAllMonitors(teamId); + await Promise.all( + monitors.map(async (monitor) => { + try { + await this.jobQueue.deleteJob(monitor); + await this.db.checkModule.deleteChecks(monitor._id); + await this.db.pageSpeedCheckModule.deletePageSpeedChecksByMonitorId(monitor._id); + await this.db.notificationsModule.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.monitorModule.editMonitor({ monitorId, body }); + await this.jobQueue.updateJob(editedMonitor); + }; + + pauseMonitor = async ({ teamId, monitorId }) => { + await this.verifyTeamAccess({ teamId, monitorId }); + const monitor = await this.db.monitorModule.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.monitorModule.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.monitorModule.getMonitorsByTeamId({ + limit, + type, + page, + rowsPerPage, + filter, + field, + order, + teamId, + }); + return monitors; + }; + + getMonitorsAndSummaryByTeamId = async ({ teamId, type, explain }) => { + const result = await this.db.monitorModule.getMonitorsAndSummaryByTeamId({ + type, + explain, + teamId, + }); + return result; + }; + + getMonitorsWithChecksByTeamId = async ({ teamId, limit, type, page, rowsPerPage, filter, field, order, explain }) => { + const result = await this.db.monitorModule.getMonitorsWithChecksByTeamId({ + limit, + type, + page, + rowsPerPage, + filter, + field, + order, + teamId, + explain, + }); + return result; + }; + + exportMonitorsToCSV = async ({ teamId }) => { + const monitors = await this.db.monitorModule.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; + }; + + getAllGames = () => { + return this.games; + }; +} + +export default MonitorService; diff --git a/server/src/service/business/userService.js b/server/src/service/business/userService.js new file mode 100644 index 000000000..0a965bced --- /dev/null +++ b/server/src/service/business/userService.js @@ -0,0 +1,215 @@ +const SERVICE_NAME = "userService"; + +class UserService { + static SERVICE_NAME = SERVICE_NAME; + + constructor({ crypto, db, emailService, settingsService, logger, stringService, jwt, errorService, jobQueue }) { + this.db = db; + this.emailService = emailService; + this.settingsService = settingsService; + this.logger = logger; + this.stringService = stringService; + this.jwt = jwt; + this.errorService = errorService; + this.jobQueue = jobQueue; + this.crypto = crypto; + } + + get serviceName() { + return UserService.SERVICE_NAME; + } + + 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.userModule.checkSuperadmin(); + if (superAdminExists) { + const invitedUser = await this.db.inviteModule.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 = this.crypto.randomBytes(64).toString("hex"); + await this.db.settingsModule.updateAppSettings({ jwtSecret }); + } + + const newUser = await this.db.userModule.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.userModule.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.userModule.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.userModule.updateUser({ userId: currentUser?._id, user: updates, file: file }); + return updatedUser; + }; + + checkSuperadminExists = async () => { + const superAdminExists = await this.db.userModule.checkSuperadmin(); + return superAdminExists; + }; + + requestRecovery = async (email) => { + const user = await this.db.userModule.getUserByEmail(email); + const recoveryToken = await this.db.recoveryModule.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.recoveryModule.validateRecoveryToken(recoveryToken); + }; + + resetPassword = async (password, recoveryToken) => { + const user = await this.db.recoveryModule.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.monitorModule.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.userModule.deleteUser(userId); + }; + + getAllUsers = async () => { + const users = await this.db.userModule.getAllUsers(); + return users; + }; + + getUserById = async (roles, userId) => { + const user = await this.db.userModule.getUserById(roles, userId); + return user; + }; + + editUserById = async (userId, user) => { + await this.db.userModule.editUserById(userId, user); + }; +} +export default UserService; diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js new file mode 100644 index 000000000..60bcb0754 --- /dev/null +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js @@ -0,0 +1,170 @@ +import Scheduler from "super-simple-scheduler"; +const SERVICE_NAME = "JobQueue"; + +class SuperSimpleQueue { + static SERVICE_NAME = SERVICE_NAME; + + constructor({ envSettings, db, logger, helper }) { + this.envSettings = envSettings; + this.db = db; + this.logger = logger; + this.helper = helper; + } + + get serviceName() { + return SuperSimpleQueue.SERVICE_NAME; + } + + static async create({ envSettings, db, logger, helper }) { + const instance = new SuperSimpleQueue({ envSettings, 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.envSettings.dbConnectionString, + }); + this.scheduler.start(); + + this.scheduler.addTemplate("monitor-job", this.helper.getMonitorJob()); + const monitors = await this.db.monitorModule.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/PulseQueue/PulseQueueHelper.js b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js similarity index 79% rename from server/service/PulseQueue/PulseQueueHelper.js rename to server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js index 959c12e3b..ca2e5f642 100644 --- a/server/service/PulseQueue/PulseQueueHelper.js +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js @@ -1,6 +1,7 @@ -const SERVICE_NAME = "PulseQueueHelper"; +const SERVICE_NAME = "JobQueueHelper"; -class PulseQueueHelper { +class SuperSimpleQueueHelper { + static SERVICE_NAME = SERVICE_NAME; constructor({ db, logger, networkService, statusService, notificationService }) { this.db = db; this.logger = logger; @@ -9,11 +10,14 @@ class PulseQueueHelper { this.notificationService = notificationService; } + get serviceName() { + return SuperSimpleQueueHelper.SERVICE_NAME; + } + getMonitorJob = () => { - return async (job) => { + return async (monitor) => { try { - const monitor = job.attrs.data.monitor; - const monitorId = job.attrs.data.monitor._id; + const monitorId = monitor._id; if (!monitorId) { throw new Error("No monitor id"); } @@ -27,18 +31,12 @@ class PulseQueueHelper { }); 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); + const { monitor: updatedMonitor, statusChanged, prevStatus } = await this.statusService.updateStatus(networkResponse); this.notificationService .handleNotifications({ @@ -52,7 +50,7 @@ class PulseQueueHelper { message: error.message, service: SERVICE_NAME, method: "getMonitorJob", - details: `Error sending notifications for job ${job.id}: ${error.message}`, + details: `Error sending notifications for job ${monitor._id}: ${error.message}`, stack: error.stack, }); }); @@ -69,7 +67,7 @@ class PulseQueueHelper { }; async isInMaintenanceWindow(monitorId) { - const maintenanceWindows = await this.db.getMaintenanceWindowsByMonitorId(monitorId); + const maintenanceWindows = await this.db.maintenanceWindowModule.getMaintenanceWindowsByMonitorId(monitorId); // Check for active maintenance window: const maintenanceWindowIsActive = maintenanceWindows.reduce((acc, window) => { if (window.active) { @@ -99,4 +97,4 @@ class PulseQueueHelper { } } -export default PulseQueueHelper; +export default SuperSimpleQueueHelper; diff --git a/server/service/bufferService.js b/server/src/service/infrastructure/bufferService.js similarity index 80% rename from server/service/bufferService.js rename to server/src/service/infrastructure/bufferService.js index 84f3efd30..dc86597b2 100755 --- a/server/service/bufferService.js +++ b/server/src/service/infrastructure/bufferService.js @@ -1,5 +1,4 @@ const SERVICE_NAME = "BufferService"; -const BUFFER_TIMEOUT = process.env.NODE_ENV === "development" ? 5000 : 1000 * 60 * 1; // 1 minute const TYPE_MAP = { http: "checks", ping: "checks", @@ -7,10 +6,13 @@ const TYPE_MAP = { docker: "checks", pagespeed: "pagespeedChecks", hardware: "hardwareChecks", + game: "checks", }; class BufferService { - constructor({ db, logger }) { + static SERVICE_NAME = SERVICE_NAME; + constructor({ db, logger, envSettings }) { + this.BUFFER_TIMEOUT = envSettings.nodeEnv === "development" ? 5000 : 1000 * 60 * 1; // 1 minute this.db = db; this.logger = logger; this.SERVICE_NAME = SERVICE_NAME; @@ -20,19 +22,22 @@ class BufferService { hardwareChecks: [], }; this.OPERATION_MAP = { - checks: this.db.createChecks, - pagespeedChecks: this.db.createPageSpeedChecks, - hardwareChecks: this.db.createHardwareChecks, + checks: this.db.checkModule.createChecks, + pagespeedChecks: this.db.pageSpeedCheckModule.createPageSpeedChecks, + hardwareChecks: this.db.hardwareCheckModule.createHardwareChecks, }; this.scheduleNextFlush(); this.logger.info({ - message: `Buffer service initialized, flushing every ${BUFFER_TIMEOUT / 1000}s`, + message: `Buffer service initialized, flushing every ${this.BUFFER_TIMEOUT / 1000}s`, service: this.SERVICE_NAME, method: "constructor", }); } - static SERVICE_NAME = SERVICE_NAME; + + get serviceName() { + return BufferService.SERVICE_NAME; + } addToBuffer({ check, type }) { try { @@ -62,7 +67,7 @@ class BufferService { // Schedule the next flush only after the current one completes this.scheduleNextFlush(); } - }, BUFFER_TIMEOUT); + }, this.BUFFER_TIMEOUT); } async flushBuffers() { let items = 0; diff --git a/server/service/emailService.js b/server/src/service/infrastructure/emailService.js similarity index 96% rename from server/service/emailService.js rename to server/src/service/infrastructure/emailService.js index 433fd0871..bc040d51a 100755 --- a/server/service/emailService.js +++ b/server/src/service/infrastructure/emailService.js @@ -33,6 +33,10 @@ class EmailService { this.init(); } + get serviceName() { + return EmailService.SERVICE_NAME; + } + init = async () => { /** * Loads an email template from the filesystem. @@ -42,10 +46,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/src/service/infrastructure/errorService.js b/server/src/service/infrastructure/errorService.js new file mode 100644 index 000000000..680bb3765 --- /dev/null +++ b/server/src/service/infrastructure/errorService.js @@ -0,0 +1,101 @@ +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() {} + + get serviceName() { + return ErrorService.SERVICE_NAME; + } + + 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/src/service/infrastructure/networkService.js similarity index 87% rename from server/service/networkService.js rename to server/src/service/infrastructure/networkService.js index 08b2f065e..8f904bf56 100755 --- a/server/service/networkService.js +++ b/server/src/service/infrastructure/networkService.js @@ -1,5 +1,6 @@ import jmespath from "jmespath"; import https from "https"; +import { GameDig } from "gamedig"; const SERVICE_NAME = "NetworkService"; const UPROCK_ENDPOINT = "https://api.uprock.com/checkmate/push"; @@ -23,6 +24,7 @@ class NetworkService { this.TYPE_HARDWARE = "hardware"; this.TYPE_DOCKER = "docker"; this.TYPE_PORT = "port"; + this.TYPE_GAME = "game"; this.SERVICE_NAME = SERVICE_NAME; this.NETWORK_ERROR = 5000; this.PING_ERROR = 5001; @@ -36,6 +38,10 @@ class NetworkService { this.settingsService = settingsService; } + get serviceName() { + return NetworkService.SERVICE_NAME; + } + /** * Times the execution of an asynchronous operation. * @@ -78,9 +84,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 +137,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 +148,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 +162,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 +217,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 +308,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 +319,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; @@ -487,6 +472,62 @@ class NetworkService { } } + /** + * Requests the status of a game monitor. + * + * @param {Object} monitor - The monitor object to request the status for. + * @returns {Promise} The response from the game status request. + * @throws {Error} Throws an error if the request fails or if the monitor is not configured correctly. + * @property {string} monitorId - The ID of the monitor. + * @property {string} type - The type of the monitor (should be "game"). + * @property {number} responseTime - The time taken for the request. + * @property {Object|null} payload - The game state response or null if the request failed. + * @property {boolean} status - Indicates if the request was successful (true) or not (false). + * @property {number} code - The status code of the request (200 for success, NETWORK_ERROR for failure). + * @property {string} message - A message indicating the result of the request. + */ + async requestGame(monitor) { + try { + const { url, port, gameId } = monitor; + + const gameResponse = { + code: 200, + status: true, + message: "Success", + monitorId: monitor._id, + type: "game", + }; + + const state = await GameDig.query({ + type: gameId, + host: url, + port: port, + }).catch((error) => { + this.logger.warn({ + message: error.message, + service: this.SERVICE_NAME, + method: "requestGame", + details: { url, port, gameId }, + }); + }); + + if (!state) { + gameResponse.status = false; + gameResponse.code = this.NETWORK_ERROR; + gameResponse.message = "No response"; + return gameResponse; + } + + gameResponse.responseTime = state.ping; + gameResponse.payload = state; + return gameResponse; + } catch (error) { + error.service = this.SERVICE_NAME; + error.method = "requestPing"; + throw error; + } + } + /** * Gets the status of a job based on its type and returns the appropriate response. * @@ -511,6 +552,8 @@ class NetworkService { return await this.requestDocker(monitor); case this.TYPE_PORT: return await this.requestPort(monitor); + case this.TYPE_GAME: + return await this.requestGame(monitor); default: return this.handleUnsupportedType(type); } diff --git a/server/service/notificationService.js b/server/src/service/infrastructure/notificationService.js similarity index 87% rename from server/service/notificationService.js rename to server/src/service/infrastructure/notificationService.js index a6048b8cd..8c34519f3 100644 --- a/server/service/notificationService.js +++ b/server/src/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; @@ -19,6 +12,10 @@ class NotificationService { this.notificationUtils = notificationUtils; } + get serviceName() { + return NotificationService.SERVICE_NAME; + } + sendNotification = async ({ notification, subject, content, html }) => { const { type, address } = notification; @@ -69,27 +66,22 @@ 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; } async notifyAll({ notificationIDs, subject, html, content }) { - const notifications = await this.db.getNotificationsByIds(notificationIDs); + const notifications = await this.db.notificationModule.getNotificationsByIds(notificationIDs); // Map each notification to a test promise const promises = notifications.map(async (notification) => { diff --git a/server/service/notificationUtils.js b/server/src/service/infrastructure/notificationUtils.js similarity index 80% rename from server/service/notificationUtils.js rename to server/src/service/infrastructure/notificationUtils.js index 61c3ec36a..22c7f4df7 100644 --- a/server/service/notificationUtils.js +++ b/server/src/service/infrastructure/notificationUtils.js @@ -1,9 +1,17 @@ +const SERVICE_NAME = "NotificationUtils"; + class NotificationUtils { + static SERVICE_NAME = SERVICE_NAME; + constructor({ stringService, emailService }) { this.stringService = stringService; this.emailService = emailService; } + get serviceName() { + return NotificationUtils.SERVICE_NAME; + } + buildTestEmail = async () => { const context = { testName: "Monitoring System" }; const html = await this.emailService.buildEmail("testEmailTemplate", context); @@ -26,10 +34,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 +55,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 +76,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 +99,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/src/service/infrastructure/statusService.js similarity index 92% rename from server/service/statusService.js rename to server/src/service/infrastructure/statusService.js index 3d7bb4543..72916a0ce 100755 --- a/server/service/statusService.js +++ b/server/src/service/infrastructure/statusService.js @@ -1,9 +1,10 @@ -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 { static SERVICE_NAME = SERVICE_NAME; + /** * Creates an instance of StatusService. * @@ -14,7 +15,10 @@ class StatusService { this.db = db; this.logger = logger; this.buffer = buffer; - this.SERVICE_NAME = SERVICE_NAME; + } + + get serviceName() { + return StatusService.SERVICE_NAME; } async updateRunningStats({ monitor, networkResponse }) { @@ -48,9 +52,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; @@ -120,7 +122,7 @@ class StatusService { this.insertCheck(networkResponse); try { const { monitorId, status, code } = networkResponse; - const monitor = await this.db.getMonitorById(monitorId); + const monitor = await this.db.monitorModule.getMonitorById(monitorId); // Update running stats this.updateRunningStats({ monitor, networkResponse }); @@ -138,9 +140,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, }); @@ -236,7 +236,7 @@ class StatusService { } if (type === "hardware") { - const { cpu, memory, disk, host } = payload?.data ?? {}; + const { cpu, memory, disk, host, net } = payload?.data ?? {}; const { errors } = payload?.errors ?? []; check.cpu = cpu ?? {}; check.memory = memory ?? {}; @@ -244,6 +244,7 @@ class StatusService { check.host = host ?? {}; check.errors = errors ?? []; check.capture = payload?.capture ?? {}; + check.net = net ?? {}; } return check; }; @@ -279,9 +280,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/src/service/system/serviceRegistry.js b/server/src/service/system/serviceRegistry.js new file mode 100755 index 000000000..4e3e28464 --- /dev/null +++ b/server/src/service/system/serviceRegistry.js @@ -0,0 +1,63 @@ +const SERVICE_NAME = "ServiceRegistry"; + +class ServiceRegistry { + static SERVICE_NAME = SERVICE_NAME; + + constructor({ logger }) { + this.services = {}; + this.logger = logger; + } + + get serviceName() { + return ServiceRegistry.SERVICE_NAME; + } + + // Instance methods + register(name, service) { + this.logger.info({ + message: `Registering service ${name}`, + service: SERVICE_NAME, + method: "register", + }); + this.services[name] = service; + } + + get(name) { + if (!this.services[name]) { + this.logger.error({ + message: `Service ${name} is not registered`, + service: SERVICE_NAME, + method: "get", + }); + throw new Error(`Service ${name} is not registered`); + } + return this.services[name]; + } + + listServices() { + return Object.keys(this.services); + } + + static get(name) { + if (!ServiceRegistry.instance) { + throw new Error("ServiceRegistry not initialized"); + } + return ServiceRegistry.instance.get(name); + } + + static register(name, service) { + if (!ServiceRegistry.instance) { + throw new Error("ServiceRegistry not initialized"); + } + return ServiceRegistry.instance.register(name, service); + } + + static listServices() { + if (!ServiceRegistry.instance) { + throw new Error("ServiceRegistry not initialized"); + } + return ServiceRegistry.instance.listServices(); + } +} + +export default ServiceRegistry; diff --git a/server/service/settingsService.js b/server/src/service/system/settingsService.js similarity index 67% rename from server/service/settingsService.js rename to server/src/service/system/settingsService.js index de035922c..70d8d192c 100755 --- a/server/service/settingsService.js +++ b/server/src/service/system/settingsService.js @@ -1,21 +1,14 @@ const SERVICE_NAME = "SettingsService"; + const envConfig = { - nodeEnv: process.env.NODE_ENV, - logLevel: process.env.LOG_LEVEL, - systemEmailHost: process.env.SYSTEM_EMAIL_HOST, - systemEmailPort: process.env.SYSTEM_EMAIL_PORT, - systemEmailUser: process.env.SYSTEM_EMAIL_USER, - systemEmailAddress: process.env.SYSTEM_EMAIL_ADDRESS, - systemEmailPassword: process.env.SYSTEM_EMAIL_PASSWORD, jwtSecret: process.env.JWT_SECRET, jwtTTL: process.env.TOKEN_TTL, + systemEmailHost: process.env.SYSTEM_EMAIL_HOST, + nodeEnv: process.env.NODE_ENV, + logLevel: process.env.LOG_LEVEL, clientHost: process.env.CLIENT_HOST, dbConnectionString: process.env.DB_CONNECTION_STRING, - redisUrl: process.env.REDIS_URL, - callbackUrl: process.env.CALLBACK_URL, port: process.env.PORT, - pagespeedApiKey: process.env.PAGESPEED_API_KEY, - uprockApiKey: process.env.UPROCK_API_KEY, }; /** * SettingsService @@ -23,7 +16,8 @@ const envConfig = { * This service is responsible for loading and managing the application settings. */ class SettingsService { - static SERVICE_NAME = SERVICE_NAME; + static SERVICE_NAME = "SettingsService"; + /** * Constructs a new SettingsService * @constructor @@ -32,6 +26,11 @@ class SettingsService { this.AppSettings = AppSettings; this.settings = { ...envConfig }; } + + get serviceName() { + return SettingsService.SERVICE_NAME; + } + /** * Load settings from env settings * @returns {Object>} The settings. @@ -39,13 +38,7 @@ class SettingsService { loadSettings() { return this.settings; } - /** - * Reload settings by calling loadSettings. - * @returns {Promise} The reloaded settings. - */ - reloadSettings() { - return this.loadSettings(); - } + /** * Get the current settings. * @returns {Object} The current settings. @@ -62,14 +55,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/src/service/system/stringService.js similarity index 94% rename from server/service/stringService.js rename to server/src/service/system/stringService.js index 6f9ad8cd8..5df3410f9 100755 --- a/server/service/stringService.js +++ b/server/src/service/system/stringService.js @@ -1,5 +1,7 @@ +const SERVICE_NAME = "StringService"; + class StringService { - static SERVICE_NAME = "StringService"; + static SERVICE_NAME = SERVICE_NAME; constructor(translationService) { if (StringService.instance) { @@ -11,6 +13,10 @@ class StringService { StringService.instance = this; } + get serviceName() { + return StringService.SERVICE_NAME; + } + setLanguage(language) { this._language = language; } @@ -211,23 +217,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 +377,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 +421,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/src/service/system/translationService.js similarity index 91% rename from server/service/translationService.js rename to server/src/service/system/translationService.js index de29b5eb1..20b3431e0 100755 --- a/server/service/translationService.js +++ b/server/src/service/system/translationService.js @@ -3,7 +3,6 @@ import path from "path"; class TranslationService { static SERVICE_NAME = "TranslationService"; - constructor(logger) { this.logger = logger; this.translations = {}; @@ -11,6 +10,10 @@ class TranslationService { this.localesDir = path.join(process.cwd(), "locales"); } + get serviceName() { + return TranslationService.SERVICE_NAME; + } + setLanguage(language) { this._language = language; } @@ -38,9 +41,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/src/shutdown.js b/server/src/shutdown.js new file mode 100644 index 000000000..27eb0f21d --- /dev/null +++ b/server/src/shutdown.js @@ -0,0 +1,34 @@ +import { logger } from "./utils/logger.js"; + +export const initShutdownListener = (server, services) => { + const SERVICE_NAME = "Server"; + + let isShuttingDown = false; + + const shutdown = async () => { + if (isShuttingDown) { + return; + } + isShuttingDown = true; + logger.info({ message: "Attempting graceful shutdown" }); + + try { + server.close(); + await services.jobQueue.shutdown(); + await services.db.disconnect(); + logger.info({ message: "Graceful shutdown complete" }); + process.exit(0); + } catch (error) { + logger.error({ + message: error.message, + service: SERVICE_NAME, + method: "shutdown", + stack: error.stack, + }); + process.exit(1); + } + }; + process.on("SIGUSR2", shutdown); + process.on("SIGINT", shutdown); + process.on("SIGTERM", shutdown); +}; diff --git a/server/templates/addReview.mjml b/server/src/templates/addReview.mjml similarity index 74% rename from server/templates/addReview.mjml rename to server/src/templates/addReview.mjml index 9d00be7d0..e193ac1ba 100755 --- a/server/templates/addReview.mjml +++ b/server/src/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/src/templates/employeeActivation.mjml similarity index 92% rename from server/templates/employeeActivation.mjml rename to server/src/templates/employeeActivation.mjml index 3e35db6e8..317832c1f 100755 --- a/server/templates/employeeActivation.mjml +++ b/server/src/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/src/templates/hardwareIncident.mjml similarity index 91% rename from server/templates/hardwareIncident.mjml rename to server/src/templates/hardwareIncident.mjml index 19fb589fc..3f50a3d58 100755 --- a/server/templates/hardwareIncident.mjml +++ b/server/src/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/src/templates/noIncidentsThisWeek.mjml similarity index 97% rename from server/templates/noIncidentsThisWeek.mjml rename to server/src/templates/noIncidentsThisWeek.mjml index d31d63d10..d160e44de 100755 --- a/server/templates/noIncidentsThisWeek.mjml +++ b/server/src/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/src/templates/passwordReset.mjml similarity index 90% rename from server/templates/passwordReset.mjml rename to server/src/templates/passwordReset.mjml index e0b85a4f4..568b363ee 100755 --- a/server/templates/passwordReset.mjml +++ b/server/src/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/src/templates/serverIsDown.mjml similarity index 82% rename from server/templates/serverIsDown.mjml rename to server/src/templates/serverIsDown.mjml index b75d970e6..bf6424a6b 100755 --- a/server/templates/serverIsDown.mjml +++ b/server/src/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/src/templates/serverIsUp.mjml similarity index 91% rename from server/templates/serverIsUp.mjml rename to server/src/templates/serverIsUp.mjml index beb34481d..074c5e3dd 100755 --- a/server/templates/serverIsUp.mjml +++ b/server/src/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/testEmailTemplate.mjml b/server/src/templates/testEmailTemplate.mjml similarity index 100% rename from server/templates/testEmailTemplate.mjml rename to server/src/templates/testEmailTemplate.mjml diff --git a/server/templates/welcomeEmail.mjml b/server/src/templates/welcomeEmail.mjml similarity index 75% rename from server/templates/welcomeEmail.mjml rename to server/src/templates/welcomeEmail.mjml index 663518ec2..e9f569e3f 100755 --- a/server/templates/welcomeEmail.mjml +++ b/server/src/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/src/utils/cookieHelpers.js b/server/src/utils/cookieHelpers.js new file mode 100644 index 000000000..7005b66ca --- /dev/null +++ b/server/src/utils/cookieHelpers.js @@ -0,0 +1,26 @@ +/** + * Get standardized cookie options for authentication tokens + * @param {Object} options - Additional cookie options + * @returns {Object} Cookie options object + */ +export const getAuthCookieOptions = (options = {}) => { + return { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + maxAge: 2 * 60 * 60 * 1000, // 2 hours (matches JWT TTL) + ...options, + }; +}; + +/** + * Clear cookie options for authentication tokens + * @returns {Object} Cookie clear options object + */ +export const getClearAuthCookieOptions = () => { + return { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + }; +}; diff --git a/server/utils/dataUtils.js b/server/src/utils/dataUtils.js similarity index 80% rename from server/utils/dataUtils.js rename to server/src/utils/dataUtils.js index d164b92fd..e7fd334dd 100755 --- a/server/utils/dataUtils.js +++ b/server/src/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/demoMonitors.json b/server/src/utils/demoMonitors.json similarity index 100% rename from server/utils/demoMonitors.json rename to server/src/utils/demoMonitors.json diff --git a/server/utils/demoMonitorsOld.json b/server/src/utils/demoMonitorsOld.json similarity index 100% rename from server/utils/demoMonitorsOld.json rename to server/src/utils/demoMonitorsOld.json diff --git a/server/utils/imageProcessing.js b/server/src/utils/imageProcessing.js similarity index 100% rename from server/utils/imageProcessing.js rename to server/src/utils/imageProcessing.js diff --git a/server/utils/logger.js b/server/src/utils/logger.js similarity index 63% rename from server/utils/logger.js rename to server/src/utils/logger.js index 49b875d96..0917862fa 100755 --- a/server/utils/logger.js +++ b/server/src/utils/logger.js @@ -2,61 +2,58 @@ import { createLogger, format, transports } from "winston"; import dotenv from "dotenv"; dotenv.config(); +const SERVICE_NAME = "Logger"; + class Logger { - constructor() { + static SERVICE_NAME = SERVICE_NAME; + constructor({ envSettings }) { + this.envSettings = envSettings; 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); } - ); - const logLevel = process.env.LOG_LEVEL || "info"; + 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 = this.envSettings.logLevel || "info"; this.logger = createLogger({ level: logLevel, 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()), @@ -65,6 +62,11 @@ class Logger { ], }); } + + get serviceName() { + return Logger.SERVICE_NAME; + } + /** * Logs an informational message. * @param {Object} config - The configuration object. @@ -144,7 +146,8 @@ class Logger { } } -const logger = new Logger(); -export { Logger }; +export default Logger; -export default logger; +// Legacy logger +const logger = new Logger({ envSettings: { logLevel: "debug" } }); +export { logger }; diff --git a/server/src/utils/roleUtils.js b/server/src/utils/roleUtils.js new file mode 100644 index 000000000..f359246b2 --- /dev/null +++ b/server/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/server/utils/utils.js b/server/src/utils/utils.js similarity index 71% rename from server/utils/utils.js rename to server/src/utils/utils.js index 57d181886..bcb4d8e5e 100755 --- a/server/utils/utils.js +++ b/server/src/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/src/validation/joi.js similarity index 89% rename from server/validation/joi.js rename to server/src/validation/joi.js index d44bb14eb..31f8e97c0 100755 --- a/server/validation/joi.js +++ b/server/src/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", @@ -133,12 +124,8 @@ const getMonitorsByTeamIdQueryValidation = joi.object({ type: joi .alternatives() .try( - joi.string().valid("http", "ping", "pagespeed", "docker", "hardware", "port"), - joi - .array() - .items( - joi.string().valid("http", "ping", "pagespeed", "docker", "hardware", "port") - ) + joi.string().valid("http", "ping", "pagespeed", "docker", "hardware", "port", "game"), + joi.array().items(joi.string().valid("http", "ping", "pagespeed", "docker", "hardware", "port", "game")) ), page: joi.number(), rowsPerPage: joi.number(), @@ -184,6 +171,7 @@ const createMonitorBodyValidation = joi.object({ jsonPath: joi.string().allow(""), expectedValue: joi.string().allow(""), matchMethod: joi.string(), + gameId: joi.string().allow(""), }); const createMonitorsBodyValidation = joi.array().items( @@ -210,6 +198,7 @@ const editMonitorBodyValidation = joi.object({ usage_disk: joi.number(), usage_temperature: joi.number(), }), + gameId: joi.string(), }); const pauseMonitorParamValidation = joi.object({ @@ -307,7 +296,7 @@ const getChecksParamValidation = joi.object({ }); const getChecksQueryValidation = joi.object({ - type: joi.string().valid("http", "ping", "pagespeed", "hardware", "docker", "port"), + type: joi.string().valid("http", "ping", "pagespeed", "hardware", "docker", "port", "game"), sortOrder: joi.string().valid("asc", "desc"), limit: joi.number(), dateRange: joi.string().valid("recent", "hour", "day", "week", "month", "all"), @@ -318,8 +307,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(), @@ -431,6 +418,15 @@ const updateAppSettingsBodyValidation = joi.object({ systemEmailIgnoreTLS: joi.boolean(), systemEmailRequireTLS: joi.boolean(), systemEmailRejectUnauthorized: joi.boolean(), + + globalThresholds: joi + .object({ + cpu: joi.number().min(1).max(100).allow("").optional(), + memory: joi.number().min(1).max(100).allow("").optional(), + disk: joi.number().min(1).max(100).allow("").optional(), + temperature: joi.number().min(1).max(150).allow("").optional(), + }) + .optional(), }); //**************************************** @@ -454,8 +450,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 +480,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 +568,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 +582,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 +622,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 +675,6 @@ export { editMonitorBodyValidation, pauseMonitorParamValidation, getMonitorURLByQueryValidation, - editUserParamValidation, editUserBodyValidation, createAlertParamValidation, createAlertBodyValidation, @@ -666,7 +688,6 @@ export { createCheckBodyValidation, getChecksParamValidation, getChecksQueryValidation, - getTeamChecksParamValidation, getTeamChecksQueryValidation, ackCheckBodyValidation, ackAllChecksParamValidation, @@ -696,4 +717,8 @@ export { webhookConfigValidation, createAnnouncementValidation, sendTestEmailBodyValidation, + getUserByIdParamValidation, + editUserByIdParamValidation, + editUserByIdBodyValidation, + editSuperadminUserByIdBodyValidation, }; 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