Merge branch 'develop' into feature/backend-reachability-with-translations

This commit is contained in:
Alexander Holliday
2025-05-07 10:38:35 -07:00
committed by GitHub
106 changed files with 2317 additions and 15559 deletions

View File

@@ -0,0 +1,77 @@
name: Distribution deploy
on:
push:
branches: ["master"]
workflow_dispatch:
jobs:
docker-build-and-push-client:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Client Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate:frontend-dist \
-f ./docker/dist/client.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
.
- name: Push Client Docker image
run: docker push ghcr.io/bluewave-labs/checkmate:frontend-dist
docker-build-and-push-server:
needs: docker-build-and-push-client
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Server Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate:backend-dist \
-f ./docker/dist/server.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
.
- name: Push Server Docker image
run: docker push ghcr.io/bluewave-labs/checkmate:backend-dist
- name: Build Mongo Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate:mongo-dist \
-f ./docker/dist/mongoDB.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
.
- name: Push MongoDB Docker image
run: docker push ghcr.io/bluewave-labs/checkmate:mongo-dist
- name: Build Redis Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate:redis-dist \
-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-dist

View File

@@ -13,9 +13,6 @@ on:
required: false
default: "key_value_json"
# For automatic execution at a specific time (every day at midnight)
schedule:
- cron: "0 0 * * *"
permissions:
contents: write
@@ -88,9 +85,9 @@ jobs:
- name: Copy translations to project
run: |
mkdir -p src/locales
cp -r temp/* src/locales/
echo "Translation files copied to src/locales/"
mkdir -p client/src/locales
cp -r temp/* client/src/locales/
echo "Translation files copied to client/src/locales/"
- name: Get current date
id: date
@@ -113,6 +110,6 @@ jobs:
delete-branch: true
base: develop
add-paths: |
src/locales/*.json
client/src/locales/*.json
committer: GitHub Action <github-actions[bot]@users.noreply.github.com>
author: GitHub Action <github-actions[bot]@users.noreply.github.com>

95
.github/workflows/production-deploy.yml vendored Normal file
View File

@@ -0,0 +1,95 @@
name: Demo deploy
on:
push:
branches: ["demo"]
workflow_dispatch:
jobs:
docker-build-and-push-client:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Client Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate:frontend-demo \
-f ./docker/prod/client.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
.
- name: Push Client Docker image
run: docker push ghcr.io/bluewave-labs/checkmate:frontend-demo
docker-build-and-push-server:
needs: docker-build-and-push-client
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Server Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate:backend-demo \
-f ./docker/prod/server.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
.
- name: Push Server Docker image
run: docker push ghcr.io/bluewave-labs/checkmate:backend-demo
- name: Build Mongo Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate:mongo-demo \
-f ./docker/prod/mongoDB.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
.
- 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
steps:
- name: SSH into server and restart container using Docker Compose
uses: appleboy/ssh-action@v1.2.2
with:
host: ${{ secrets.DEMO_SERVER_HOST }}
username: ${{ secrets.DEMO_SERVER_USER }}
key: ${{ secrets.DEMO_SERVER_SSH_KEY }}
script: |
cd checkmate
git pull
cd docker/prod
docker compose down
docker compose pull
docker compose up -d

View File

@@ -87,7 +87,9 @@ jobs:
username: ${{ secrets.STAGING_SERVER_USER }}
key: ${{ secrets.STAGING_SERVER_SSH_KEY }}
script: |
cd checkmate/docker/staging
cd checkmate
git pull
cd docker/staging
docker compose down
docker compose pull
docker compose up -d

View File

@@ -6,7 +6,7 @@ on:
branches:
- develop
paths:
- "src/locales/**"
- "client/src/locales/**"
jobs:
upload-translations:
@@ -58,8 +58,8 @@ jobs:
echo "Base SHA: $BASE_SHA"
echo "Head SHA: $HEAD_SHA"
# Get list of changed files in src/locales directory
CHANGED_FILES=$(git diff --name-only $BASE_SHA..$HEAD_SHA -- 'src/locales/*.json' || git ls-files 'src/locales/*.json')
# Get list of changed files in client/src/locales directory
CHANGED_FILES=$(git diff --name-only $BASE_SHA..$HEAD_SHA -- 'client/src/locales/*.json' || git ls-files 'client/src/locales/*.json')
if [ -z "$CHANGED_FILES" ]; then
echo "No changes detected in locale files"
@@ -81,7 +81,7 @@ jobs:
# Process each changed file
for FILE in $CHANGED_FILES; do
if [[ -f "$FILE" ]]; then
# Extract language code from filename (e.g., src/locales/en.json -> en)
# Extract language code from filename (e.g., client/src/locales/en.json -> en)
FILENAME=$(basename "$FILE")
# Special case: map gb.json to en language code

View File

@@ -48,24 +48,23 @@ Ask anything in our [Discord server](https://discord.com/invite/NAb6H3UTjK) —
### Set up Checkmate locally?
Frontend:
Frontend & backend:
```bash
npm install
npm run dev
```
Backend:
Clone and follow instructions in [checkmate-backend](https://github.com/bluewave-labs/checkmate-backend).
By default, the frontend expects the backend on `http://localhost:3001`. Update configs if needed.
### Start contributing code?
1. Pick or open an issue (check `good-first-issue`s first)
2. Ask to be assigned. If there is alrady someone assigned and it's been more than 7 days, you can raise the flag and ask to be assigned as well.
3. Create a branch from `develop`.
4. Write your code.
5. Run and test locally.
6. Open a PR to `develop`.
2. (optional but highly suggested) Read a detailed structure of [Checkmate](https://deepwiki.com/bluewave-labs/Checkmate) if you would like to deep dive into the architecture.
3. Ask to be assigned. If there is alrady someone assigned and it's been more than 7 days, you can raise the flag and ask to be assigned as well.
4. Create a branch from `develop`.
5. Write your code.
6. Run and test locally.
7. Open a PR to `develop`.
Start with [good first issues](https://github.com/bluewave-labs/checkmate/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22).

View File

@@ -15,7 +15,7 @@
![dashboard](https://github.com/user-attachments/assets/252d6047-522b-4576-8f14-233510e464b8)
This repository contains the **frontend** 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.
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.
@@ -23,11 +23,9 @@ Checkmate has been stress-tested with 1000+ active monitors without any particul
We **love** what we are building here, and we continuously learn a few things about Reactjs, Nodejs, MongoDB, and Docker while building Checkmate.
For backend files, please check [Checkmate backend](https://github.com/bluewave-labs/checkmate-backend) repository.
## 📦 Demo
See [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).
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
@@ -71,11 +69,10 @@ If you have any questions, suggestions or comments, please use our [Discord chan
**Short term roadmap:**
- Global (distributed) uptime checking on Solana network (**in progress**) https://github.com/bluewave-labs/Checkmate/issues/1593
- Status pages (**in progress**) https://github.com/bluewave-labs/Checkmate/issues/1131
- Translations (i18n) (**in progress**)
- Better notification options (Webhooks, Discord, Telegram, Slack) (**in progress**) https://github.com/bluewave-labs/Checkmate/issues/1545
- JSON query monitoring https://github.com/bluewave-labs/Checkmate/issues/1573
- JSON query monitoring (**in progress**) https://github.com/bluewave-labs/Checkmate/issues/1573
- Tagging/grouping monitors https://github.com/bluewave-labs/Checkmate/issues/1546
- More configuration options
- DNS monitoring
@@ -120,10 +117,11 @@ Here's how you can contribute:
0. Star this repo :)
1. Check [Contributor's guideline](https://github.com/bluewave-labs/Checkmate/blob/develop/CONTRIBUTING.md). First timers are encouraged to check `good-first-issue` tag.
2. Optionally, read [project structure](https://docs.checkmate.so/developers-guide/general-project-structure) and [high level overview](https://bluewavelabs.gitbook.io/checkmate/developers-guide/high-level-overview).
3. Open an issue if you believe you've encountered a bug.
4. Check for good-first-issue's if you are a newcomer.
5. Make a pull request to add new features/make quality-of-life improvements/fix bugs.
2. Check [project structure](https://docs.checkmate.so/developers-guide/general-project-structure) and [high level overview](https://bluewavelabs.gitbook.io/checkmate/developers-guide/high-level-overview).
3. Read a detailed structure of [Checkmate](https://deepwiki.com/bluewave-labs/Checkmate) if you would like to deep dive into the architecture.
4. Open an issue if you believe you've encountered a bug.
5. Check for good-first-issue's if you are a newcomer.
6. Make a pull request to add new features/make quality-of-life improvements/fix bugs.
<a href="https://github.com/bluewave-labs/checkmate/graphs/contributors">
<img src="https://contrib.rocks/image?repo=bluewave-labs/checkmate" />
@@ -137,8 +135,6 @@ Thanks to [Gitbook](https://gitbook.io/) for giving us a free tier for their doc
Also check other developer and contributor-friendly projects of BlueWave:
- [LangRoute](https://github.com/bluewave-labs/langroute), an LLM proxy and gateway
- [DataRoom](https://github.com/bluewave-labs/bluewave-dataroom), an secure file sharing application, aka dataroom.
- [Headcount](https://github.com/bluewave-labs/bluewave-hrm), a complete Human Resource Management platform.
- [Guidefox](https://github.com/bluewave-labs/guidefox), an application that helps new users learn how to use your product via hints, tours, popups and banners.
- [VerifyWise](https://github.com/bluewave-labs/verifywise), the first open source AI governance platform.
- [DataRoom](https://github.com/bluewave-labs/bluewave-dataroom), an secure file sharing application, aka dataroom.
- [Guidefox](https://github.com/bluewave-labs/guidefox), an application that helps new users learn how to use your product via hints, tours, popups and banners.

View File

@@ -1,144 +1,2 @@
<p align=center> <a href="https://trendshift.io/repositories/12443" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12443" alt="bluewave-labs%2Fcheckmate | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a></p>
![](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 Best Practices](https://www.bestpractices.dev/projects/9901/badge)](https://www.bestpractices.dev/projects/9901)
<h1 align="center"><a href="https://bluewavelabs.ca" target="_blank">Checkmate</a></h1>
<p align="center"><strong>An open source uptime and infrastructure monitoring application</strong></p>
![dashboard](https://github.com/user-attachments/assets/252d6047-522b-4576-8f14-233510e464b8)
This repository contains the **frontend** 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 has been stress-tested with 1000+ active monitors without any particular issues or performance bottlenecks.
We **love** what we are building here, and we continuously learn a few things about Reactjs, Nodejs, MongoDB, and Docker while building Checkmate.
For backend files, please check [Checkmate backend](https://github.com/bluewave-labs/checkmate-backend) repository.
## 📦 Demo
See [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
Usage instructions can be found [here](https://docs.checkmate.so/). 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
See installation instructions in [Checkmate documentation portal](https://docs.checkmate.so/quickstart). Alternatively, you can also use [Coolify](https://coolify.io/) or [Elestio](https://elest.io/open-source/checkmate) for a one-click Docker deployment. If you would like to monitor your server infrastructure, you'll need [Capture agent](https://github.com/bluewave-labs/capture). Capture repository also contains the installation instructions.
## 🏁 Translations
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
Thanks to extensive optimizations, Checkmate operates with an exceptionally small memory footprint, requiring minimal memory and CPU resources. Heres the memory usage of a Node.js instance running on a server that monitors 323 servers every minute:
![image](https://github.com/user-attachments/assets/37e04a75-d83a-488f-b25c-025511b492c9)
You can see the memory footprint of MongoDB and Redis on the same server (398Mb and 15Mb) for the same amount of servers:
![image](https://github.com/user-attachments/assets/3b469e85-e675-4040-a162-3f24c1afc751)
## 💚 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!
## 🧩 Features
- Completely open source, deployable on your servers
- Website monitoring
- Page speed monitoring
- Infrastructure monitoring (memory, disk usage, CPU performance etc) - requires [Capture](https://github.com/bluewave-labs/capture)
- Docker monitoring
- Ping monitoring
- SSL monitoring
- Port monitoring
- Incidents at a glance
- E-mail notifications
- Scheduled maintenance
**Short term roadmap:**
- Global (distributed) uptime checking on Solana network (**in progress**) https://github.com/bluewave-labs/Checkmate/issues/1593
- Status pages (**in progress**) https://github.com/bluewave-labs/Checkmate/issues/1131
- Translations (i18n) (**in progress**)
- Better notification options (Webhooks, Discord, Telegram, Slack) (**in progress**) https://github.com/bluewave-labs/Checkmate/issues/1545
- JSON query monitoring https://github.com/bluewave-labs/Checkmate/issues/1573
- Tagging/grouping monitors https://github.com/bluewave-labs/Checkmate/issues/1546
- More configuration options
- DNS monitoring
## 🏗️ Screenshots
<p>
<img width="2714" alt="server" src="https://github.com/user-attachments/assets/f7cb272a-69a6-48c5-93b0-249ecf20ecc6" />
</p>
<p>
<img width="2714" alt="uptime" src="https://github.com/user-attachments/assets/98ddc6c0-3384-47fd-96ce-7e53e6b688ac" />
</p>
<p>
<img width="2714" alt="page speed" src="https://github.com/user-attachments/assets/b5589f79-da30-4239-9846-1f8bb2637ff9" />
</p>
## 🏗️ Tech stack
- [ReactJs](https://react.dev/)
- [MUI (React framework)](https://mui.com/)
- [Node.js](https://nodejs.org/en)
- [MongoDB](https://mongodb.com)
- [Recharts](https://recharts.org)
- Lots of other open source components!
## A few links
- If you would like to support us, please consider giving it a ⭐ and click on "watch".
- Have a question or suggestion for the roadmap/featureset? Check our [Discord channel](https://discord.gg/NAb6H3UTjK) or [Discussions](https://github.com/bluewave-labs/checkmate/discussions) forum.
- 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
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), and [Aryaman](https://github.com/Br0wnHammer) 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 4.6K+ stars and attracted 60 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 dont hold back — jump in, contribute and learn with us!
Here's how you can contribute:
0. Star this repo :)
1. Check [Contributor's guideline](https://github.com/bluewave-labs/Checkmate/blob/develop/CONTRIBUTING.md). First timers are encouraged to check `good-first-issue` tag.
2. Optionally, read [project structure](https://docs.checkmate.so/developers-guide/general-project-structure) and [high level overview](https://bluewavelabs.gitbook.io/checkmate/developers-guide/high-level-overview).
3. Open an issue if you believe you've encountered a bug.
4. Check for good-first-issue's if you are a newcomer.
5. Make a pull request to add new features/make quality-of-life improvements/fix bugs.
<a href="https://github.com/bluewave-labs/checkmate/graphs/contributors">
<img src="https://contrib.rocks/image?repo=bluewave-labs/checkmate" />
</a>
## 💰 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
[![Star History Chart](https://api.star-history.com/svg?repos=bluewave-labs/checkmate&type=Date)](https://star-history.com/#bluewave-labs/bluewave-uptime&Date)
Also check other developer and contributor-friendly projects of BlueWave:
- [LangRoute](https://github.com/bluewave-labs/langroute), an LLM proxy and gateway
- [DataRoom](https://github.com/bluewave-labs/bluewave-dataroom), an secure file sharing application, aka dataroom.
- [Headcount](https://github.com/bluewave-labs/bluewave-hrm), a complete Human Resource Management platform.
- [Guidefox](https://github.com/bluewave-labs/guidefox), an application that helps new users learn how to use your product via hints, tours, popups and banners.
- [VerifyWise](https://github.com/bluewave-labs/verifywise), the first open source AI governance platform.
This directory contains the client side (frontend) of Checkmate.

13698
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -22,12 +22,6 @@
"@mui/lab": "6.0.0-dev.240424162023-9968b4889d",
"@mui/material": "6.4.11",
"@reduxjs/toolkit": "2.7.0",
"@solana/wallet-adapter-base": "0.9.25",
"@solana/wallet-adapter-material-ui": "0.16.35",
"@solana/wallet-adapter-react": "0.15.37",
"@solana/wallet-adapter-react-ui": "0.9.37",
"@solana/wallet-adapter-wallets": "0.19.34",
"@solana/web3.js": "1.98.0",
"axios": "^1.7.4",
"dayjs": "1.11.13",
"flag-icons": "7.3.2",
@@ -52,6 +46,14 @@
"redux-persist": "6.0.0",
"vite-plugin-svgr": "^4.2.0"
},
"unusedDepencies": {
"@solana/wallet-adapter-base": "0.9.25",
"@solana/wallet-adapter-material-ui": "0.16.35",
"@solana/wallet-adapter-react": "0.15.37",
"@solana/wallet-adapter-react-ui": "0.9.37",
"@solana/wallet-adapter-wallets": "0.19.34",
"@solana/web3.js": "1.98.0"
},
"devDependencies": {
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",

View File

@@ -0,0 +1,6 @@
type,name,url,interval,port,expectedValue,jsonPath,matchMethod
http,HTTP Monitor,https://www.google.com,60000,,,,
http,API Monitor,http://reqres.in/api/users/2,120000,,Janet,data.first_name,equal
ping,Ping monitor,127.0.0.1,60000,,,,
docker,Docker monitor,bbdd20b4e3757d63e070bc16e04d3f770c4a8f6e62e813de380f23b141e2ca01,60000,,,,
port,Port monitor,127.0.0.1,60000,5000,,,
1 type name url interval port expectedValue jsonPath matchMethod
2 http HTTP Monitor https://www.google.com 60000
3 http API Monitor http://reqres.in/api/users/2 120000 Janet data.first_name equal
4 ping Ping monitor 127.0.0.1 60000
5 docker Docker monitor bbdd20b4e3757d63e070bc16e04d3f770c4a8f6e62e813de380f23b141e2ca01 60000
6 port Port monitor 127.0.0.1 60000 5000

View File

@@ -0,0 +1 @@
type,name,url,interval,port,expectedValue,jsonPath,matchMethod
1 type name url interval port expectedValue jsonPath matchMethod

View File

@@ -1,43 +1,18 @@
import { useEffect } from "react";
import { useSelector } from "react-redux";
import { useDispatch } from "react-redux";
import "react-toastify/dist/ReactToastify.css";
import { ToastContainer } from "react-toastify";
import { ThemeProvider } from "@emotion/react";
import lightTheme from "./Utils/Theme/lightTheme";
import darkTheme from "./Utils/Theme/darkTheme";
import { CssBaseline, GlobalStyles } from "@mui/material";
import { getAppSettings } from "./Features/Settings/settingsSlice";
import { logger } from "./Utils/Logger"; // Import the logger
import { networkService } from "./main";
import { Routes } from "./Routes";
import WalletProvider from "./Components/WalletProvider";
import { useTranslation } from "react-i18next";
import { setLanguage } from "./Features/UI/uiSlice";
function App() {
const mode = useSelector((state) => state.ui.mode);
const { authToken } = useSelector((state) => state.auth);
const dispatch = useDispatch();
const { i18n } = useTranslation();
useEffect(() => {
if (authToken) {
dispatch(getAppSettings({ authToken })).then((action) => {
if (action.payload && action.payload.success) {
const { language } = action.payload.data;
const availableLanguages = Object.keys(i18n.options.resources || {});
if (language && availableLanguages.includes(language)) {
dispatch(setLanguage(language));
i18n.changeLanguage(language);
} else {
dispatch(setLanguage(availableLanguages[0]));
i18n.changeLanguage(availableLanguages[0]);
}
}
});
}
}, [dispatch, authToken, i18n]);
// Cleanup
useEffect(() => {

View File

@@ -118,6 +118,17 @@ const Fallback = ({ title, checks, link = "/", isAdmin, vowelStart = false, show
>
Let's create your first {title}
</Button>
{/* Bulk create of uptime monitors */}
{title === "uptime monitor" && (
<Button
variant="contained"
color="accent"
sx={{ alignSelf: "center" }}
onClick={() => navigate("/uptime/bulk-import")}
>
{t("bulkImport.fallbackPage")}
</Button>
)}
{/* Warning box for PageSpeed monitor */}
{(title === "pagespeed monitor" && showPageSpeedWarning) && (

View File

@@ -1,18 +0,0 @@
.MuiStack-root:has(#modal-update-picture) h1.MuiTypography-root {
font-weight: 600;
}
.image-field-wrapper h2.MuiTypography-root,
.MuiStack-root:has(#modal-update-picture) button,
.MuiStack-root:has(#modal-update-picture) h1.MuiTypography-root {
font-size: var(--env-var-font-size-medium);
}
.image-field-wrapper h2.MuiTypography-root {
margin-top: 10px;
}
.image-field-wrapper + p.MuiTypography-root {
margin-top: 8px;
}
.image-field-wrapper + p.MuiTypography-root,
.image-field-wrapper p.MuiTypography-root {
font-size: var(--env-var-font-size-small-plus);
}

View File

@@ -1,175 +0,0 @@
import React, { useState } from "react";
import PropTypes from "prop-types";
import { Box, IconButton, Stack, TextField, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import CloudUploadIcon from "@mui/icons-material/CloudUpload";
import "./index.css";
import { checkImage } from "../../../Utils/fileUtils";
/**
* @param {Object} props - The component props.
* @param {string} props.id - The unique identifier for the input field.
* @param {string} props.src - The URL of the image to display.
* @param {function} props.onChange - The function to handle file input change.
* @param {boolean} props.isRound - Whether the shape of the image to display is round.
* @param {string} props.maxSize - Custom message for the max uploaded file size
* @returns {JSX.Element} The rendered component.
*/
const ImageField = ({ id, src, loading, onChange, error, isRound = true, maxSize }) => {
const theme = useTheme();
const error_border_style = error ? { borderColor: theme.palette.error.main } : {};
const roundShape = isRound ? { borderRadius: "50%" } : {};
const [isDragging, setIsDragging] = useState(false);
const handleDragEnter = () => {
setIsDragging(true);
};
const handleDragLeave = () => {
setIsDragging(false);
};
return (
<>
{!checkImage(src) || loading ? (
<>
<Box
className="image-field-wrapper"
mt={theme.spacing(8)}
sx={{
position: "relative",
height: "fit-content",
border: "dashed",
borderRadius: theme.shape.borderRadius,
borderColor: isDragging
? theme.palette.primary.main
: theme.palette.primary.lowContrast,
borderWidth: "2px",
transition: "0.2s",
"&:hover": {
borderColor: theme.palette.primary.main,
backgroundColor: "hsl(215, 87%, 51%, 0.05)",
},
...error_border_style,
}}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDrop={handleDragLeave}
>
<TextField
id={id}
type="file"
onChange={onChange}
sx={{
width: "100%",
"& .MuiInputBase-input[type='file']": {
opacity: 0,
cursor: "pointer",
maxWidth: "500px",
minHeight: "175px",
zIndex: 1,
},
"& fieldset": {
padding: 0,
border: "none",
},
}}
/>
<Stack
className="custom-file-text"
alignItems="center"
gap="4px"
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
zIndex: 0,
width: "100%",
}}
>
<IconButton
sx={{
pointerEvents: "none",
borderRadius: theme.shape.borderRadius,
border: `solid ${theme.shape.borderThick}px ${theme.palette.primary.lowContrast}`,
boxShadow: theme.shape.boxShadow,
}}
>
<CloudUploadIcon />
</IconButton>
<Typography
component="h2"
color={theme.palette.primary.contrastTextTertiary}
>
<Typography
component="span"
fontSize="inherit"
color="info"
fontWeight={500}
>
Click to upload
</Typography>{" "}
or drag and drop
</Typography>
<Typography
component="p"
color={theme.palette.primary.contrastTextTertiary}
sx={{ opacity: 0.6 }}
>
(maximum size: {maxSize ?? "3MB"})
</Typography>
</Stack>
</Box>
<Typography
component="p"
color={theme.palette.primary.contrastTextTertiary}
sx={{ opacity: 0.6 }}
>
Supported formats: JPG, PNG
</Typography>
{error && (
<Typography
component="span"
className="input-error"
color={theme.palette.error.main}
mt={theme.spacing(2)}
sx={{
opacity: 0.8,
}}
>
{error}
</Typography>
)}
</>
) : (
<Stack
direction="row"
justifyContent="center"
>
<Box
sx={{
width: "250px",
height: "250px",
overflow: "hidden",
backgroundImage: `url(${src})`,
backgroundSize: "cover",
...roundShape,
}}
></Box>
</Stack>
)}
</>
);
};
ImageField.propTypes = {
id: PropTypes.string.isRequired,
src: PropTypes.string,
onChange: PropTypes.func.isRequired,
isRound: PropTypes.bool,
maxSize: PropTypes.string,
};
export default ImageField;

View File

@@ -0,0 +1,243 @@
// Components
import { Box, Stack, Typography } from "@mui/material";
import CloudUploadIcon from "@mui/icons-material/CloudUpload";
import Image from "../../../Components/Image";
import TextField from "@mui/material/TextField";
import IconButton from "@mui/material/IconButton";
import ProgressUpload from "../../ProgressBars";
import ImageIcon from "@mui/icons-material/Image";
// Utils
import PropTypes from "prop-types";
import { useCallback, useState, useRef, useEffect } from "react";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
/**
* ImageUpload component allows users to upload images with drag-and-drop functionality.
* It supports file size and format validation.
*
* @component
* @param {Object} props - Component props
* @param {boolean} [props.previewIsRound=false] - Determines if the image preview should be round
* @param {string} [props.src] - Source URL of the image to display
* @param {function} props.onChange - Callback function to handle file change, takes a file as an argument
* @param {number} [props.maxSize=3145728] - Maximum file size allowed in bytes (default is 3MB)
* @param {Array<string>} [props.accept=['jpg', 'jpeg', 'png']] - Array of accepted file formats
* @param {Object} [props.errors] - Object containing error messages
* @returns {JSX.Element} The rendered component
*/
const ImageUpload = ({
previewIsRound = false,
src,
onChange,
maxSize = 3 * 1024 * 1024,
accept = ["jpg", "jpeg", "png"],
error,
}) => {
const theme = useTheme();
const { t } = useTranslation();
const [uploadComplete, setUploadComplete] = useState(false);
const [completedFile, setCompletedFile] = useState(null);
const [file, setFile] = useState(null);
const [progress, setProgress] = useState({ value: 0, isLoading: false });
const intervalRef = useRef(null);
const [localError, setLocalError] = useState(null);
const [isDragging, setIsDragging] = useState(false);
const roundStyle = previewIsRound ? { borderRadius: "50%" } : {};
const handleImageChange = useCallback(
(file) => {
if (!file) return;
const isValidType = accept.some((type) =>
file.type.includes(type)
);
const isValidSize = file.size <= maxSize;
if (!isValidType) {
setLocalError(t('invalidFileFormat'));
return;
}
if (!isValidSize) {
setLocalError(t('invalidFileSize'));
return;
}
setLocalError(null);
const previewFile = {
src: URL.createObjectURL(file),
name: file.name,
file,
};
setFile(previewFile);
setProgress({ value: 0, isLoading: true });
intervalRef.current = setInterval(() => {
setProgress((prev) => {
const buffer = 12;
if (prev.value + buffer >= 100) {
clearInterval(intervalRef.current);
setUploadComplete(true);
setCompletedFile(previewFile);
return { value: 100, isLoading: false };
}
return { value: prev.value + buffer, isLoading: true };
});
}, 120);
},
[maxSize, accept]
);
useEffect(() => {
if (uploadComplete && completedFile) {
onChange?.(completedFile);
setUploadComplete(false);
setCompletedFile(null);
}
}, [uploadComplete, completedFile, onChange]);
return (
<>
{src ? (
<Stack direction="row" justifyContent="center">
<Image
alt="Uploaded preview"
src={src}
width="250px"
height="250px"
sx={{ ...roundStyle }}
/>
</Stack>
) : (
<>
<Box
className="image-field-wrapper"
mt={theme.spacing(8)}
onDragEnter={() => setIsDragging(true)}
onDragLeave={() => setIsDragging(false)}
onDrop={() => setIsDragging(false)}
sx={{
position: "relative",
height: "fit-content",
border: "dashed",
borderRadius: theme.shape.borderRadius,
borderColor: isDragging
? theme.palette.primary.main
: theme.palette.primary.lowContrast,
backgroundColor: isDragging
? "hsl(215, 87%, 51%, 0.05)"
: "transparent",
borderWidth: "2px",
transition: "0.2s",
"&:hover": {
borderColor: theme.palette.primary.main,
backgroundColor: "hsl(215, 87%, 51%, 0.05)",
},
}}
>
<TextField
type="file"
onChange={(e) => handleImageChange(e?.target?.files?.[0])}
sx={{
width: "100%",
"& .MuiInputBase-input[type='file']": {
opacity: 0,
cursor: "pointer",
maxWidth: "500px",
minHeight: "175px",
zIndex: 1,
},
"& fieldset": {
padding: 0,
border: "none",
},
}}
/>
<Stack
alignItems="center"
gap="4px"
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
zIndex: 0,
width: "100%",
}}
>
<IconButton
sx={{
pointerEvents: "none",
borderRadius: theme.shape.borderRadius,
border: `solid ${theme.shape.borderThick}px ${theme.palette.primary.lowContrast}`,
boxShadow: theme.shape.boxShadow,
}}
>
<CloudUploadIcon />
</IconButton>
<Typography
component="h2"
color={theme.palette.primary.contrastTextTertiary}
>
<Typography
component="span"
fontSize="inherit"
color="info"
fontWeight={500}
>
{t('ClickUpload')}
</Typography>{" "}
or {t('DragandDrop')}
</Typography>
<Typography
component="p"
color={theme.palette.primary.contrastTextTertiary}
sx={{ opacity: 0.6 }}
>({t('MaxSize')}: {Math.round(maxSize / 1024 / 1024)}MB)
</Typography>
</Stack>
</Box>
{(localError || progress.isLoading || progress.value !== 0) && (
<ProgressUpload
icon={<ImageIcon />}
label={file?.name || "Upload failed"}
size={file?.size}
progress={progress.value}
onClick={() => {
clearInterval(intervalRef.current);
setFile(null);
setProgress({ value: 0, isLoading: false });
setLocalError(null);
onChange(undefined);
}}
error={localError || error}
/>
)}
<Typography
component="p"
color={theme.palette.primary.contrastTextTertiary}
sx={{ opacity: 0.6 }}
>
{t('SupportedFormats')}: {accept.join(", ").toUpperCase()}
</Typography>
</>
)}
</>
);
};
ImageUpload.propTypes = {
previewIsRound: PropTypes.bool,
src: PropTypes.string,
onChange: PropTypes.func,
maxSize: PropTypes.number,
accept: PropTypes.array,
error: PropTypes.string,
};
export default ImageUpload;

View File

@@ -1,30 +1,49 @@
import { Stack, Button } from "@mui/material";
import { useNavigate } from "react-router-dom";
import PropTypes from "prop-types";
import SkeletonLayout from "./skeleton";
import { useTranslation } from "react-i18next";
import { useTheme } from "@emotion/react";
const CreateMonitorHeader = ({
isAdmin,
label = "Create new",
shouldRender = true,
isLoading = true,
path,
bulkPath,
}) => {
const navigate = useNavigate();
const { t } = useTranslation();
const theme = useTheme();
if (!isAdmin) return null;
if (!shouldRender) return <SkeletonLayout />;
return (
<Stack
direction="row"
justifyContent="end"
alignItems="center"
gap={theme.spacing(6)}
>
<Button
loading={isLoading}
variant="contained"
color="accent"
onClick={() => navigate(path)}
>
{label}
</Button>
{bulkPath && (
<Button
loading={isLoading}
variant="contained"
color="accent"
onClick={() => {
navigate(`${bulkPath}`);
}}
>
{t("bulkImport.title")}
</Button>
)}
</Stack>
);
};
@@ -33,7 +52,8 @@ export default CreateMonitorHeader;
CreateMonitorHeader.propTypes = {
isAdmin: PropTypes.bool.isRequired,
shouldRender: PropTypes.bool,
isLoading: PropTypes.bool,
path: PropTypes.string.isRequired,
label: PropTypes.string,
bulkPath: PropTypes.string,
};

View File

@@ -25,10 +25,12 @@ const MonitorStatusHeader = ({ path, isLoading = false, isAdmin, monitor }) => {
<Stack
direction="row"
alignItems={"center"}
gap={theme.spacing(2)}
gap={theme.spacing(4)}
>
<PulseDot color={statusColor[determineState(monitor)]} />
<Typography variant="h2">
<Typography variant="h2"
style={{fontFamily : "monospace" , fontWeight : 'bolder'}}
>
{monitor?.url?.replace(/^https?:\/\//, "") || "..."}
</Typography>
<Dot />

View File

@@ -55,10 +55,9 @@ const MonitorTimeFrameHeader = ({
return (
<Stack
direction="row"
justifyContent="space-between"
alignItems="flex-end"
justifyContent="flex-end"
alignItems="center"
gap={theme.spacing(4)}
mb={theme.spacing(8)}
>
<Typography variant="body2">
Showing statistics for past{" "}

View File

@@ -1,16 +1,17 @@
import { useState, useMemo } from "react";
import { useState, useMemo, useEffect, useCallback } from "react";
import { useTranslation } from "react-i18next";
import PropTypes from "prop-types";
import {
Dialog,
DialogContent,
DialogActions,
Button,
Typography,
Box,
Tabs,
Tab,
import {
Dialog,
DialogContent,
DialogActions,
Button,
Typography,
Box,
Tabs,
Tab,
Stack,
} from "@mui/material";
import { useTheme } from "@emotion/react";
import TabPanel from "./TabPanel";
@@ -19,335 +20,400 @@ import useNotifications from "../Hooks/useNotification";
// Define constants for notification types to avoid magic values
const NOTIFICATION_TYPES = {
SLACK: 'slack',
DISCORD: 'discord',
TELEGRAM: 'telegram',
WEBHOOK: 'webhook'
SLACK: "slack",
DISCORD: "discord",
TELEGRAM: "telegram",
WEBHOOK: "webhook",
};
// Define constants for field IDs
const FIELD_IDS = {
WEBHOOK: 'webhook',
TOKEN: 'token',
CHAT_ID: 'chatId',
URL: 'url'
WEBHOOK: "webhook",
TOKEN: "token",
CHAT_ID: "chatId",
URL: "url",
};
const NotificationIntegrationModal = ({
open,
onClose,
monitor,
setMonitor,
// Optional prop to configure available notification types
notificationTypes = null
const NotificationIntegrationModal = ({
open,
onClose,
monitor,
setMonitor,
// Optional prop to configure available notification types
notificationTypes = null,
}) => {
const { t } = useTranslation();
const theme = useTheme();
const [tabValue, setTabValue] = useState(0);
const [loading, _, sendTestNotification] = useNotifications();
// Helper to get the field state key with error handling
const getFieldKey = (typeId, fieldId) => {
if (typeof typeId !== 'string' || typeId === '') {
throw new Error(t('errorInvalidTypeId'));
}
if (typeof fieldId !== 'string' || fieldId === '') {
throw new Error(t('errorInvalidFieldId'));
}
return `${typeId}${fieldId.charAt(0).toUpperCase() + fieldId.slice(1)}`;
};
// Define notification types
const DEFAULT_NOTIFICATION_TYPES = [
{
id: NOTIFICATION_TYPES.SLACK,
label: t('notifications.slack.label'),
description: t('notifications.slack.description'),
fields: [
{
id: FIELD_IDS.WEBHOOK,
label: t('notifications.slack.webhookLabel'),
placeholder: t('notifications.slack.webhookPlaceholder'),
type: 'text'
}
]
},
{
id: NOTIFICATION_TYPES.DISCORD,
label: t('notifications.discord.label'),
description: t('notifications.discord.description'),
fields: [
{
id: FIELD_IDS.WEBHOOK,
label: t('notifications.discord.webhookLabel'),
placeholder: t('notifications.discord.webhookPlaceholder'),
type: 'text'
}
]
},
{
id: NOTIFICATION_TYPES.TELEGRAM,
label: t('notifications.telegram.label'),
description: t('notifications.telegram.description'),
fields: [
{
id: FIELD_IDS.TOKEN,
label: t('notifications.telegram.tokenLabel'),
placeholder: t('notifications.telegram.tokenPlaceholder'),
type: 'text'
},
{
id: FIELD_IDS.CHAT_ID,
label: t('notifications.telegram.chatIdLabel'),
placeholder: t('notifications.telegram.chatIdPlaceholder'),
type: 'text'
}
]
},
{
id: NOTIFICATION_TYPES.WEBHOOK,
label: t('notifications.webhook.label'),
description: t('notifications.webhook.description'),
fields: [
{
id: FIELD_IDS.URL,
label: t('notifications.webhook.urlLabel'),
placeholder: t('notifications.webhook.urlPlaceholder'),
type: 'text'
}
]
}
];
const { t } = useTranslation();
const theme = useTheme();
const [tabValue, setTabValue] = useState(0);
// Use provided notification types or default to our translated ones
const activeNotificationTypes = notificationTypes || DEFAULT_NOTIFICATION_TYPES;
// Memoized function to initialize integrations state
const initialIntegrationsState = useMemo(() => {
const state = {};
activeNotificationTypes.forEach(type => {
// Add enabled flag for each notification type
state[type.id] = monitor?.notifications?.some(n => n.type === type.id) || false;
// Add state for each field in the notification type
type.fields.forEach(field => {
const fieldKey = getFieldKey(type.id, field.id);
state[fieldKey] = monitor?.notifications?.find(n => n.type === type.id)?.[field.id] || "";
});
});
return state;
}, [monitor, activeNotificationTypes]); // Only recompute when these dependencies change
const [integrations, setIntegrations] = useState(initialIntegrationsState);
const [loading, _, sendTestNotification] = useNotifications();
const handleChangeTab = (event, newValue) => {
setTabValue(newValue);
};
// Helper to get the field state key with error handling
const getFieldKey = useCallback(
(typeId, fieldId) => {
if (typeof typeId !== "string" || typeId === "") {
throw new Error(t("errorInvalidTypeId"));
}
const handleIntegrationChange = (type, checked) => {
setIntegrations(prev => ({
...prev,
[type]: checked
}));
};
if (typeof fieldId !== "string" || fieldId === "") {
throw new Error(t("errorInvalidFieldId"));
}
const handleInputChange = (type, value) => {
setIntegrations(prev => ({
...prev,
[type]: value
}));
};
return `${typeId}${fieldId.charAt(0).toUpperCase() + fieldId.slice(1)}`;
},
[t]
);
const handleTestNotification = async (type) => {
// Get the notification type details
const notificationType = activeNotificationTypes.find(t => t.id === type);
if (typeof notificationType === "undefined") {
return;
}
// Prepare config object based on notification type
const config = {};
// Add each field value to the config object
notificationType.fields.forEach(field => {
const fieldKey = getFieldKey(type, field.id);
config[field.id] = integrations[fieldKey];
});
await sendTestNotification(type, config);
};
// In NotificationIntegrationModal.jsx, update the handleSave function:
// Define notification types
const DEFAULT_NOTIFICATION_TYPES = [
{
id: NOTIFICATION_TYPES.SLACK,
label: t("notifications.slack.label"),
description: t("notifications.slack.description"),
fields: [
{
id: FIELD_IDS.WEBHOOK,
label: t("notifications.slack.webhookLabel"),
placeholder: t("notifications.slack.webhookPlaceholder"),
type: "text",
},
],
},
{
id: NOTIFICATION_TYPES.DISCORD,
label: t("notifications.discord.label"),
description: t("notifications.discord.description"),
fields: [
{
id: FIELD_IDS.WEBHOOK,
label: t("notifications.discord.webhookLabel"),
placeholder: t("notifications.discord.webhookPlaceholder"),
type: "text",
},
],
},
{
id: NOTIFICATION_TYPES.TELEGRAM,
label: t("notifications.telegram.label"),
description: t("notifications.telegram.description"),
fields: [
{
id: FIELD_IDS.TOKEN,
label: t("notifications.telegram.tokenLabel"),
placeholder: t("notifications.telegram.tokenPlaceholder"),
type: "text",
},
{
id: FIELD_IDS.CHAT_ID,
label: t("notifications.telegram.chatIdLabel"),
placeholder: t("notifications.telegram.chatIdPlaceholder"),
type: "text",
},
],
},
{
id: NOTIFICATION_TYPES.WEBHOOK,
label: t("notifications.webhook.label"),
description: t("notifications.webhook.description"),
fields: [
{
id: FIELD_IDS.URL,
label: t("notifications.webhook.urlLabel"),
placeholder: t("notifications.webhook.urlPlaceholder"),
type: "text",
},
],
},
];
const handleSave = () => {
// Get existing notifications
const notifications = [...(monitor?.notifications || [])];
// Get all notification types IDs
const existingTypes = activeNotificationTypes.map(type => type.id);
// Filter out notifications that are configurable in this modal
const filteredNotifications = notifications.filter(
notification => {
if (notification.platform) {
return !existingTypes.includes(notification.platform);
}
return !existingTypes.includes(notification.type);
}
);
// Use provided notification types or default to our translated ones
const activeNotificationTypes = notificationTypes || DEFAULT_NOTIFICATION_TYPES;
// Add each enabled notification with its configured fields
activeNotificationTypes.forEach(type => {
if (integrations[type.id]) {
// Memoized function to initialize integrations state
const initialIntegrationsState = useMemo(() => {
const state = {};
let notificationObject = {
type: "webhook",
platform: type.id, // Set platform to identify the specific service
config: {}
};
// Configure based on notification type
switch(type.id) {
case "slack":
case "discord":
notificationObject.config.webhookUrl = integrations[getFieldKey(type.id, 'webhook')];
break;
case "telegram":
notificationObject.config.botToken = integrations[getFieldKey(type.id, 'token')];
notificationObject.config.chatId = integrations[getFieldKey(type.id, 'chatId')];
break;
case "webhook":
notificationObject.config.webhookUrl = integrations[getFieldKey(type.id, 'url')];
break;
}
filteredNotifications.push(notificationObject);
}
});
activeNotificationTypes.forEach((type) => {
// Add enabled flag for each notification type
state[type.id] = false;
// Update monitor with new notifications
setMonitor(prev => ({
...prev,
notifications: filteredNotifications
}));
onClose();
};
// Add state for each field in the notification type
type.fields.forEach((field) => {
const fieldKey = getFieldKey(type.id, field.id);
state[fieldKey] = "";
});
});
return (
<Dialog
open={open}
onClose={onClose}
fullWidth
maxWidth="md"
sx={{
'& .MuiDialog-paper': {
width: `calc(80% - ${theme.spacing(40)})`,
maxWidth: `${theme.breakpoints.values.md - 70}px`
}
}}
>
<DialogContent>
<Box sx={{
display: 'flex',
height: `calc(26vh - ${theme.spacing(20)})`
}}>
{/* Left sidebar with tabs */}
<Box sx={{
borderRight: 1,
borderColor: theme.palette.primary.lowContrast,
width: '30%',
maxWidth: theme.spacing(120),
pr: theme.spacing(10)
}}>
<Typography variant="subtitle1" sx={{
my: theme.spacing(1),
fontWeight: 'bold',
fontSize: theme.typography.fontSize * 0.9,
color: theme.palette.primary.contrastTextSecondary,
pl: theme.spacing(4)
}}>
{t('notifications.addOrEditNotifications')}
</Typography>
<Tabs
orientation="vertical"
variant="scrollable"
value={tabValue}
onChange={handleChangeTab}
aria-label="Notification tabs"
>
{activeNotificationTypes.map((type) => (
<Tab
key={type.id}
label={type.label}
orientation="vertical"
disableRipple
/>
))}
</Tabs>
</Box>
return state;
}, [activeNotificationTypes, getFieldKey]); // Only recompute when these dependencies change
{/* Right side content */}
<Box sx={{
flex: 1,
pl: theme.spacing(7.5),
overflowY: 'auto'
}}>
{activeNotificationTypes.map((type, index) => (
<TabPanel key={type.id} value={tabValue} index={index}>
<TabComponent
type={type}
integrations={integrations}
handleIntegrationChange={handleIntegrationChange}
handleInputChange={handleInputChange}
handleTestNotification={handleTestNotification}
isLoading={loading}
/>
</TabPanel>
))}
</Box>
</Box>
</DialogContent>
<DialogActions sx={{
p: theme.spacing(4),
display: 'flex',
justifyContent: 'flex-end',
mb: theme.spacing(5),
mr: theme.spacing(5)
}}>
<Button
variant="contained"
color="accent"
onClick={handleSave}
loading={loading}
sx={{
width: 'auto',
minWidth: theme.spacing(60),
px: theme.spacing(8)
}}
>
{t('commonSave')}
</Button>
</DialogActions>
</Dialog>
);
const [integrations, setIntegrations] = useState(initialIntegrationsState);
useEffect(() => {
if (open) {
const extractNotificationValues = () => {
const values = {};
if (!monitor?.notifications || !Array.isArray(monitor.notifications)) {
return values;
}
monitor.notifications.forEach((notification) => {
// Handle notification based on its structure
if (notification.type === "webhook" && notification.platform) {
if (typeof notification.config === "undefined") return;
const platform = notification.platform;
values[platform] = true; // Set platform as enabled
// Extract configuration based on platform
switch (platform) {
case NOTIFICATION_TYPES.SLACK:
case NOTIFICATION_TYPES.DISCORD:
if (notification.config.webhookUrl) {
values[getFieldKey(platform, FIELD_IDS.WEBHOOK)] =
notification.config.webhookUrl;
}
break;
case NOTIFICATION_TYPES.TELEGRAM:
if (notification.config.botToken) {
values[getFieldKey(platform, FIELD_IDS.TOKEN)] =
notification.config.botToken;
}
if (notification.config.chatId) {
values[getFieldKey(platform, FIELD_IDS.CHAT_ID)] =
notification.config.chatId;
}
break;
case NOTIFICATION_TYPES.WEBHOOK:
if (notification.config.webhookUrl) {
values[getFieldKey(platform, FIELD_IDS.URL)] =
notification.config.webhookUrl;
}
break;
}
}
});
return values;
};
const extractedValues = extractNotificationValues();
setIntegrations((prev) => ({
...prev,
...extractedValues,
}));
}
}, [open, monitor, getFieldKey]);
const handleChangeTab = (event, newValue) => {
setTabValue(newValue);
};
const handleIntegrationChange = (type, checked) => {
setIntegrations((prev) => ({
...prev,
[type]: checked,
}));
};
const handleInputChange = (type, value) => {
setIntegrations((prev) => ({
...prev,
[type]: value,
}));
};
const handleTestNotification = async (type) => {
// Get the notification type details
const notificationType = activeNotificationTypes.find((t) => t.id === type);
if (typeof notificationType === "undefined") {
return;
}
// Prepare config object based on notification type
const config = {};
// Add each field value to the config object
notificationType.fields.forEach((field) => {
const fieldKey = getFieldKey(type, field.id);
config[field.id] = integrations[fieldKey];
});
await sendTestNotification(type, config);
};
// In NotificationIntegrationModal.jsx, update the handleSave function:
const handleSave = () => {
// Get existing notifications
const notifications = [...(monitor?.notifications || [])];
// Get all notification types IDs
const existingTypes = activeNotificationTypes.map((type) => type.id);
// Filter out notifications that are configurable in this modal
const filteredNotifications = notifications.filter((notification) => {
if (notification.platform) {
return !existingTypes.includes(notification.platform);
}
return !existingTypes.includes(notification.type);
});
// Add each enabled notification with its configured fields
activeNotificationTypes.forEach((type) => {
if (integrations[type.id]) {
let notificationObject = {
type: "webhook",
platform: type.id, // Set platform to identify the specific service
config: {},
};
// Configure based on notification type
switch (type.id) {
case "slack":
case "discord":
notificationObject.config.webhookUrl =
integrations[getFieldKey(type.id, "webhook")];
break;
case "telegram":
notificationObject.config.botToken =
integrations[getFieldKey(type.id, "token")];
notificationObject.config.chatId =
integrations[getFieldKey(type.id, "chatId")];
break;
case "webhook":
notificationObject.config.webhookUrl =
integrations[getFieldKey(type.id, "url")];
break;
}
filteredNotifications.push(notificationObject);
}
});
// Update monitor with new notifications
setMonitor((prev) => ({
...prev,
notifications: filteredNotifications,
}));
onClose();
};
return (
<Dialog
open={open}
onClose={onClose}
fullWidth
maxWidth="md"
sx={{
"& .MuiDialog-paper": {
width: `calc(80% - ${theme.spacing(40)})`,
maxWidth: `${theme.breakpoints.values.md - 70}px`,
},
}}
>
<DialogContent>
<Stack
direction="row"
sx={{
height: `calc(30vh - ${theme.spacing(20)})`,
}}
>
{/* Left sidebar with tabs */}
<Box
sx={{
borderRight: 1,
borderColor: theme.palette.primary.lowContrast,
width: "30%",
maxWidth: theme.spacing(120),
pr: theme.spacing(10),
}}
>
<Typography variant="h2">
{t("notifications.addOrEditNotifications")}
</Typography>
<Tabs
orientation="vertical"
variant="scrollable"
value={tabValue}
onChange={handleChangeTab}
aria-label="Notification tabs"
>
{activeNotificationTypes.map((type) => (
<Tab
key={type.id}
label={type.label}
orientation="vertical"
disableRipple
/>
))}
</Tabs>
</Box>
{/* Right side content */}
<Box
sx={{
flex: 1,
pl: theme.spacing(7.5),
overflowY: "auto",
}}
>
{activeNotificationTypes.map((type, index) => (
<TabPanel
key={type.id}
value={tabValue}
index={index}
>
<TabComponent
type={type}
integrations={integrations}
handleIntegrationChange={handleIntegrationChange}
handleInputChange={handleInputChange}
handleTestNotification={handleTestNotification}
isLoading={loading}
/>
</TabPanel>
))}
</Box>
</Stack>
</DialogContent>
<DialogActions
sx={{
p: theme.spacing(4),
display: "flex",
justifyContent: "flex-end",
mb: theme.spacing(5),
mr: theme.spacing(5),
}}
>
<Button
variant="contained"
color="accent"
onClick={handleSave}
loading={loading}
sx={{
width: "auto",
minWidth: theme.spacing(60),
px: theme.spacing(8),
}}
>
{t("commonSave")}
</Button>
</DialogActions>
</Dialog>
);
};
NotificationIntegrationModal.propTypes = {
open: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
monitor: PropTypes.object.isRequired,
setMonitor: PropTypes.func.isRequired,
notificationTypes: PropTypes.array
open: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
monitor: PropTypes.object.isRequired,
setMonitor: PropTypes.func.isRequired,
notificationTypes: PropTypes.array,
};
export default NotificationIntegrationModal;
export default NotificationIntegrationModal;

View File

@@ -10,6 +10,7 @@ import { update } from "../../../Features/Auth/authSlice";
import { useDispatch, useSelector } from "react-redux";
import { createToast } from "../../../Utils/toastUtils";
import { getTouchedFieldErrors } from "../../../Validation/error";
import { useTranslation } from "react-i18next";
const defaultPasswordsState = {
password: "",
@@ -26,6 +27,7 @@ const defaultPasswordsState = {
const PasswordPanel = () => {
const theme = useTheme();
const dispatch = useDispatch();
const { t } = useTranslation();
const SPACING_GAP = theme.spacing(12);
@@ -203,7 +205,7 @@ const PasswordPanel = () => {
<TextInput
type="password"
id="edit-confirm-password"
placeholder="Reenter your new password"
placeholder={t("confirmPassword")}
autoComplete="new-password"
value={localData.confirm}
onChange={handleChange}

View File

@@ -1,21 +1,19 @@
import { useTheme } from "@emotion/react";
import { useRef, useState } from "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 ImageField from "../../Inputs/Image";
import { credentials, imageValidation } from "../../../Validation/validation";
import ImageUpload from "../../Inputs/ImageUpload";
import { credentials } from "../../../Validation/validation";
import { useDispatch, useSelector } from "react-redux";
import { clearAuthState, deleteUser, update } from "../../../Features/Auth/authSlice";
import ImageIcon from "@mui/icons-material/Image";
import ProgressUpload from "../../ProgressBars";
import { formatBytes } from "../../../Utils/fileUtils";
import { clearUptimeMonitorState } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
import { createToast } from "../../../Utils/toastUtils";
import { logger } from "../../../Utils/Logger";
import { GenericDialog } from "../../Dialog/genericDialog";
import Dialog from "../../Dialog";
import { useTranslation } from "react-i18next";
/**
* ProfilePanel component displays a form for editing user profile information
@@ -28,7 +26,7 @@ import Dialog from "../../Dialog";
const ProfilePanel = () => {
const theme = useTheme();
const dispatch = useDispatch();
const { t } = useTranslation();
const SPACING_GAP = theme.spacing(12);
//redux state
@@ -49,8 +47,6 @@ const ProfilePanel = () => {
});
const [errors, setErrors] = useState({});
const [file, setFile] = useState();
const intervalRef = useRef(null);
const [progress, setProgress] = useState({ value: 0, isLoading: false });
// Handles input field changes and performs validation
const handleChange = (event) => {
@@ -65,33 +61,6 @@ const ProfilePanel = () => {
validateField({ [name]: value }, credentials, name);
};
// Handles image file
const handlePicture = (event) => {
const pic = event.target.files[0];
let error = validateField({ type: pic.type, size: pic.size }, imageValidation);
if (error) return;
setProgress((prev) => ({ ...prev, isLoading: true }));
setFile({
src: URL.createObjectURL(pic),
name: pic.name,
size: formatBytes(pic.size),
delete: false,
});
//TODO - potentitally remove, will revisit in the future
intervalRef.current = setInterval(() => {
const buffer = 12;
setProgress((prev) => {
if (prev.value + buffer >= 100) {
clearInterval(intervalRef.current);
return { value: 100, isLoading: false };
}
return { ...prev, value: prev.value + buffer };
});
}, 120);
};
// Validates input against provided schema and updates error state
const validateField = (toValidate, schema, name = "picture") => {
const { error } = schema.validate(toValidate, { abortEarly: false });
@@ -116,52 +85,55 @@ const ProfilePanel = () => {
// Resets picture-related states and clears interval
const removePicture = () => {
errors["picture"] && clearError("picture");
setFile({ delete: true });
clearInterval(intervalRef.current); // interrupt interval if image upload is canceled prior to completing the process
setProgress({ value: 0, isLoading: false });
};
setFile(undefined);
setLocalData((prev) => ({
...prev,
file: undefined,
deleteProfileImage: true,
}));
};
// Opens the picture update modal
const openPictureModal = () => {
setIsOpen("picture");
setFile({ delete: localData.deleteProfileImage });
setFile(undefined);
};
// Closes the picture update modal and resets related states
const closePictureModal = () => {
errors["picture"] && clearError("picture");
setFile(); //reset file
clearInterval(intervalRef.current); // interrupt interval if image upload is canceled prior to completing the process
setProgress({ value: 0, isLoading: false });
setIsOpen("");
};
if (errors["picture"]) clearError("picture");
setFile(undefined);
setIsOpen("");
};
// Updates profile image displayed on UI
const handleUpdatePicture = () => {
setProgress({ value: 0, isLoading: false });
setLocalData((prev) => ({
...prev,
file: file.src,
file: file?.src,
deleteProfileImage: false,
}));
setIsOpen("");
errors["unchanged"] && clearError("unchanged");
if (errors["unchanged"]) clearError("unchanged");
};
// Handles form submission to update user profile
const handleSaveProfile = async (event) => {
event.preventDefault();
if (
localData.firstName === user.firstName &&
localData.lastName === user.lastName &&
localData.deleteProfileImage === undefined &&
localData.file === undefined
) {
createToast({
body: "Unable to update profile — no changes detected.",
});
setErrors({ unchanged: "unable to update profile" });
return;
const nameChanged =
localData.firstName !== user.firstName ||
localData.lastName !== user.lastName;
const avatarChanged =
localData.deleteProfileImage === true ||
(localData.file && localData.file !== `data:image/png;base64,${user.avatarImage}`);
if (!nameChanged && !avatarChanged) {
createToast({
body: "Unable to update profile — no changes detected.",
});
setErrors({ unchanged: "unable to update profile" });
return;
}
const action = await dispatch(update({ localData }));
@@ -182,6 +154,7 @@ const ProfilePanel = () => {
...prev,
deleteProfileImage: true,
}));
setFile(undefined);
errors["unchanged"] && clearError("unchanged");
};
@@ -232,7 +205,7 @@ const ProfilePanel = () => {
>
{/* This 0.9 is a bit magic numbering, refactor */}
<Box flex={0.9}>
<Typography component="h1">First name</Typography>
<Typography component="h1">{t('FirstName')}</Typography>
</Box>
<TextInput
id="edit-first-name"
@@ -250,7 +223,7 @@ const ProfilePanel = () => {
gap={SPACING_GAP}
>
<Box flex={0.9}>
<Typography component="h1">Last name</Typography>
<Typography component="h1">{t('LastName')}</Typography>
</Box>
<TextInput
id="edit-last-name"
@@ -268,12 +241,12 @@ const ProfilePanel = () => {
gap={SPACING_GAP}
>
<Stack flex={0.9}>
<Typography component="h1">Email</Typography>
<Typography component="h1">{t('email')}</Typography>
<Typography
component="p"
sx={{ opacity: 0.6 }}
>
This is your current email address it cannot be changed.
{t('EmailDescriptionText')}
</Typography>
</Stack>
<TextInput
@@ -291,12 +264,12 @@ const ProfilePanel = () => {
gap={SPACING_GAP}
>
<Stack flex={0.9}>
<Typography component="h1">Your photo</Typography>
<Typography component="h1">{t('YourPhoto')}</Typography>
<Typography
component="p"
sx={{ opacity: 0.6 }}
>
This photo will be displayed in your profile page.
{t('PhotoDescriptionText')}
</Typography>
</Stack>
<Stack
@@ -320,14 +293,14 @@ const ProfilePanel = () => {
color="error"
onClick={handleDeletePicture}
>
Delete
{t('delete')}
</Button>
<Button
variant="contained" // CAIO_REVIEW
color="accent"
onClick={openPictureModal}
>
Update
{t('update')}
</Button>
</Stack>
</Stack>
@@ -352,7 +325,7 @@ const ProfilePanel = () => {
disabled={Object.keys(errors).length !== 0 && !errors?.picture && true}
sx={{ px: theme.spacing(12) }}
>
Save
{t('save')}
</Button>
</Box>
</Stack>
@@ -371,13 +344,12 @@ const ProfilePanel = () => {
spellCheck="false"
>
<Box mb={theme.spacing(6)}>
<Typography component="h1">Delete account</Typography>
<Typography component="h1">{t('DeleteAccountTitle')}</Typography>
<Typography
component="p"
sx={{ opacity: 0.6 }}
>
Note that deleting your account will remove all data from the server. This
is permanent and non-recoverable.
{t('DeleteDescriptionText')}
</Typography>
</Box>
<Button
@@ -385,19 +357,17 @@ const ProfilePanel = () => {
color="error"
onClick={() => setIsOpen("delete")}
>
Delete account
{t('DeleteAccountButton')}
</Button>
</Box>
)}
<Dialog
open={isModalOpen("delete")}
theme={theme}
title={"Really delete this account?"}
description={
"If you delete your account, you will no longer be able to sign in, and all of your data will be deleted. Deleting your account is permanent and non-recoverable action."
}
title={t('DeleteWarningTitle')}
description={t('DeleteAccountWarning')}
onCancel={() => setIsOpen("")}
confirmationButtonLabel={"Delete account"}
confirmationButtonLabel={t('DeleteAccountButton')}
onConfirm={handleDeleteAccount}
isLoading={isLoading}
/>
@@ -408,34 +378,27 @@ const ProfilePanel = () => {
onClose={closePictureModal}
theme={theme}
>
<ImageField
id="update-profile-picture"
<ImageUpload
src={
file?.delete
? ""
: file?.src
? file.src
file?.src
? file.src
: localData?.deleteProfileImage
? ""
: localData?.file
? localData.file
: user?.avatarImage
? `data:image/png;base64,${user.avatarImage}`
: ""
}
loading={progress.isLoading && progress.value !== 100}
onChange={handlePicture}
}
onChange={(newFile) => {
if (newFile) {
setFile(newFile);
clearError("unchanged");
}
}}
previewIsRound
maxSize={3 * 1024 * 1024}
/>
{progress.isLoading || progress.value !== 0 || errors["picture"] ? (
<ProgressUpload
icon={<ImageIcon />}
label={file?.name}
size={file?.size}
progress={progress.value}
onClick={removePicture}
error={errors["picture"]}
/>
) : (
""
)}
<Stack
direction="row"
mt={theme.spacing(10)}
@@ -447,20 +410,15 @@ const ProfilePanel = () => {
color="info"
onClick={removePicture}
>
Remove
{t('remove')}
</Button>
<Button
variant="contained"
color="accent"
onClick={handleUpdatePicture}
disabled={
(Object.keys(errors).length !== 0 && errors?.picture) ||
progress.value !== 100
? true
: false
}
disabled={!!errors.picture || !file?.src}
>
Update
{t('update')}
</Button>
</Stack>
</GenericDialog>

View File

@@ -25,7 +25,7 @@ const TeamPanel = () => {
const [toInvite, setToInvite] = useState({
email: "",
role: ["0"],
role: ["user"],
});
const [data, setData] = useState([]);
const [members, setMembers] = useState([]);
@@ -107,7 +107,7 @@ const TeamPanel = () => {
const handleChange = (event) => {
const { value } = event.target;
const newEmail = value?.toLowerCase() || value
const newEmail = value?.toLowerCase() || value;
setToInvite((prev) => ({
...prev,
email: newEmail,

View File

@@ -1,46 +1,50 @@
import { useMemo } from "react";
import { ConnectionProvider, WalletProvider } from "@solana/wallet-adapter-react";
import { WalletAdapterNetwork } from "@solana/wallet-adapter-base";
import {
UnsafeBurnerWalletAdapter,
PhantomWalletAdapter,
} from "@solana/wallet-adapter-wallets";
// import { useMemo } from "react";
// import { ConnectionProvider, WalletProvider } from "@solana/wallet-adapter-react";
// import { WalletAdapterNetwork } from "@solana/wallet-adapter-base";
// import {
// UnsafeBurnerWalletAdapter,
// PhantomWalletAdapter,
// } from "@solana/wallet-adapter-wallets";
import { WalletModalProvider } from "@solana/wallet-adapter-react-ui";
import { clusterApiUrl } from "@solana/web3.js";
import PropTypes from "prop-types";
import "./index.css";
// import { WalletModalProvider } from "@solana/wallet-adapter-react-ui";
// import { clusterApiUrl } from "@solana/web3.js";
// import PropTypes from "prop-types";
// import "./index.css";
// Default styles that can be overridden by your app
import "@solana/wallet-adapter-react-ui/styles.css";
// // Default styles that can be overridden by your app
// import "@solana/wallet-adapter-react-ui/styles.css";
export const Wallet = ({ children }) => {
// The network can be set to 'devnet', 'testnet', or 'mainnet-beta'.
const network = WalletAdapterNetwork.Mainnet;
// export const Wallet = ({ children }) => {
// // The network can be set to 'devnet', 'testnet', or 'mainnet-beta'.
// const network = WalletAdapterNetwork.Mainnet;
// You can also provide a custom RPC endpoint.
const endpoint = useMemo(() => clusterApiUrl(network), [network]);
// // You can also provide a custom RPC endpoint.
// const endpoint = useMemo(() => clusterApiUrl(network), [network]);
const wallets = useMemo(
() => [new PhantomWalletAdapter()],
// eslint-disable-next-line react-hooks/exhaustive-deps
[network]
);
// const wallets = useMemo(
// () => [new PhantomWalletAdapter()],
// // eslint-disable-next-line react-hooks/exhaustive-deps
// [network]
// );
return (
<ConnectionProvider endpoint={endpoint}>
<WalletProvider
wallets={wallets}
autoConnect
>
<WalletModalProvider>{children}</WalletModalProvider>
</WalletProvider>
</ConnectionProvider>
);
};
// return (
// <ConnectionProvider endpoint={endpoint}>
// <WalletProvider
// wallets={wallets}
// autoConnect
// >
// <WalletModalProvider>{children}</WalletModalProvider>
// </WalletProvider>
// </ConnectionProvider>
// );
// };
Wallet.propTypes = {
children: PropTypes.node,
// Wallet.propTypes = {
// children: PropTypes.node,
// };
const Wallet = ({ children }) => {
return children;
};
export default Wallet;

View File

@@ -101,7 +101,7 @@ export const updatePageSpeed = createAsyncThunk(
name: monitor.name,
description: monitor.description,
interval: monitor.interval,
// notifications: monitor.notifications,
notifications: monitor.notifications,
};
const res = await networkService.updateMonitor({
monitorId: monitor._id,

View File

@@ -5,7 +5,7 @@ const initialState = {
isLoading: false,
apiBaseUrl: "",
logLevel: "debug",
language: "",
language: "gb",
};
export const getAppSettings = createAsyncThunk(
@@ -30,26 +30,9 @@ export const getAppSettings = createAsyncThunk(
export const updateAppSettings = createAsyncThunk(
"settings/updateSettings",
async ({ settings }, thunkApi) => {
// The reason for commenting is that, previously, we had the flexibility to set the API base.
// However, now this could lead to an issue where it gets set to undefined.
// networkService.setBaseUrl(settings.apiBaseUrl);
try {
const parsedSettings = {
apiBaseUrl: settings.apiBaseUrl,
logLevel: settings.logLevel,
language: settings.language,
clientHost: settings.clientHost,
jwtSecret: settings.jwtSecret,
dbType: settings.dbType,
dbConnectionString: settings.dbConnectionString,
redisHost: settings.redisHost,
redisPort: settings.redisPort,
jwtTTL: settings.jwtTTL,
pagespeedApiKey: settings.pagespeedApiKey,
systemEmailHost: settings.systemEmailHost,
systemEmailPort: settings.systemEmailPort,
systemEmailAddress: settings.systemEmailAddress,
systemEmailPassword: settings.systemEmailPassword,
};
const res = await networkService.updateAppSettings({ settings: parsedSettings });
return res.data;

View File

@@ -2,7 +2,7 @@ import { useState } from "react";
import { networkService } from "../main";
import { useTranslation } from "react-i18next";
const CLIENT_HOST = import.meta.env.VITE_CLIENT_HOST;
const CLIENT_HOST = import.meta.env.VITE_APP_CLIENT_HOST;
const useGetInviteToken = () => {
const { t } = useTranslation();

View File

@@ -0,0 +1,27 @@
import { useState } from "react";
import { networkService } from "../main";
export const useBulkMonitors = () => {
const [isLoading, setIsLoading] = useState(false);
const createBulkMonitors = async (file, user) => {
setIsLoading(true);
const formData = new FormData();
formData.append("csvFile", file);
formData.append("userId", user._id);
formData.append("teamId", user.teamId);
try {
const response = await networkService.createBulkMonitors(formData);
return [true, response.data, null]; // [success, data, error]
} catch (err) {
const errorMessage = err?.response?.data?.msg || err.message;
return [false, null, errorMessage];
} finally {
setIsLoading(false);
}
};
return [createBulkMonitors, isLoading];
};

View File

@@ -58,7 +58,7 @@ const EmailStep = ({ form, errors, onSubmit, onChange }) => {
onInput={(e) => (e.target.value = e.target.value.toLowerCase())}
onChange={onChange}
error={errors.email ? true : false}
helperText={errors.email}
helperText={errors.email ? t(errors.email) : ""}
ref={inputRef}
/>
<Stack

View File

@@ -6,6 +6,7 @@ import { credentials } from "../../../Validation/validation";
import { login } from "../../../Features/Auth/authSlice";
import { useDispatch, useSelector } from "react-redux";
import { createToast } from "../../../Utils/toastUtils";
import { networkService } from "../../../main";
import Background from "../../../assets/Images/background-grid.svg?react";
import Logo from "../../../assets/icons/checkmate-icon.svg?react";
import "../index.css";
@@ -23,11 +24,10 @@ const DEMO = import.meta.env.VITE_APP_DEMO;
*/
const Login = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
const theme = useTheme();
const dispatch = useDispatch();
const { t } = useTranslation();
const navigate = useNavigate();
const authState = useSelector((state) => state.auth);
const { authToken } = authState;
@@ -54,13 +54,17 @@ const Login = () => {
const handleChange = (event) => {
const { value, id } = event.target;
const name = idMap[id];
const lowerCasedValue = name === idMap["login-email-input"]? value?.toLowerCase()||value : value
const lowerCasedValue =
name === idMap["login-email-input"] ? value?.toLowerCase() || value : value;
setForm((prev) => ({
...prev,
[name]: lowerCasedValue,
}));
const { error } = credentials.validate({ [name]: lowerCasedValue }, { abortEarly: false });
const { error } = credentials.validate(
{ [name]: lowerCasedValue },
{ abortEarly: false }
);
setErrors((prev) => {
const prevErrors = { ...prev };
@@ -79,8 +83,10 @@ const Login = () => {
{ abortEarly: false }
);
if (error) {
setErrors((prev) => ({ ...prev, email: error.details[0].message }));
createToast({ body: error.details[0].message });
const errorMessage = error.details[0].message;
const translatedMessage = errorMessage.startsWith('auth') ? t(errorMessage) : errorMessage;
setErrors((prev) => ({ ...prev, email: translatedMessage }));
createToast({ body: translatedMessage });
} else {
setStep(1);
}
@@ -97,15 +103,15 @@ const Login = () => {
createToast({
body:
error.details && error.details.length > 0
? error.details[0].message
: "Error validating data.",
? (error.details[0].message.startsWith('auth') ? t(error.details[0].message) : error.details[0].message)
: t("Error validating data."),
});
} else {
const action = await dispatch(login(form));
if (action.payload.success) {
navigate("/uptime");
createToast({
body: "Welcome back! You're successfully logged in.",
body: t("welcomeBack"),
});
} else {
if (action.payload) {

View File

@@ -15,7 +15,7 @@ import Background from "../../../assets/Images/background-grid.svg?react";
import Logo from "../../../assets/icons/checkmate-icon.svg?react";
import Mail from "../../../assets/icons/mail.svg?react";
import "../index.css";
import { useTranslation } from "react-i18next";
import { useTranslation, Trans } from "react-i18next";
/**
* Displays the initial landing page.
*
@@ -67,42 +67,47 @@ const LandingPage = ({ isSuperAdmin, onSignup }) => {
</Box>
<Box maxWidth={400}>
<Typography className="tos-p">
{t("authRegisterBySigningUp")}{" "}
<Typography
component="span"
onClick={() => {
window.open(
"https://bluewavelabs.ca/terms-of-service-open-source",
"_blank",
"noreferrer"
);
<Trans
i18nKey="authRegisterBySigningUp"
components={{
a1: (
<Typography
component="span"
sx={{
cursor: "pointer",
"&:hover": {
color: theme.palette.primary.contrastTextTertiary,
},
}}
onClick={() => {
window.open(
"https://bluewavelabs.ca/terms-of-service-open-source",
"_blank",
"noreferrer"
);
}}
/>
),
a2: (
<Typography
component="span"
sx={{
cursor: "pointer",
"&:hover": {
color: theme.palette.primary.contrastTextTertiary,
},
}}
onClick={() => {
window.open(
"https://bluewavelabs.ca/privacy-policy-open-source",
"_blank",
"noreferrer"
);
}}
/>
)
}}
sx={{
"&:hover": {
color: theme.palette.primary.contrastTextTertiary,
},
}}
>
{t("authRegisterTerms")}
</Typography>{" "}
{t("and")}{" "}
<Typography
component="span"
onClick={() => {
window.open(
"https://bluewavelabs.ca/privacy-policy-open-source",
"_blank",
"noreferrer"
);
}}
sx={{
"&:hover": {
color: theme.palette.primary.contrastTextTertiary,
},
}}
>
{t("authRegisterPrivacy")}
</Typography>
/>
</Typography>
</Box>
</Stack>
@@ -148,8 +153,8 @@ const Register = ({ isSuperAdmin }) => {
try {
const res = await networkService.verifyInvitationToken(token);
const invite = res.data.data;
const { role, email, teamId } = invite;
setForm({ ...form, email, role, teamId });
const { email } = invite;
setForm({ ...form, email });
} catch (error) {
navigate("/register", { replace: true });
}
@@ -259,7 +264,8 @@ const Register = ({ isSuperAdmin }) => {
const handleChange = (event) => {
const { value, id } = event.target;
const name = idMap[id];
const lowerCasedValue = name === idMap["register-email-input"]? value?.toLowerCase() || value : value
const lowerCasedValue =
name === idMap["register-email-input"] ? value?.toLowerCase() || value : value;
setForm((prev) => ({
...prev,
[name]: lowerCasedValue,
@@ -381,7 +387,7 @@ const Register = ({ isSuperAdmin }) => {
}}
sx={{ userSelect: "none", color: theme.palette.accent.main }}
>
{t("authLoginTitle")}
{t("authRegisterLoginLink")}
</Typography>
</Box>
</Stack>

View File

@@ -64,7 +64,7 @@ function StepOne({ form, errors, onSubmit, onChange, onBack }) {
>
<TextInput
id="register-firstname-input"
label="Name"
label={t("authRegisterFirstName")}
isRequired={true}
placeholder="Jordan"
autoComplete="given-name"
@@ -76,7 +76,7 @@ function StepOne({ form, errors, onSubmit, onChange, onBack }) {
/>
<TextInput
id="register-lastname-input"
label="Surname"
label={t("authRegisterLastName")}
isRequired={true}
placeholder="Ellis"
autoComplete="family-name"

View File

@@ -64,7 +64,7 @@ function StepThree({ onSubmit, onBack }) {
type="password"
id="register-password-input"
name="password"
label={t("commonPassword")}
label={t("authLoginEnterPassword")}
isRequired={true}
placeholder={t("createAPassword")}
autoComplete="current-password"

View File

@@ -60,7 +60,7 @@ function StepTwo({ form, errors, onSubmit, onChange, onBack }) {
<TextInput
type="email"
id="register-email-input"
label={t("commonEmail")}
label={t("authRegisterEmail")}
isRequired={true}
placeholder="jordan.ellis@domain.com"
autoComplete="email"
@@ -68,7 +68,11 @@ function StepTwo({ form, errors, onSubmit, onChange, onBack }) {
onInput={(e) => (e.target.value = e.target.value.toLowerCase())}
onChange={onChange}
error={errors.email ? true : false}
helperText={errors.email}
helperText={errors.email && (
errors.email.includes("required") ? t("authRegisterEmailRequired") :
errors.email.includes("valid email") ? t("authRegisterEmailInvalid") :
errors.email
)}
ref={inputRef}
/>
<Stack

View File

@@ -11,7 +11,7 @@ import { useTranslation } from "react-i18next";
const InfraStatBoxes = ({ shouldRender, monitor }) => {
// Utils
const { formatBytes } = useHardwareUtils();
const { statusStyles, determineState } = useUtils();
const { determineState } = useUtils();
const { t } = useTranslation();
const { stats, uptimePercentage } = monitor ?? {};
@@ -41,7 +41,8 @@ const InfraStatBoxes = ({ shouldRender, monitor }) => {
flexWrap="wrap"
>
<StatBox
sx={statusStyles[determineState(monitor)]}
gradient={true}
status={determineState(monitor)}
heading={t("status")}
subHeading={determineState(monitor)}
/>

View File

@@ -52,7 +52,12 @@ const Filter = ({
}, [selectedStatus]);
return (
<Box>
<Box
sx={{
m: theme.spacing(2),
ml: theme.spacing(4),
}}
>
<FilterHeader
header={t("status")}
options={statusOptions}

View File

@@ -75,6 +75,7 @@ const InfrastructureMenu = ({ monitor, isAdmin, updateCallback }) => {
<IconButton
aria-label="monitor actions"
onClick={openMenu}
disabled={!isAdmin}
sx={{
"&:focus": {
outline: "none",

View File

@@ -94,19 +94,21 @@ const InfrastructureMonitors = () => {
<Breadcrumbs list={BREADCRUMBS} />
<MonitorCreateHeader
isAdmin={isAdmin}
shouldRender={!isLoading}
isLoading={isLoading}
path="/infrastructure/create"
/>
<MonitorCountHeader
shouldRender={!isLoading}
monitorCount={summary?.totalMonitors ?? 0}
/>
<Filter
selectedStatus={selectedStatus}
setSelectedStatus={setSelectedStatus}
setToFilterStatus={setToFilterStatus}
handleReset={handleReset}
/>
<Stack direction={"row"}>
<MonitorCountHeader
shouldRender={!isLoading}
monitorCount={summary?.totalMonitors ?? 0}
/>
<Filter
selectedStatus={selectedStatus}
setSelectedStatus={setSelectedStatus}
setToFilterStatus={setToFilterStatus}
handleReset={handleReset}
/>
</Stack>
<MonitorsTable
shouldRender={!isLoading}
monitors={monitors}

View File

@@ -62,7 +62,7 @@ const PageSpeed = () => {
<Breadcrumbs list={BREADCRUMBS} />
<CreateMonitorHeader
isAdmin={isAdmin}
shouldRender={!isLoading}
isLoading={isLoading}
path="/pagespeed/create"
/>
<MonitorCountHeader

View File

@@ -6,10 +6,10 @@ import Select from "../../Components/Inputs/Select";
import { useIsAdmin } from "../../Hooks/useIsAdmin";
import Dialog from "../../Components/Dialog";
import ConfigBox from "../../Components/ConfigBox";
import {
WalletMultiButton,
WalletDisconnectButton,
} from "@solana/wallet-adapter-react-ui";
// import {
// WalletMultiButton,
// WalletDisconnectButton,
// } from "@solana/wallet-adapter-react-ui";
//Utils
import { useTheme } from "@emotion/react";
@@ -123,31 +123,17 @@ const Settings = () => {
ttl: form.ttl,
});
const updatedUser = { ...user, checkTTL: form.ttl };
const action = await dispatch(update({ localData: updatedUser }));
const settingsAction = await dispatch(
updateAppSettings({ settings: { language: language } })
);
const [userAction, settingsAction] = await Promise.all([
dispatch(update({ localData: updatedUser })),
dispatch(updateAppSettings({ settings: { language: language } })),
]);
if (action.payload.success && settingsAction.payload.success) {
createToast({
body: t("settingsSuccessSaved"),
});
if (userAction.payload.success && settingsAction.payload.success) {
createToast({ body: t("settingsSuccessSaved") });
} else {
if (action.payload) {
console.log(action.payload);
// dispatch errors
createToast({
body: action.payload.msg,
});
} else {
// unknown errors
createToast({
body: "Unknown error.",
});
}
throw new Error("Failed to save settings");
}
} catch (error) {
console.log(error);
createToast({ body: t("settingsFailedToSave") });
} finally {
setChecksIsLoading(false);
@@ -268,7 +254,7 @@ const Settings = () => {
></Select>
</Stack>
</ConfigBox>
{isAdmin && (
{/* {isAdmin && (
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsDistributedUptime")}</Typography>
@@ -290,8 +276,8 @@ const Settings = () => {
: t("settingsDisabled")}
</Box>
</ConfigBox>
)}
{isAdmin && (
)} */}
{/* {isAdmin && (
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsWallet")}</Typography>
@@ -316,7 +302,7 @@ const Settings = () => {
</Stack>
</Box>
</ConfigBox>
)}
)} */}
{isAdmin && (
<ConfigBox>
<Box>
@@ -362,14 +348,15 @@ const Settings = () => {
</ConfigBox>
)}
{isAdmin && (
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsDemoMonitors")}</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
{t("settingsDemoMonitorsDescription")}
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<>
{/* Demo Monitors Section */}
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsDemoMonitors")}</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
{t("settingsDemoMonitorsDescription")}
</Typography>
</Box>
<Box>
<Typography>{t("settingsAddDemoMonitors")}</Typography>
<Button
@@ -382,6 +369,16 @@ const Settings = () => {
{t("settingsAddDemoMonitorsButton")}
</Button>
</Box>
</ConfigBox>
{/* System Reset Section */}
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsSystemReset")}</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
{t("settingsSystemResetDescription")}
</Typography>
</Box>
<Box>
<Typography>{t("settingsRemoveAllMonitors")}</Typography>
<Button
@@ -396,17 +393,17 @@ const Settings = () => {
{t("settingsRemoveAllMonitorsButton")}
</Button>
</Box>
</Stack>
<Dialog
open={isOpen.deleteMonitors}
theme={theme}
title={t("settingsRemoveAllMonitorsDialogTitle")}
onCancel={() => setIsOpen(deleteStatsMonitorsInitState)}
confirmationButtonLabel={t("settingsRemoveAllMonitorsDialogConfirm")}
onConfirm={handleDeleteAllMonitors}
isLoading={isLoading || authIsLoading || checksIsLoading}
/>
</ConfigBox>
<Dialog
open={isOpen.deleteMonitors}
theme={theme}
title={t("settingsRemoveAllMonitorsDialogTitle")}
onCancel={() => setIsOpen(deleteStatsMonitorsInitState)}
confirmationButtonLabel={t("settingsRemoveAllMonitorsDialogConfirm")}
onConfirm={handleDeleteAllMonitors}
isLoading={isLoading || authIsLoading || checksIsLoading}
/>
</ConfigBox>
</>
)}
<ConfigBox>

View File

@@ -5,7 +5,7 @@ import ConfigBox from "../../../../../Components/ConfigBox";
import Checkbox from "../../../../../Components/Inputs/Checkbox";
import TextInput from "../../../../../Components/Inputs/TextInput";
import Select from "../../../../../Components/Inputs/Select";
import ImageField from "../../../../../Components/Inputs/Image";
import ImageUpload from "../../../../../Components/Inputs/ImageUpload";
import ColorPicker from "../../../../../Components/Inputs/ColorPicker";
import Progress from "../Progress";
@@ -106,11 +106,10 @@ const TabSettings = ({
</Typography>
</Stack>
<Stack gap={theme.spacing(6)}>
<ImageField
id="logo"
<ImageUpload
src={form?.logo?.src}
isRound={false}
onChange={handleImageChange}
previewIsRound={false}
/>
<Progress
isLoading={progress.isLoading}

View File

@@ -3,7 +3,6 @@ import { Stack, Button, Typography } from "@mui/material";
import Tabs from "./Components/Tabs";
import GenericFallback from "../../../Components/GenericFallback";
import SkeletonLayout from "./Components/Skeleton";
import Breadcrumbs from "../../../Components/Breadcrumbs/index.jsx";
//Utils
import { useTheme } from "@emotion/react";
import { useState, useEffect, useRef, useCallback } from "react";
@@ -59,19 +58,6 @@ const CreateStatusPage = () => {
const [statusPage, statusPageMonitors, statusPageIsLoading, statusPageNetworkError] =
useStatusPageFetch(isCreate, url);
// Breadcrumbs
const crumbs = [
{ name: t("statusBreadCrumbsStatusPages"), path: "/status" },
];
if (isCreate) {
crumbs.push({ name: t("statusBreadCrumbsCreate"), path: "/status/uptime/create" });
} else {
crumbs.push(
{ name: t("statusBreadCrumbsDetails"), path: `/status/uptime/${statusPage?.url}` },
{ name: t("configure"), path: `/status/uptime/configure/${statusPage?.url}` }
);
}
// Handlers
const handleFormChange = (e) => {
@@ -101,30 +87,31 @@ const CreateStatusPage = () => {
}));
};
const handleImageChange = useCallback((event) => {
const img = event.target?.files?.[0];
const newLogo = {
src: URL.createObjectURL(img),
name: img.name,
type: img.type,
size: img.size,
};
const handleImageChange = useCallback((fileObj) => {
if (!fileObj || !fileObj.file) return;
setForm((prev) => ({
...prev,
logo: newLogo,
...prev,
logo: {
src: fileObj.src,
name: fileObj.name,
type: fileObj.file.type,
size: fileObj.file.size,
},
}));
intervalRef.current = setInterval(() => {
const buffer = 12;
setProgress((prev) => {
if (prev.value + buffer >= 100) {
clearInterval(intervalRef.current);
return { value: 100, isLoading: false };
}
return { ...prev, value: prev.value + buffer };
});
const buffer = 12;
setProgress((prev) => {
if (prev.value + buffer >= 100) {
clearInterval(intervalRef.current);
return { value: 100, isLoading: false };
}
return { ...prev, value: prev.value + buffer };
});
}, 120);
}, []);
const removeLogo = () => {
setForm((prev) => ({
...prev,
@@ -232,7 +219,6 @@ const CreateStatusPage = () => {
// Load fields
return (
<Stack gap={theme.spacing(10)}>
<Breadcrumbs list={crumbs} />
<Tabs
form={form}
errors={errors}

View File

@@ -3,6 +3,7 @@ import { Box, Stack, Typography, Button } from "@mui/material";
import Image from "../../../../../Components/Image";
import SettingsIcon from "../../../../../assets/icons/settings-bold.svg?react";
import ThemeSwitch from "../../../../../Components/ThemeSwitch";
import ArrowOutwardIcon from "@mui/icons-material/ArrowOutward";
//Utils
import { useTheme } from "@mui/material/styles";
import { useNavigate } from "react-router-dom";
@@ -86,6 +87,8 @@ const ControlsHeader = ({
type = "uptime",
}) => {
const theme = useTheme();
const { t } = useTranslation();
const publicUrl = `/status/uptime/public/${url}`;
return (
<Stack
@@ -118,6 +121,26 @@ const ControlsHeader = ({
>
{statusPage?.companyName}
</Typography>
{statusPage?.isPublished && !isPublic && (
<Stack
direction="row"
alignItems="center"
justifyContent="center"
onClick={() => {
window.open(publicUrl, "_blank", "noopener,noreferrer");
}}
sx={{
display: "inline-flex",
":hover": {
cursor: "pointer",
borderBottom: 1,
},
}}
>
<Typography>{t("publicLink")}</Typography>
<ArrowOutwardIcon />
</Stack>
)}
</Stack>
<Controls
isDeleting={isDeleting}

View File

@@ -139,6 +139,7 @@ const PublicStatus = () => {
isDeleteOpen={isDeleteOpen}
setIsDeleteOpen={setIsDeleteOpen}
url={url}
isPublic={isPublic}
/>
<Typography variant="h2">{t("statusPageStatusServiceStatus")}</Typography>
<StatusBar monitors={monitors} />

View File

@@ -61,6 +61,7 @@ const StatusPages = () => {
label="Create status page"
isAdmin={isAdmin}
path="/status/uptime/create"
isLoading={isLoading}
/>
<StatusPagesTable data={statusPages} />
</Stack>

View File

@@ -0,0 +1,73 @@
import { useTheme } from "@emotion/react";
import { useState, useRef } from "react";
import { Button, Typography } from "@mui/material";
import { useTranslation } from "react-i18next";
import PropTypes from "prop-types";
const UploadFile = ({ onFileSelect }) => { // Changed prop to onFileSelect
const theme = useTheme();
const [file, setFile] = useState();
const [error, setError] = useState("");
const inputRef = useRef();
const { t } = useTranslation();
const handleSelectFile = () => {
inputRef.current.click();
};
const handleFileChange = (e) => {
setError("");
const selectedFile = e.target.files[0];
// Basic file validation
if (!selectedFile) return;
if (!selectedFile.name.endsWith('.csv')) {
setError(t("bulkImport.invalidFileType"));
return;
}
setFile(selectedFile);
onFileSelect(selectedFile); // Pass the file directly to parent
};
return (
<div>
<input
ref={inputRef}
type="file"
accept=".csv"
style={{ display: "none" }}
onChange={handleFileChange}
/>
<Typography
component="h2"
mb={theme.spacing(1.5)}
sx={{ wordBreak: "break-all" }}
>
{file?.name || t("bulkImport.noFileSelected")}
</Typography>
<Typography
component="div"
mb={theme.spacing(1.5)}
color={theme.palette.error.main}
>
{error}
</Typography>
<Button
variant="contained"
color="accent"
onClick={handleSelectFile}
>
{t("bulkImport.selectFile")}
</Button>
</div>
);
}
UploadFile.prototype = {
onFileSelect: PropTypes.func.isRequired,
};
export default UploadFile;

View File

@@ -0,0 +1,112 @@
// React, Redux, Router
import { useTheme } from "@emotion/react";
import { useState } from "react";
// MUI
import { Box, Stack, Typography, Button, Link } from "@mui/material";
//Components
import { createToast } from "../../../Utils/toastUtils";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import ConfigBox from "../../../Components/ConfigBox";
import UploadFile from "./Upload";
import { useSelector } from "react-redux";
import { useNavigate } from "react-router";
import { Trans, useTranslation } from "react-i18next";
import { useBulkMonitors } from "../../../Hooks/useBulkMonitors";
const BulkImport = () => {
const theme = useTheme();
const { user } = useSelector((state) => state.auth);
const navigate = useNavigate();
const { t } = useTranslation();
const [selectedFile, setSelectedFile] = useState(null);
const crumbs = [
{ name: t("uptime"), path: "/uptime" },
{ name: t("bulkImport.title"), path: `/uptime/bulk-import` },
];
const [createBulkMonitors, hookLoading] = useBulkMonitors();
const handleSubmit = async () => {
if (!selectedFile) {
createToast({ body: t("bulkImport.noFileSelected") });
return;
}
const [success, data, error] = await createBulkMonitors(selectedFile, user);
if (success) {
// You can use `data` here if needed
createToast({ body: t("bulkImport.uploadSuccess") });
navigate("/uptime");
} else {
createToast({ body: error || t("bulkImport.uploadFailed") });
}
};
return (
<Box className="bulk-import-monitor">
<Breadcrumbs list={crumbs} />
<Stack
component="form"
gap={theme.spacing(12)}
mt={theme.spacing(6)}
>
<Typography
component="h1"
variant="h1"
>
{t("bulkImport.title")}
</Typography>
<ConfigBox>
<Box>
<Typography component="h2">{t("bulkImport.selectFileTips")}</Typography>
<Typography component="p">
<Trans
i18nKey="bulkImport.selectFileDescription"
components={{
template: (
<Link
color="info"
download
href="/bulk_import_monitors_template.csv"
/>
),
sample: (
<Link
color="info"
download
href="/bulk_import_monitors_sample.csv"
/>
),
}}
/>
</Typography>
</Box>
<Stack gap={theme.spacing(12)}>
<Stack gap={theme.spacing(6)}>
<UploadFile onFileSelect={(file) => setSelectedFile(file)} />
</Stack>
</Stack>
</ConfigBox>
<Stack
direction="row"
justifyContent="flex-end"
>
<Button
variant="contained"
color="accent"
onClick={handleSubmit}
disabled={hookLoading}
loading={hookLoading}
>
{t("submit")}
</Button>
</Stack>
</Stack>
</Box>
);
};
export default BulkImport;

View File

@@ -25,6 +25,7 @@ import PulseDot from "../../../Components/Animated/PulseDot";
import SkeletonLayout from "./skeleton";
import "./index.css";
import Dialog from "../../../Components/Dialog";
import NotificationIntegrationModal from "../../../Components/NotificationIntegrationModal/Components/NotificationIntegrationModal";
/**
* Parses a URL string and returns a URL object.
@@ -196,6 +197,18 @@ const Configure = () => {
const parsedUrl = parseUrl(monitor?.url);
const protocol = parsedUrl?.protocol?.replace(":", "") || "";
// Notification modal state
const [isNotificationModalOpen, setIsNotificationModalOpen] = useState(false);
const handleOpenNotificationModal = () => {
setIsNotificationModalOpen(true);
};
const handleClosenNotificationModal = () => {
setIsNotificationModalOpen(false);
};
const statusColor = {
true: theme.palette.success.main,
false: theme.palette.error.main,
@@ -371,6 +384,17 @@ const Configure = () => {
value={parsedUrl?.host || monitor?.url || ""}
disabled={true}
/>
<TextInput
type="number"
id="monitor-port"
label={t("portToMonitor")}
placeholder="5173"
value={monitor.port || ""}
onChange={(event) => handleChange(event, "port")}
error={errors["port"] ? true : false}
helperText={errors["port"]}
hidden={monitor.type !== "port"}
/>
<TextInput
type="text"
id="monitor-name"
@@ -413,6 +437,15 @@ const Configure = () => {
value={user?.email}
onChange={(event) => handleChange(event)}
/>
<Box mt={theme.spacing(2)}>
<Button
variant="contained"
color="accent"
onClick={handleOpenNotificationModal}
>
{t("notifications.integrationButton")}
</Button>
</Box>
{/* <Checkbox
id="notify-email"
label="Also notify via email to multiple addresses (coming soon)"
@@ -546,6 +579,13 @@ const Configure = () => {
onConfirm={handleRemove}
isLoading={isLoading}
/>
<NotificationIntegrationModal
open={isNotificationModalOpen}
onClose={handleClosenNotificationModal}
monitor={monitor}
setMonitor={setMonitor}
/>
</Stack>
);
};

View File

@@ -350,7 +350,7 @@ const CreateMonitor = () => {
<ConfigBox>
<Box>
<Typography component="h2">{t("settingsGeneralSettings")}</Typography>
<Typography component="p">{t("distributedUptimeCreateSelectURL")}</Typography>
<Typography component="p">{t("uptimeCreateSelectURL")}</Typography>
</Box>
<Stack gap={theme.spacing(15)}>
<TextInput

View File

@@ -61,18 +61,20 @@ const UptimeStatusBoxes = ({
</>
}
/>
<StatBox
heading="certificate expiry"
subHeading={
<Typography
component="span"
fontSize={13}
color={theme.palette.primary.contrastText}
>
{certificateExpiry}
</Typography>
}
/>
{monitor?.type === "http" && (
<StatBox
heading="certificate expiry"
subHeading={
<Typography
component="span"
fontSize={13}
color={theme.palette.primary.contrastText}
>
{certificateExpiry}
</Typography>
}
/>
)}
</StatusBoxes>
);
};

View File

@@ -189,8 +189,9 @@ const UptimeMonitors = () => {
<Breadcrumbs list={BREADCRUMBS} />
<CreateMonitorHeader
isAdmin={isAdmin}
shouldRender={!isLoading}
isLoading={isLoading}
path="/uptime/create"
bulkPath="/uptime/bulk-import"
/>
<Greeting type="uptime" />
<StatusBoxes

View File

@@ -59,6 +59,7 @@ import ProtectedRoute from "../Components/ProtectedRoute";
import ProtectedDistributedUptimeRoute from "../Components/ProtectedDistributedUptimeRoute";
import CreateNewMaintenanceWindow from "../Pages/Maintenance/CreateMaintenance";
import withAdminCheck from "../Components/HOC/withAdminCheck";
import BulkImport from "../Pages/Uptime/BulkImport";
const Routes = () => {
const AdminCheckedRegister = withAdminCheck(AuthRegister);
@@ -81,6 +82,11 @@ const Routes = () => {
element={<Uptime />}
/>
<Route
path="/uptime/bulk-import"
element={<BulkImport />}
/>
<Route
path="/uptime/create/:monitorId?"
element={<UptimeCreate />}
@@ -93,16 +99,16 @@ const Routes = () => {
path="/uptime/configure/:monitorId/"
element={<UptimeConfigure />}
/>
<Route
{/* <Route
path="/distributed-uptime"
element={
<ProtectedDistributedUptimeRoute>
<DistributedUptimeMonitors />{" "}
</ProtectedDistributedUptimeRoute>
}
/>
/> */}
<Route
{/* <Route
path="/distributed-uptime/create"
element={
<ProtectedDistributedUptimeRoute>
@@ -117,15 +123,15 @@ const Routes = () => {
<CreateDistributedUptime />
</ProtectedDistributedUptimeRoute>
}
/>
<Route
/> */}
{/* <Route
path="/distributed-uptime/:monitorId"
element={
<ProtectedDistributedUptimeRoute>
<DistributedUptimeDetails />
</ProtectedDistributedUptimeRoute>
}
/>
/> */}
<Route
path="pagespeed"
@@ -151,9 +157,9 @@ const Routes = () => {
path="infrastructure/create"
element={<InfrastructureCreate />}
/>
<Route
path="/infrastructure/configure/:monitorId"
element={<InfrastructureCreate />}
<Route
path="/infrastructure/configure/:monitorId"
element={<InfrastructureCreate />}
/>
<Route
path="infrastructure/:monitorId"
@@ -174,42 +180,42 @@ const Routes = () => {
element={<Status />}
/>
<Route
{/* <Route
path="/status/distributed/:url"
element={
<ProtectedDistributedUptimeRoute>
<DistributedUptimeStatus />
</ProtectedDistributedUptimeRoute>
}
/>
/> */}
<Route
path="status/uptime/create"
element={<CreateStatus />}
/>
<Route
{/* <Route
path="/status/distributed/create/:monitorId"
element={
<ProtectedDistributedUptimeRoute>
<CreateDistributedUptimeStatus />
</ProtectedDistributedUptimeRoute>
}
/>
/> */}
<Route
path="status/uptime/configure/:url"
element={<CreateStatus />}
/>
<Route
{/* <Route
path="/status/distributed/configure/:url"
element={
<ProtectedDistributedUptimeRoute>
<CreateDistributedUptimeStatus />
</ProtectedDistributedUptimeRoute>
}
/>
/> */}
<Route
path="integrations"
@@ -277,10 +283,10 @@ const Routes = () => {
path="/status/uptime/public/:url"
element={<Status />}
/>
<Route
{/* <Route
path="/status/distributed/public/:url"
element={<DistributedUptimeStatus />}
/>
/> */}
<Route
path="/server-unreachable"

View File

@@ -32,7 +32,7 @@ class NetworkService {
config.headers = {
Authorization: `Bearer ${authToken}`,
"Accept-Language": currentLanguage,
"Accept-Language": currentLanguage === "gb" ? "en" : currentLanguage,
...config.headers,
};
@@ -54,6 +54,7 @@ class NetworkService {
}
// Handle authentication errors
if (error.response && error.response.status === 401) {
dispatch(clearAuthState());
dispatch(clearUptimeMonitorState());
@@ -275,6 +276,7 @@ class NetworkService {
matchMethod: monitor.matchMethod,
expectedValue: monitor.expectedValue,
jsonPath: monitor.jsonPath,
...(monitor.type === "port" && { port: monitor.port }),
};
return this.axiosInstance.put(`/monitors/${monitorId}`, payload, {
headers: {
@@ -931,7 +933,7 @@ class NetworkService {
onOpen?.();
};
this.eventSource.addEventListener("open", (e) => {});
this.eventSource.addEventListener("open", (e) => { });
this.eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
@@ -1097,6 +1099,19 @@ class NetworkService {
return this.axiosInstance.delete(`/status-page/${encodedUrl}`, {});
}
// ************************************
// Create bulk monitors
// ************************************
async createBulkMonitors(formData) {
const response = await this.axiosInstance.post(`/monitors/bulk`, formData, {
headers: {
"Content-Type": "multipart/form-data",
},
});
return response.data;
}
// ************************************
// Fetch monitors with summary by TeamID
// ************************************

View File

@@ -16,7 +16,7 @@ Object.keys(translations).forEach((path) => {
});
const savedLanguage = store.getState()?.ui?.language;
const initialLanguage = savedLanguage || primaryLanguage;
const initialLanguage = savedLanguage;
i18n.use(initReactI18next).init({
resources,

View File

@@ -7,18 +7,11 @@ const nameSchema = joi
.string()
.max(50)
.trim()
.pattern(/^(?=.*[\p{L}\p{Sc}])[\p{L}\p{Sc}\s']+$/u, {
name: "name.containsLetterOrSymbol",
})
.pattern(/^[\p{L}\p{Sc}\s']+$/u, {
name: "name.validCharacters",
})
.pattern(/^[\p{L}\p{M}''\- ]+$/u)
.messages({
"string.empty": "Name is required",
"string.max": "Name must be less than 50 characters",
"string.pattern.name": "Name must contain at least 1 letter or currency symbol",
"string.pattern.base.validCharacters":
"Can only contain letters, spaces, apostrophes, and currency symbols",
"string.pattern.base": "Name must contain only letters, spaces, apostrophes, or hyphens"
});
const passwordSchema = joi
@@ -73,8 +66,8 @@ const credentials = joi.object({
return lowercasedValue;
})
.messages({
"string.empty": "Email is required",
"string.email": "Must be a valid email address",
"string.empty": "authRegisterEmailRequired",
"string.email": "authRegisterEmailInvalid",
}),
password: passwordSchema,
newPassword: passwordSchema,
@@ -159,7 +152,20 @@ const monitorValidation = joi.object({
"string.invalidUrl": "Please enter a valid URL with optional port",
"string.pattern.base": "Please enter a valid container ID.",
}),
port: joi.number(),
port: joi.number()
.integer()
.min(1)
.max(65535)
.when("type", {
is: "port",
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.",
}),
otherwise: joi.optional(),
}),
name: joi.string().trim().max(50).allow("").messages({
"string.max": "This field should not exceed the 50 characters limit.",
}),
@@ -247,7 +253,9 @@ const statusPageValidation = joi.object({
});
const settingsValidation = joi.object({
ttl: joi.number().required().messages({
"string.empty": "TTL is required",
"string.empty": "Please enter a value",
"number.base": "Please enter a valid number",
"any.required": "Please enter a value"
}),
});
@@ -366,5 +374,5 @@ export {
advancedSettingsValidation,
infrastructureMonitorValidation,
statusPageValidation,
logoImageValidation,
logoImageValidation
};

View File

@@ -4,8 +4,8 @@
"registerHere": "Register here",
"email": "E-mail",
"forgotPassword": "Forgot Password",
"password": "password",
"signUp": "Sign up",
"password": "Password",
"signUp": "Sign Up",
"submit": "Submit",
"title": "Title",
"continue": "Continue",
@@ -17,9 +17,11 @@
"authForgotPasswordTitle": "Forgot password?",
"authForgotPasswordResetPassword": "Reset password",
"createPassword": "Create your password",
"createAPassword": "Create a password",
"createAPassword": "Password",
"authRegisterAlreadyHaveAccount": "Already have an account?",
"authRegisterLoginLink": "Log In",
"commonAppName": "Checkmate",
"welcomeBack": "Welcome back! You're successfully logged in.",
"authLoginEnterEmail": "Enter your email",
"authRegisterTitle": "Create an account",
"authRegisterStepOneTitle": "Create your account",
@@ -42,7 +44,7 @@
"authSetNewPasswordDescription": "Your new password must be different from previously used passwords.",
"authSetNewPasswordNewPassword": "New password",
"authSetNewPasswordConfirmPassword": "Confirm password",
"confirmPassword": "Confirm your password",
"confirmPassword": "Re-enter password to confirm",
"authSetNewPasswordResetPassword": "Reset password",
"authSetNewPasswordBackTo": "Back to",
"authPasswordMustBeAtLeast": "Must be at least",
@@ -53,16 +55,19 @@
"authPasswordUpperCharacter": "one upper character",
"authPasswordLowerCharacter": "one lower character",
"authPasswordConfirmAndPassword": "Confirm password and password",
"authPasswordMustMatch": "must match",
"authPasswordMustMatch": "Passwords must match",
"validationNameRequired": "Please enter your name",
"validationNameTooLong": "Name should be less than 50 characters",
"validationNameInvalidCharacters": "Please use only letters, spaces, apostrophes, or hyphens",
"authRegisterCreateAccount": "Create your account to get started",
"authRegisterCreateSuperAdminAccount": "Create your Super admin account to get started",
"authRegisterSignUpWithEmail": "Sign up with Email",
"authRegisterBySigningUp": "By signing up, you agree to our",
"authRegisterCreateSuperAdminAccount": "Create your super admin account to get started",
"authRegisterSignUpWithEmail": "Create super admin account",
"authRegisterBySigningUp": "By creating an account, you agree to our <a1>Terms of Service</a1> and <a2>Privacy Policy</a2>.",
"distributedStatusHeaderText": "Real-time, real-device coverage",
"distributedStatusSubHeaderText": "Powered by millions devices worldwide, view a system performance by global region, country or city",
"settingsGeneralSettings": "General settings",
"settingsDisplayTimezone": "Display timezone",
"settingsDisplayTimezoneDescription": "The timezone of the dashboard you publicly display.",
"settingsDisplayTimezoneDescription": "Select the timezone used to display dates and times throughout the application.",
"settingsAppearance": "Appearance",
"settingsAppearanceDescription": "Switch between light and dark mode, or change user interface language",
"settingsThemeMode": "Theme Mode",
@@ -71,23 +76,25 @@
"settingsDistributedUptimeDescription": "Enable/disable distributed uptime monitoring.",
"settingsEnabled": "Enabled",
"settingsDisabled": "Disabled",
"settingsHistoryAndMonitoring": "History and monitoring",
"settingsHistoryAndMonitoringDescription": "Define here for how long you want to keep the data. You can also remove all past data.",
"settingsHistoryAndMonitoring": "History of monitoring",
"settingsHistoryAndMonitoringDescription": "Define how long you want to retain historical data. You can also clear all existing data.",
"settingsTTLLabel": "The days you want to keep monitoring history.",
"settingsTTLOptionalLabel": "0 for infinite",
"settingsClearAllStats": "Clear all stats. This is irreversible.",
"settingsClearAllStatsButton": "Clear all stats",
"settingsClearAllStatsDialogTitle": "Do you want to clear all stats?",
"settingsClearAllStatsDialogDescription": "Once deleted, your monitors cannot be retrieved.",
"settingsClearAllStatsDialogDescription": "Once removed, the monitoring history and stats cannot be retrieved.",
"settingsClearAllStatsDialogConfirm": "Yes, clear all stats",
"settingsDemoMonitors": "Demo monitors",
"settingsDemoMonitorsDescription": "Here you can add and remove demo monitors.",
"settingsAddDemoMonitors": "Add demo monitors",
"settingsDemoMonitorsDescription": "Add sample monitors for demonstration purposes.",
"settingsAddDemoMonitors": "Adding demo monitors",
"settingsAddDemoMonitorsButton": "Add demo monitors",
"settingsRemoveAllMonitors": "Remove all monitors",
"settingsSystemReset": "System reset",
"settingsSystemResetDescription": "Remove all monitors from your system.",
"settingsRemoveAllMonitors": "Removing all monitors",
"settingsRemoveAllMonitorsButton": "Remove all monitors",
"settingsRemoveAllMonitorsDialogTitle": "Do you want to remove all monitors?",
"settingsRemoveAllMonitorsDialogConfirm": "Yes, clear all monitors",
"settingsRemoveAllMonitorsDialogConfirm": "Yes, remove all monitors",
"settingsWallet": "Wallet",
"settingsWalletDescription": "Connect your wallet here. This is required for the Distributed Uptime monitor to connect to multiple nodes globally.",
"settingsAbout": "About",
@@ -115,12 +122,12 @@
"http": "HTTP",
"monitor": "monitor",
"aboutus": "About Us",
"signUP": "Sign Up",
"now": "Now",
"delete": "Delete",
"configure": "Configure",
"networkError": "Network error",
"responseTime": "Response time:",
"responseTime": "Response time",
"ms": "ms",
"bar": "Bar",
"area": "Area",
@@ -388,5 +395,41 @@
"inviteNoTokenFound": "No invite token found",
"pageSpeedWarning": "Warning: You haven't added a Google PageSpeed API key. Without it, the PageSpeed monitor won't function.",
"pageSpeedLearnMoreLink": "Click here to learn",
"pageSpeedAddApiKey": "how to add your API key."
"pageSpeedAddApiKey": "how to add your API key.",
"update": "Update",
"invalidFileFormat": "Unsupported file format!",
"invalidFileSize": "File size is too large!",
"ClickUpload": "Click to upload",
"DragandDrop": "drag and drop",
"MaxSize": "Maximum Size",
"SupportedFormats": "Supported formats",
"FirstName": "Name",
"LastName": "Surname",
"EmailDescriptionText": "Your current email—it cannot be changed.",
"YourPhoto": "Profile photo",
"PhotoDescriptionText": "This photo will be displayed in your profile page.",
"save": "Save",
"DeleteAccountTitle": "Remove account",
"DeleteAccountButton": "Remove account",
"DeleteDescriptionText": "This will remove the account and all associated data from the server. This isn't reversible.",
"DeleteAccountWarning": "Removing your account means you won't be able to sign in again and all your data will be removed. This isn't reversible.",
"DeleteWarningTitle": "Really remove this account?",
"authRegisterFirstName": "Name",
"authRegisterLastName": "Surname",
"authRegisterEmail": "Email",
"authRegisterEmailRequired": "To continue, please enter your email address",
"authRegisterEmailInvalid": "Please enter a valid email address",
"bulkImport": {
"title": "Bulk Import",
"selectFileTips": "Select CSV file to upload",
"selectFileDescription": "You can download our <template>template</template> or <sample>sample</sample>",
"selectFile": "Select File",
"parsingFailed": "Parsing failed",
"uploadSuccess": "Monitors created successfully!",
"validationFailed": "Validation failed",
"noFileSelected": "No file selected",
"fallbackPage": "Import a file to upload a list of servers in bulk"
},
"publicLink": "Public link"
}

View File

@@ -4,7 +4,7 @@
"registerHere": "Зарегистрироваться здесь",
"email": "Почта",
"forgotPassword": "Забыли пароль",
"password": "пароль",
"password": "Пароль",
"signUp": "Зарегистрироваться",
"submit": "Подтвердить",
"title": "Название",
@@ -17,9 +17,11 @@
"authForgotPasswordTitle": "Забыли пароль?",
"authForgotPasswordResetPassword": "Сбросить пароль",
"createPassword": "Создайте свой пароль",
"createAPassword": "Создайте пароль",
"createAPassword": "Пароль",
"authRegisterAlreadyHaveAccount": "Уже есть аккаунт?",
"authRegisterLoginLink": "Войти",
"commonAppName": "Checkmate",
"welcomeBack": "Добро пожаловать обратно! Вы успешно вошли в систему.",
"authLoginEnterEmail": "Введите свой email",
"authRegisterTitle": "Создать аккаунт",
"authRegisterStepOneTitle": "Создайте свой аккаут",
@@ -42,7 +44,8 @@
"authSetNewPasswordDescription": "Ваш новый пароль должен отличаться от ранее использованных паролей.",
"authSetNewPasswordNewPassword": "Новый пароль",
"authSetNewPasswordConfirmPassword": "Подтвердите пароль",
"confirmPassword": "Подтвердите ваш пароль",
"confirmPassword": "Введите пароль еще раз для подтверждения",
"confirmNewPasswordPlaceholder": "Подтвердите ваш новый пароль",
"authSetNewPasswordResetPassword": "Сбросить пароль",
"authSetNewPasswordBackTo": "Назад к",
"authPasswordMustBeAtLeast": "Должно быть как минимум",
@@ -53,16 +56,21 @@
"authPasswordUpperCharacter": "один верхний символ",
"authPasswordLowerCharacter": "один нижний символ",
"authPasswordConfirmAndPassword": "Подтвердите пароль и пароль",
"authPasswordMustMatch": "должен совпадать",
"authPasswordMustMatch": "Пароли должны совпадать",
"authRegisterCreateAccount": "Создайте свою учетную запись, чтобы начать",
"authRegisterCreateSuperAdminAccount": "Создайте учетную запись суперадминистратора, чтобы начать работу",
"authRegisterSignUpWithEmail": "Зарегистрироваться по электронной почте",
"authRegisterBySigningUp": "Регистрируясь, вы соглашаетесь с нашими",
"authRegisterSignUpWithEmail": "Создать учетную запись суперадминистратора",
"authRegisterBySigningUp": "Создавая учетную запись, вы соглашаетесь с нашими <a1>Условиями использования</a1> и <a2>Политикой конфиденциальности</a2>.",
"authRegisterFirstName": "Имя",
"authRegisterLastName": "Фамилия",
"authRegisterEmail": "Эл. почта",
"authRegisterEmailRequired": "Чтобы продолжить, пожалуйста, введите ваш адрес электронной почты",
"authRegisterEmailInvalid": "Пожалуйста, введите корректный адрес электронной почты",
"distributedStatusHeaderText": "Охват реального времени и реального устройства",
"distributedStatusSubHeaderText": "Работает на миллионах устройств по всему миру, просматривайте производительность системы по глобальному региону, стране или городу",
"settingsGeneralSettings": "Общие настройки",
"settingsDisplayTimezone": "Отображать часовой пояс",
"settingsDisplayTimezoneDescription": "Часовой пояс панели мониторинга, которую вы публично отображаете.",
"settingsDisplayTimezoneDescription": "Выберите часовой пояс, используемый для отображения дат и времени в приложении.",
"settingsAppearance": "Внешний вид",
"settingsAppearanceDescription": "Переключение между светлым и темным режимом или изменение языка пользовательского интерфейса",
"settingsThemeMode": "Тема",
@@ -71,23 +79,25 @@
"settingsDistributedUptimeDescription": "Включить/выключить distributed uptime monitoring.",
"settingsEnabled": "Включено",
"settingsDisabled": "Выключено",
"settingsHistoryAndMonitoring": "История и мониторинг",
"settingsHistoryAndMonitoringDescription": "Определите здесь, как долго вы хотите хранить данные. Вы также можете удалить все прошлые данные.",
"settingsHistoryAndMonitoring": "История мониторинга",
"settingsHistoryAndMonitoringDescription": "Определите, как долго вы хотите хранить исторические данные. Вы также можете очистить все существующие данные.",
"settingsTTLLabel": "Дни, за которыми вы хотите следить.",
"settingsTTLOptionalLabel": "0 для бесконечности",
"settingsClearAllStats": "Очистить всю статистику. Это необратимо.",
"settingsClearAllStatsButton": "Очистить всю статистику",
"settingsClearAllStatsDialogTitle": "Хотите очистить всю статистику?",
"settingsClearAllStatsDialogDescription": "После удаления ваши мониторы не могут быть восстановлены.",
"settingsClearAllStatsDialogDescription": "После удаления история мониторинга и статистика не могут быть восстановлены.",
"settingsClearAllStatsDialogConfirm": "Да, очистить всю статистику",
"settingsDemoMonitors": "Демо мониторы",
"settingsDemoMonitorsDescription": "Здесь вы можете добавлять и удалять демонстрационные мониторы.",
"settingsAddDemoMonitors": "Добавьте демонстрационные мониторы",
"settingsDemoMonitorsDescription": "Добавьте примеры мониторов для демонстрации.",
"settingsAddDemoMonitors": "Добавление демонстрационных мониторов",
"settingsAddDemoMonitorsButton": "Добавьте демонстрационные мониторы",
"settingsRemoveAllMonitors": "Удалить все демонстрационные мониторы",
"settingsRemoveAllMonitorsButton": "Удалить все демонстрационные мониторы",
"settingsSystemReset": "Сброс системы",
"settingsSystemResetDescription": "Удалить все мониторы из вашей системы.",
"settingsRemoveAllMonitors": "Удаление всех мониторов",
"settingsRemoveAllMonitorsButton": "Удалить все мониторы",
"settingsRemoveAllMonitorsDialogTitle": "Хотите удалить все мониторы?",
"settingsRemoveAllMonitorsDialogConfirm": "Да, очистить все мониторы",
"settingsRemoveAllMonitorsDialogConfirm": "Да, удалить все мониторы",
"settingsWallet": "Кошелёк",
"settingsWalletDescription": "Подключите свой кошелек здесь. Это необходимо для того, чтобы монитор Distributed Uptime мог подключиться к нескольким узлам по всему миру.",
"settingsAbout": "О",
@@ -97,6 +107,16 @@
"settingsFailedToSave": "Не удалось сохранить настройки",
"settingsStatsCleared": "Статистика успешно очищена",
"settingsFailedToClearStats": "Не удалось очистить статистику",
"FirstName": "Имя",
"LastName": "Фамилия",
"YourPhoto": "Фото профиля",
"PhotoDescriptionText": "Это фото будет отображаться на странице вашего профиля.",
"EmailDescriptionText": "Ваш текущий email—его нельзя изменить.",
"DeleteAccountTitle": "Удалить аккаунт",
"DeleteAccountButton": "Удалить аккаунт",
"DeleteDescriptionText": "Это удалит аккаунт и все связанные данные с сервера. Это необратимо.",
"DeleteAccountWarning": "Удаление аккаунта означает, что вы не сможете снова войти в систему, и все ваши данные будут удалены. Это необратимо.",
"DeleteWarningTitle": "Действительно удалить этот аккаунт?",
"settingsDemoMonitorsAdded": "Успешно добавлены демонстрационные мониторы",
"settingsFailedToAddDemoMonitors": "Не удалось добавить демонстрационные мониторы",
"settingsMonitorsDeleted": "Успешно удалены все мониторы",
@@ -115,12 +135,12 @@
"http": "HTTP",
"monitor": "монитор",
"aboutus": "О Нас",
"signUP": "Зарегистрироваться",
"now": "Сейчас",
"delete": "Удалить",
"configure": "Настроить",
"networkError": "Ошибка сети",
"responseTime": "Время ответа:",
"responseTime": "Время ответа",
"ms": "мс",
"bar": "Bar",
"area": "Area",
@@ -374,4 +394,4 @@
"pageSpeedWarning": "Предупреждение: Вы не добавили ключ API Google PageSpeed. Без него монитор PageSpeed не будет работать.",
"pageSpeedLearnMoreLink": "Нажмите здесь, чтобы узнать",
"pageSpeedAddApiKey": "как добавить ваш ключ API."
}
}

View File

@@ -17,9 +17,11 @@
"authForgotPasswordTitle": "Parolanızı mı unuttunuz?",
"authForgotPasswordResetPassword": "Parola sıfırla",
"createPassword": "Parolanızı oluşturun",
"createAPassword": "Bir parola oluşturun",
"createAPassword": "Parola",
"authRegisterAlreadyHaveAccount": "Zaten hesabınız var mı?",
"authRegisterLoginLink": "Giriş Yap",
"commonAppName": "Checkmate",
"welcomeBack": "Tekrar hoş geldiniz! Başarıyla giriş yaptınız.",
"authLoginEnterEmail": "E-posta adresinizi girin",
"authRegisterTitle": "Hesap oluştur",
"authRegisterStepOneTitle": "Hesabınızı oluşturun",
@@ -42,7 +44,8 @@
"authSetNewPasswordDescription": "Yeni şifreniz daha önce kullanılan şifrelerden farklı olmalıdır.",
"authSetNewPasswordNewPassword": "Yeni şifre",
"authSetNewPasswordConfirmPassword": "Parolayı onayla",
"confirmPassword": "Parolanızı onaylayın",
"confirmPassword": "Onaylamak için parolayı tekrar girin",
"confirmNewPasswordPlaceholder": "Yeni parolanızı onaylayın",
"authSetNewPasswordResetPassword": "Parola sıfırla",
"authSetNewPasswordBackTo": "Geri dön",
"authPasswordMustBeAtLeast": "En az",
@@ -53,7 +56,7 @@
"authPasswordUpperCharacter": "bir büyük harf",
"authPasswordLowerCharacter": "bir küçük harf",
"authPasswordConfirmAndPassword": "Onay şifresi ve şifre",
"authPasswordMustMatch": "eşleşmelidir",
"authPasswordMustMatch": "Parolalar eşleşmelidir",
"authRegisterCreateAccount": "Hesap oluşturmak için devam et",
"authRegisterCreateSuperAdminAccount": "Super admin hesabınızı oluşturmak için devam edin",
"authRegisterSignUpWithEmail": "E-posta ile kayıt ol",
@@ -62,7 +65,7 @@
"distributedStatusSubHeaderText": "Dünya çapında milyonlarca cihaz tarafından desteklenen sistem performansını küresel bölgeye, ülkeye veya şehre göre görüntüleyin",
"settingsGeneralSettings": "Genel ayarlar",
"settingsDisplayTimezone": "Görüntüleme saat dilimi",
"settingsDisplayTimezoneDescription": "Herkese açık olarak görüntülediğiniz kontrol panelinin saat dilimi.",
"settingsDisplayTimezoneDescription": "Uygulama genelinde tarih ve saatlerin görüntülenmesi için kullanılacak saat dilimini seçin.",
"settingsAppearance": "Görünüm",
"settingsAppearanceDescription": "Açık ve koyu mod arasında geçiş yapın veya kullanıcı arayüzü dilini değiştirin",
"settingsThemeMode": "Tema",
@@ -71,23 +74,25 @@
"settingsDistributedUptimeDescription": "Dağıtılmış çalışma süresi izlemeyi etkinleştirin/devre dışı bırakın.",
"settingsEnabled": "Etkin",
"settingsDisabled": "Devre dışı",
"settingsHistoryAndMonitoring": "Geçmiş ve izleme",
"settingsHistoryAndMonitoringDescription": "Verileri ne kadar süreyle saklamak istediğinizi burada tanımlayın. Ayrıca tüm geçmiş verileri kaldırabilirsiniz.",
"settingsHistoryAndMonitoring": "İzleme geçmişi",
"settingsHistoryAndMonitoringDescription": "Geçmiş verileri ne kadar süre saklamak istediğinizi tanımlayın. Ayrıca mevcut tüm verileri temizleyebilirsiniz.",
"settingsTTLLabel": "İzleme geçmişini saklamak istediğiniz gün sayısı.",
"settingsTTLOptionalLabel": "Sınırsız için 0",
"settingsClearAllStats": "Tüm istatistikleri temizle. Bu geri alınamaz.",
"settingsClearAllStatsButton": "Tüm istatistikleri temizle",
"settingsClearAllStatsDialogTitle": "Tüm istatistikleri temizlemek istiyor musunuz?",
"settingsClearAllStatsDialogDescription": "Silindikten sonra, monitörleriniz geri alınamaz.",
"settingsClearAllStatsDialogDescription": "Kaldırıldıktan sonra, izleme geçmişi ve istatistikleri geri alınamaz.",
"settingsClearAllStatsDialogConfirm": "Evet, tüm istatistikleri temizle",
"settingsDemoMonitors": "Demo monitörler",
"settingsDemoMonitorsDescription": "Burada demo monitörler ekleyebilir ve kaldırabilirsiniz.",
"settingsAddDemoMonitors": "Demo monitörler ekle",
"settingsDemoMonitorsDescription": "Gösterim amaçlı örnek monitörler ekleyin.",
"settingsAddDemoMonitors": "Demo monitörler ekleniyor",
"settingsAddDemoMonitorsButton": "Demo monitörler ekle",
"settingsRemoveAllMonitors": "Tüm monitörleri kaldır",
"settingsSystemReset": "Sistem sıfırlama",
"settingsSystemResetDescription": "Sisteminizden tüm monitörleri kaldırın.",
"settingsRemoveAllMonitors": "Tüm monitörler kaldırılıyor",
"settingsRemoveAllMonitorsButton": "Tüm monitörleri kaldır",
"settingsRemoveAllMonitorsDialogTitle": "Tüm monitörleri kaldırmak istiyor musunuz?",
"settingsRemoveAllMonitorsDialogConfirm": "Evet, tüm monitörleri temizle",
"settingsRemoveAllMonitorsDialogConfirm": "Evet, tüm monitörleri kaldır",
"settingsWallet": "Cüzdan",
"settingsWalletDescription": "Cüzdanınızı buradan bağlayın. Bu, Dağıtılmış Çalışma Süresi monitörünün küresel olarak birden çok düğüme bağlanması için gereklidir.",
"settingsAbout": "Hakkında",
@@ -115,12 +120,12 @@
"http": "HTTP",
"monitor": "monitör",
"aboutus": "Hakkımızda",
"signUP": "Hesap Oluştur",
"now": "Şimdi",
"delete": "Sil",
"configure": "Yapılandır",
"networkError": "Ağ hatası",
"responseTime": "Yanıt süresi:",
"responseTime": "Yanıt süresi",
"ms": "ms",
"bar": "Çubuk",
"area": "Alan",
@@ -388,5 +393,39 @@
"inviteNoTokenFound": "",
"pageSpeedWarning": "",
"pageSpeedLearnMoreLink": "",
"pageSpeedAddApiKey": ""
"pageSpeedAddApiKey": "",
"update": "",
"invalidFileFormat": "",
"invalidFileSize": "",
"ClickUpload": "",
"DragandDrop": "",
"MaxSize": "",
"SupportedFormats": "",
"FirstName": "Ad",
"LastName": "Soyad",
"EmailDescriptionText": "Mevcut e-postanız—değiştirilemez.",
"YourPhoto": "Profil fotoğrafı",
"PhotoDescriptionText": "Bu fotoğraf profil sayfanızda görüntülenecektir.",
"save": "",
"DeleteAccountTitle": "Hesabı kaldır",
"DeleteAccountButton": "Hesabı kaldır",
"DeleteDescriptionText": "Bu, hesabı ve tüm ilişkili verileri sunucudan kaldıracaktır. Bu geri alınamaz.",
"DeleteAccountWarning": "Hesabınızı kaldırmak, tekrar oturum açamayacağınız ve tüm verilerinizin kaldırılacağı anlamına gelir. Bu geri alınamaz.",
"DeleteWarningTitle": "Bu hesabı gerçekten kaldırmak istiyor musunuz?",
"authRegisterFirstName": "Ad",
"authRegisterLastName": "Soyad",
"authRegisterEmail": "E-posta",
"authRegisterEmailRequired": "Devam etmek için lütfen e-posta adresinizi girin",
"authRegisterEmailInvalid": "Lütfen geçerli bir e-posta adresi girin",
"bulkImport": {
"title": "",
"selectFileTips": "",
"selectFileDescription": "",
"selectFile": "",
"parsingFailed": "",
"uploadSuccess": "",
"validationFailed": "",
"noFileSelected": "",
"fallbackPage": ""
}
}

1
docker/.gitignore vendored
View File

@@ -7,5 +7,6 @@ dist/mongo/data/*
dist/redis/data/*
prod/mongo/data/*
prod/redis/data/*
dist/docker-compose-test.yaml
*.env
prod/certbot/*

View File

@@ -4,23 +4,27 @@
cd "$(dirname "$0")"
cd ../..
# Define an array of services and their Dockerfiles
declare -A services=(
["uptime_client"]="./docker/dev/client.Dockerfile"
["uptime_database_mongo"]="./docker/dev/mongoDB.Dockerfile"
["uptime_redis"]="./docker/dev/redis.Dockerfile"
["uptime_server"]="./docker/dev/server.Dockerfile"
# Define service names and their corresponding Dockerfiles in parallel arrays
services=("uptime_client" "uptime_mongo" "uptime_redis" "uptime_server")
dockerfiles=(
"./docker/dev/client.Dockerfile"
"./docker/dev/mongoDB.Dockerfile"
"./docker/dev/redis.Dockerfile"
"./docker/dev/server.Dockerfile"
)
# Loop through each service and build the corresponding image
for service in "${!services[@]}"; do
docker build -f "${services[$service]}" -t "$service" .
for i in "${!services[@]}"; do
service="${services[$i]}"
dockerfile="${dockerfiles[$i]}"
## Check if the build succeeded
docker build -f "$dockerfile" -t "$service" .
# Check if the build succeeded
if [ $? -ne 0 ]; then
echo "Error building $service image. Exiting..."
exit 1
fi
done
echo "All images built successfully"
echo "All images built successfully"

View File

@@ -18,10 +18,13 @@ COPY ./client/package*.json ./
RUN npm install
COPY ./client .
COPY ./client ./
RUN npm run build-dev
RUN npm run build
RUN npm install -g serve
FROM nginx:1.27.1-alpine
CMD ["serve","-s", "dist", "-l", "5173"]
COPY --from=build /app/dist /usr/share/nginx/html
COPY --from=build /app/env.sh /docker-entrypoint.d/env.sh
RUN chmod +x /docker-entrypoint.d/env.sh
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -3,8 +3,12 @@ services:
image: uptime_client:latest
restart: always
ports:
- "5173:5173"
- "80:80"
environment:
UPTIME_APP_API_BASE_URL: "http://localhost:5000/api/v1"
UPTIME_APP_CLIENT_HOST: "http://localhost"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d/
depends_on:
- server
server:
@@ -31,10 +35,17 @@ services:
retries: 5
start_period: 5s
mongodb:
image: uptime_database_mongo:latest
image: uptime_mongo:latest
restart: always
command: ["mongod", "--quiet"]
command: ["mongod", "--quiet", "--replSet", "rs0", "--bind_ip_all"]
ports:
- "27017:27017"
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
interval: 5s
timeout: 30s
start_period: 0s
start_interval: 1s
retries: 30

View File

@@ -0,0 +1,35 @@
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name localhost; # Set server name to localhost
server_tokens off;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
# location /api/ {
# proxy_pass http://server:5000/api/;
# proxy_http_version 1.1;
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme;
# proxy_set_header Connection '';
# chunked_transfer_encoding off;
# proxy_buffering off;
# proxy_cache off;
# }
location /api-docs/ {
proxy_pass http://server:5000/api-docs/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

View File

@@ -1,11 +1,11 @@
#!/bin/bash
# Change directory to root Server directory for correct Docker Context
cd "$(dirname "$0")"
cd ../../../
cd ../../
# Define an array of services and their Dockerfiles
declare -A services=(
["bluewaveuptime/uptime_client"]=".docker/dist/client.Dockerfile"
["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"

View File

@@ -23,7 +23,7 @@ RUN npm run build
FROM nginx:1.27.1-alpine
COPY ./server/docker/dist/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf
COPY ./docker/dist/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html
COPY --from=build /app/env.sh /docker-entrypoint.d/env.sh
RUN chmod +x /docker-entrypoint.d/env.sh

View File

@@ -1,17 +1,17 @@
services:
client:
image: bluewaveuptime/uptime_client:latest
image: ghcr.io/bluewave-labs/checkmate:frontend-dist
restart: always
environment:
UPTIME_APP_API_BASE_URL: "http://localhost:5000/api/v1"
UPTIME_STATUS_PAGE_SUBDOMAIN_PREFIX: "http://uptimegenie.com/"
UPTIME_APP_CLIENT_HOST: "http://localhost"
ports:
- "80:80"
- "443:443"
depends_on:
- server
server:
image: bluewaveuptime/uptime_server:latest
image: ghcr.io/bluewave-labs/checkmate:backend-dist
restart: always
ports:
- "5000:5000"
@@ -19,12 +19,13 @@ services:
- redis
- mongodb
environment:
- DB_CONNECTION_STRING=mongodb://mongodb:27017/uptime_db
- REDIS_HOST=redis
- DB_CONNECTION_STRING=mongodb://mongodb:27017/uptime_db?replicaSet=rs0
- REDIS_URL=redis://redis:6379
- CLIENT_HOST=http://localhost
# volumes:
# - /var/run/docker.sock:/var/run/docker.sock:ro
redis:
image: bluewaveuptime/uptime_redis:latest
image: ghcr.io/bluewave-labs/checkmate:redis-dist
restart: always
ports:
- "6379:6379"
@@ -37,10 +38,17 @@ services:
retries: 5
start_period: 5s
mongodb:
image: bluewaveuptime/uptime_database_mongo:latest
image: ghcr.io/bluewave-labs/checkmate:mongo-dist
restart: always
volumes:
- ./mongo/data:/data/db
command: ["mongod", "--quiet"]
command: ["mongod", "--quiet", "--replSet", "rs0", "--bind_ip_all"]
ports:
- "27017:27017"
healthcheck:
test: echo "try { rs.status() } catch (err) { rs.initiate({_id:'rs0',members:[{_id:0,host:'mongodb:27017'}]}) }" | mongosh --port 27017 --quiet
interval: 5s
timeout: 30s
start_period: 0s
start_interval: 1s
retries: 30

View File

@@ -15,14 +15,14 @@ server {
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://server:5000/api/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# location /api/ {
# proxy_pass http://server:5000/api/;
# proxy_http_version 1.1;
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme;
# }
location /api-docs/ {
proxy_pass http://server:5000/api-docs/;

View File

@@ -1,10 +1,10 @@
services:
client:
image: uptime_client:latest
image: ghcr.io/bluewave-labs/checkmate:frontend-demo
restart: always
environment:
UPTIME_APP_API_BASE_URL: "https://checkmate-demo.bluewavelabs.ca/api/v1"
UPTIME_STATUS_PAGE_SUBDOMAIN_PREFIX: "http://uptimegenie.com/"
UPTIME_APP_CLIENT_HOST: "https://checkmate-demo.bluewavelabs.ca"
ports:
- "80:80"
- "443:443"
@@ -22,7 +22,7 @@ services:
- ./certbot/www/:/var/www/certbot/:rw
- ./certbot/conf/:/etc/letsencrypt/:rw
server:
image: uptime_server:latest
image: ghcr.io/bluewave-labs/checkmate:backend-demo
restart: always
ports:
- "5000:5000"
@@ -31,8 +31,10 @@ services:
depends_on:
- redis
- mongodb
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
redis:
image: uptime_redis:latest
image: ghcr.io/bluewave-labs/checkmate:redis-demo
restart: always
ports:
- "6379:6379"
@@ -45,13 +47,20 @@ services:
retries: 5
start_period: 5s
mongodb:
image: uptime_database_mongo:latest
image: ghcr.io/bluewave-labs/checkmate:mongo-demo
restart: always
command: ["mongod", "--quiet", "--auth"]
command: ["mongod", "--quiet", "--replSet", "rs0", "--bind_ip_all"]
ports:
- "27017:27017"
volumes:
- ./mongo/data:/data/db
- ./mongo/init/create_users.js:/docker-entrypoint-initdb.d/create_users.js
# - ./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
interval: 5s
timeout: 30s
start_period: 0s
start_interval: 1s
retries: 30

View File

@@ -0,0 +1,41 @@
function initiateReplicaSet() {
try {
rs.initiate({
_id: "rs0",
members: [{ _id: 0, host: "localhost:27017" }],
});
} catch (e) {
print("Replica set already initiated or error occurred: " + e);
}
}
function waitForPrimary() {
while (true) {
const isMaster = db.isMaster();
if (isMaster.ismaster) {
print("This node is now PRIMARY.");
break;
}
print("Waiting to become PRIMARY...");
sleep(2000);
}
}
function createUser() {
db = db.getSiblingDB("uptime_db");
var username = process.env.USERNAME_ENV_VAR;
var password = process.env.PASSWORD_ENV_VAR;
db.createUser({
user: username,
pwd: password,
roles: [{ role: "readWrite", db: "uptime_db" }],
});
print("User '" + username + "' created successfully");
}
initiateReplicaSet();
waitForPrimary();
createUser();

View File

@@ -4,7 +4,7 @@ services:
restart: always
environment:
UPTIME_APP_API_BASE_URL: "https://checkmate-test.bluewavelabs.ca/api/v1"
UPTIME_STATUS_PAGE_SUBDOMAIN_PREFIX: "http://uptimegenie.com/"
UPTIME_APP_CLIENT_HOST: "https://checkmate-test.bluewavelabs.ca"
ports:
- "80:80"
- "443:443"
@@ -54,6 +54,13 @@ services:
- "27017:27017"
volumes:
- ./mongo/data:/data/db
- ./mongo/init/create_users.js:/docker-entrypoint-initdb.d/create_users.js
# - ./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
interval: 5s
timeout: 30s
start_period: 0s
start_interval: 1s
retries: 30

View File

@@ -0,0 +1,15 @@
try {
const status = rs.status();
printjson(status);
} catch (e) {
if (e.codeName === "NotYetInitialized") {
print("Replica set not initialized. Initiating...");
rs.initiate({
_id: "rs0",
members: [{ _id: 0, host: "mongodb:27017" }],
});
} else {
print("Unexpected error during rs.status():");
printjson(e);
}
}

View File

@@ -1,16 +0,0 @@
var username = process.env.USERNAME_ENV_VAR;
var password = process.env.PASSWORD_ENV_VAR;
db = db.getSiblingDB("uptime_db");
db.createUser({
user: username,
pwd: password,
roles: [
{
role: "readWrite",
db: "uptime_db",
},
],
});
print("User uptime_user created successfully");

View File

@@ -57,9 +57,9 @@ class AuthController {
*/
registerUser = async (req, res, next) => {
try {
if(req.body?.email){
if (req.body?.email) {
req.body.email = req.body.email?.toLowerCase();
}
}
await registrationBodyValidation.validateAsync(req.body);
} catch (error) {
const validationError = handleValidationError(error, SERVICE_NAME);
@@ -68,11 +68,13 @@ class AuthController {
}
// Create a new user
try {
const { inviteToken } = req.body;
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) {
await this.db.getInviteTokenAndDelete(inviteToken);
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");
@@ -133,8 +135,8 @@ class AuthController {
*/
loginUser = async (req, res, next) => {
try {
if(req.body?.email){
req.body.email = req.body.email?.toLowerCase();
if (req.body?.email) {
req.body.email = req.body.email?.toLowerCase();
}
await loginValidation.validateAsync(req.body);
} catch (error) {

View File

@@ -24,13 +24,15 @@ import axios from "axios";
import seedDb from "../db/mongo/utils/seedDb.js";
import { seedDistributedTest } from "../db/mongo/utils/seedDb.js";
const SERVICE_NAME = "monitorController";
import pkg from "papaparse";
class MonitorController {
constructor(db, settingsService, jobQueue, stringService) {
constructor(db, settingsService, jobQueue, stringService, emailService) {
this.db = db;
this.settingsService = settingsService;
this.jobQueue = jobQueue;
this.stringService = stringService;
this.emailService = emailService;
}
/**
@@ -243,56 +245,119 @@ class MonitorController {
};
/**
* Creates bulk monitors and adds them to the job queue.
* Creates bulk monitors and adds them to the job queue after parsing CSV.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.body - The body of the request.
* @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, a message indicating the creation of the monitor, and the created monitor data.
* @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 {
await createMonitorsBodyValidation.validateAsync(req.body);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
const { parse } = pkg;
try {
// create monitors
const monitors = await this.db.createBulkMonitors(req);
// validate the file
if (!req.file) {
throw new Error("No file uploaded");
}
// create notifications for each monitor
await Promise.all(
monitors.map(async (monitor, index) => {
const notifications = req.body[index].notifications;
// Check if the file is a CSV
if (!req.file.mimetype.includes("csv")) {
throw new Error("File is not a CSV");
}
if (notifications?.length) {
monitor.notifications = await Promise.all(
notifications.map(async (notification) => {
notification.monitorId = monitor._id;
return await this.db.createNotification(notification);
})
);
await monitor.save();
// Validate if the file is empty
if (req.file.size === 0) {
throw new Error("File is empty");
}
const { userId, teamId } = req.body;
if (!userId || !teamId) {
throw new Error("Missing userId or teamId in form data");
}
// 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;
}
// Add monitor to job queue
this.jobQueue.addJob(monitor._id, monitor);
})
);
return value;
},
complete: async ({ data, errors }) => {
try {
if (errors.length > 0) {
throw new Error("Error parsing CSV");
}
return res.success({
msg: this.stringService.bulkMonitorsCreate,
data: monitors,
if (!data || data.length === 0) {
throw new Error("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);
try {
const monitors = await this.db.createBulkMonitors(enrichedData);
await Promise.all(
monitors.map(async (monitor, index) => {
const notifications = enrichedData[index].notifications;
if (notifications?.length) {
monitor.notifications = await Promise.all(
notifications.map(async (notification) => {
notification.monitorId = monitor._id;
return await this.db.createNotification(notification);
})
);
await monitor.save();
}
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) {
next(handleError(error, SERVICE_NAME, "createBulkMonitors"));
return next(handleError(error, SERVICE_NAME, "createBulkMonitors"));
}
};
/**
* Checks if the endpoint can be resolved
* @async
@@ -473,18 +538,16 @@ class MonitorController {
const monitorBeforeEdit = await this.db.getMonitorById(monitorId);
// Get notifications from the request body
const notifications = req.body.notifications;
const notifications = req.body.notifications ?? [];
const editedMonitor = await this.db.editMonitor(monitorId, req.body);
await this.db.deleteNotificationsByMonitorId(editedMonitor._id);
await Promise.all(
notifications &&
notifications.map(async (notification) => {
notification.monitorId = editedMonitor._id;
await this.db.createNotification(notification);
})
notifications.map(async (notification) => {
notification.monitorId = editedMonitor._id;
await this.db.createNotification(notification);
})
);
// Delete the old job(editedMonitor has the same ID as the old monitor)
@@ -572,6 +635,50 @@ class MonitorController {
}
};
/**
* 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") {
return res.error({ msg: this.stringService.errorForValidEmailAddress });
}
const subject = this.stringService.testEmailSubject;
const context = { testName: "Monitoring System" };
const messageId = await this.emailService.buildAndSendEmail(
"testEmailTemplate",
context,
to,
subject
);
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);

View File

@@ -1,138 +0,0 @@
// **************************
// The idea here is to provide a layer of abstraction between the database and whoever is using it.
// Instead of directly calling mongoose methods, we can call the methods on the DB object.
// If this were Typescript or Java or Golang an interface would be implemented to ensure the methods are available.
// But we do the best we can with Javascript.
//
// If the methods are consistent all we have to do to swap out one DB for another is simply change the import.
//
// Example:
// We start with the fake DB:
//
// const db = require("../db/FakeDb");
// const monitors = await db.getAllMonitors();
//
// And when we want to swtich to a real DB, all we have to do is swap the import
//
// const db = require("../db/MongoDb");
// const monitors = await db.getAllMonitors();
//
// The rest of the code is the same, as all the `db` methods are standardized.
// **************************
const Monitor = require("./models/Monitor");
const UserModel = require("./models/User");
const bcrypt = require("bcrypt");
let FAKE_MONITOR_DATA = [];
const USERS = [];
const connect = async () => {
try {
await console.log("Connected to FakeDB");
} catch (error) {
console.error(error);
}
};
const insertUser = async (req, res) => {
try {
const newUser = new UserModel({ ...req.body });
const salt = await bcrypt.genSalt(10); //genSalt is asynchronous, need to wait
newUser.password = await bcrypt.hash(newUser.password, salt); // hash is also async, need to eitehr await or use hashSync
USERS.push(newUser);
const userToReturn = { ...newUser._doc };
delete userToReturn.password;
return userToReturn;
} catch (error) {
throw error;
}
};
const getUserByEmail = async (req, res) => {
const email = req.body.email;
try {
const idx = USERS.findIndex((user) => {
return user.email === email;
});
if (idx === -1) {
return null;
}
return USERS[idx];
} catch (error) {
throw new Error(`User with email ${email} not found`);
}
};
const getAllMonitors = async () => {
return FAKE_MONITOR_DATA;
};
const getMonitorById = async (monitorId) => {
const idx = FAKE_MONITOR_DATA.findIndex((monitor) => {
return monitor.id === monitorId;
});
if (idx === -1) {
throw new Error(`Monitor with id ${monitorId} not found`);
}
return FAKE_MONITOR_DATA[idx];
};
const getMonitorsByUserId = async (userId) => {
const userMonitors = FAKE_MONITOR_DATA.filter((monitor) => {
return monitor.userId === userId;
});
if (userMonitors.length === 0) {
throw new Error(`Monitors for user ${userId} not found`);
}
return userMonitors;
};
const createMonitor = async (req, res) => {
const monitor = new Monitor(req.body);
monitor.createdAt = Date.now();
monitor.updatedAt = Date.now();
FAKE_MONITOR_DATA.push(monitor);
return monitor;
};
const deleteMonitor = async (req, res) => {
const monitorId = req.params.monitorId;
try {
const monitor = getMonitorById(monitorId);
FAKE_MONITOR_DATA = FAKE_MONITOR_DATA.filter((monitor) => {
return monitor.id !== monitorId;
});
return monitor;
} catch (error) {
throw error;
}
};
const editMonitor = async (req, res) => {
const monitorId = req.params.monitorId;
const idx = FAKE_MONITOR_DATA.findIndex((monitor) => {
return monitor._id.toString() === monitorId;
});
const oldMonitor = FAKE_MONITOR_DATA[idx];
const editedMonitor = new Monitor({ ...req.body });
editedMonitor._id = oldMonitor._id;
editedMonitor.userId = oldMonitor.userId;
editedMonitor.updatedAt = Date.now();
editedMonitor.createdAt = oldMonitor.createdAt;
FAKE_MONITOR_DATA[idx] = editedMonitor;
return FAKE_MONITOR_DATA[idx];
};
module.exports = {
connect,
insertUser,
getUserByEmail,
getAllMonitors,
getMonitorById,
getMonitorsByUserId,
createMonitor,
deleteMonitor,
editMonitor,
};

View File

@@ -7,10 +7,6 @@ const AppSettingsSchema = mongoose.Schema(
required: true,
default: "http://localhost:5000/api/v1",
},
language: {
type: String,
default: "en",
},
logLevel: {
type: String,
default: "debug",

View File

@@ -77,7 +77,6 @@ CheckSchema.index({ updatedAt: 1 });
CheckSchema.index({ monitorId: 1, updatedAt: 1 });
CheckSchema.index({ monitorId: 1, updatedAt: -1 });
CheckSchema.index({ teamId: 1, updatedAt: -1 });
CheckSchema.index({ teamId: 1 });
export default mongoose.model("Check", CheckSchema);
export { BaseCheckSchema };

View File

@@ -15,6 +15,7 @@ const InviteTokenSchema = mongoose.Schema(
role: {
type: Array,
required: true,
default: ["user"],
},
token: {
type: String,

View File

@@ -39,6 +39,10 @@ const MonitorSchema = mongoose.Schema(
"distributed_test",
],
},
ignoreTlsErrors: {
type: Boolean,
default: false,
},
jsonPath: {
type: String,
},

View File

@@ -1,5 +1,5 @@
import mongoose from "mongoose";
import bcrypt from "bcrypt";
import bcrypt from "bcryptjs";
import logger from "../../utils/logger.js";
const UserSchema = mongoose.Schema(
@@ -55,20 +55,20 @@ const UserSchema = mongoose.Schema(
}
);
UserSchema.pre("save", async function (next) {
UserSchema.pre("save", function (next) {
if (!this.isModified("password")) {
next();
return next();
}
const salt = await bcrypt.genSalt(10); //genSalt is asynchronous, need to wait
this.password = await bcrypt.hash(this.password, salt); // hash is also async, need to eitehr await or use hashSync
const salt = bcrypt.genSaltSync(10);
this.password = bcrypt.hashSync(this.password, salt);
next();
});
UserSchema.pre("findOneAndUpdate", async function (next) {
UserSchema.pre("findOneAndUpdate", function (next) {
const update = this.getUpdate();
if ("password" in update) {
const salt = await bcrypt.genSalt(10); //genSalt is asynchronous, need to wait
update.password = await bcrypt.hash(update.password, salt); // hash is also async, need to eitehr await or use hashSync
const salt = bcrypt.genSaltSync(10);
update.password = bcrypt.hashSync(update.password, salt);
}
next();
@@ -89,4 +89,4 @@ User.init().then(() => {
});
});
export default mongoose.model("User", UserSchema);
export default User;

View File

@@ -96,6 +96,7 @@ class MongoDB {
try {
const connectionString =
process.env.DB_CONNECTION_STRING || "mongodb://localhost:27017/uptime_db";
console.log("Connecting to MongoDB with connection string:", connectionString);
await mongoose.connect(connectionString);
// If there are no AppSettings, create one
await AppSettings.findOneAndUpdate(

View File

@@ -751,7 +751,7 @@ const createMonitor = async (req, res) => {
*/
const createBulkMonitors = async (req) => {
try {
const monitors = req.body.map(
const monitors = req.map(
(item) => new Monitor({ ...item, notifications: undefined })
);
await Monitor.bulkSave(monitors);

View File

@@ -38,6 +38,8 @@ const updateStatusPage = async (statusPageData, image) => {
data: image.buffer,
contentType: image.mimetype,
};
}else{
statusPageData.logo = null;
}
if (statusPageData.deleteSubmonitors === "true") {

View File

@@ -81,7 +81,9 @@ 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";
@@ -113,21 +115,16 @@ const shutdown = async () => {
method: "shutdown",
});
// flush Redis
const settings =
ServiceRegistry.get(SettingsService.SERVICE_NAME).getSettings() || {};
const redisService = ServiceRegistry.get(RedisService.SERVICE_NAME);
await redisService.flushall();
const { redisUrl } = settings;
const redis = new IORedis(redisUrl, { maxRetriesPerRequest: null });
logger.info({ message: "Flushing Redis" });
await redis.flushall();
logger.info({ message: "Redis flushed" });
process.exit(1);
}, SHUTDOWN_TIMEOUT);
try {
server.close();
await ServiceRegistry.get(JobQueue.SERVICE_NAME).obliterate();
await ServiceRegistry.get(MongoDB.SERVICE_NAME).disconnect();
await ServiceRegistry.get(RedisService.SERVICE_NAME).flushall();
logger.info({ message: "Graceful shutdown complete" });
process.exit(0);
} catch (error) {
@@ -185,7 +182,13 @@ const startApp = async () => {
stringService
);
const jobQueue = new JobQueue(
const redisService = await RedisService.createInstance({
logger,
IORedis,
SettingsService: settingsService,
});
const jobQueue = new JobQueue({
db,
statusService,
networkService,
@@ -194,8 +197,9 @@ const startApp = async () => {
stringService,
logger,
Queue,
Worker
);
Worker,
redisService,
});
// Register services
ServiceRegistry.register(JobQueue.SERVICE_NAME, jobQueue);
@@ -207,6 +211,7 @@ const startApp = async () => {
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();
@@ -231,7 +236,8 @@ const startApp = async () => {
ServiceRegistry.get(MongoDB.SERVICE_NAME),
ServiceRegistry.get(SettingsService.SERVICE_NAME),
ServiceRegistry.get(JobQueue.SERVICE_NAME),
ServiceRegistry.get(StringService.SERVICE_NAME)
ServiceRegistry.get(StringService.SERVICE_NAME),
ServiceRegistry.get(EmailService.SERVICE_NAME),
);
const settingsController = new SettingsController(
@@ -343,7 +349,7 @@ const startApp = async () => {
app.use("/api/v1/queue", verifyJWT, queueRoutes.getRouter());
app.use("/api/v1/distributed-uptime", distributedUptimeRoutes.getRouter());
app.use("/api/v1/status-page", statusPageRoutes.getRouter());
app.use("/api/v1/notifications", verifyJWT, notificationRoutes.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({

View File

@@ -158,5 +158,8 @@
"platformRequired": "Platform is required",
"testNotificationFailed": "Failed to send test notification",
"monitorUpAlert": "Uptime Alert: One of your monitors is back online.\n📌 Monitor: {monitorName}\n📅 Time: {time}\n⚠ Status: UP\n📟 Status Code: {code}\n\u200B\n",
"monitorDownAlert": "Downtime Alert: One of your monitors went offline.\n📌 Monitor: {monitorName}\n📅 Time: {time}\n⚠ Status: DOWN\n📟 Status Code: {code}\n\u200B\n"
"monitorDownAlert": "Downtime Alert: One of your monitors went offline.\n📌 Monitor: {monitorName}\n📅 Time: {time}\n⚠ Status: DOWN\n📟 Status Code: {code}\n\u200B\n",
"sendTestEmail": "Test email sent successfully",
"errorForValidEmailAddress": "A valid recipient email address is required.",
"testEmailSubject": "Test Email from Monitoring System"
}

View File

@@ -41,7 +41,7 @@ const isAllowed = (allowedRoles) => {
return;
} else {
const error = new Error(stringService.insufficientPermissions);
error.status = 401;
error.status = 403;
error.service = SERVICE_NAME;
next(error);
return;

View File

@@ -1,16 +1,10 @@
import logger from "../utils/logger.js";
const languageMiddleware =
(stringService, translationService, settingsService) => async (req, res, next) => {
(stringService, translationService) => async (req, res, next) => {
try {
const settings = await settingsService.getSettings();
let language = settings && settings.language ? settings.language : null;
if (!language) {
const acceptLanguage = req.headers["accept-language"] || "en";
language = acceptLanguage.split(",")[0].slice(0, 2).toLowerCase();
}
const acceptLanguage = req.headers["accept-language"] || "en";
const language = acceptLanguage.split(",")[0].slice(0, 2).toLowerCase();
translationService.setLanguage(language);
stringService.setLanguage(language);

468
server/package-lock.json generated
View File

@@ -10,7 +10,7 @@
"license": "ISC",
"dependencies": {
"axios": "^1.7.2",
"bcrypt": "5.1.1",
"bcryptjs": "3.0.2",
"bullmq": "5.41.2",
"compression": "1.8.0",
"cors": "^2.8.5",
@@ -850,26 +850,6 @@
"url": "https://opencollective.com/js-sdsl"
}
},
"node_modules/@mapbox/node-pre-gyp": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
"integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==",
"license": "BSD-3-Clause",
"dependencies": {
"detect-libc": "^2.0.0",
"https-proxy-agent": "^5.0.0",
"make-dir": "^3.1.0",
"node-fetch": "^2.6.7",
"nopt": "^5.0.0",
"npmlog": "^5.0.1",
"rimraf": "^3.0.2",
"semver": "^7.3.5",
"tar": "^6.1.11"
},
"bin": {
"node-pre-gyp": "bin/node-pre-gyp"
}
},
"node_modules/@mongodb-js/saslprep": {
"version": "1.1.9",
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz",
@@ -1166,12 +1146,6 @@
"@types/webidl-conversions": "*"
}
},
"node_modules/abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
"license": "ISC"
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -1289,26 +1263,6 @@
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
"license": "MIT"
},
"node_modules/aproba": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
"integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==",
"license": "ISC"
},
"node_modules/are-we-there-yet": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
"integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
"deprecated": "This package is no longer supported.",
"license": "ISC",
"dependencies": {
"delegates": "^1.0.0",
"readable-stream": "^3.6.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -1389,20 +1343,6 @@
],
"license": "MIT"
},
"node_modules/bcrypt": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz",
"integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@mapbox/node-pre-gyp": "^1.0.11",
"node-addon-api": "^5.0.0"
},
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/bcrypt-pbkdf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
@@ -1412,6 +1352,15 @@
"tweetnacl": "^0.14.3"
}
},
"node_modules/bcryptjs": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz",
"integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==",
"license": "BSD-3-Clause",
"bin": {
"bcrypt": "bin/bcrypt"
}
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@@ -1852,15 +1801,6 @@
"fsevents": "~2.3.2"
}
},
"node_modules/chownr": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
@@ -1983,15 +1923,6 @@
"simple-swizzle": "^0.2.2"
}
},
"node_modules/color-support": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
"integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
"license": "ISC",
"bin": {
"color-support": "bin.js"
}
},
"node_modules/colord": {
"version": "2.9.3",
"resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz",
@@ -2112,6 +2043,7 @@
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true,
"license": "MIT"
},
"node_modules/concat-stream": {
@@ -2159,12 +2091,6 @@
"safe-buffer": "~5.1.0"
}
},
"node_modules/console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
"integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
"license": "ISC"
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@@ -2522,12 +2448,6 @@
"node": ">=0.4.0"
}
},
"node_modules/delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
"integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
"license": "MIT"
},
"node_modules/denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
@@ -3439,36 +3359,6 @@
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"license": "MIT"
},
"node_modules/fs-minipass": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
"license": "ISC",
"dependencies": {
"minipass": "^3.0.0"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/fs-minipass/node_modules/minipass": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
"license": "ISC",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"license": "ISC"
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -3492,74 +3382,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gauge": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
"integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
"deprecated": "This package is no longer supported.",
"license": "ISC",
"dependencies": {
"aproba": "^1.0.3 || ^2.0.0",
"color-support": "^1.1.2",
"console-control-strings": "^1.0.0",
"has-unicode": "^2.0.1",
"object-assign": "^4.1.1",
"signal-exit": "^3.0.0",
"string-width": "^4.2.3",
"strip-ansi": "^6.0.1",
"wide-align": "^1.1.2"
},
"engines": {
"node": ">=10"
}
},
"node_modules/gauge/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/gauge/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/gauge/node_modules/signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"license": "ISC"
},
"node_modules/gauge/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/gauge/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/gaxios": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz",
@@ -3708,12 +3530,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
"license": "ISC"
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@@ -3920,17 +3736,6 @@
"node": ">=0.8.19"
}
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
"license": "ISC",
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
@@ -4572,30 +4377,6 @@
"qs": "^6.11.0"
}
},
"node_modules/make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
"license": "MIT",
"dependencies": {
"semver": "^6.0.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/make-dir/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -4716,31 +4497,6 @@
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/minizlib": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
"license": "MIT",
"dependencies": {
"minipass": "^3.0.0",
"yallist": "^4.0.0"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/minizlib/node_modules/minipass": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
"license": "ISC",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/mjml": {
"version": "5.0.0-alpha.6",
"resolved": "https://registry.npmjs.org/mjml/-/mjml-5.0.0-alpha.6.tgz",
@@ -5459,12 +5215,6 @@
"integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==",
"license": "MIT"
},
"node_modules/node-addon-api": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==",
"license": "MIT"
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
@@ -5613,21 +5363,6 @@
"node": ">=4"
}
},
"node_modules/nopt": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
"integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
"license": "ISC",
"dependencies": {
"abbrev": "1"
},
"bin": {
"nopt": "bin/nopt.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -5637,19 +5372,6 @@
"node": ">=0.10.0"
}
},
"node_modules/npmlog": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
"integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
"deprecated": "This package is no longer supported.",
"license": "ISC",
"dependencies": {
"are-we-there-yet": "^2.0.0",
"console-control-strings": "^1.1.0",
"gauge": "^3.0.0",
"set-blocking": "^2.0.0"
}
},
"node_modules/nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
@@ -5863,15 +5585,6 @@
"node": ">=8"
}
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@@ -6743,65 +6456,6 @@
"node": ">=4"
}
},
"node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"deprecated": "Rimraf versions prior to v4 are no longer supported",
"license": "ISC",
"dependencies": {
"glob": "^7.1.3"
},
"bin": {
"rimraf": "bin.js"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/rimraf/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==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/rimraf/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/rimraf/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -6922,12 +6576,6 @@
"node": ">= 0.8.0"
}
},
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"license": "ISC"
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@@ -7443,23 +7091,6 @@
"express": ">=4.0.0 || >=5.0.0-beta"
}
},
"node_modules/tar": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
"license": "ISC",
"dependencies": {
"chownr": "^2.0.0",
"fs-minipass": "^2.0.0",
"minipass": "^5.0.0",
"minizlib": "^2.1.1",
"mkdirp": "^1.0.3",
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/tar-fs": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz",
@@ -7494,27 +7125,6 @@
"node": ">=6"
}
},
"node_modules/tar/node_modules/minipass": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
"integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
"license": "ISC",
"engines": {
"node": ">=8"
}
},
"node_modules/tar/node_modules/mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"license": "MIT",
"bin": {
"mkdirp": "bin/cmd.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/test-exclude": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz",
@@ -8004,56 +7614,6 @@
"node": ">= 8"
}
},
"node_modules/wide-align": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
"integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
"license": "ISC",
"dependencies": {
"string-width": "^1.0.2 || 2 || 3 || 4"
}
},
"node_modules/wide-align/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/wide-align/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/wide-align/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/wide-align/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/winston": {
"version": "3.17.0",
"resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz",
@@ -8224,12 +7784,6 @@
"node": ">=10"
}
},
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"license": "ISC"
},
"node_modules/yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",

View File

@@ -17,7 +17,7 @@
"license": "ISC",
"dependencies": {
"axios": "^1.7.2",
"bcrypt": "5.1.1",
"bcryptjs": "3.0.2",
"bullmq": "5.41.2",
"compression": "1.8.0",
"cors": "^2.8.5",
@@ -33,8 +33,9 @@
"mailersend": "^2.2.0",
"mjml": "^5.0.0-alpha.4",
"mongoose": "^8.3.3",
"multer": "1.4.5-lts.1",
"multer": "^1.4.5-lts.1",
"nodemailer": "^6.9.14",
"papaparse": "^5.5.2",
"ping": "0.4.4",
"sharp": "0.33.5",
"ssl-checker": "2.0.10",

View File

@@ -1,7 +1,12 @@
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();
@@ -89,10 +94,17 @@ class MonitorRoutes {
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() {

View File

@@ -12,7 +12,7 @@ class SettingsRoutes {
this.router.get("/", this.settingsController.getAppSettings);
this.router.put(
"/",
isAllowed(["superadmin"]),
isAllowed(["admin", "superadmin"]),
this.settingsController.updateAppSettings
);
}

View File

@@ -67,6 +67,7 @@ class EmailService {
serverIsUpTemplate: this.loadTemplate("serverIsUp"),
passwordResetTemplate: this.loadTemplate("passwordReset"),
hardwareIncidentTemplate: this.loadTemplate("hardwareIncident"),
testEmailTemplate: this.loadTemplate("testEmailTemplate")
};
/**

View File

@@ -1,5 +1,3 @@
import IORedis from "ioredis";
const QUEUE_NAMES = ["uptime", "pagespeed", "hardware", "distributed"];
const SERVICE_NAME = "JobQueue";
const JOBS_PER_WORKER = 5;
@@ -18,7 +16,7 @@ const getSchedulerId = (monitor) => `scheduler:${monitor.type}:${monitor._id}`;
class NewJobQueue {
static SERVICE_NAME = SERVICE_NAME;
constructor(
constructor({
db,
statusService,
networkService,
@@ -27,16 +25,14 @@ class NewJobQueue {
stringService,
logger,
Queue,
Worker
) {
const settings = settingsService.getSettings() || {};
const { redisUrl } = settings;
const connection = new IORedis(redisUrl, { maxRetriesPerRequest: null });
Worker,
redisService,
}) {
this.connection = redisService.getConnection();
this.queues = {};
this.workers = {};
this.lastJobProcessedTime = {};
this.connection = connection;
this.db = db;
this.networkService = networkService;
this.statusService = statusService;
@@ -47,7 +43,7 @@ class NewJobQueue {
this.stringService = stringService;
QUEUE_NAMES.forEach((name) => {
const q = new Queue(name, { connection });
const q = new Queue(name, { connection: this.connection });
this.lastJobProcessedTime[q.name] = Date.now();
q.on("error", (error) => {
this.logger.error({

View File

@@ -1,4 +1,6 @@
import jmespath from "jmespath";
import https from "https";
const SERVICE_NAME = "NetworkService";
const UPROCK_ENDPOINT = "https://api.uprock.com/checkmate/push";
@@ -133,6 +135,7 @@ class NetworkService {
name,
teamId,
type,
ignoreTlsErrors,
jsonPath,
matchMethod,
expectedValue,
@@ -141,6 +144,12 @@ class NetworkService {
secret !== undefined && (config.headers = { Authorization: `Bearer ${secret}` });
if (ignoreTlsErrors === true) {
config.httpsAgent = new https.Agent({
rejectUnauthorized: false,
});
}
const { response, responseTime, error } = await this.timeRequest(() =>
this.axios.get(url, config)
);

Some files were not shown because too many files have changed in this diff Show More