Merge branch 'master' into websocket_test
This commit is contained in:
commit
ec93dd2116
@ -78,6 +78,7 @@ module.exports = {
|
||||
"keyword-spacing": "warn",
|
||||
"space-infix-ops": "error",
|
||||
"arrow-spacing": "warn",
|
||||
"no-throw-literal": "error",
|
||||
"no-trailing-spaces": "error",
|
||||
"no-constant-condition": [ "error", {
|
||||
"checkLoops": false,
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/ask_for_help.yml
vendored
2
.github/ISSUE_TEMPLATE/ask_for_help.yml
vendored
@ -3,7 +3,7 @@ name: ❓ Ask for help
|
||||
description: |
|
||||
Submit any question related to Uptime Kuma
|
||||
#title: "[Help]"
|
||||
labels: ["help", "P3-low"]
|
||||
labels: ["help"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -3,7 +3,7 @@ name: 🐛 Bug Report
|
||||
description: |
|
||||
Submit a bug report to help us improve
|
||||
#title: "[Bug]"
|
||||
labels: ["bug", "P2-medium"]
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
19
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
19
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@ -3,28 +3,15 @@ name: 🚀 Feature Request
|
||||
description: |
|
||||
Submit a proposal for a new feature
|
||||
# title: "[Feature]"
|
||||
labels: ["feature-request", "P3-low"]
|
||||
labels: ["feature-request"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## ❗Important Announcement
|
||||
|
||||
### 🚧 Temporary Delay in Feature Requests and Pull Request Reviews
|
||||
|
||||
**At this time, we may be slower to respond to new feature requests and review pull requests. Existing requests and PRs will remain in the backlog but may not be prioritized immediately.**
|
||||
|
||||
- **Reason**: Our current focus is on addressing bugs, improving system performance, and implementing essential updates. This will help stabilize the project and ensure smoother management.
|
||||
- **Impact**: While no new feature requests or pull requests are being outright rejected, there may be significant delays in reviews. We encourage the community to help by reviewing PRs or assisting other users in the meantime.
|
||||
- **What You Can Do**: If you're interested in contributing, reviewing open PRs by following our [Review Guidelines](https://github.com/louislam/uptime-kuma/blob/master/.github/REVIEW_GUIDELINES.md) or offering help to other users is greatly appreciated. All feature requests and PRs will be revisited once the suspension period is lifted.
|
||||
|
||||
We appreciate your patience and understanding as we continue to improve Uptime Kuma.
|
||||
|
||||
### 🚫 Please Avoid Unnecessary Pinging of Maintainers
|
||||
|
||||
**We kindly ask you to refrain from pinging maintainers unless absolutely necessary. Pings are reserved for critical/urgent pull requests that require immediate attention.**
|
||||
|
||||
**Why**: Reserving pings for urgent matters ensures maintainers can prioritize critical tasks effectively.
|
||||
We kindly ask you to refrain from pinging maintainers unless absolutely necessary.
|
||||
Pings are for critical/urgent pull requests that require immediate attention.
|
||||
- type: textarea
|
||||
id: related-issues
|
||||
validations:
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/security_issue.yml
vendored
2
.github/ISSUE_TEMPLATE/security_issue.yml
vendored
@ -3,7 +3,7 @@ name: 🛡️ Security Issue
|
||||
description: |
|
||||
Notify Louis Lam about a security concern. Please do NOT include any sensitive details in this issue.
|
||||
# title: "Security Issue"
|
||||
labels: ["security", "P1-high"]
|
||||
labels: ["security"]
|
||||
assignees: [louislam]
|
||||
body:
|
||||
- type: markdown
|
||||
|
||||
76
.github/PULL_REQUEST_TEMPLATE.md
vendored
76
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,44 +1,35 @@
|
||||
**⚠️ Please Note: We do not accept all types of pull requests, and we want to ensure we don’t waste your time. Before submitting, make sure you have read our pull request guidelines: [Pull Request Rules](https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md#can-i-create-a-pull-request-for-uptime-kuma)**
|
||||
|
||||
## ❗ Important Announcement
|
||||
## ❗ Important Announcements
|
||||
|
||||
<details><summary>Click here for more details:</summary>
|
||||
</p>
|
||||
|
||||
### 🚧 Temporary Delay in Feature Requests and Pull Request Reviews
|
||||
|
||||
**At this time, we may be slower to respond to new feature requests and review pull requests. Existing requests and PRs will remain in the backlog but may not be prioritized immediately.**
|
||||
|
||||
- **Reason**: Our current focus is on addressing bugs, improving system performance, and implementing essential updates. This will help stabilize the project and ensure smoother management.
|
||||
- **Impact**: While no new feature requests or pull requests are being outright rejected, there may be significant delays in reviews. We encourage the community to help by reviewing PRs or assisting other users in the meantime.
|
||||
- **What You Can Do**: If you're interested in contributing, reviewing open PRs by following our [Review Guidelines](https://github.com/louislam/uptime-kuma/blob/master/.github/REVIEW_GUIDELINES.md) or offering support to other users is greatly appreciated. All feature requests and PRs will be revisited once the suspension period is lifted.
|
||||
|
||||
We appreciate your patience and understanding as we continue to improve Uptime Kuma.
|
||||
**⚠️ Please Note: We do not accept all types of pull requests, and we want to ensure we don’t waste your time. Before submitting, make sure you have read our pull request guidelines: [Pull Request Rules](https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md#can-i-create-a-pull-request-for-uptime-kuma)**
|
||||
|
||||
### 🚫 Please Avoid Unnecessary Pinging of Maintainers
|
||||
|
||||
**We kindly ask you to refrain from pinging maintainers unless absolutely necessary. Pings are reserved for critical/urgent pull requests that require immediate attention.**
|
||||
|
||||
**Why**: Reserving pings for urgent matters ensures maintainers can prioritize critical tasks effectively.
|
||||
We kindly ask you to refrain from pinging maintainers unless absolutely necessary. Pings are for critical/urgent pull requests that require immediate attention.
|
||||
|
||||
</p>
|
||||
</details>
|
||||
|
||||
## 📋 Overview
|
||||
|
||||
Provide a clear summary of the purpose and scope of this pull request:
|
||||
<!-- Provide a clear summary of the purpose and scope of this pull request:-->
|
||||
|
||||
- **What problem does this pull request address?**
|
||||
|
||||
- Please provide a detailed explanation here.
|
||||
|
||||
- **What features or functionality does this pull request introduce or enhance?**
|
||||
|
||||
- Please provide a detailed explanation here.
|
||||
|
||||
## 🔄 Changes
|
||||
<!--
|
||||
Please link any GitHub issues or tasks that this pull request addresses.
|
||||
Use the appropriate issue numbers or links to enable auto-closing.
|
||||
-->
|
||||
|
||||
### 🛠️ Type of change
|
||||
- Relates to #issue-number
|
||||
- Resolves #issue-number
|
||||
|
||||
## 🛠️ Type of change
|
||||
|
||||
<!-- Please select all options that apply -->
|
||||
|
||||
@ -52,23 +43,12 @@ Provide a clear summary of the purpose and scope of this pull request:
|
||||
- [ ] 🔧 Other (please specify):
|
||||
- Provide additional details here.
|
||||
|
||||
## 🔗 Related Issues
|
||||
|
||||
<!--
|
||||
Please link any GitHub issues or tasks that this pull request addresses. Use the appropriate issue numbers or links.
|
||||
|
||||
**Note**: Include only issues directly related to this PR. Remove any irrelevant reference.
|
||||
-->
|
||||
|
||||
- Relates to #issue-number
|
||||
- Resolves #issue-number
|
||||
- Fixes #issue-number
|
||||
|
||||
## 📄 Checklist *
|
||||
## 📄 Checklist
|
||||
|
||||
<!-- Please select all options that apply -->
|
||||
|
||||
- [ ] 🔍 My code adheres to the style guidelines of this project.
|
||||
- [ ] 🦿 I have indicated where (if any) I used an LLM for the contributions
|
||||
- [ ] ✅ I ran ESLint and other code linters for modified files.
|
||||
- [ ] 🛠️ I have reviewed and tested my code.
|
||||
- [ ] 📝 I have commented my code, especially in hard-to-understand areas (e.g., using JSDoc for methods).
|
||||
@ -82,10 +62,11 @@ Please link any GitHub issues or tasks that this pull request addresses. Use the
|
||||
## 📷 Screenshots or Visual Changes
|
||||
|
||||
<!--
|
||||
Please upload the image directly here by pasting it or dragging and dropping. Avoid using external image services as the image will be uploaded automatically.
|
||||
|
||||
If this pull request introduces visual changes, please provide the following details.
|
||||
If not, remove this section.
|
||||
|
||||
Please upload the image directly here by pasting it or dragging and dropping.
|
||||
Avoid using external image services as the image will be uploaded automatically.
|
||||
-->
|
||||
|
||||
- **UI Modifications**: Highlight any changes made to the user interface.
|
||||
@ -97,26 +78,3 @@ If not, remove this section.
|
||||
| `DOWN` |  |  |
|
||||
| Certificate-expiry |  |  |
|
||||
| Testing |  |  |
|
||||
|
||||
## ℹ️ Additional Context
|
||||
|
||||
Provide any relevant details to assist reviewers in understanding the changes.
|
||||
|
||||
<details><summary>Click here for more details:</summary>
|
||||
</p>
|
||||
|
||||
**Key Considerations**:
|
||||
|
||||
- **Design decisions** – Key choices or trade-offs made during development.
|
||||
- **Alternative solutions** – Approaches considered but not implemented, along with reasons.
|
||||
- **Relevant links** – Specifications, discussions, or resources that provide context.
|
||||
- **Dependencies** – Related pull requests or issues that must be resolved before merging.
|
||||
- **Additional context** – Any other details that may help reviewers understand the changes.
|
||||
|
||||
Provide details here
|
||||
|
||||
## 💬 Requested Feedback
|
||||
|
||||
<!-- If a part of our docs is unclear, you are unsure how to do something/.. this is where we would appreciate your feedback -->
|
||||
|
||||
- `Mention documents needing feedback here`
|
||||
|
||||
167
.github/copilot-instructions.md
vendored
Normal file
167
.github/copilot-instructions.md
vendored
Normal file
@ -0,0 +1,167 @@
|
||||
# Copilot Instructions for Uptime Kuma
|
||||
|
||||
## Copilot's Goals/Tasks
|
||||
|
||||
- Check spelling
|
||||
- Do not show "Pull Request Overview"
|
||||
- You do not have to reply if there are no issues
|
||||
|
||||
## Repository Overview
|
||||
|
||||
**Uptime Kuma** is a self-hosted monitoring tool for HTTP(s), TCP, DNS, Docker, etc. Built with Vue 3 (frontend) and Node.js/Express (backend), using Socket.IO for real-time communication.
|
||||
|
||||
- **Languages**: JavaScript, Vue 3, TypeScript (limited), HTML, CSS/SCSS
|
||||
- **Backend**: Node.js >= 20.4, Express.js, Socket.IO, SQLite
|
||||
- **Frontend**: Vue 3, Vite, Bootstrap 5, Chart.js
|
||||
- **Package Manager**: npm with `legacy-peer-deps=true` (.npmrc)
|
||||
|
||||
## Build & Validation Commands
|
||||
|
||||
### Prerequisites
|
||||
- Node.js >= 20.4.0, npm >= 9.3, Git
|
||||
|
||||
### Essential Command Sequence
|
||||
|
||||
1. **Install Dependencies**:
|
||||
```bash
|
||||
npm ci # Use npm ci NOT npm install (~60-90 seconds)
|
||||
```
|
||||
|
||||
2. **Linting** (required before committing):
|
||||
```bash
|
||||
npm run lint # Both linters (~15-30 seconds)
|
||||
npm run lint:prod # For production (zero warnings)
|
||||
```
|
||||
|
||||
3. **Build Frontend**:
|
||||
```bash
|
||||
npm run build # Takes ~90-120 seconds, builds to dist/
|
||||
```
|
||||
|
||||
4. **Run Tests**:
|
||||
```bash
|
||||
npm run test-backend # Backend tests (~50-60 seconds)
|
||||
npm test # All tests
|
||||
```
|
||||
|
||||
### Development Workflow
|
||||
|
||||
```bash
|
||||
npm run dev # Starts frontend (port 3000) and backend (port 3001)
|
||||
```
|
||||
|
||||
## Project Architecture
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
/
|
||||
├── server/ Backend source code
|
||||
│ ├── model/ Database models (auto-mapped to tables)
|
||||
│ ├── monitor-types/ Monitor type implementations
|
||||
│ ├── notification-providers/ Notification integrations
|
||||
│ ├── routers/ Express routers
|
||||
│ ├── socket-handlers/ Socket.IO event handlers
|
||||
│ ├── server.js Server entry point
|
||||
│ └── uptime-kuma-server.js Main server logic
|
||||
├── src/ Frontend source code (Vue 3 SPA)
|
||||
│ ├── components/ Vue components
|
||||
│ ├── pages/ Page components
|
||||
│ ├── lang/ i18n translations
|
||||
│ ├── router.js Vue Router configuration
|
||||
│ └── main.js Frontend entry point
|
||||
├── db/ Database related
|
||||
│ ├── knex_migrations/ Knex migration files
|
||||
│ └── kuma.db SQLite database (gitignored)
|
||||
├── test/ Test files
|
||||
│ ├── backend-test/ Backend unit tests
|
||||
│ └── e2e/ Playwright E2E tests
|
||||
├── config/ Build configuration
|
||||
│ ├── vite.config.js Vite build config
|
||||
│ └── playwright.config.js Playwright test config
|
||||
├── dist/ Frontend build output (gitignored)
|
||||
├── data/ App data directory (gitignored)
|
||||
├── public/ Static frontend assets (dev only)
|
||||
├── docker/ Docker build files
|
||||
└── extra/ Utility scripts
|
||||
```
|
||||
|
||||
### Key Configuration Files
|
||||
|
||||
- **package.json**: Scripts, dependencies, Node.js version requirement
|
||||
- **.eslintrc.js**: ESLint rules (4 spaces, double quotes, unix line endings, JSDoc required)
|
||||
- **.stylelintrc**: Stylelint rules (4 spaces indentation)
|
||||
- **.editorconfig**: Editor settings (4 spaces, LF, UTF-8)
|
||||
- **tsconfig-backend.json**: TypeScript config for backend (only src/util.ts)
|
||||
- **.npmrc**: `legacy-peer-deps=true` (required for dependency resolution)
|
||||
- **.gitignore**: Excludes node_modules, dist, data, tmp, private
|
||||
|
||||
### Code Style (strictly enforced by linters)
|
||||
|
||||
- 4 spaces indentation, double quotes, Unix line endings (LF), semicolons required
|
||||
- **Naming**: JavaScript/TypeScript (camelCase), SQLite (snake_case), CSS/SCSS (kebab-case)
|
||||
- JSDoc required for all functions/methods
|
||||
|
||||
## CI/CD Workflows
|
||||
|
||||
**auto-test.yml** (runs on PR/push to master/1.23.X):
|
||||
- Linting, building, backend tests on multiple OS/Node versions (15 min timeout)
|
||||
- E2E Playwright tests
|
||||
|
||||
**validate.yml**: Validates JSON/YAML files, language files, knex migrations
|
||||
|
||||
**PR Requirements**: All linters pass, tests pass, code follows style guidelines
|
||||
|
||||
## Common Issues
|
||||
|
||||
1. **npm install vs npm ci**: Always use `npm ci` for reproducible builds
|
||||
2. **TypeScript errors**: `npm run tsc` shows 1400+ errors - ignore them, they don't affect builds
|
||||
3. **Stylelint warnings**: Deprecation warnings are expected, ignore them
|
||||
4. **Test failures**: Always run `npm run build` before running tests
|
||||
5. **Port conflicts**: Dev server uses ports 3000 and 3001
|
||||
6. **First run**: Server shows "db-config.json not found" - this is expected, starts setup wizard
|
||||
|
||||
## Translations
|
||||
|
||||
- Managed via Weblate. Add keys to `src/lang/en.json` only
|
||||
- Don't include other languages in PRs
|
||||
- Use `$t("key")` in Vue templates
|
||||
|
||||
## Database
|
||||
|
||||
- Primary: SQLite (also supports MariaDB/MySQL/PostgreSQL)
|
||||
- Migrations in `db/knex_migrations/` using Knex.js
|
||||
- Filename format validated by CI: `node ./extra/check-knex-filenames.mjs`
|
||||
|
||||
## Testing
|
||||
|
||||
- **Backend**: Node.js test runner, fast unit tests
|
||||
- **E2E**: Playwright (requires `npx playwright install` first time)
|
||||
- Test data in `data/playwright-test`
|
||||
|
||||
## Adding New Features
|
||||
|
||||
### New Notification Provider
|
||||
Files to modify:
|
||||
1. `server/notification-providers/PROVIDER_NAME.js` (backend logic)
|
||||
2. `server/notification.js` (register provider)
|
||||
3. `src/components/notifications/PROVIDER_NAME.vue` (frontend UI)
|
||||
4. `src/components/notifications/index.js` (register frontend)
|
||||
5. `src/components/NotificationDialog.vue` (add to list)
|
||||
6. `src/lang/en.json` (add translation keys)
|
||||
|
||||
### New Monitor Type
|
||||
Files to modify:
|
||||
1. `server/monitor-types/MONITORING_TYPE.js` (backend logic)
|
||||
2. `server/uptime-kuma-server.js` (register monitor type)
|
||||
3. `src/pages/EditMonitor.vue` (frontend UI)
|
||||
4. `src/lang/en.json` (add translation keys)
|
||||
|
||||
## Important Notes
|
||||
|
||||
1. **Trust these instructions** - based on testing. Search only if incomplete/incorrect
|
||||
2. **Dependencies**: 5 known vulnerabilities (3 moderate, 2 high) - acknowledged, don't fix without discussion
|
||||
3. **Git Branches**: `master` (v2 development), `1.23.X` (v1 maintenance)
|
||||
4. **Node Version**: >= 20.4.0 required
|
||||
5. **Socket.IO**: Most backend logic in `server/socket-handlers/`, not REST
|
||||
6. **Never commit**: `data/`, `dist/`, `tmp/`, `private/`, `node_modules/`
|
||||
18
.github/workflows/auto-test.yml
vendored
18
.github/workflows/auto-test.yml
vendored
@ -21,16 +21,20 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-latest, windows-latest, ARM64]
|
||||
node: [ 18, 20 ]
|
||||
os: [macos-latest, ubuntu-22.04, windows-latest, ARM64]
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
node: [ 20, 24 ]
|
||||
# Also test non-LTS, but only on Ubuntu.
|
||||
include:
|
||||
- os: ubuntu-22.04
|
||||
node: 25
|
||||
|
||||
steps:
|
||||
- run: git config --global core.autocrlf false # Mainly for Windows
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js ${{ matrix.node }}
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
- run: npm install
|
||||
@ -49,7 +53,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ARMv7 ]
|
||||
node: [ 18, 20 ]
|
||||
node: [ 20, 22 ]
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
@ -57,7 +61,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js ${{ matrix.node }}
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
- run: npm ci --production
|
||||
@ -70,7 +74,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 20
|
||||
- run: npm install
|
||||
@ -84,7 +88,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 20
|
||||
- run: npm install
|
||||
|
||||
4
.github/workflows/close-incorrect-issue.yml
vendored
4
.github/workflows/close-incorrect-issue.yml
vendored
@ -11,13 +11,13 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
node-version: [18]
|
||||
node-version: [20]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
|
||||
2
.github/workflows/validate.yml
vendored
2
.github/workflows/validate.yml
vendored
@ -32,7 +32,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
|
||||
@ -86,7 +86,7 @@ to review the appropriate one for your contribution.
|
||||
PR:
|
||||
|
||||
- A text may not be currently localisable. In this case, **adding a new
|
||||
language key** via `$t("languageKey")` might be nessesary
|
||||
language key** via `$t("languageKey")` might be necessary
|
||||
- language keys need to be **added to `en.json`** to be visible in weblate. If
|
||||
this has not happened, a PR is appreciated.
|
||||
- **Adding a new language** requires a new file see
|
||||
|
||||
54
README.md
54
README.md
@ -17,7 +17,7 @@ Uptime Kuma is an easy-to-use self-hosted monitoring tool.
|
||||
|
||||
Try it!
|
||||
|
||||
Demo Server (Location: Frankfurt - Germany): https://demo.kuma.pet/start-demo
|
||||
Demo Server (Location: Frankfurt - Germany): <https://demo.kuma.pet/start-demo>
|
||||
|
||||
It is a temporary live demo, all data will be deleted after 10 minutes. Sponsored by [Uptime Kuma Sponsors](https://github.com/louislam/uptime-kuma#%EF%B8%8F-sponsors).
|
||||
|
||||
@ -37,35 +37,44 @@ It is a temporary live demo, all data will be deleted after 10 minutes. Sponsore
|
||||
|
||||
## 🔧 How to Install
|
||||
|
||||
### 🐳 Docker
|
||||
### 🐳 Docker Compose
|
||||
|
||||
```bash
|
||||
docker run -d --restart=always -p 3001:3001 -v uptime-kuma:/app/data --name uptime-kuma louislam/uptime-kuma:1
|
||||
mkdir uptime-kuma
|
||||
cd uptime-kuma
|
||||
curl -o compose.yaml https://raw.githubusercontent.com/louislam/uptime-kuma/master/compose.yaml
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Uptime Kuma is now running on <http://0.0.0.0:3001>.
|
||||
Uptime Kuma is now running on all network interfaces (e.g. http://localhost:3001 or http://your-ip:3001).
|
||||
|
||||
> [!WARNING]
|
||||
> File Systems like **NFS** (Network File System) are **NOT** supported. Please map to a local directory or volume.
|
||||
|
||||
> [!NOTE]
|
||||
> If you want to limit exposure to localhost (without exposing port for other users or to use a [reverse proxy](https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy)), you can expose the port like this:
|
||||
>
|
||||
> ```bash
|
||||
> docker run -d --restart=always -p 127.0.0.1:3001:3001 -v uptime-kuma:/app/data --name uptime-kuma louislam/uptime-kuma:1
|
||||
> ```
|
||||
### 🐳 Docker Command
|
||||
|
||||
```bash
|
||||
docker run -d --restart=always -p 3001:3001 -v uptime-kuma:/app/data --name uptime-kuma louislam/uptime-kuma:2
|
||||
```
|
||||
Uptime Kuma is now running on all network interfaces (e.g. http://localhost:3001 or http://your-ip:3001).
|
||||
|
||||
If you want to limit exposure to localhost only:
|
||||
|
||||
```bash
|
||||
docker run ... -p 127.0.0.1:3001:3001 ...
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 💪🏻 Non-Docker
|
||||
|
||||
Requirements:
|
||||
|
||||
- Platform
|
||||
- ✅ Major Linux distros such as Debian, Ubuntu, CentOS, Fedora and ArchLinux etc.
|
||||
- ✅ Major Linux distros such as Debian, Ubuntu, Fedora and ArchLinux etc.
|
||||
- ✅ Windows 10 (x64), Windows Server 2012 R2 (x64) or higher
|
||||
- ❌ FreeBSD / OpenBSD / NetBSD
|
||||
- ❌ Replit / Heroku
|
||||
- [Node.js](https://nodejs.org/en/download/) 18 / 20.4
|
||||
- [npm](https://docs.npmjs.com/cli/) 9
|
||||
- [Node.js](https://nodejs.org/en/download/) >= 20.4
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
- [pm2](https://pm2.keymetrics.io/) - For running Uptime Kuma in the background
|
||||
|
||||
@ -84,8 +93,7 @@ npm install pm2 -g && pm2 install pm2-logrotate
|
||||
# Start Server
|
||||
pm2 start server/server.js --name uptime-kuma
|
||||
```
|
||||
|
||||
Uptime Kuma is now running on http://localhost:3001
|
||||
Uptime Kuma is now running on all network interfaces (e.g. http://localhost:3001 or http://your-ip:3001).
|
||||
|
||||
More useful PM2 Commands
|
||||
|
||||
@ -94,26 +102,26 @@ More useful PM2 Commands
|
||||
pm2 monit
|
||||
|
||||
# If you want to add it to startup
|
||||
pm2 save && pm2 startup
|
||||
pm2 startup && pm2 save
|
||||
```
|
||||
|
||||
### Advanced Installation
|
||||
|
||||
If you need more options or need to browse via a reverse proxy, please read:
|
||||
|
||||
https://github.com/louislam/uptime-kuma/wiki/%F0%9F%94%A7-How-to-Install
|
||||
<https://github.com/louislam/uptime-kuma/wiki/%F0%9F%94%A7-How-to-Install>
|
||||
|
||||
## 🆙 How to Update
|
||||
|
||||
Please read:
|
||||
|
||||
https://github.com/louislam/uptime-kuma/wiki/%F0%9F%86%99-How-to-Update
|
||||
<https://github.com/louislam/uptime-kuma/wiki/%F0%9F%86%99-How-to-Update>
|
||||
|
||||
## 🆕 What's Next?
|
||||
|
||||
I will assign requests/issues to the next milestone.
|
||||
|
||||
https://github.com/louislam/uptime-kuma/milestones
|
||||
<https://github.com/louislam/uptime-kuma/milestones>
|
||||
|
||||
## ❤️ Sponsors
|
||||
|
||||
@ -174,11 +182,11 @@ We DO NOT accept all types of pull requests and do not want to waste your time.
|
||||
There are a lot of pull requests right now, but I don't have time to test them all.
|
||||
|
||||
If you want to help, you can check this:
|
||||
https://github.com/louislam/uptime-kuma/wiki/Test-Pull-Requests
|
||||
<https://github.com/louislam/uptime-kuma/wiki/Test-Pull-Requests>
|
||||
|
||||
### Test Beta Version
|
||||
|
||||
Check out the latest beta release here: https://github.com/louislam/uptime-kuma/releases
|
||||
Check out the latest beta release here: <https://github.com/louislam/uptime-kuma/releases>
|
||||
|
||||
### Bug Reports / Feature Requests
|
||||
|
||||
@ -192,5 +200,3 @@ If you want to translate Uptime Kuma into your language, please visit [Weblate R
|
||||
|
||||
Feel free to correct the grammar in the documentation or code.
|
||||
My mother language is not English and my grammar is not that great.
|
||||
|
||||
|
||||
|
||||
29
SECURITY.md
29
SECURITY.md
@ -8,7 +8,8 @@
|
||||
do not send a notification, I probably will miss it without this.
|
||||
<https://github.com/louislam/uptime-kuma/issues/new?assignees=&labels=help&template=security.md>
|
||||
|
||||
Do not use the public issue tracker or discuss it in public as it will cause
|
||||
- Do not report any upstream dependency issues / scan result by any tools. It will be closed immediately without explanations. Unless you have PoC to prove that the upstream issue affected Uptime Kuma.
|
||||
- Do not use the public issue tracker or discuss it in public as it will cause
|
||||
more damage.
|
||||
|
||||
## Do you accept other 3rd-party bug bounty platforms?
|
||||
@ -22,17 +23,21 @@ Advisories only. I will ignore all 3rd-party bug bounty platforms emails.
|
||||
|
||||
### Uptime Kuma Versions
|
||||
|
||||
You should use or upgrade to the latest version of Uptime Kuma. All `1.X.X`
|
||||
versions are upgradable to the latest version.
|
||||
You should use or upgrade to the latest version of Uptime Kuma.
|
||||
All versions are upgradable to the latest version.
|
||||
|
||||
### Upgradable Docker Tags
|
||||
|
||||
| Tag | Supported |
|
||||
| -------------- | ------------------ |
|
||||
| 1 | :white_check_mark: |
|
||||
| 1-debian | :white_check_mark: |
|
||||
| latest | :white_check_mark: |
|
||||
| debian | :white_check_mark: |
|
||||
| 1-alpine | ⚠️ Deprecated |
|
||||
| alpine | ⚠️ Deprecated |
|
||||
| All other tags | ❌ |
|
||||
| Tag | Supported |
|
||||
| --------------- | ------------------------------------------------------------------------------------- |
|
||||
| 2 | :white_check_mark: |
|
||||
| 2-slim | :white_check_mark: |
|
||||
| next | :white_check_mark: |
|
||||
| next-slim | :white_check_mark: |
|
||||
| 2-rootless | :white_check_mark: |
|
||||
| 2-slim-rootless | :white_check_mark: |
|
||||
| 1 | [⚠️ Deprecated](https://github.com/louislam/uptime-kuma/wiki/Migration-From-v1-To-v2) |
|
||||
| 1-debian | [⚠️ Deprecated](https://github.com/louislam/uptime-kuma/wiki/Migration-From-v1-To-v2) |
|
||||
| latest | [⚠️ Deprecated](https://github.com/louislam/uptime-kuma/wiki/Migration-From-v1-To-v2) |
|
||||
| debian | [⚠️ Deprecated](https://github.com/louislam/uptime-kuma/wiki/Migration-From-v1-To-v2) |
|
||||
| All other tags | ❌ |
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
services:
|
||||
uptime-kuma:
|
||||
image: louislam/uptime-kuma:1
|
||||
image: louislam/uptime-kuma:2
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
ports:
|
||||
# <Host Port>:<Container Port>
|
||||
- 3001:3001
|
||||
restart: unless-stopped
|
||||
- "3001:3001"
|
||||
|
||||
@ -2,7 +2,7 @@ import vue from "@vitejs/plugin-vue";
|
||||
import { defineConfig } from "vite";
|
||||
import visualizer from "rollup-plugin-visualizer";
|
||||
import viteCompression from "vite-plugin-compression";
|
||||
import VueDevTools from "vite-plugin-vue-devtools";
|
||||
import { VitePWA } from "vite-plugin-pwa";
|
||||
|
||||
const postCssScss = require("postcss-scss");
|
||||
const postcssRTLCSS = require("postcss-rtlcss");
|
||||
@ -31,7 +31,12 @@ export default defineConfig({
|
||||
algorithm: "brotliCompress",
|
||||
filter: viteCompressionFilter,
|
||||
}),
|
||||
VueDevTools(),
|
||||
VitePWA({
|
||||
registerType: null,
|
||||
srcDir: "src",
|
||||
filename: "serviceWorker.ts",
|
||||
strategies: "injectManifest",
|
||||
}),
|
||||
],
|
||||
css: {
|
||||
postcss: {
|
||||
|
||||
12
db/knex_migrations/2025-01-01-0000-add-smtp.js
Normal file
12
db/knex_migrations/2025-01-01-0000-add-smtp.js
Normal file
@ -0,0 +1,12 @@
|
||||
exports.up = function (knex) {
|
||||
return knex.schema
|
||||
.alterTable("monitor", function (table) {
|
||||
table.string("smtp_security").defaultTo(null);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.alterTable("monitor", function (table) {
|
||||
table.dropColumn("smtp_security");
|
||||
});
|
||||
};
|
||||
24
db/knex_migrations/2025-03-04-0000-ping-advanced-options.js
Normal file
24
db/knex_migrations/2025-03-04-0000-ping-advanced-options.js
Normal file
@ -0,0 +1,24 @@
|
||||
/* SQL:
|
||||
ALTER TABLE monitor ADD ping_count INTEGER default 1 not null;
|
||||
ALTER TABLE monitor ADD ping_numeric BOOLEAN default true not null;
|
||||
ALTER TABLE monitor ADD ping_per_request_timeout INTEGER default 2 not null;
|
||||
*/
|
||||
exports.up = function (knex) {
|
||||
// Add new columns to table monitor
|
||||
return knex.schema
|
||||
.alterTable("monitor", function (table) {
|
||||
table.integer("ping_count").defaultTo(1).notNullable();
|
||||
table.boolean("ping_numeric").defaultTo(true).notNullable();
|
||||
table.integer("ping_per_request_timeout").defaultTo(2).notNullable();
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
exports.down = function (knex) {
|
||||
return knex.schema
|
||||
.alterTable("monitor", function (table) {
|
||||
table.dropColumn("ping_count");
|
||||
table.dropColumn("ping_numeric");
|
||||
table.dropColumn("ping_per_request_timeout");
|
||||
});
|
||||
};
|
||||
13
db/knex_migrations/2025-05-09-0000-add-custom-url.js
Normal file
13
db/knex_migrations/2025-05-09-0000-add-custom-url.js
Normal file
@ -0,0 +1,13 @@
|
||||
// Add column custom_url to monitor_group table
|
||||
exports.up = function (knex) {
|
||||
return knex.schema
|
||||
.alterTable("monitor_group", function (table) {
|
||||
table.text("custom_url", "text");
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.alterTable("monitor_group", function (table) {
|
||||
table.dropColumn("custom_url");
|
||||
});
|
||||
};
|
||||
13
db/knex_migrations/2025-06-03-0000-add-ip-family.js
Normal file
13
db/knex_migrations/2025-06-03-0000-add-ip-family.js
Normal file
@ -0,0 +1,13 @@
|
||||
exports.up = function (knex) {
|
||||
return knex.schema
|
||||
.alterTable("monitor", function (table) {
|
||||
table.boolean("ip_family").defaultTo(null);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (knex) {
|
||||
return knex.schema
|
||||
.alterTable("monitor", function (table) {
|
||||
table.dropColumn("ip_family");
|
||||
});
|
||||
};
|
||||
12
db/knex_migrations/2025-06-11-0000-add-manual-monitor.js
Normal file
12
db/knex_migrations/2025-06-11-0000-add-manual-monitor.js
Normal file
@ -0,0 +1,12 @@
|
||||
exports.up = function (knex) {
|
||||
return knex.schema
|
||||
.alterTable("monitor", function (table) {
|
||||
table.string("manual_status").defaultTo(null);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.alterTable("monitor", function (table) {
|
||||
table.dropColumn("manual_status");
|
||||
});
|
||||
};
|
||||
@ -0,0 +1,34 @@
|
||||
// Add column last_start_date to maintenance table
|
||||
exports.up = async function (knex) {
|
||||
await knex.schema
|
||||
.alterTable("maintenance", function (table) {
|
||||
table.datetime("last_start_date");
|
||||
});
|
||||
|
||||
// Perform migration for recurring-interval strategy
|
||||
const recurringMaintenances = await knex("maintenance").where({
|
||||
strategy: "recurring-interval",
|
||||
cron: "* * * * *"
|
||||
}).select("id", "start_time");
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
const maintenanceUpdates = recurringMaintenances.map(async ({ start_time, id }) => {
|
||||
// eslint-disable-next-line camelcase
|
||||
const [ hourStr, minuteStr ] = start_time.split(":");
|
||||
const hour = parseInt(hourStr, 10);
|
||||
const minute = parseInt(minuteStr, 10);
|
||||
|
||||
const cron = `${minute} ${hour} * * *`;
|
||||
|
||||
await knex("maintenance")
|
||||
.where({ id })
|
||||
.update({ cron });
|
||||
});
|
||||
await Promise.all(maintenanceUpdates);
|
||||
};
|
||||
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.alterTable("maintenance", function (table) {
|
||||
table.dropColumn("last_start_date");
|
||||
});
|
||||
};
|
||||
13
db/knex_migrations/2025-06-15-0001-manual-monitor-fix.js
Normal file
13
db/knex_migrations/2025-06-15-0001-manual-monitor-fix.js
Normal file
@ -0,0 +1,13 @@
|
||||
// Fix: Change manual_status column type to smallint
|
||||
exports.up = function (knex) {
|
||||
return knex.schema
|
||||
.alterTable("monitor", function (table) {
|
||||
table.smallint("manual_status").alter();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.alterTable("monitor", function (table) {
|
||||
table.string("manual_status").alter();
|
||||
});
|
||||
};
|
||||
12
db/knex_migrations/2025-06-24-0000-add-audience-to-oauth.js
Normal file
12
db/knex_migrations/2025-06-24-0000-add-audience-to-oauth.js
Normal file
@ -0,0 +1,12 @@
|
||||
exports.up = function (knex) {
|
||||
return knex.schema
|
||||
.alterTable("monitor", function (table) {
|
||||
table.string("oauth_audience").nullable().defaultTo(null);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.alterTable("monitor", function (table) {
|
||||
table.string("oauth_audience").alter();
|
||||
});
|
||||
};
|
||||
15
db/knex_migrations/2025-07-17-0000-mqtt-websocket-path.js
Normal file
15
db/knex_migrations/2025-07-17-0000-mqtt-websocket-path.js
Normal file
@ -0,0 +1,15 @@
|
||||
exports.up = function (knex) {
|
||||
// Add new column monitor.mqtt_websocket_path
|
||||
return knex.schema
|
||||
.alterTable("monitor", function (table) {
|
||||
table.string("mqtt_websocket_path", 255).nullable();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (knex) {
|
||||
// Drop column monitor.mqtt_websocket_path
|
||||
return knex.schema
|
||||
.alterTable("monitor", function (table) {
|
||||
table.dropColumn("mqtt_websocket_path");
|
||||
});
|
||||
};
|
||||
16
db/knex_migrations/2025-10-14-0000-add-ip-family-fix.js
Normal file
16
db/knex_migrations/2025-10-14-0000-add-ip-family-fix.js
Normal file
@ -0,0 +1,16 @@
|
||||
exports.up = function (knex) {
|
||||
return knex.schema
|
||||
.alterTable("monitor", function (table) {
|
||||
// Fix ip_family, change to varchar instead of boolean
|
||||
// possible values are "ipv4" and "ipv6"
|
||||
table.string("ip_family", 4).defaultTo(null).alter();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (knex) {
|
||||
return knex.schema
|
||||
.alterTable("monitor", function (table) {
|
||||
// Rollback to boolean
|
||||
table.boolean("ip_family").defaultTo(null).alter();
|
||||
});
|
||||
};
|
||||
27
db/knex_migrations/2025-10-15-0000-stat-table-fix.js
Normal file
27
db/knex_migrations/2025-10-15-0000-stat-table-fix.js
Normal file
@ -0,0 +1,27 @@
|
||||
// Fix for #4315. Logically, setting it to 0 ping may not be correct, but it is better than throwing errors
|
||||
|
||||
exports.up = function (knex) {
|
||||
return knex.schema
|
||||
.alterTable("stat_daily", function (table) {
|
||||
table.integer("ping").defaultTo(0).alter();
|
||||
})
|
||||
.alterTable("stat_hourly", function (table) {
|
||||
table.integer("ping").defaultTo(0).alter();
|
||||
})
|
||||
.alterTable("stat_minutely", function (table) {
|
||||
table.integer("ping").defaultTo(0).alter();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (knex) {
|
||||
return knex.schema
|
||||
.alterTable("stat_daily", function (table) {
|
||||
table.integer("ping").alter();
|
||||
})
|
||||
.alterTable("stat_hourly", function (table) {
|
||||
table.integer("ping").alter();
|
||||
})
|
||||
.alterTable("stat_minutely", function (table) {
|
||||
table.integer("ping").alter();
|
||||
});
|
||||
};
|
||||
@ -0,0 +1,15 @@
|
||||
exports.up = function (knex) {
|
||||
// Add new column status_page.show_only_last_heartbeat
|
||||
return knex.schema
|
||||
.alterTable("status_page", function (table) {
|
||||
table.boolean("show_only_last_heartbeat").notNullable().defaultTo(false);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (knex) {
|
||||
// Drop column status_page.show_only_last_heartbeat
|
||||
return knex.schema
|
||||
.alterTable("status_page", function (table) {
|
||||
table.dropColumn("show_only_last_heartbeat");
|
||||
});
|
||||
};
|
||||
@ -2,11 +2,17 @@
|
||||
# Build in Golang
|
||||
# Run npm run build-healthcheck-armv7 in the host first, another it will be super slow where it is building the armv7 healthcheck
|
||||
############################################
|
||||
FROM golang:1.19-buster
|
||||
FROM golang:1-buster
|
||||
WORKDIR /app
|
||||
ARG TARGETPLATFORM
|
||||
COPY ./extra/ ./extra/
|
||||
|
||||
## Switch to archive.debian.org
|
||||
RUN sed -i '/^deb/s/^/#/' /etc/apt/sources.list \
|
||||
&& echo "deb http://archive.debian.org/debian buster main contrib non-free" | tee -a /etc/apt/sources.list \
|
||||
&& echo "deb http://archive.debian.org/debian-security buster/updates main contrib non-free" | tee -a /etc/apt/sources.list \
|
||||
&& echo "deb http://archive.debian.org/debian buster-updates main contrib non-free" | tee -a /etc/apt/sources.list
|
||||
|
||||
# Compile healthcheck.go
|
||||
RUN apt update && \
|
||||
apt --yes --no-install-recommends install curl && \
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
# Download Apprise deb package
|
||||
FROM node:20-bookworm-slim AS download-apprise
|
||||
FROM node:22-bookworm-slim AS download-apprise
|
||||
WORKDIR /app
|
||||
COPY ./extra/download-apprise.mjs ./download-apprise.mjs
|
||||
RUN apt update && \
|
||||
@ -9,7 +9,7 @@ RUN apt update && \
|
||||
|
||||
# Base Image (Slim)
|
||||
# If the image changed, the second stage image should be changed too
|
||||
FROM node:20-bookworm-slim AS base2-slim
|
||||
FROM node:22-bookworm-slim AS base2-slim
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
# Specify --no-install-recommends to skip unused dependencies, make the base much smaller!
|
||||
@ -47,9 +47,9 @@ RUN apt update && \
|
||||
|
||||
# Install cloudflared
|
||||
RUN curl https://pkg.cloudflare.com/cloudflare-main.gpg --output /usr/share/keyrings/cloudflare-main.gpg && \
|
||||
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared bullseye main' | tee /etc/apt/sources.list.d/cloudflared.list && \
|
||||
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared bookworm main' | tee /etc/apt/sources.list.d/cloudflared.list && \
|
||||
apt update && \
|
||||
apt install --yes --no-install-recommends -t stable cloudflared && \
|
||||
apt install --yes --no-install-recommends cloudflared && \
|
||||
cloudflared version && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
apt --yes autoremove
|
||||
|
||||
@ -79,6 +79,10 @@ USER node
|
||||
RUN git config --global user.email "no-reply@no-reply.com"
|
||||
RUN git config --global user.name "PR Tester"
|
||||
RUN git clone https://github.com/louislam/uptime-kuma.git .
|
||||
|
||||
# Hide the warning when running in detached head state
|
||||
RUN git config --global advice.detachedHead false
|
||||
|
||||
RUN npm ci
|
||||
|
||||
EXPOSE 3000 3001
|
||||
|
||||
@ -24,9 +24,7 @@ if (! exists) {
|
||||
// Also update package-lock.json
|
||||
const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm";
|
||||
childProcess.spawnSync(npm, [ "install" ]);
|
||||
|
||||
commit(version);
|
||||
tag(version);
|
||||
|
||||
} else {
|
||||
console.log("version tag exists, please delete the tag or use another tag");
|
||||
@ -54,19 +52,6 @@ function commit(version) {
|
||||
console.log(res.stdout.toString().trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a tag with the specified version
|
||||
* @param {string} version Tag to create
|
||||
* @returns {void}
|
||||
*/
|
||||
function tag(version) {
|
||||
let res = childProcess.spawnSync("git", [ "tag", version ]);
|
||||
console.log(res.stdout.toString().trim());
|
||||
|
||||
res = childProcess.spawnSync("git", [ "push", "origin", version ]);
|
||||
console.log(res.stdout.toString().trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tag exists for the specified version
|
||||
* @param {string} version Version to check
|
||||
|
||||
@ -1,33 +0,0 @@
|
||||
const childProcess = require("child_process");
|
||||
|
||||
if (!process.env.UPTIME_KUMA_GH_REPO) {
|
||||
console.error("Please set a repo to the environment variable 'UPTIME_KUMA_GH_REPO' (e.g. mhkarimi1383:goalert-notification)");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let inputArray = process.env.UPTIME_KUMA_GH_REPO.split(":");
|
||||
|
||||
if (inputArray.length !== 2) {
|
||||
console.error("Invalid format. Please set a repo to the environment variable 'UPTIME_KUMA_GH_REPO' (e.g. mhkarimi1383:goalert-notification)");
|
||||
}
|
||||
|
||||
let name = inputArray[0];
|
||||
let branch = inputArray[1];
|
||||
|
||||
console.log("Checkout pr");
|
||||
|
||||
// Checkout the pr
|
||||
let result = childProcess.spawnSync("git", [ "remote", "add", name, `https://github.com/${name}/uptime-kuma` ]);
|
||||
|
||||
console.log(result.stdout.toString());
|
||||
console.error(result.stderr.toString());
|
||||
|
||||
result = childProcess.spawnSync("git", [ "fetch", name, branch ]);
|
||||
|
||||
console.log(result.stdout.toString());
|
||||
console.error(result.stderr.toString());
|
||||
|
||||
result = childProcess.spawnSync("git", [ "checkout", `${name}/${branch}`, "--force" ]);
|
||||
|
||||
console.log(result.stdout.toString());
|
||||
console.error(result.stderr.toString());
|
||||
34
extra/checkout-pr.mjs
Normal file
34
extra/checkout-pr.mjs
Normal file
@ -0,0 +1,34 @@
|
||||
import childProcess from "child_process";
|
||||
import { parsePrName } from "./kuma-pr/pr-lib.mjs";
|
||||
|
||||
let { name, branch } = parsePrName(process.env.UPTIME_KUMA_GH_REPO);
|
||||
|
||||
console.log(`Checking out PR from ${name}:${branch}`);
|
||||
|
||||
// Checkout the pr
|
||||
let result = childProcess.spawnSync("git", [ "remote", "add", name, `https://github.com/${name}/uptime-kuma` ], {
|
||||
stdio: "inherit"
|
||||
});
|
||||
|
||||
if (result.status !== 0) {
|
||||
console.error("Failed to add remote repository.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
result = childProcess.spawnSync("git", [ "fetch", name, branch ], {
|
||||
stdio: "inherit"
|
||||
});
|
||||
|
||||
if (result.status !== 0) {
|
||||
console.error("Failed to fetch the branch.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
result = childProcess.spawnSync("git", [ "checkout", `${name}/${branch}`, "--force" ], {
|
||||
stdio: "inherit"
|
||||
});
|
||||
|
||||
if (result.status !== 0) {
|
||||
console.error("Failed to checkout the branch.");
|
||||
process.exit(1);
|
||||
}
|
||||
@ -37,7 +37,7 @@ const github = require("@actions/github");
|
||||
owner: issue.owner,
|
||||
repo: issue.repo,
|
||||
issue_number: issue.number,
|
||||
body: `@${username}: Hello! :wave:\n\nThis issue is being automatically closed because it does not follow the issue template. Please **DO NOT open blank issues and use our [issue-templates](https://github.com/louislam/uptime-kuma/issues/new/choose) instead**.\nBlank Issues do not contain the context nessesary for a good discussions.`
|
||||
body: `@${username}: Hello! :wave:\n\nThis issue is being automatically closed because it does not follow the issue template. Please **DO NOT open blank issues and use our [issue-templates](https://github.com/louislam/uptime-kuma/issues/new/choose) instead**.\nBlank Issues do not contain the context necessary for a good discussions.`
|
||||
});
|
||||
|
||||
// Close the issue
|
||||
|
||||
201
extra/generate-changelog.mjs
Normal file
201
extra/generate-changelog.mjs
Normal file
@ -0,0 +1,201 @@
|
||||
// Script to generate changelog
|
||||
// Usage: node generate-changelog.mjs <previous-version-tag>
|
||||
// GitHub CLI (gh command) is required
|
||||
|
||||
import * as childProcess from "child_process";
|
||||
|
||||
const ignoreList = [
|
||||
"louislam",
|
||||
"CommanderStorm",
|
||||
"UptimeKumaBot",
|
||||
"weblate",
|
||||
"Copilot"
|
||||
];
|
||||
|
||||
const mergeList = [
|
||||
"Translations Update from Weblate",
|
||||
"Update dependencies",
|
||||
];
|
||||
|
||||
const template = `
|
||||
|
||||
LLM Task: Please help to put above PRs into the following sections based on their content. If a PR fits multiple sections, choose the most relevant one. If a PR doesn't fit any section, place it in "Others". If there are grammatical errors in the PR titles, please correct them. Don't change the PR numbers and authors, and keep the format. Output as markdown.
|
||||
|
||||
Changelog:
|
||||
|
||||
### 🆕 New Features
|
||||
|
||||
### 💇♀️ Improvements
|
||||
|
||||
### 🐞 Bug Fixes
|
||||
|
||||
### ⬆️ Security Fixes
|
||||
|
||||
### 🦎 Translation Contributions
|
||||
|
||||
### Others
|
||||
- Other small changes, code refactoring and comment/doc updates in this repo:
|
||||
`;
|
||||
|
||||
await main();
|
||||
|
||||
/**
|
||||
* Main Function
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function main() {
|
||||
const previousVersion = process.argv[2];
|
||||
|
||||
if (!previousVersion) {
|
||||
console.error("Please provide the previous version as the first argument.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Generating changelog since version ${previousVersion}...`);
|
||||
|
||||
try {
|
||||
const prList = await getPullRequestList(previousVersion);
|
||||
const list = [];
|
||||
|
||||
let i = 1;
|
||||
for (const pr of prList) {
|
||||
console.log(`Progress: ${i++}/${prList.length}`);
|
||||
let authorSet = await getAuthorList(pr.number);
|
||||
authorSet = await mainAuthorToFront(pr.author.login, authorSet);
|
||||
|
||||
if (mergeList.includes(pr.title)) {
|
||||
// Check if it is already in the list
|
||||
const existingItem = list.find(item => item.title === pr.title);
|
||||
if (existingItem) {
|
||||
existingItem.numbers.push(pr.number);
|
||||
for (const author of authorSet) {
|
||||
existingItem.authors.add(author);
|
||||
// Sort the authors
|
||||
existingItem.authors = new Set([ ...existingItem.authors ].sort((a, b) => a.localeCompare(b)));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const item = {
|
||||
numbers: [ pr.number ],
|
||||
title: pr.title,
|
||||
authors: authorSet,
|
||||
};
|
||||
|
||||
list.push(item);
|
||||
}
|
||||
|
||||
for (const item of list) {
|
||||
// Concat pr numbers into a string like #123 #456
|
||||
const prPart = item.numbers.map(num => `#${num}`).join(" ");
|
||||
|
||||
// Concat authors into a string like @user1 @user2
|
||||
let authorPart = [ ...item.authors ].map(author => `@${author}`).join(" ");
|
||||
|
||||
if (authorPart) {
|
||||
authorPart = `(Thanks ${authorPart})`;
|
||||
}
|
||||
|
||||
console.log(`- ${prPart} ${item.title} ${authorPart}`);
|
||||
}
|
||||
|
||||
console.log(template);
|
||||
|
||||
} catch (e) {
|
||||
console.error("Failed to get pull request list:", e);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} previousVersion Previous Version Tag
|
||||
* @returns {Promise<object>} List of Pull Requests merged since previousVersion
|
||||
*/
|
||||
async function getPullRequestList(previousVersion) {
|
||||
// Get the date of previousVersion in YYYY-MM-DD format from git
|
||||
const previousVersionDate = childProcess.execSync(`git log -1 --format=%cd --date=short ${previousVersion}`).toString().trim();
|
||||
|
||||
if (!previousVersionDate) {
|
||||
throw new Error(`Unable to find the date of version ${previousVersion}. Please make sure the version tag exists.`);
|
||||
}
|
||||
|
||||
const ghProcess = childProcess.spawnSync("gh", [
|
||||
"pr",
|
||||
"list",
|
||||
"--state",
|
||||
"merged",
|
||||
"--base",
|
||||
"master",
|
||||
"--search",
|
||||
`merged:>=${previousVersionDate}`,
|
||||
"--json",
|
||||
"number,title,author",
|
||||
"--limit",
|
||||
"1000"
|
||||
], {
|
||||
encoding: "utf-8"
|
||||
});
|
||||
|
||||
if (ghProcess.error) {
|
||||
throw ghProcess.error;
|
||||
}
|
||||
|
||||
if (ghProcess.status !== 0) {
|
||||
throw new Error(`gh command failed with status ${ghProcess.status}: ${ghProcess.stderr}`);
|
||||
}
|
||||
|
||||
return JSON.parse(ghProcess.stdout);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} prID Pull Request ID
|
||||
* @returns {Promise<Set<string>>} Set of Authors' GitHub Usernames
|
||||
*/
|
||||
async function getAuthorList(prID) {
|
||||
const ghProcess = childProcess.spawnSync("gh", [
|
||||
"pr",
|
||||
"view",
|
||||
prID,
|
||||
"--json",
|
||||
"commits"
|
||||
], {
|
||||
encoding: "utf-8"
|
||||
});
|
||||
|
||||
if (ghProcess.error) {
|
||||
throw ghProcess.error;
|
||||
}
|
||||
|
||||
if (ghProcess.status !== 0) {
|
||||
throw new Error(`gh command failed with status ${ghProcess.status}: ${ghProcess.stderr}`);
|
||||
}
|
||||
|
||||
const prInfo = JSON.parse(ghProcess.stdout);
|
||||
const commits = prInfo.commits;
|
||||
|
||||
const set = new Set();
|
||||
|
||||
for (const commit of commits) {
|
||||
for (const author of commit.authors) {
|
||||
if (author.login && !ignoreList.includes(author.login)) {
|
||||
set.add(author.login);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the set
|
||||
return new Set([ ...set ].sort((a, b) => a.localeCompare(b)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} mainAuthor Main Author
|
||||
* @param {Set<string>} authorSet Set of Authors
|
||||
* @returns {Set<string>} New Set with mainAuthor at the front
|
||||
*/
|
||||
async function mainAuthorToFront(mainAuthor, authorSet) {
|
||||
if (ignoreList.includes(mainAuthor)) {
|
||||
return authorSet;
|
||||
}
|
||||
return new Set([ mainAuthor, ...authorSet ]);
|
||||
}
|
||||
26
extra/kuma-pr/index.mjs
Normal file
26
extra/kuma-pr/index.mjs
Normal file
@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env node
|
||||
import { spawn } from "child_process";
|
||||
import { parsePrName } from "./pr-lib.mjs";
|
||||
|
||||
const prName = process.argv[2];
|
||||
|
||||
// Pre-check the prName here, so testers don't need to wait until the Docker image is pulled to see the error.
|
||||
try {
|
||||
parsePrName(prName);
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
spawn("docker", [
|
||||
"run",
|
||||
"--rm",
|
||||
"-it",
|
||||
"-p", "3000:3000",
|
||||
"-p", "3001:3001",
|
||||
"--pull", "always",
|
||||
"-e", `UPTIME_KUMA_GH_REPO=${prName}`,
|
||||
"louislam/uptime-kuma:pr-test2"
|
||||
], {
|
||||
stdio: "inherit",
|
||||
});
|
||||
8
extra/kuma-pr/package.json
Normal file
8
extra/kuma-pr/package.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "kuma-pr",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"kuma-pr": "./index.mjs"
|
||||
}
|
||||
}
|
||||
39
extra/kuma-pr/pr-lib.mjs
Normal file
39
extra/kuma-pr/pr-lib.mjs
Normal file
@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Parse <name>:<branch> to an object.
|
||||
* @param {string} prName <name>:<branch>
|
||||
* @returns {object} An object with name and branch properties.
|
||||
*/
|
||||
export function parsePrName(prName) {
|
||||
let name = "louislam";
|
||||
let branch;
|
||||
|
||||
const errorMessage = "Please set a repo to the environment variable 'UPTIME_KUMA_GH_REPO' (e.g. mhkarimi1383:goalert-notification)";
|
||||
|
||||
if (!prName) {
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
prName = prName.trim();
|
||||
if (prName === "") {
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
let inputArray = prName.split(":");
|
||||
|
||||
// Just realized that owner's prs are not prefixed with "louislam:"
|
||||
if (inputArray.length === 1) {
|
||||
branch = inputArray[0];
|
||||
|
||||
} else if (inputArray.length === 2) {
|
||||
name = inputArray[0];
|
||||
branch = inputArray[1];
|
||||
|
||||
} else {
|
||||
throw new Error("Invalid format. The format is like this: mhkarimi1383:goalert-notification");
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
branch
|
||||
};
|
||||
}
|
||||
@ -1,44 +0,0 @@
|
||||
// Generate on GitHub
|
||||
const input = `
|
||||
* Add Korean translation by @Alanimdeo in https://github.com/louislam/dockge/pull/86
|
||||
`;
|
||||
|
||||
const template = `
|
||||
### 🆕 New Features
|
||||
|
||||
### 💇♀️ Improvements
|
||||
|
||||
### 🐞 Bug Fixes
|
||||
|
||||
### ⬆️ Security Fixes
|
||||
|
||||
### 🦎 Translation Contributions
|
||||
|
||||
### Others
|
||||
- Other small changes, code refactoring and comment/doc updates in this repo:
|
||||
`;
|
||||
|
||||
const lines = input.split("\n").filter((line) => line.trim() !== "");
|
||||
|
||||
for (const line of lines) {
|
||||
// Split the last " by "
|
||||
const usernamePullRequesURL = line.split(" by ").pop();
|
||||
|
||||
if (!usernamePullRequesURL) {
|
||||
console.log("Unable to parse", line);
|
||||
continue;
|
||||
}
|
||||
|
||||
const [ username, pullRequestURL ] = usernamePullRequesURL.split(" in ");
|
||||
const pullRequestID = "#" + pullRequestURL.split("/").pop();
|
||||
let message = line.split(" by ").shift();
|
||||
|
||||
if (!message) {
|
||||
console.log("Unable to parse", line);
|
||||
continue;
|
||||
}
|
||||
|
||||
message = message.split("* ").pop();
|
||||
console.log("-", pullRequestID, message, `(Thanks ${username})`);
|
||||
}
|
||||
console.log(template);
|
||||
@ -8,7 +8,7 @@ import {
|
||||
checkVersionFormat,
|
||||
getRepoNames,
|
||||
pressAnyKey,
|
||||
execSync, uploadArtifacts,
|
||||
execSync, uploadArtifacts, checkReleaseBranch,
|
||||
} from "./lib.mjs";
|
||||
import semver from "semver";
|
||||
|
||||
@ -23,6 +23,9 @@ if (!githubToken) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Check if the current branch is "release"
|
||||
checkReleaseBranch();
|
||||
|
||||
// Check if the version is a valid semver
|
||||
checkVersionFormat(version);
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ import {
|
||||
checkTagExists,
|
||||
checkVersionFormat,
|
||||
getRepoNames,
|
||||
pressAnyKey, execSync, uploadArtifacts
|
||||
pressAnyKey, execSync, uploadArtifacts, checkReleaseBranch
|
||||
} from "./lib.mjs";
|
||||
|
||||
const repoNames = getRepoNames();
|
||||
@ -21,6 +21,9 @@ if (!githubToken) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Check if the current branch is "release"
|
||||
checkReleaseBranch();
|
||||
|
||||
// Check if the version is a valid semver
|
||||
checkVersionFormat(version);
|
||||
|
||||
|
||||
@ -249,3 +249,16 @@ export function execSync(cmd) {
|
||||
console.info(`[DRY RUN] ${cmd}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current branch is "release"
|
||||
* @returns {void}
|
||||
*/
|
||||
export function checkReleaseBranch() {
|
||||
const res = childProcess.spawnSync("git", [ "rev-parse", "--abbrev-ref", "HEAD" ]);
|
||||
const branch = res.stdout.toString().trim();
|
||||
if (branch !== "release") {
|
||||
console.error(`Current branch is ${branch}, please switch to "release" branch`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,10 +27,19 @@ if (! exists) {
|
||||
|
||||
// Also update package-lock.json
|
||||
const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm";
|
||||
childProcess.spawnSync(npm, [ "install" ]);
|
||||
|
||||
const resultVersion = childProcess.spawnSync(npm, [ "--no-git-tag-version", "version", newVersion ], { shell: true });
|
||||
if (resultVersion.error) {
|
||||
console.error(resultVersion.error);
|
||||
console.error("error npm version!");
|
||||
process.exit(1);
|
||||
}
|
||||
const resultInstall = childProcess.spawnSync(npm, [ "install" ], { shell: true });
|
||||
if (resultInstall.error) {
|
||||
console.error(resultInstall.error);
|
||||
console.error("error update package-lock!");
|
||||
process.exit(1);
|
||||
}
|
||||
commit(newVersion);
|
||||
tag(newVersion);
|
||||
|
||||
} else {
|
||||
console.log("version exists");
|
||||
@ -54,16 +63,6 @@ function commit(version) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a tag with the specified version
|
||||
* @param {string} version Tag to create
|
||||
* @returns {void}
|
||||
*/
|
||||
function tag(version) {
|
||||
let res = childProcess.spawnSync("git", [ "tag", version ]);
|
||||
console.log(res.stdout.toString().trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tag exists for the specified version
|
||||
* @param {string} version Version to check
|
||||
|
||||
6299
package-lock.json
generated
6299
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
30
package.json
30
package.json
@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "uptime-kuma",
|
||||
"version": "2.0.0-beta.2",
|
||||
"version": "2.0.2",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/louislam/uptime-kuma.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || >= 20.4.0"
|
||||
"node": ">= 20.4.0"
|
||||
},
|
||||
"scripts": {
|
||||
"lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .",
|
||||
@ -27,12 +27,14 @@
|
||||
"build": "vite build --config ./config/vite.config.js",
|
||||
"test": "npm run test-backend && npm run test-e2e",
|
||||
"test-with-build": "npm run build && npm test",
|
||||
"test-backend": "cross-env TEST_BACKEND=1 node --test test/backend-test",
|
||||
"test-backend": "node test/test-backend.mjs",
|
||||
"test-backend-22": "cross-env TEST_BACKEND=1 node --test \"test/backend-test/**/*.js\"",
|
||||
"test-backend-20": "cross-env TEST_BACKEND=1 node --test test/backend-test",
|
||||
"test-e2e": "playwright test --config ./config/playwright.config.js",
|
||||
"test-e2e-ui": "playwright test --config ./config/playwright.config.js --ui --ui-port=51063",
|
||||
"playwright-codegen": "playwright codegen localhost:3000 --save-storage=./private/e2e-auth.json",
|
||||
"playwright-show-report": "playwright show-report ./private/playwright-report",
|
||||
"tsc": "tsc",
|
||||
"tsc": "tsc --project ./tsconfig-backend.json",
|
||||
"vite-preview-dist": "vite preview --host --config ./config/vite.config.js",
|
||||
"build-docker-base": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base2 --target base2 . --push",
|
||||
"build-docker-base-slim": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base2-slim --target base2-slim . --push",
|
||||
@ -41,7 +43,7 @@
|
||||
"build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test2 --target pr-test2 . --push",
|
||||
"upload-artifacts": "node extra/release/upload-artifacts.mjs",
|
||||
"upload-artifacts-beta": "node extra/release/upload-artifacts-beta.mjs",
|
||||
"setup": "git checkout 1.23.16 && npm ci --omit dev && npm run download-dist",
|
||||
"setup": "git checkout 2.0.2 && npm ci --omit dev --no-audit && npm run download-dist",
|
||||
"download-dist": "node extra/download-dist.js",
|
||||
"mark-as-nightly": "node extra/mark-as-nightly.js",
|
||||
"reset-password": "node extra/reset-password.js",
|
||||
@ -57,14 +59,15 @@
|
||||
"release-nightly": "node ./extra/release/nightly.mjs",
|
||||
"git-remove-tag": "git tag -d",
|
||||
"build-dist-and-restart": "npm run build && npm run start-server-dev",
|
||||
"start-pr-test": "node extra/checkout-pr.js && npm install && npm run dev",
|
||||
"start-pr-test": "node extra/checkout-pr.mjs && npm install && npm run dev",
|
||||
"build-healthcheck-armv7": "cross-env GOOS=linux GOARCH=arm GOARM=7 go build -x -o ./extra/healthcheck-armv7 ./extra/healthcheck.go",
|
||||
"deploy-demo-server": "node extra/deploy-demo-server.js",
|
||||
"sort-contributors": "node extra/sort-contributors.js",
|
||||
"quick-run-nightly": "docker run --rm --env NODE_ENV=development -p 3001:3001 louislam/uptime-kuma:nightly2",
|
||||
"start-dev-container": "cd docker && docker-compose -f docker-compose-dev.yml up --force-recreate",
|
||||
"rebase-pr-to-1.23.X": "node extra/rebase-pr.js 1.23.X",
|
||||
"reset-migrate-aggregate-table-state": "node extra/reset-migrate-aggregate-table-state.js"
|
||||
"reset-migrate-aggregate-table-state": "node extra/reset-migrate-aggregate-table-state.js",
|
||||
"generate-changelog": "node ./extra/generate-changelog.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@grpc/grpc-js": "~1.8.22",
|
||||
@ -81,7 +84,7 @@
|
||||
"chroma-js": "~2.4.2",
|
||||
"command-exists": "~1.2.9",
|
||||
"compare-versions": "~3.6.0",
|
||||
"compression": "~1.7.4",
|
||||
"compression": "~1.8.1",
|
||||
"country-flag-emoji-polyfill": "^0.1.8",
|
||||
"croner": "~8.1.0",
|
||||
"dayjs": "~1.11.5",
|
||||
@ -99,6 +102,7 @@
|
||||
"http-proxy-agent": "~7.0.2",
|
||||
"https-proxy-agent": "~7.0.6",
|
||||
"iconv-lite": "~0.6.3",
|
||||
"is-url": "^1.2.4",
|
||||
"isomorphic-ws": "^5.0.0",
|
||||
"jsesc": "~3.0.2",
|
||||
"jsonata": "^2.0.3",
|
||||
@ -117,7 +121,6 @@
|
||||
"nanoid": "~3.3.4",
|
||||
"net-snmp": "^3.11.2",
|
||||
"node-cloudflared-tunnel": "~1.0.9",
|
||||
"node-radius-client": "~1.0.0",
|
||||
"nodemailer": "~6.9.13",
|
||||
"nostr-tools": "^2.10.4",
|
||||
"notp": "~2.0.3",
|
||||
@ -131,16 +134,20 @@
|
||||
"promisify-child-process": "~4.1.2",
|
||||
"protobufjs": "~7.2.4",
|
||||
"qs": "~6.10.4",
|
||||
"radius": "~1.1.4",
|
||||
"node-radius-utils": "~1.2.0",
|
||||
"redbean-node": "~0.3.0",
|
||||
"redis": "~4.5.1",
|
||||
"redis": "~5.9.0",
|
||||
"semver": "~7.5.4",
|
||||
"socket.io": "~4.8.0",
|
||||
"socket.io-client": "~4.8.0",
|
||||
"socks-proxy-agent": "~8.0.5",
|
||||
"sqlstring": "~2.3.3",
|
||||
"tar": "~6.2.1",
|
||||
"tcp-ping": "~0.1.1",
|
||||
"thirty-two": "~1.0.2",
|
||||
"tough-cookie": "~4.1.3",
|
||||
"web-push": "^3.6.7",
|
||||
"ws": "^8.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -155,6 +162,7 @@
|
||||
"@testcontainers/rabbitmq": "^10.13.2",
|
||||
"@types/bootstrap": "~5.1.9",
|
||||
"@types/node": "^20.8.6",
|
||||
"@types/web-push": "^3.6.4",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.5",
|
||||
"@typescript-eslint/parser": "^6.7.5",
|
||||
"@vitejs/plugin-vue": "~5.0.1",
|
||||
@ -193,7 +201,7 @@
|
||||
"v-pagination-3": "~0.1.7",
|
||||
"vite": "~5.4.15",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-vue-devtools": "^7.0.15",
|
||||
"vite-plugin-pwa": "^1.1.0",
|
||||
"vue": "~3.4.2",
|
||||
"vue-chartjs": "~5.2.0",
|
||||
"vue-confirm-dialog": "~1.0.2",
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
{
|
||||
"name": "Uptime Kuma",
|
||||
"short_name": "Uptime Kuma",
|
||||
"description": "An easy-to-use self-hosted monitoring tool.",
|
||||
"theme_color": "#5cdd8b",
|
||||
"start_url": "/",
|
||||
"background_color": "#fff",
|
||||
"display": "standalone",
|
||||
@ -15,5 +17,72 @@
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "Dashboard",
|
||||
"short_name": "Dashboard",
|
||||
"description": "View monitoring dashboard",
|
||||
"url": "/dashboard",
|
||||
"icons": [
|
||||
{
|
||||
"src": "icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Add Monitor",
|
||||
"short_name": "Add Monitor",
|
||||
"description": "Add a new monitor",
|
||||
"url": "/add",
|
||||
"icons": [
|
||||
{
|
||||
"src": "icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Monitor List",
|
||||
"short_name": "List",
|
||||
"description": "View all monitors",
|
||||
"url": "/list",
|
||||
"icons": [
|
||||
{
|
||||
"src": "icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Settings",
|
||||
"short_name": "Settings",
|
||||
"description": "Open settings",
|
||||
"url": "/settings",
|
||||
"icons": [
|
||||
{
|
||||
"src": "icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Maintenance",
|
||||
"short_name": "Maintenance",
|
||||
"description": "Manage maintenance windows",
|
||||
"url": "/maintenance",
|
||||
"icons": [
|
||||
{
|
||||
"src": "icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -18,15 +18,15 @@ exports.login = async function (username, password) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let user = await R.findOne("user", " username = ? AND active = 1 ", [
|
||||
username,
|
||||
let user = await R.findOne("user", "TRIM(username) = ? AND active = 1 ", [
|
||||
username.trim(),
|
||||
]);
|
||||
|
||||
if (user && passwordHash.verify(password, user.password)) {
|
||||
// Upgrade the hash to bcrypt
|
||||
if (passwordHash.needRehash(user.password)) {
|
||||
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
|
||||
passwordHash.generate(password),
|
||||
await passwordHash.generate(password),
|
||||
user.id,
|
||||
]);
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
const fs = require("fs");
|
||||
const fsAsync = fs.promises;
|
||||
const { R } = require("redbean-node");
|
||||
const { setSetting, setting } = require("./util-server");
|
||||
const { log, sleep } = require("../src/util");
|
||||
@ -11,6 +12,7 @@ const { UptimeCalculator } = require("./uptime-calculator");
|
||||
const dayjs = require("dayjs");
|
||||
const { SimpleMigrationServer } = require("./utils/simple-migration-server");
|
||||
const KumaColumnCompiler = require("./utils/knex/lib/dialects/mysql2/schema/mysql2-columncompiler");
|
||||
const SqlString = require("sqlstring");
|
||||
|
||||
/**
|
||||
* Database & App Data Folder
|
||||
@ -18,7 +20,7 @@ const KumaColumnCompiler = require("./utils/knex/lib/dialects/mysql2/schema/mysq
|
||||
class Database {
|
||||
|
||||
/**
|
||||
* Boostrap database for SQLite
|
||||
* Bootstrap database for SQLite
|
||||
* @type {string}
|
||||
*/
|
||||
static templatePath = "./db/kuma.db";
|
||||
@ -255,10 +257,6 @@ class Database {
|
||||
}
|
||||
};
|
||||
} else if (dbConfig.type === "mariadb") {
|
||||
if (!/^\w+$/.test(dbConfig.dbName)) {
|
||||
throw Error("Invalid database name. A database name can only consist of letters, numbers and underscores");
|
||||
}
|
||||
|
||||
const connection = await mysql.createConnection({
|
||||
host: dbConfig.hostname,
|
||||
port: dbConfig.port,
|
||||
@ -266,7 +264,11 @@ class Database {
|
||||
password: dbConfig.password,
|
||||
});
|
||||
|
||||
await connection.execute("CREATE DATABASE IF NOT EXISTS " + dbConfig.dbName + " CHARACTER SET utf8mb4");
|
||||
// Set to true, so for example "uptime.kuma", becomes `uptime.kuma`, not `uptime`.`kuma`
|
||||
// Doc: https://github.com/mysqljs/sqlstring?tab=readme-ov-file#escaping-query-identifiers
|
||||
const escapedDBName = SqlString.escapeId(dbConfig.dbName, true);
|
||||
|
||||
await connection.execute("CREATE DATABASE IF NOT EXISTS " + escapedDBName + " CHARACTER SET utf8mb4");
|
||||
connection.end();
|
||||
|
||||
config = {
|
||||
@ -707,12 +709,12 @@ class Database {
|
||||
|
||||
/**
|
||||
* Get the size of the database (SQLite only)
|
||||
* @returns {number} Size of database
|
||||
* @returns {Promise<number>} Size of database
|
||||
*/
|
||||
static getSize() {
|
||||
static async getSize() {
|
||||
if (Database.dbConfig.type === "sqlite") {
|
||||
log.debug("db", "Database.getSize()");
|
||||
let stats = fs.statSync(Database.sqlitePath);
|
||||
let stats = await fsAsync.stat(Database.sqlitePath);
|
||||
log.debug("db", stats);
|
||||
return stats.size;
|
||||
}
|
||||
@ -736,7 +738,7 @@ class Database {
|
||||
if (Database.dbConfig.type === "sqlite") {
|
||||
return "DATETIME('now', ? || ' hours')";
|
||||
} else {
|
||||
return "DATE_ADD(NOW(), INTERVAL ? HOUR)";
|
||||
return "DATE_ADD(UTC_TIMESTAMP(), INTERVAL ? HOUR)";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
const axios = require("axios");
|
||||
const { R } = require("redbean-node");
|
||||
const https = require("https");
|
||||
const fs = require("fs");
|
||||
const fsAsync = require("fs").promises;
|
||||
const path = require("path");
|
||||
const Database = require("./database");
|
||||
const { axiosAbortSignal } = require("./util-server");
|
||||
const { axiosAbortSignal, fsExists } = require("./util-server");
|
||||
|
||||
class DockerHost {
|
||||
|
||||
@ -81,7 +81,7 @@ class DockerHost {
|
||||
options.socketPath = dockerHost.dockerDaemon;
|
||||
} else if (dockerHost.dockerType === "tcp") {
|
||||
options.baseURL = DockerHost.patchDockerURL(dockerHost.dockerDaemon);
|
||||
options.httpsAgent = new https.Agent(DockerHost.getHttpsAgentOptions(dockerHost.dockerType, options.baseURL));
|
||||
options.httpsAgent = new https.Agent(await DockerHost.getHttpsAgentOptions(dockerHost.dockerType, options.baseURL));
|
||||
}
|
||||
|
||||
try {
|
||||
@ -141,9 +141,9 @@ class DockerHost {
|
||||
* File names can also be overridden via 'DOCKER_TLS_FILE_NAME_(CA|KEY|CERT)'.
|
||||
* @param {string} dockerType i.e. "tcp" or "socket"
|
||||
* @param {string} url The docker host URL rewritten to https://
|
||||
* @returns {object} HTTP agent options
|
||||
* @returns {Promise<object>} HTTP agent options
|
||||
*/
|
||||
static getHttpsAgentOptions(dockerType, url) {
|
||||
static async getHttpsAgentOptions(dockerType, url) {
|
||||
let baseOptions = {
|
||||
maxCachedSessions: 0,
|
||||
rejectUnauthorized: true
|
||||
@ -156,10 +156,10 @@ class DockerHost {
|
||||
let certPath = path.join(Database.dockerTLSDir, dirName, DockerHost.CertificateFileNameCert);
|
||||
let keyPath = path.join(Database.dockerTLSDir, dirName, DockerHost.CertificateFileNameKey);
|
||||
|
||||
if (dockerType === "tcp" && fs.existsSync(caPath) && fs.existsSync(certPath) && fs.existsSync(keyPath)) {
|
||||
let ca = fs.readFileSync(caPath);
|
||||
let key = fs.readFileSync(keyPath);
|
||||
let cert = fs.readFileSync(certPath);
|
||||
if (dockerType === "tcp" && await fsExists(caPath) && await fsExists(certPath) && await fsExists(keyPath)) {
|
||||
let ca = await fsAsync.readFile(caPath);
|
||||
let key = await fsAsync.readFile(keyPath);
|
||||
let cert = await fsAsync.readFile(certPath);
|
||||
certOptions = {
|
||||
ca,
|
||||
key,
|
||||
|
||||
@ -33,7 +33,7 @@ class Group extends BeanModel {
|
||||
*/
|
||||
async getMonitorList() {
|
||||
return R.convertToBeans("monitor", await R.getAll(`
|
||||
SELECT monitor.*, monitor_group.send_url FROM monitor, monitor_group
|
||||
SELECT monitor.*, monitor_group.send_url, monitor_group.custom_url FROM monitor, monitor_group
|
||||
WHERE monitor.id = monitor_group.monitor_id
|
||||
AND group_id = ?
|
||||
ORDER BY monitor_group.weight
|
||||
|
||||
@ -158,12 +158,22 @@ class Maintenance extends BeanModel {
|
||||
bean.active = obj.active;
|
||||
|
||||
if (obj.dateRange[0]) {
|
||||
const parsedDate = new Date(obj.dateRange[0]);
|
||||
if (isNaN(parsedDate.getTime()) || parsedDate.getFullYear() > 9999) {
|
||||
throw new Error("Invalid start date");
|
||||
}
|
||||
|
||||
bean.start_date = obj.dateRange[0];
|
||||
} else {
|
||||
bean.start_date = null;
|
||||
}
|
||||
|
||||
if (obj.dateRange[1]) {
|
||||
const parsedDate = new Date(obj.dateRange[1]);
|
||||
if (isNaN(parsedDate.getTime()) || parsedDate.getFullYear() > 9999) {
|
||||
throw new Error("Invalid end date");
|
||||
}
|
||||
|
||||
bean.end_date = obj.dateRange[1];
|
||||
} else {
|
||||
bean.end_date = null;
|
||||
@ -192,7 +202,7 @@ class Maintenance extends BeanModel {
|
||||
* @returns {void}
|
||||
*/
|
||||
static validateCron(cron) {
|
||||
let job = new Cron(cron, () => {});
|
||||
let job = new Cron(cron, () => { });
|
||||
job.stop();
|
||||
}
|
||||
|
||||
@ -229,11 +239,13 @@ class Maintenance extends BeanModel {
|
||||
apicache.clear();
|
||||
});
|
||||
} else if (this.cron != null) {
|
||||
let current = dayjs();
|
||||
|
||||
// Here should be cron or recurring
|
||||
try {
|
||||
this.beanMeta.status = "scheduled";
|
||||
|
||||
let startEvent = (customDuration = 0) => {
|
||||
let startEvent = async (customDuration = 0) => {
|
||||
log.info("maintenance", "Maintenance id: " + this.id + " is under maintenance now");
|
||||
|
||||
this.beanMeta.status = "under-maintenance";
|
||||
@ -248,6 +260,10 @@ class Maintenance extends BeanModel {
|
||||
this.beanMeta.status = "scheduled";
|
||||
UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id);
|
||||
}, duration);
|
||||
|
||||
// Set last start date to current time
|
||||
this.last_start_date = current.toISOString();
|
||||
await R.store(this);
|
||||
};
|
||||
|
||||
// Create Cron
|
||||
@ -256,11 +272,34 @@ class Maintenance extends BeanModel {
|
||||
const startDate = dayjs(this.startDate);
|
||||
const [ hour, minute ] = this.startTime.split(":");
|
||||
const startDateTime = startDate.hour(hour).minute(minute);
|
||||
|
||||
// Fix #6118, since the startDateTime is optional, it will throw error if the date is null when using toISOString()
|
||||
let startAt = undefined;
|
||||
try {
|
||||
startAt = startDateTime.toISOString();
|
||||
} catch (_) {}
|
||||
|
||||
this.beanMeta.job = new Cron(this.cron, {
|
||||
timezone: await this.getTimezone(),
|
||||
interval: this.interval_day * 24 * 60 * 60,
|
||||
startAt: startDateTime.toISOString(),
|
||||
}, startEvent);
|
||||
startAt,
|
||||
}, () => {
|
||||
if (!this.lastStartDate || this.interval_day === 1) {
|
||||
return startEvent();
|
||||
}
|
||||
|
||||
// If last start date is set, it means the maintenance has been started before
|
||||
let lastStartDate = dayjs(this.lastStartDate)
|
||||
.subtract(1.1, "hour"); // Subtract 1.1 hour to avoid issues with timezone differences
|
||||
|
||||
// Check if the interval is enough
|
||||
if (current.diff(lastStartDate, "day") < this.interval_day) {
|
||||
log.debug("maintenance", "Maintenance id: " + this.id + " is still in the window, skipping start event");
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug("maintenance", "Maintenance id: " + this.id + " is not in the window, starting event");
|
||||
return startEvent();
|
||||
});
|
||||
} else {
|
||||
this.beanMeta.job = new Cron(this.cron, {
|
||||
timezone: await this.getTimezone(),
|
||||
@ -269,7 +308,6 @@ class Maintenance extends BeanModel {
|
||||
|
||||
// Continue if the maintenance is still in the window
|
||||
let runningTimeslot = this.getRunningTimeslot();
|
||||
let current = dayjs();
|
||||
|
||||
if (runningTimeslot) {
|
||||
let duration = dayjs(runningTimeslot.endDate).diff(current, "second") * 1000;
|
||||
@ -413,8 +451,11 @@ class Maintenance extends BeanModel {
|
||||
} else if (!this.strategy.startsWith("recurring-")) {
|
||||
this.cron = "";
|
||||
} else if (this.strategy === "recurring-interval") {
|
||||
// For intervals, the pattern is calculated in the run function as the interval-option is set
|
||||
this.cron = "* * * * *";
|
||||
// For intervals, the pattern is used to check if the execution should be started
|
||||
let array = this.start_time.split(":");
|
||||
let hour = parseInt(array[0]);
|
||||
let minute = parseInt(array[1]);
|
||||
this.cron = `${minute} ${hour} * * *`;
|
||||
this.duration = this.calcDuration();
|
||||
log.debug("maintenance", "Cron: " + this.cron);
|
||||
log.debug("maintenance", "Duration: " + this.duration);
|
||||
|
||||
@ -2,10 +2,14 @@ const dayjs = require("dayjs");
|
||||
const axios = require("axios");
|
||||
const { Prometheus } = require("../prometheus");
|
||||
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND,
|
||||
SQL_DATETIME_FORMAT, evaluateJsonQuery
|
||||
SQL_DATETIME_FORMAT, evaluateJsonQuery,
|
||||
PING_PACKET_SIZE_MIN, PING_PACKET_SIZE_MAX, PING_PACKET_SIZE_DEFAULT,
|
||||
PING_GLOBAL_TIMEOUT_MIN, PING_GLOBAL_TIMEOUT_MAX, PING_GLOBAL_TIMEOUT_DEFAULT,
|
||||
PING_COUNT_MIN, PING_COUNT_MAX, PING_COUNT_DEFAULT,
|
||||
PING_PER_REQUEST_TIMEOUT_MIN, PING_PER_REQUEST_TIMEOUT_MAX, PING_PER_REQUEST_TIMEOUT_DEFAULT
|
||||
} = require("../../src/util");
|
||||
const { tcping, ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, setSetting, httpNtlm, radius, grpcQuery,
|
||||
redisPingAsync, kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal
|
||||
const { ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, setSetting, httpNtlm, radius, grpcQuery,
|
||||
kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal
|
||||
} = require("../util-server");
|
||||
const { R } = require("redbean-node");
|
||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
@ -53,7 +57,7 @@ class Monitor extends BeanModel {
|
||||
};
|
||||
|
||||
if (this.sendUrl) {
|
||||
obj.url = this.url;
|
||||
obj.url = this.customUrl ?? this.url;
|
||||
}
|
||||
|
||||
if (showTags) {
|
||||
@ -155,8 +159,15 @@ class Monitor extends BeanModel {
|
||||
snmpOid: this.snmpOid,
|
||||
jsonPathOperator: this.jsonPathOperator,
|
||||
snmpVersion: this.snmpVersion,
|
||||
smtpSecurity: this.smtpSecurity,
|
||||
rabbitmqNodes: JSON.parse(this.rabbitmqNodes),
|
||||
conditions: JSON.parse(this.conditions),
|
||||
ipFamily: this.ipFamily,
|
||||
|
||||
// ping advanced options
|
||||
ping_numeric: this.isPingNumeric(),
|
||||
ping_count: this.ping_count,
|
||||
ping_per_request_timeout: this.ping_per_request_timeout,
|
||||
};
|
||||
|
||||
if (includeSensitiveData) {
|
||||
@ -172,6 +183,7 @@ class Monitor extends BeanModel {
|
||||
oauth_client_secret: this.oauth_client_secret,
|
||||
oauth_token_url: this.oauth_token_url,
|
||||
oauth_scopes: this.oauth_scopes,
|
||||
oauth_audience: this.oauth_audience,
|
||||
oauth_auth_method: this.oauth_auth_method,
|
||||
pushToken: this.pushToken,
|
||||
databaseConnectionString: this.databaseConnectionString,
|
||||
@ -180,6 +192,7 @@ class Monitor extends BeanModel {
|
||||
radiusSecret: this.radiusSecret,
|
||||
mqttUsername: this.mqttUsername,
|
||||
mqttPassword: this.mqttPassword,
|
||||
mqttWebsocketPath: this.mqttWebsocketPath,
|
||||
authWorkstation: this.authWorkstation,
|
||||
authDomain: this.authDomain,
|
||||
tlsCa: this.tlsCa,
|
||||
@ -249,6 +262,14 @@ class Monitor extends BeanModel {
|
||||
return Boolean(this.expiryNotification);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if ping should use numeric output only
|
||||
* @returns {boolean} True if IP addresses will be output instead of symbolic hostnames
|
||||
*/
|
||||
isPingNumeric() {
|
||||
return Boolean(this.ping_numeric);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse to boolean
|
||||
* @returns {boolean} Should TLS errors be ignored?
|
||||
@ -338,7 +359,7 @@ class Monitor extends BeanModel {
|
||||
let previousBeat = null;
|
||||
let retries = 0;
|
||||
|
||||
this.prometheus = new Prometheus(this);
|
||||
this.prometheus = new Prometheus(this, await this.getTags());
|
||||
|
||||
const beat = async () => {
|
||||
|
||||
@ -390,39 +411,6 @@ class Monitor extends BeanModel {
|
||||
if (await Monitor.isUnderMaintenance(this.id)) {
|
||||
bean.msg = "Monitor under maintenance";
|
||||
bean.status = MAINTENANCE;
|
||||
} else if (this.type === "group") {
|
||||
const children = await Monitor.getChildren(this.id);
|
||||
|
||||
if (children.length > 0) {
|
||||
bean.status = UP;
|
||||
bean.msg = "All children up and running";
|
||||
for (const child of children) {
|
||||
if (!child.active) {
|
||||
// Ignore inactive childs
|
||||
continue;
|
||||
}
|
||||
const lastBeat = await Monitor.getPreviousHeartbeat(child.id);
|
||||
|
||||
// Only change state if the monitor is in worse conditions then the ones before
|
||||
// lastBeat.status could be null
|
||||
if (!lastBeat) {
|
||||
bean.status = PENDING;
|
||||
} else if (bean.status === UP && (lastBeat.status === PENDING || lastBeat.status === DOWN)) {
|
||||
bean.status = lastBeat.status;
|
||||
} else if (bean.status === PENDING && lastBeat.status === DOWN) {
|
||||
bean.status = lastBeat.status;
|
||||
}
|
||||
}
|
||||
|
||||
if (bean.status !== UP) {
|
||||
bean.msg = "Child inaccessible";
|
||||
}
|
||||
} else {
|
||||
// Set status pending if group is empty
|
||||
bean.status = PENDING;
|
||||
bean.msg = "Group empty";
|
||||
}
|
||||
|
||||
} else if (this.type === "http" || this.type === "keyword" || this.type === "json-query") {
|
||||
// Do not do any queries/high loading things before the "bean.ping"
|
||||
let startTime = dayjs().valueOf();
|
||||
@ -451,10 +439,26 @@ class Monitor extends BeanModel {
|
||||
}
|
||||
}
|
||||
|
||||
let agentFamily = undefined;
|
||||
if (this.ipFamily === "ipv4") {
|
||||
agentFamily = 4;
|
||||
}
|
||||
if (this.ipFamily === "ipv6") {
|
||||
agentFamily = 6;
|
||||
}
|
||||
|
||||
const httpsAgentOptions = {
|
||||
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
||||
rejectUnauthorized: !this.getIgnoreTls(),
|
||||
secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT,
|
||||
autoSelectFamily: true,
|
||||
...(agentFamily ? { family: agentFamily } : {})
|
||||
};
|
||||
|
||||
const httpAgentOptions = {
|
||||
maxCachedSessions: 0,
|
||||
autoSelectFamily: true,
|
||||
...(agentFamily ? { family: agentFamily } : {})
|
||||
};
|
||||
|
||||
log.debug("monitor", `[${this.name}] Prepare Options for axios`);
|
||||
@ -516,6 +520,7 @@ class Monitor extends BeanModel {
|
||||
if (proxy && proxy.active) {
|
||||
const { httpAgent, httpsAgent } = Proxy.createAgents(proxy, {
|
||||
httpsAgentOptions: httpsAgentOptions,
|
||||
httpAgentOptions: httpAgentOptions,
|
||||
});
|
||||
|
||||
options.proxy = false;
|
||||
@ -524,6 +529,10 @@ class Monitor extends BeanModel {
|
||||
}
|
||||
}
|
||||
|
||||
if (!options.httpAgent) {
|
||||
options.httpAgent = new http.Agent(httpAgentOptions);
|
||||
}
|
||||
|
||||
if (!options.httpsAgent) {
|
||||
let jar = new CookieJar();
|
||||
let httpsCookieAgentOptions = {
|
||||
@ -579,7 +588,8 @@ class Monitor extends BeanModel {
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.UPTIME_KUMA_LOG_RESPONSE_BODY_MONITOR_ID === this.id) {
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (process.env.UPTIME_KUMA_LOG_RESPONSE_BODY_MONITOR_ID == this.id) {
|
||||
log.info("monitor", res.data);
|
||||
}
|
||||
|
||||
@ -621,13 +631,8 @@ class Monitor extends BeanModel {
|
||||
|
||||
}
|
||||
|
||||
} else if (this.type === "port") {
|
||||
bean.ping = await tcping(this.hostname, this.port);
|
||||
bean.msg = "";
|
||||
bean.status = UP;
|
||||
|
||||
} else if (this.type === "ping") {
|
||||
bean.ping = await ping(this.hostname, this.packetSize);
|
||||
bean.ping = await ping(this.hostname, this.ping_count, "", this.ping_numeric, this.packetSize, this.timeout, this.ping_per_request_timeout);
|
||||
bean.msg = "";
|
||||
bean.status = UP;
|
||||
} else if (this.type === "push") { // Type: Push
|
||||
@ -699,7 +704,7 @@ class Monitor extends BeanModel {
|
||||
bean.msg = res.data.response.servers[0].name;
|
||||
|
||||
try {
|
||||
bean.ping = await ping(this.hostname, this.packetSize);
|
||||
bean.ping = await ping(this.hostname, PING_COUNT_DEFAULT, "", true, this.packetSize, PING_GLOBAL_TIMEOUT_DEFAULT, PING_PER_REQUEST_TIMEOUT_DEFAULT);
|
||||
} catch (_) { }
|
||||
} else {
|
||||
throw new Error("Server not found on Steam");
|
||||
@ -749,7 +754,7 @@ class Monitor extends BeanModel {
|
||||
} else if (dockerHost._dockerType === "tcp") {
|
||||
options.baseURL = DockerHost.patchDockerURL(dockerHost._dockerDaemon);
|
||||
options.httpsAgent = new https.Agent(
|
||||
DockerHost.getHttpsAgentOptions(dockerHost._dockerType, options.baseURL)
|
||||
await DockerHost.getHttpsAgentOptions(dockerHost._dockerType, options.baseURL)
|
||||
);
|
||||
}
|
||||
|
||||
@ -851,13 +856,6 @@ class Monitor extends BeanModel {
|
||||
bean.msg = resp.code;
|
||||
bean.status = UP;
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
} else if (this.type === "redis") {
|
||||
let startTime = dayjs().valueOf();
|
||||
|
||||
bean.msg = await redisPingAsync(this.databaseConnectionString, !this.ignoreTls);
|
||||
bean.status = UP;
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
|
||||
} else if (this.type in UptimeKumaServer.monitorTypeList) {
|
||||
let startTime = dayjs().valueOf();
|
||||
const monitorType = UptimeKumaServer.monitorTypeList[this.type];
|
||||
@ -1316,7 +1314,7 @@ class Monitor extends BeanModel {
|
||||
/**
|
||||
* Send a notification about a monitor
|
||||
* @param {boolean} isFirstBeat Is this beat the first of this monitor?
|
||||
* @param {Monitor} monitor The monitor to send a notificaton about
|
||||
* @param {Monitor} monitor The monitor to send a notification about
|
||||
* @param {Bean} bean Status information about monitor
|
||||
* @returns {void}
|
||||
*/
|
||||
@ -1337,7 +1335,8 @@ class Monitor extends BeanModel {
|
||||
try {
|
||||
const heartbeatJSON = bean.toJSON();
|
||||
const monitorData = [{ id: monitor.id,
|
||||
active: monitor.active
|
||||
active: monitor.active,
|
||||
name: monitor.name
|
||||
}];
|
||||
const preloadData = await Monitor.preparePreloadData(monitorData);
|
||||
// Prevent if the msg is undefined, notifications such as Discord cannot send out.
|
||||
@ -1510,6 +1509,31 @@ class Monitor extends BeanModel {
|
||||
if (this.interval < MIN_INTERVAL_SECOND) {
|
||||
throw new Error(`Interval cannot be less than ${MIN_INTERVAL_SECOND} seconds`);
|
||||
}
|
||||
|
||||
if (this.type === "ping") {
|
||||
// ping parameters validation
|
||||
if (this.packetSize && (this.packetSize < PING_PACKET_SIZE_MIN || this.packetSize > PING_PACKET_SIZE_MAX)) {
|
||||
throw new Error(`Packet size must be between ${PING_PACKET_SIZE_MIN} and ${PING_PACKET_SIZE_MAX} (default: ${PING_PACKET_SIZE_DEFAULT})`);
|
||||
}
|
||||
|
||||
if (this.ping_per_request_timeout && (this.ping_per_request_timeout < PING_PER_REQUEST_TIMEOUT_MIN || this.ping_per_request_timeout > PING_PER_REQUEST_TIMEOUT_MAX)) {
|
||||
throw new Error(`Per-ping timeout must be between ${PING_PER_REQUEST_TIMEOUT_MIN} and ${PING_PER_REQUEST_TIMEOUT_MAX} seconds (default: ${PING_PER_REQUEST_TIMEOUT_DEFAULT})`);
|
||||
}
|
||||
|
||||
if (this.ping_count && (this.ping_count < PING_COUNT_MIN || this.ping_count > PING_COUNT_MAX)) {
|
||||
throw new Error(`Echo requests count must be between ${PING_COUNT_MIN} and ${PING_COUNT_MAX} (default: ${PING_COUNT_DEFAULT})`);
|
||||
}
|
||||
|
||||
if (this.timeout) {
|
||||
const pingGlobalTimeout = Math.round(Number(this.timeout));
|
||||
|
||||
if (pingGlobalTimeout < this.ping_per_request_timeout || pingGlobalTimeout < PING_GLOBAL_TIMEOUT_MIN || pingGlobalTimeout > PING_GLOBAL_TIMEOUT_MAX) {
|
||||
throw new Error(`Timeout must be between ${PING_GLOBAL_TIMEOUT_MIN} and ${PING_GLOBAL_TIMEOUT_MAX} seconds (default: ${PING_GLOBAL_TIMEOUT_DEFAULT})`);
|
||||
}
|
||||
|
||||
this.timeout = pingGlobalTimeout;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1635,7 +1659,7 @@ class Monitor extends BeanModel {
|
||||
/**
|
||||
* Gets all Children of the monitor
|
||||
* @param {number} monitorID ID of monitor to get
|
||||
* @returns {Promise<LooseObject<any>>} Children
|
||||
* @returns {Promise<LooseObject<any>[]>} Children
|
||||
*/
|
||||
static async getChildren(monitorID) {
|
||||
return await R.getAll(`
|
||||
@ -1701,6 +1725,55 @@ class Monitor extends BeanModel {
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a monitor from the system
|
||||
* @param {number} monitorID ID of the monitor to delete
|
||||
* @param {number} userID ID of the user who owns the monitor
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async deleteMonitor(monitorID, userID) {
|
||||
const server = UptimeKumaServer.getInstance();
|
||||
|
||||
// Stop the monitor if it's running
|
||||
if (monitorID in server.monitorList) {
|
||||
await server.monitorList[monitorID].stop();
|
||||
delete server.monitorList[monitorID];
|
||||
}
|
||||
|
||||
// Delete from database
|
||||
await R.exec("DELETE FROM monitor WHERE id = ? AND user_id = ? ", [
|
||||
monitorID,
|
||||
userID,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively delete a monitor and all its descendants
|
||||
* @param {number} monitorID ID of the monitor to delete
|
||||
* @param {number} userID ID of the user who owns the monitor
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async deleteMonitorRecursively(monitorID, userID) {
|
||||
// Check if this monitor is a group
|
||||
const monitor = await R.findOne("monitor", " id = ? AND user_id = ? ", [
|
||||
monitorID,
|
||||
userID,
|
||||
]);
|
||||
|
||||
if (monitor && monitor.type === "group") {
|
||||
// Get all children and delete them recursively
|
||||
const children = await Monitor.getChildren(monitorID);
|
||||
if (children && children.length > 0) {
|
||||
for (const child of children) {
|
||||
await Monitor.deleteMonitorRecursively(child.id, userID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the monitor itself
|
||||
await Monitor.deleteMonitor(monitorID, userID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks recursive if parent (ancestors) are active
|
||||
* @param {number} monitorID ID of the monitor to get
|
||||
@ -1723,7 +1796,7 @@ class Monitor extends BeanModel {
|
||||
*/
|
||||
async makeOidcTokenClientCredentialsRequest() {
|
||||
log.debug("monitor", `[${this.name}] The oauth access-token undefined or expired. Requesting a new token`);
|
||||
const oAuthAccessToken = await getOidcTokenClientCredentials(this.oauth_token_url, this.oauth_client_id, this.oauth_client_secret, this.oauth_scopes, this.oauth_auth_method);
|
||||
const oAuthAccessToken = await getOidcTokenClientCredentials(this.oauth_token_url, this.oauth_client_id, this.oauth_client_secret, this.oauth_scopes, this.oauth_audience, this.oauth_auth_method);
|
||||
if (this.oauthAccessToken?.expires_at) {
|
||||
log.debug("monitor", `[${this.name}] Obtained oauth access-token. Expires at ${new Date(this.oauthAccessToken?.expires_at * 1000)}`);
|
||||
} else {
|
||||
|
||||
@ -120,8 +120,8 @@ class StatusPage extends BeanModel {
|
||||
|
||||
const head = $("head");
|
||||
|
||||
if (statusPage.googleAnalyticsTagId) {
|
||||
let escapedGoogleAnalyticsScript = googleAnalytics.getGoogleAnalyticsScript(statusPage.googleAnalyticsTagId);
|
||||
if (statusPage.google_analytics_tag_id) {
|
||||
let escapedGoogleAnalyticsScript = googleAnalytics.getGoogleAnalyticsScript(statusPage.google_analytics_tag_id);
|
||||
head.append($(escapedGoogleAnalyticsScript));
|
||||
}
|
||||
|
||||
@ -409,6 +409,7 @@ class StatusPage extends BeanModel {
|
||||
showPoweredBy: !!this.show_powered_by,
|
||||
googleAnalyticsId: this.google_analytics_tag_id,
|
||||
showCertificateExpiry: !!this.show_certificate_expiry,
|
||||
showOnlyLastHeartbeat: !!this.show_only_last_heartbeat
|
||||
};
|
||||
}
|
||||
|
||||
@ -432,6 +433,7 @@ class StatusPage extends BeanModel {
|
||||
showPoweredBy: !!this.show_powered_by,
|
||||
googleAnalyticsId: this.google_analytics_tag_id,
|
||||
showCertificateExpiry: !!this.show_certificate_expiry,
|
||||
showOnlyLastHeartbeat: !!this.show_only_last_heartbeat
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ class User extends BeanModel {
|
||||
*/
|
||||
static async resetPassword(userID, newPassword) {
|
||||
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
|
||||
passwordHash.generate(newPassword),
|
||||
await passwordHash.generate(newPassword),
|
||||
userID
|
||||
]);
|
||||
}
|
||||
@ -25,7 +25,7 @@ class User extends BeanModel {
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async resetPassword(newPassword) {
|
||||
const hashedPassword = passwordHash.generate(newPassword);
|
||||
const hashedPassword = await passwordHash.generate(newPassword);
|
||||
|
||||
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
|
||||
hashedPassword,
|
||||
|
||||
@ -82,7 +82,7 @@ function createNTLMv2Response(type2message, username, ntlmhash, nonce, targetNam
|
||||
//reserved
|
||||
buf.writeUInt32LE(0, 20);
|
||||
//timestamp
|
||||
//TODO: we are loosing precision here since js is not able to handle those large integers
|
||||
//TODO: we are losing precision here since js is not able to handle those large integers
|
||||
// maybe think about a different solution here
|
||||
// 11644473600000 = diff between 1970 and 1601
|
||||
var timestamp = ((Date.now() + 11644473600000) * 10000).toString(16);
|
||||
|
||||
@ -89,6 +89,9 @@ function NtlmClient(credentials, AxiosConfig) {
|
||||
switch (_b.label) {
|
||||
case 0:
|
||||
error = err.response;
|
||||
// The header may look like this: `Negotiate, NTLM, Basic realm="itsahiddenrealm.example.net"`Add commentMore actions
|
||||
// so extract the 'NTLM' part first
|
||||
const ntlmheader = error.headers['www-authenticate'].split(',').find(_ => _.match(/ *NTLM/))?.trim() || '';
|
||||
if (!(error && error.status === 401
|
||||
&& error.headers['www-authenticate']
|
||||
&& error.headers['www-authenticate'].includes('NTLM'))) return [3 /*break*/, 3];
|
||||
@ -96,12 +99,12 @@ function NtlmClient(credentials, AxiosConfig) {
|
||||
// include the Negotiate option when responding with the T2 message
|
||||
// There is nore we could do to ensure we are processing correctly,
|
||||
// but this is the easiest option for now
|
||||
if (error.headers['www-authenticate'].length < 50) {
|
||||
if (ntlmheader.length < 50) {
|
||||
t1Msg = ntlm.createType1Message(credentials.workstation, credentials.domain);
|
||||
error.config.headers["Authorization"] = t1Msg;
|
||||
}
|
||||
else {
|
||||
t2Msg = ntlm.decodeType2Message((error.headers['www-authenticate'].match(/^NTLM\s+(.+?)(,|\s+|$)/) || [])[1]);
|
||||
t2Msg = ntlm.decodeType2Message((ntlmheader.match(/^NTLM\s+(.+?)(,|\s+|$)/) || [])[1]);
|
||||
t3Msg = ntlm.createType3Message(t2Msg, credentials.username, credentials.password, credentials.workstation, credentials.domain);
|
||||
error.config.headers["X-retry"] = "false";
|
||||
error.config.headers["Authorization"] = t3Msg;
|
||||
|
||||
81
server/monitor-types/group.js
Normal file
81
server/monitor-types/group.js
Normal file
@ -0,0 +1,81 @@
|
||||
const { UP, PENDING, DOWN } = require("../../src/util");
|
||||
const { MonitorType } = require("./monitor-type");
|
||||
const Monitor = require("../model/monitor");
|
||||
|
||||
class GroupMonitorType extends MonitorType {
|
||||
name = "group";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async check(monitor, heartbeat, _server) {
|
||||
const children = await Monitor.getChildren(monitor.id);
|
||||
|
||||
if (children.length === 0) {
|
||||
// Set status pending if group is empty
|
||||
heartbeat.status = PENDING;
|
||||
heartbeat.msg = "Group empty";
|
||||
return;
|
||||
}
|
||||
|
||||
let worstStatus = UP;
|
||||
const downChildren = [];
|
||||
const pendingChildren = [];
|
||||
|
||||
for (const child of children) {
|
||||
if (!child.active) {
|
||||
// Ignore inactive (=paused) children
|
||||
continue;
|
||||
}
|
||||
|
||||
const label = child.name || `#${child.id}`;
|
||||
const lastBeat = await Monitor.getPreviousHeartbeat(child.id);
|
||||
|
||||
if (!lastBeat) {
|
||||
if (worstStatus === UP) {
|
||||
worstStatus = PENDING;
|
||||
}
|
||||
pendingChildren.push(label);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (lastBeat.status === DOWN) {
|
||||
worstStatus = DOWN;
|
||||
downChildren.push(label);
|
||||
} else if (lastBeat.status === PENDING) {
|
||||
if (worstStatus !== DOWN) {
|
||||
worstStatus = PENDING;
|
||||
}
|
||||
pendingChildren.push(label);
|
||||
}
|
||||
}
|
||||
|
||||
if (worstStatus === UP) {
|
||||
heartbeat.status = UP;
|
||||
heartbeat.msg = "All children up and running";
|
||||
return;
|
||||
}
|
||||
|
||||
if (worstStatus === PENDING) {
|
||||
heartbeat.status = PENDING;
|
||||
heartbeat.msg = `Pending child monitors: ${pendingChildren.join(", ")}`;
|
||||
return;
|
||||
}
|
||||
|
||||
heartbeat.status = DOWN;
|
||||
|
||||
let message = `Child monitors down: ${downChildren.join(", ")}`;
|
||||
|
||||
if (pendingChildren.length > 0) {
|
||||
message += `; pending: ${pendingChildren.join(", ")}`;
|
||||
}
|
||||
|
||||
// Throw to leverage the generic retry handling and notification flow
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
GroupMonitorType,
|
||||
};
|
||||
|
||||
36
server/monitor-types/manual.js
Normal file
36
server/monitor-types/manual.js
Normal file
@ -0,0 +1,36 @@
|
||||
const { MonitorType } = require("./monitor-type");
|
||||
const { UP, DOWN, PENDING } = require("../../src/util");
|
||||
|
||||
class ManualMonitorType extends MonitorType {
|
||||
name = "Manual";
|
||||
type = "manual";
|
||||
description = "A monitor that allows manual control of the status";
|
||||
supportsConditions = false;
|
||||
conditionVariables = [];
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async check(monitor, heartbeat) {
|
||||
if (monitor.manual_status !== null) {
|
||||
heartbeat.status = monitor.manual_status;
|
||||
switch (monitor.manual_status) {
|
||||
case UP:
|
||||
heartbeat.msg = "Up";
|
||||
break;
|
||||
case DOWN:
|
||||
heartbeat.msg = "Down";
|
||||
break;
|
||||
default:
|
||||
heartbeat.msg = "Pending";
|
||||
}
|
||||
} else {
|
||||
heartbeat.status = PENDING;
|
||||
heartbeat.msg = "Manual monitoring - No status set";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ManualMonitorType
|
||||
};
|
||||
@ -10,11 +10,12 @@ class MqttMonitorType extends MonitorType {
|
||||
* @inheritdoc
|
||||
*/
|
||||
async check(monitor, heartbeat, server) {
|
||||
const receivedMessage = await this.mqttAsync(monitor.hostname, monitor.mqttTopic, {
|
||||
const [ messageTopic, receivedMessage ] = await this.mqttAsync(monitor.hostname, monitor.mqttTopic, {
|
||||
port: monitor.port,
|
||||
username: monitor.mqttUsername,
|
||||
password: monitor.mqttPassword,
|
||||
interval: monitor.interval,
|
||||
websocketPath: monitor.mqttWebsocketPath,
|
||||
});
|
||||
|
||||
if (monitor.mqttCheckType == null || monitor.mqttCheckType === "") {
|
||||
@ -24,7 +25,7 @@ class MqttMonitorType extends MonitorType {
|
||||
|
||||
if (monitor.mqttCheckType === "keyword") {
|
||||
if (receivedMessage != null && receivedMessage.includes(monitor.mqttSuccessMessage)) {
|
||||
heartbeat.msg = `Topic: ${monitor.mqttTopic}; Message: ${receivedMessage}`;
|
||||
heartbeat.msg = `Topic: ${messageTopic}; Message: ${receivedMessage}`;
|
||||
heartbeat.status = UP;
|
||||
} else {
|
||||
throw Error(`Message Mismatch - Topic: ${monitor.mqttTopic}; Message: ${receivedMessage}`);
|
||||
@ -52,12 +53,12 @@ class MqttMonitorType extends MonitorType {
|
||||
* @param {string} hostname Hostname / address of machine to test
|
||||
* @param {string} topic MQTT topic
|
||||
* @param {object} options MQTT options. Contains port, username,
|
||||
* password and interval (interval defaults to 20)
|
||||
* password, websocketPath and interval (interval defaults to 20)
|
||||
* @returns {Promise<string>} Received MQTT message
|
||||
*/
|
||||
mqttAsync(hostname, topic, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { port, username, password, interval = 20 } = options;
|
||||
const { port, username, password, websocketPath, interval = 20 } = options;
|
||||
|
||||
// Adds MQTT protocol to the hostname if not already present
|
||||
if (!/^(?:http|mqtt|ws)s?:\/\//.test(hostname)) {
|
||||
@ -70,7 +71,15 @@ class MqttMonitorType extends MonitorType {
|
||||
reject(new Error("Timeout, Message not received"));
|
||||
}, interval * 1000 * 0.8);
|
||||
|
||||
const mqttUrl = `${hostname}:${port}`;
|
||||
// Construct the URL based on protocol
|
||||
let mqttUrl = `${hostname}:${port}`;
|
||||
if (hostname.startsWith("ws://") || hostname.startsWith("wss://")) {
|
||||
if (websocketPath && !websocketPath.startsWith("/")) {
|
||||
mqttUrl = `${hostname}:${port}/${websocketPath || ""}`;
|
||||
} else {
|
||||
mqttUrl = `${hostname}:${port}${websocketPath || ""}`;
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("mqtt", `MQTT connecting to ${mqttUrl}`);
|
||||
|
||||
@ -101,11 +110,9 @@ class MqttMonitorType extends MonitorType {
|
||||
});
|
||||
|
||||
client.on("message", (messageTopic, message) => {
|
||||
if (messageTopic === topic) {
|
||||
client.end();
|
||||
clearTimeout(timeoutID);
|
||||
resolve(message.toString("utf8"));
|
||||
}
|
||||
client.end();
|
||||
clearTimeout(timeoutID);
|
||||
resolve([ messageTopic, message.toString("utf8") ]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@ -2,13 +2,13 @@ const { MonitorType } = require("./monitor-type");
|
||||
const { chromium } = require("playwright-core");
|
||||
const { UP, log } = require("../../src/util");
|
||||
const { Settings } = require("../settings");
|
||||
const commandExistsSync = require("command-exists").sync;
|
||||
const childProcess = require("child_process");
|
||||
const path = require("path");
|
||||
const Database = require("../database");
|
||||
const jwt = require("jsonwebtoken");
|
||||
const config = require("../config");
|
||||
const { RemoteBrowser } = require("../remote-browser");
|
||||
const { commandExists } = require("../util-server");
|
||||
|
||||
/**
|
||||
* Cached instance of a browser
|
||||
@ -122,7 +122,7 @@ async function prepareChromeExecutable(executablePath) {
|
||||
executablePath = "/usr/bin/chromium";
|
||||
|
||||
// Install chromium in container via apt install
|
||||
if ( !commandExistsSync(executablePath)) {
|
||||
if (! await commandExists(executablePath)) {
|
||||
await new Promise((resolve, reject) => {
|
||||
log.info("Chromium", "Installing Chromium...");
|
||||
let child = childProcess.exec("apt update && apt --yes --no-install-recommends install chromium fonts-indic fonts-noto fonts-noto-cjk");
|
||||
@ -146,7 +146,7 @@ async function prepareChromeExecutable(executablePath) {
|
||||
}
|
||||
|
||||
} else {
|
||||
executablePath = findChrome(allowedList);
|
||||
executablePath = await findChrome(allowedList);
|
||||
}
|
||||
} else {
|
||||
// User specified a path
|
||||
@ -160,20 +160,20 @@ async function prepareChromeExecutable(executablePath) {
|
||||
|
||||
/**
|
||||
* Find the chrome executable
|
||||
* @param {any[]} executables Executables to search through
|
||||
* @returns {any} Executable
|
||||
* @throws Could not find executable
|
||||
* @param {string[]} executables Executables to search through
|
||||
* @returns {Promise<string>} Executable
|
||||
* @throws {Error} Could not find executable
|
||||
*/
|
||||
function findChrome(executables) {
|
||||
async function findChrome(executables) {
|
||||
// Use the last working executable, so we don't have to search for it again
|
||||
if (lastAutoDetectChromeExecutable) {
|
||||
if (commandExistsSync(lastAutoDetectChromeExecutable)) {
|
||||
if (await commandExists(lastAutoDetectChromeExecutable)) {
|
||||
return lastAutoDetectChromeExecutable;
|
||||
}
|
||||
}
|
||||
|
||||
for (let executable of executables) {
|
||||
if (commandExistsSync(executable)) {
|
||||
if (await commandExists(executable)) {
|
||||
lastAutoDetectChromeExecutable = executable;
|
||||
return executable;
|
||||
}
|
||||
|
||||
57
server/monitor-types/redis.js
Normal file
57
server/monitor-types/redis.js
Normal file
@ -0,0 +1,57 @@
|
||||
const { MonitorType } = require("./monitor-type");
|
||||
const { UP } = require("../../src/util");
|
||||
const redis = require("redis");
|
||||
|
||||
class RedisMonitorType extends MonitorType {
|
||||
name = "redis";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async check(monitor, heartbeat, _server) {
|
||||
heartbeat.msg = await this.redisPingAsync(monitor.databaseConnectionString, !monitor.ignoreTls);
|
||||
heartbeat.status = UP;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redis server ping
|
||||
* @param {string} dsn The redis connection string
|
||||
* @param {boolean} rejectUnauthorized If false, allows unverified server certificates.
|
||||
* @returns {Promise<any>} Response from redis server
|
||||
*/
|
||||
redisPingAsync(dsn, rejectUnauthorized) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const client = redis.createClient({
|
||||
url: dsn,
|
||||
socket: {
|
||||
rejectUnauthorized
|
||||
}
|
||||
});
|
||||
client.on("error", (err) => {
|
||||
if (client.isOpen) {
|
||||
client.disconnect();
|
||||
}
|
||||
reject(err);
|
||||
});
|
||||
client.connect().then(() => {
|
||||
if (!client.isOpen) {
|
||||
client.emit("error", new Error("connection isn't open"));
|
||||
}
|
||||
client.ping().then((res, err) => {
|
||||
if (client.isOpen) {
|
||||
client.disconnect();
|
||||
}
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(res);
|
||||
}
|
||||
}).catch(error => reject(error));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
RedisMonitorType,
|
||||
};
|
||||
35
server/monitor-types/smtp.js
Normal file
35
server/monitor-types/smtp.js
Normal file
@ -0,0 +1,35 @@
|
||||
const { MonitorType } = require("./monitor-type");
|
||||
const { UP } = require("../../src/util");
|
||||
const nodemailer = require("nodemailer");
|
||||
|
||||
class SMTPMonitorType extends MonitorType {
|
||||
name = "smtp";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async check(monitor, heartbeat, _server) {
|
||||
let options = {
|
||||
port: monitor.port || 25,
|
||||
host: monitor.hostname,
|
||||
secure: monitor.smtpSecurity === "secure", // use SMTPS (not STARTTLS)
|
||||
ignoreTLS: monitor.smtpSecurity === "nostarttls", // don't use STARTTLS even if it's available
|
||||
requireTLS: monitor.smtpSecurity === "starttls", // use STARTTLS or fail
|
||||
};
|
||||
let transporter = nodemailer.createTransport(options);
|
||||
try {
|
||||
await transporter.verify();
|
||||
|
||||
heartbeat.status = UP;
|
||||
heartbeat.msg = "SMTP connection verifies successfully";
|
||||
} catch (e) {
|
||||
throw new Error(`SMTP connection doesn't verify: ${e}`);
|
||||
} finally {
|
||||
transporter.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
SMTPMonitorType,
|
||||
};
|
||||
@ -31,7 +31,7 @@ class TailscalePing extends MonitorType {
|
||||
timeout: timeout,
|
||||
encoding: "utf8",
|
||||
});
|
||||
if (res.stderr && res.stderr.toString()) {
|
||||
if (res.stderr && res.stderr.toString() && res.code !== 0) {
|
||||
throw new Error(`Error in output: ${res.stderr.toString()}`);
|
||||
}
|
||||
if (res.stdout && res.stdout.toString()) {
|
||||
|
||||
158
server/monitor-types/tcp.js
Normal file
158
server/monitor-types/tcp.js
Normal file
@ -0,0 +1,158 @@
|
||||
const { MonitorType } = require("./monitor-type");
|
||||
const { UP, DOWN, PING_GLOBAL_TIMEOUT_DEFAULT: TIMEOUT, log } = require("../../src/util");
|
||||
const { checkCertificate } = require("../util-server");
|
||||
const tls = require("tls");
|
||||
const net = require("net");
|
||||
const tcpp = require("tcp-ping");
|
||||
|
||||
/**
|
||||
* Send TCP request to specified hostname and port
|
||||
* @param {string} hostname Hostname / address of machine
|
||||
* @param {number} port TCP port to test
|
||||
* @returns {Promise<number>} Maximum time in ms rounded to nearest integer
|
||||
*/
|
||||
const tcping = (hostname, port) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
tcpp.ping(
|
||||
{
|
||||
address: hostname,
|
||||
port: port,
|
||||
attempts: 1,
|
||||
},
|
||||
(err, data) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
|
||||
if (data.results.length >= 1 && data.results[0].err) {
|
||||
reject(data.results[0].err);
|
||||
}
|
||||
|
||||
resolve(Math.round(data.max));
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
class TCPMonitorType extends MonitorType {
|
||||
name = "port";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async check(monitor, heartbeat, _server) {
|
||||
try {
|
||||
const resp = await tcping(monitor.hostname, monitor.port);
|
||||
heartbeat.ping = resp;
|
||||
heartbeat.msg = `${resp} ms`;
|
||||
heartbeat.status = UP;
|
||||
} catch {
|
||||
heartbeat.status = DOWN;
|
||||
heartbeat.msg = "Connection failed";
|
||||
return;
|
||||
}
|
||||
|
||||
let socket_;
|
||||
|
||||
const preTLS = () =>
|
||||
new Promise((resolve, reject) => {
|
||||
let timeout;
|
||||
socket_ = net.connect(monitor.port, monitor.hostname);
|
||||
|
||||
const onTimeout = () => {
|
||||
log.debug(this.name, `[${monitor.name}] Pre-TLS connection timed out`);
|
||||
reject("Connection timed out");
|
||||
};
|
||||
|
||||
socket_.on("connect", () => {
|
||||
log.debug(this.name, `[${monitor.name}] Pre-TLS connection: ${JSON.stringify(socket_)}`);
|
||||
});
|
||||
|
||||
socket_.on("data", data => {
|
||||
const response = data.toString();
|
||||
const response_ = response.toLowerCase();
|
||||
log.debug(this.name, `[${monitor.name}] Pre-TLS response: ${response}`);
|
||||
switch (true) {
|
||||
case response_.includes("start tls") || response_.includes("begin tls"):
|
||||
timeout && clearTimeout(timeout);
|
||||
resolve({ socket: socket_ });
|
||||
break;
|
||||
case response.startsWith("* OK") || response.match(/CAPABILITY.+STARTTLS/):
|
||||
socket_.write("a001 STARTTLS\r\n");
|
||||
break;
|
||||
case response.startsWith("220") || response.includes("ESMTP"):
|
||||
socket_.write(`EHLO ${monitor.hostname}\r\n`);
|
||||
break;
|
||||
case response.includes("250-STARTTLS"):
|
||||
socket_.write("STARTTLS\r\n");
|
||||
break;
|
||||
default:
|
||||
reject(`Unexpected response: ${response}`);
|
||||
}
|
||||
});
|
||||
socket_.on("error", error => {
|
||||
log.debug(this.name, `[${monitor.name}] ${error.toString()}`);
|
||||
reject(error);
|
||||
});
|
||||
socket_.setTimeout(1000 * TIMEOUT, onTimeout);
|
||||
timeout = setTimeout(onTimeout, 1000 * TIMEOUT);
|
||||
});
|
||||
|
||||
const reuseSocket = monitor.smtpSecurity === "starttls" ? await preTLS() : {};
|
||||
|
||||
if ([ "secure", "starttls" ].includes(monitor.smtpSecurity) && monitor.isEnabledExpiryNotification()) {
|
||||
let socket = null;
|
||||
try {
|
||||
const options = {
|
||||
host: monitor.hostname,
|
||||
port: monitor.port,
|
||||
servername: monitor.hostname,
|
||||
...reuseSocket,
|
||||
};
|
||||
|
||||
const tlsInfoObject = await new Promise((resolve, reject) => {
|
||||
socket = tls.connect(options);
|
||||
|
||||
socket.on("secureConnect", () => {
|
||||
try {
|
||||
const info = checkCertificate(socket);
|
||||
resolve(info);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("error", error => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
socket.setTimeout(1000 * TIMEOUT, () => {
|
||||
reject(new Error("Connection timed out"));
|
||||
});
|
||||
});
|
||||
|
||||
await monitor.handleTlsInfo(tlsInfoObject);
|
||||
if (!tlsInfoObject.valid) {
|
||||
heartbeat.status = DOWN;
|
||||
heartbeat.msg = "Certificate is invalid";
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
heartbeat.status = DOWN;
|
||||
heartbeat.msg = `TLS Connection failed: ${message}`;
|
||||
} finally {
|
||||
if (socket && !socket.destroyed) {
|
||||
socket.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (socket_ && !socket_.destroyed) {
|
||||
socket_.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
TCPMonitorType,
|
||||
};
|
||||
@ -17,12 +17,14 @@ class Elks extends NotificationProvider {
|
||||
data.append("to", notification.elksToNumber );
|
||||
data.append("message", msg);
|
||||
|
||||
const config = {
|
||||
let config = {
|
||||
headers: {
|
||||
"Authorization": "Basic " + Buffer.from(`${notification.elksUsername}:${notification.elksAuthToken}`).toString("base64")
|
||||
}
|
||||
};
|
||||
|
||||
config = this.getAxiosConfigWithProxy(config);
|
||||
|
||||
await axios.post(url, data, config);
|
||||
|
||||
return okMsg;
|
||||
|
||||
47
server/notification-providers/Webpush.js
Normal file
47
server/notification-providers/Webpush.js
Normal file
@ -0,0 +1,47 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const { UP } = require("../../src/util");
|
||||
const webpush = require("web-push");
|
||||
const { setting } = require("../util-server");
|
||||
|
||||
class Webpush extends NotificationProvider {
|
||||
name = "Webpush";
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
const publicVapidKey = await setting("webpushPublicVapidKey");
|
||||
const privateVapidKey = await setting("webpushPrivateVapidKey");
|
||||
|
||||
webpush.setVapidDetails("https://github.com/louislam/uptime-kuma", publicVapidKey, privateVapidKey);
|
||||
|
||||
if (heartbeatJSON === null && monitorJSON === null) {
|
||||
// Test message
|
||||
const data = JSON.stringify({
|
||||
title: "TEST",
|
||||
body: `Test Alert - ${msg}`
|
||||
});
|
||||
|
||||
await webpush.sendNotification(notification.subscription, data);
|
||||
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
const data = JSON.stringify({
|
||||
title: heartbeatJSON["status"] === UP ? "Monitor Up" : "Monitor DOWN",
|
||||
body: heartbeatJSON["status"] === UP ? `❌ ${heartbeatJSON["name"]} is DOWN` : `✅ ${heartbeatJSON["name"]} is UP`
|
||||
});
|
||||
|
||||
await webpush.sendNotification(notification.subscription, data);
|
||||
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Webpush;
|
||||
@ -30,6 +30,8 @@ class Alerta extends NotificationProvider {
|
||||
type: "exceptionAlert",
|
||||
};
|
||||
|
||||
config = this.getAxiosConfigWithProxy(config);
|
||||
|
||||
if (heartbeatJSON == null) {
|
||||
let postData = Object.assign({
|
||||
event: "msg",
|
||||
|
||||
@ -41,7 +41,9 @@ class AlertNow extends NotificationProvider {
|
||||
"event_id": eventId,
|
||||
};
|
||||
|
||||
await axios.post(notification.alertNowWebhookURL, data);
|
||||
let config = this.getAxiosConfigWithProxy({});
|
||||
|
||||
await axios.post(notification.alertNowWebhookURL, data, config);
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
|
||||
@ -72,6 +72,8 @@ class AliyunSMS extends NotificationProvider {
|
||||
data: qs.stringify(params),
|
||||
};
|
||||
|
||||
config = this.getAxiosConfigWithProxy(config);
|
||||
|
||||
let result = await axios(config);
|
||||
if (result.data.Message === "OK") {
|
||||
return true;
|
||||
|
||||
34
server/notification-providers/bale.js
Normal file
34
server/notification-providers/bale.js
Normal file
@ -0,0 +1,34 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class Bale extends NotificationProvider {
|
||||
name = "bale";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
const url = "https://tapi.bale.ai";
|
||||
|
||||
try {
|
||||
await axios.post(
|
||||
`${url}/bot${notification.baleBotToken}/sendMessage`,
|
||||
{
|
||||
chat_id: notification.baleChatID,
|
||||
text: msg
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Bale;
|
||||
@ -96,20 +96,21 @@ class Bark extends NotificationProvider {
|
||||
*/
|
||||
async postNotification(notification, title, subtitle, endpoint) {
|
||||
let result;
|
||||
let config = this.getAxiosConfigWithProxy({});
|
||||
if (notification.apiVersion === "v1" || notification.apiVersion == null) {
|
||||
// url encode title and subtitle
|
||||
title = encodeURIComponent(title);
|
||||
subtitle = encodeURIComponent(subtitle);
|
||||
const params = this.additionalParameters(notification);
|
||||
result = await axios.get(`${endpoint}/${title}/${subtitle}${params}`);
|
||||
result = await axios.get(`${endpoint}/${title}/${subtitle}${params}`, config);
|
||||
} else {
|
||||
result = await axios.post(`${endpoint}/push`, {
|
||||
result = await axios.post(endpoint, {
|
||||
title,
|
||||
body: subtitle,
|
||||
icon: barkNotificationAvatar,
|
||||
sound: notification.barkSound || "telegraph", // default sound is telegraph
|
||||
group: notification.barkGroup || "UptimeKuma", // default group is UptimeKuma
|
||||
});
|
||||
}, config);
|
||||
}
|
||||
this.checkResult(result);
|
||||
if (result.statusText != null) {
|
||||
|
||||
@ -19,7 +19,8 @@ class Bitrix24 extends NotificationProvider {
|
||||
"ATTACH[BLOCKS][0][MESSAGE]": msg
|
||||
};
|
||||
|
||||
await axios.get(`${notification.bitrix24WebhookURL}/im.notify.system.add.json`, { params });
|
||||
let config = this.getAxiosConfigWithProxy({ params });
|
||||
await axios.get(`${notification.bitrix24WebhookURL}/im.notify.system.add.json`, config);
|
||||
return okMsg;
|
||||
|
||||
} catch (error) {
|
||||
|
||||
63
server/notification-providers/brevo.js
Normal file
63
server/notification-providers/brevo.js
Normal file
@ -0,0 +1,63 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class Brevo extends NotificationProvider {
|
||||
name = "Brevo";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
let config = {
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"api-key": notification.brevoApiKey,
|
||||
},
|
||||
};
|
||||
config = this.getAxiosConfigWithProxy(config);
|
||||
|
||||
let to = [{ email: notification.brevoToEmail }];
|
||||
|
||||
let data = {
|
||||
sender: {
|
||||
email: notification.brevoFromEmail.trim(),
|
||||
name: notification.brevoFromName || "Uptime Kuma"
|
||||
},
|
||||
to: to,
|
||||
subject: notification.brevoSubject || "Notification from Your Uptime Kuma",
|
||||
htmlContent: `<html><head></head><body><p>${msg.replace(/\n/g, "<br>")}</p></body></html>`
|
||||
};
|
||||
|
||||
if (notification.brevoCcEmail) {
|
||||
data.cc = notification.brevoCcEmail
|
||||
.split(",")
|
||||
.map((email) => ({ email: email.trim() }));
|
||||
}
|
||||
|
||||
if (notification.brevoBccEmail) {
|
||||
data.bcc = notification.brevoBccEmail
|
||||
.split(",")
|
||||
.map((email) => ({ email: email.trim() }));
|
||||
}
|
||||
|
||||
let result = await axios.post(
|
||||
"https://api.brevo.com/v3/smtp/email",
|
||||
data,
|
||||
config
|
||||
);
|
||||
if (result.status === 201) {
|
||||
return okMsg;
|
||||
} else {
|
||||
throw new Error(`Unexpected status code: ${result.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Brevo;
|
||||
@ -12,7 +12,8 @@ class CallMeBot extends NotificationProvider {
|
||||
try {
|
||||
const url = new URL(notification.callMeBotEndpoint);
|
||||
url.searchParams.set("text", msg);
|
||||
await axios.get(url.toString());
|
||||
let config = this.getAxiosConfigWithProxy({});
|
||||
await axios.get(url.toString(), config);
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
|
||||
@ -22,7 +22,8 @@ class Cellsynt extends NotificationProvider {
|
||||
}
|
||||
};
|
||||
try {
|
||||
const resp = await axios.post("https://se-1.cellsynt.net/sms.php", null, data);
|
||||
let config = this.getAxiosConfigWithProxy(data);
|
||||
const resp = await axios.post("https://se-1.cellsynt.net/sms.php", null, config);
|
||||
if (resp.data == null ) {
|
||||
throw new Error("Could not connect to Cellsynt, please try again.");
|
||||
} else if (resp.data.includes("Error:")) {
|
||||
|
||||
@ -29,6 +29,7 @@ class ClickSendSMS extends NotificationProvider {
|
||||
}
|
||||
]
|
||||
};
|
||||
config = this.getAxiosConfigWithProxy(config);
|
||||
let resp = await axios.post(url, data, config);
|
||||
if (resp.data.data.messages[0].status !== "SUCCESS") {
|
||||
let error = "Something gone wrong. Api returned " + resp.data.data.messages[0].status + ".";
|
||||
|
||||
@ -11,17 +11,23 @@ class DingDing extends NotificationProvider {
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
const mentionAll = notification.mentioning === "everyone";
|
||||
const mobileList = notification.mentioning === "specify-mobiles" ? notification.mobileList : [];
|
||||
const userList = notification.mentioning === "specify-users" ? notification.userList : [];
|
||||
const finalList = [ ...mobileList || [], ...userList || [] ];
|
||||
const mentionStr = finalList.length > 0 ? "\n" : "" + finalList.map(item => `@${item}`).join(" ");
|
||||
try {
|
||||
if (heartbeatJSON != null) {
|
||||
let params = {
|
||||
msgtype: "markdown",
|
||||
markdown: {
|
||||
title: `[${this.statusToString(heartbeatJSON["status"])}] ${monitorJSON["name"]}`,
|
||||
text: `## [${this.statusToString(heartbeatJSON["status"])}] ${monitorJSON["name"]} \n> ${heartbeatJSON["msg"]}\n> Time (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`,
|
||||
text: `## [${this.statusToString(heartbeatJSON["status"])}] ${monitorJSON["name"]} \n> ${heartbeatJSON["msg"]}\n> Time (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}${mentionStr}`,
|
||||
},
|
||||
"at": {
|
||||
"isAtAll": notification.mentioning === "everyone"
|
||||
at: {
|
||||
isAtAll: mentionAll,
|
||||
atUserIds: userList,
|
||||
atMobiles: mobileList
|
||||
}
|
||||
};
|
||||
if (await this.sendToDingDing(notification, params)) {
|
||||
@ -31,7 +37,12 @@ class DingDing extends NotificationProvider {
|
||||
let params = {
|
||||
msgtype: "text",
|
||||
text: {
|
||||
content: msg
|
||||
content: `${msg}${mentionStr}`
|
||||
},
|
||||
at: {
|
||||
isAtAll: mentionAll,
|
||||
atUserIds: userList,
|
||||
atMobiles: mobileList
|
||||
}
|
||||
};
|
||||
if (await this.sendToDingDing(notification, params)) {
|
||||
@ -60,6 +71,7 @@ class DingDing extends NotificationProvider {
|
||||
url: `${notification.webHookUrl}×tamp=${timestamp}&sign=${encodeURIComponent(this.sign(timestamp, notification.secretKey))}`,
|
||||
data: JSON.stringify(params),
|
||||
};
|
||||
config = this.getAxiosConfigWithProxy(config);
|
||||
|
||||
let result = await axios(config);
|
||||
if (result.data.errmsg === "ok") {
|
||||
|
||||
@ -12,24 +12,36 @@ class Discord extends NotificationProvider {
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
let config = this.getAxiosConfigWithProxy({});
|
||||
const discordDisplayName = notification.discordUsername || "Uptime Kuma";
|
||||
const webhookUrl = new URL(notification.discordWebhookUrl);
|
||||
if (notification.discordChannelType === "postToThread") {
|
||||
webhookUrl.searchParams.append("thread_id", notification.threadId);
|
||||
}
|
||||
|
||||
// Check if the webhook has an avatar
|
||||
let webhookHasAvatar = true;
|
||||
try {
|
||||
const webhookInfo = await axios.get(webhookUrl.toString(), config);
|
||||
webhookHasAvatar = !!webhookInfo.data.avatar;
|
||||
} catch (e) {
|
||||
// If we can't verify, we assume he has an avatar to avoid forcing the default avatar
|
||||
webhookHasAvatar = true;
|
||||
}
|
||||
|
||||
// If heartbeatJSON is null, assume we're testing.
|
||||
if (heartbeatJSON == null) {
|
||||
let discordtestdata = {
|
||||
username: discordDisplayName,
|
||||
content: msg,
|
||||
};
|
||||
|
||||
if (!webhookHasAvatar) {
|
||||
discordtestdata.avatar_url = "https://github.com/louislam/uptime-kuma/raw/master/public/icon.png";
|
||||
}
|
||||
if (notification.discordChannelType === "createNewForumPost") {
|
||||
discordtestdata.thread_name = notification.postName;
|
||||
}
|
||||
|
||||
await axios.post(webhookUrl.toString(), discordtestdata);
|
||||
await axios.post(webhookUrl.toString(), discordtestdata, config);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
@ -46,10 +58,10 @@ class Discord extends NotificationProvider {
|
||||
name: "Service Name",
|
||||
value: monitorJSON["name"],
|
||||
},
|
||||
{
|
||||
...(!notification.disableUrl ? [{
|
||||
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
|
||||
value: this.extractAddress(monitorJSON),
|
||||
},
|
||||
}] : []),
|
||||
{
|
||||
name: `Time (${heartbeatJSON["timezone"]})`,
|
||||
value: heartbeatJSON["localDateTime"],
|
||||
@ -61,6 +73,9 @@ class Discord extends NotificationProvider {
|
||||
],
|
||||
}],
|
||||
};
|
||||
if (!webhookHasAvatar) {
|
||||
discorddowndata.avatar_url = "https://github.com/louislam/uptime-kuma/raw/master/public/icon.png";
|
||||
}
|
||||
if (notification.discordChannelType === "createNewForumPost") {
|
||||
discorddowndata.thread_name = notification.postName;
|
||||
}
|
||||
@ -68,7 +83,7 @@ class Discord extends NotificationProvider {
|
||||
discorddowndata.content = notification.discordPrefixMessage;
|
||||
}
|
||||
|
||||
await axios.post(webhookUrl.toString(), discorddowndata);
|
||||
await axios.post(webhookUrl.toString(), discorddowndata, config);
|
||||
return okMsg;
|
||||
|
||||
} else if (heartbeatJSON["status"] === UP) {
|
||||
@ -83,10 +98,10 @@ class Discord extends NotificationProvider {
|
||||
name: "Service Name",
|
||||
value: monitorJSON["name"],
|
||||
},
|
||||
{
|
||||
...(!notification.disableUrl ? [{
|
||||
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
|
||||
value: this.extractAddress(monitorJSON),
|
||||
},
|
||||
}] : []),
|
||||
{
|
||||
name: `Time (${heartbeatJSON["timezone"]})`,
|
||||
value: heartbeatJSON["localDateTime"],
|
||||
@ -98,6 +113,9 @@ class Discord extends NotificationProvider {
|
||||
],
|
||||
}],
|
||||
};
|
||||
if (!webhookHasAvatar) {
|
||||
discordupdata.avatar_url = "https://github.com/louislam/uptime-kuma/raw/master/public/icon.png";
|
||||
}
|
||||
|
||||
if (notification.discordChannelType === "createNewForumPost") {
|
||||
discordupdata.thread_name = notification.postName;
|
||||
@ -107,7 +125,7 @@ class Discord extends NotificationProvider {
|
||||
discordupdata.content = notification.discordPrefixMessage;
|
||||
}
|
||||
|
||||
await axios.post(webhookUrl.toString(), discordupdata);
|
||||
await axios.post(webhookUrl.toString(), discordupdata, config);
|
||||
return okMsg;
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
40
server/notification-providers/evolution.js
Normal file
40
server/notification-providers/evolution.js
Normal file
@ -0,0 +1,40 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class Evolution extends NotificationProvider {
|
||||
name = "evolution";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
let config = {
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"apikey": notification.evolutionAuthToken,
|
||||
}
|
||||
};
|
||||
config = this.getAxiosConfigWithProxy(config);
|
||||
|
||||
let data = {
|
||||
"number": notification.evolutionRecipient,
|
||||
"text": msg,
|
||||
};
|
||||
|
||||
let url = (notification.evolutionApiUrl || "https://evolapicloud.com/").replace(/([^/])\/+$/, "$1") + "/message/sendText/" + encodeURIComponent(notification.evolutionInstanceName);
|
||||
|
||||
await axios.post(url, data, config);
|
||||
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Evolution;
|
||||
@ -12,6 +12,7 @@ class Feishu extends NotificationProvider {
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
let config = this.getAxiosConfigWithProxy({});
|
||||
if (heartbeatJSON == null) {
|
||||
let testdata = {
|
||||
msg_type: "text",
|
||||
@ -19,7 +20,7 @@ class Feishu extends NotificationProvider {
|
||||
text: msg,
|
||||
},
|
||||
};
|
||||
await axios.post(notification.feishuWebHookUrl, testdata);
|
||||
await axios.post(notification.feishuWebHookUrl, testdata, config);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
@ -49,7 +50,7 @@ class Feishu extends NotificationProvider {
|
||||
]
|
||||
}
|
||||
};
|
||||
await axios.post(notification.feishuWebHookUrl, downdata);
|
||||
await axios.post(notification.feishuWebHookUrl, downdata, config);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
@ -79,7 +80,7 @@ class Feishu extends NotificationProvider {
|
||||
]
|
||||
}
|
||||
};
|
||||
await axios.post(notification.feishuWebHookUrl, updata);
|
||||
await axios.post(notification.feishuWebHookUrl, updata, config);
|
||||
return okMsg;
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@ -38,7 +38,7 @@ class FlashDuty extends NotificationProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a monitor url from the monitors infomation
|
||||
* Generate a monitor url from the monitors information
|
||||
* @param {object} monitorInfo Monitor details
|
||||
* @returns {string|undefined} Monitor URL
|
||||
*/
|
||||
@ -73,13 +73,13 @@ class FlashDuty extends NotificationProvider {
|
||||
}
|
||||
const options = {
|
||||
method: "POST",
|
||||
url: "https://api.flashcat.cloud/event/push/alert/standard?integration_key=" + notification.flashdutyIntegrationKey,
|
||||
url: notification.flashdutyIntegrationKey.startsWith("http") ? notification.flashdutyIntegrationKey : "https://api.flashcat.cloud/event/push/alert/standard?integration_key=" + notification.flashdutyIntegrationKey,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
data: {
|
||||
description: `[${title}] [${monitorInfo.name}] ${body}`,
|
||||
title,
|
||||
event_status: eventStatus || "Info",
|
||||
alert_key: String(monitorInfo.id) || Math.random().toString(36).substring(7),
|
||||
alert_key: monitorInfo.id ? String(monitorInfo.id) : Math.random().toString(36).substring(7),
|
||||
labels,
|
||||
}
|
||||
};
|
||||
|
||||
@ -11,10 +11,11 @@ class FreeMobile extends NotificationProvider {
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
let config = this.getAxiosConfigWithProxy({});
|
||||
await axios.post(`https://smsapi.free-mobile.fr/sendmsg?msg=${encodeURIComponent(msg.replace("🔴", "⛔️"))}`, {
|
||||
"user": notification.freemobileUser,
|
||||
"pass": notification.freemobilePass,
|
||||
});
|
||||
}, config);
|
||||
|
||||
return okMsg;
|
||||
|
||||
|
||||
@ -24,6 +24,7 @@ class GoAlert extends NotificationProvider {
|
||||
let config = {
|
||||
headers: headers
|
||||
};
|
||||
config = this.getAxiosConfigWithProxy(config);
|
||||
await axios.post(`${notification.goAlertBaseURL}/api/v2/generic/incoming?token=${notification.goAlertToken}`, data, config);
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
|
||||
@ -12,8 +12,44 @@ class GoogleChat extends NotificationProvider {
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
// If Google Chat Webhook rate limit is reached, retry to configured max retries defaults to 3, delay between 60-180 seconds
|
||||
const post = async (url, data, config) => {
|
||||
let retries = notification.googleChatMaxRetries || 1; // Default to 1 retries
|
||||
retries = (retries > 10) ? 10 : retries; // Enforce maximum retries in backend
|
||||
while (retries > 0) {
|
||||
try {
|
||||
await axios.post(url, data, config);
|
||||
return;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.status === 429) {
|
||||
retries--;
|
||||
if (retries === 0) {
|
||||
throw error;
|
||||
}
|
||||
const delay = 60000 + Math.random() * 120000;
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
let config = this.getAxiosConfigWithProxy({});
|
||||
// Google Chat message formatting: https://developers.google.com/chat/api/guides/message-formats/basic
|
||||
if (notification.googleChatUseTemplate && notification.googleChatTemplate) {
|
||||
// Send message using template
|
||||
const renderedText = await this.renderTemplate(
|
||||
notification.googleChatTemplate,
|
||||
msg,
|
||||
monitorJSON,
|
||||
heartbeatJSON
|
||||
);
|
||||
const data = { "text": renderedText };
|
||||
await post(notification.googleChatWebhookURL, data, config);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
let chatHeader = {
|
||||
title: "Uptime Kuma Alert",
|
||||
@ -83,12 +119,11 @@ class GoogleChat extends NotificationProvider {
|
||||
],
|
||||
};
|
||||
|
||||
await axios.post(notification.googleChatWebhookURL, data);
|
||||
await post(notification.googleChatWebhookURL, data, config);
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -31,8 +31,7 @@ class Gorush extends NotificationProvider {
|
||||
}
|
||||
]
|
||||
};
|
||||
let config = {};
|
||||
|
||||
let config = this.getAxiosConfigWithProxy({});
|
||||
await axios.post(`${notification.gorushServerURL}/api/push`, data, config);
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
|
||||
@ -11,6 +11,7 @@ class Gotify extends NotificationProvider {
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
let config = this.getAxiosConfigWithProxy({});
|
||||
if (notification.gotifyserverurl && notification.gotifyserverurl.endsWith("/")) {
|
||||
notification.gotifyserverurl = notification.gotifyserverurl.slice(0, -1);
|
||||
}
|
||||
@ -18,7 +19,7 @@ class Gotify extends NotificationProvider {
|
||||
"message": msg,
|
||||
"priority": notification.gotifyPriority || 8,
|
||||
"title": "Uptime-Kuma",
|
||||
});
|
||||
}, config);
|
||||
|
||||
return okMsg;
|
||||
|
||||
|
||||
@ -16,13 +16,14 @@ class GrafanaOncall extends NotificationProvider {
|
||||
}
|
||||
|
||||
try {
|
||||
let config = this.getAxiosConfigWithProxy({});
|
||||
if (heartbeatJSON === null) {
|
||||
let grafanaupdata = {
|
||||
title: "General notification",
|
||||
message: msg,
|
||||
state: "alerting",
|
||||
};
|
||||
await axios.post(notification.GrafanaOncallURL, grafanaupdata);
|
||||
await axios.post(notification.GrafanaOncallURL, grafanaupdata, config);
|
||||
return okMsg;
|
||||
} else if (heartbeatJSON["status"] === DOWN) {
|
||||
let grafanadowndata = {
|
||||
@ -30,7 +31,7 @@ class GrafanaOncall extends NotificationProvider {
|
||||
message: heartbeatJSON["msg"],
|
||||
state: "alerting",
|
||||
};
|
||||
await axios.post(notification.GrafanaOncallURL, grafanadowndata);
|
||||
await axios.post(notification.GrafanaOncallURL, grafanadowndata, config);
|
||||
return okMsg;
|
||||
} else if (heartbeatJSON["status"] === UP) {
|
||||
let grafanaupdata = {
|
||||
@ -38,7 +39,7 @@ class GrafanaOncall extends NotificationProvider {
|
||||
message: heartbeatJSON["msg"],
|
||||
state: "ok",
|
||||
};
|
||||
await axios.post(notification.GrafanaOncallURL, grafanaupdata);
|
||||
await axios.post(notification.GrafanaOncallURL, grafanaupdata, config);
|
||||
return okMsg;
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@ -14,6 +14,7 @@ class GtxMessaging extends NotificationProvider {
|
||||
const text = msg.replaceAll("🔴 ", "").replaceAll("✅ ", "");
|
||||
|
||||
try {
|
||||
let config = this.getAxiosConfigWithProxy({});
|
||||
const data = new URLSearchParams();
|
||||
data.append("from", notification.gtxMessagingFrom.trim());
|
||||
data.append("to", notification.gtxMessagingTo.trim());
|
||||
@ -21,7 +22,7 @@ class GtxMessaging extends NotificationProvider {
|
||||
|
||||
const url = `https://rest.gtx-messaging.net/smsc/sendsms/${notification.gtxMessagingApiKey}/json`;
|
||||
|
||||
await axios.post(url, data);
|
||||
await axios.post(url, data, config);
|
||||
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
|
||||
@ -18,7 +18,7 @@ class HeiiOnCall extends NotificationProvider {
|
||||
payload["url"] = baseURL + getMonitorRelativeURL(monitorJSON.id);
|
||||
}
|
||||
|
||||
const config = {
|
||||
let config = {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
@ -28,6 +28,7 @@ class HeiiOnCall extends NotificationProvider {
|
||||
const heiiUrl = `https://heiioncall.com/triggers/${notification.heiiOnCallTriggerId}/`;
|
||||
// docs https://heiioncall.com/docs#manual-triggers
|
||||
try {
|
||||
config = this.getAxiosConfigWithProxy(config);
|
||||
if (!heartbeatJSON) {
|
||||
// Testing or general notification like certificate expiry
|
||||
payload["msg"] = msg;
|
||||
|
||||
@ -15,6 +15,13 @@ class HomeAssistant extends NotificationProvider {
|
||||
const notificationService = notification?.notificationService || defaultNotificationService;
|
||||
|
||||
try {
|
||||
let config = {
|
||||
headers: {
|
||||
Authorization: `Bearer ${notification.longLivedAccessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
};
|
||||
config = this.getAxiosConfigWithProxy(config);
|
||||
await axios.post(
|
||||
`${notification.homeAssistantUrl.trim().replace(/\/*$/, "")}/api/services/notify/${notificationService}`,
|
||||
{
|
||||
@ -26,14 +33,7 @@ class HomeAssistant extends NotificationProvider {
|
||||
channel: "Uptime Kuma",
|
||||
icon_url: "https://github.com/louislam/uptime-kuma/blob/master/public/icon.png?raw=true",
|
||||
} }),
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${notification.longLivedAccessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
}, config);
|
||||
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
|
||||
@ -31,6 +31,8 @@ class Keep extends NotificationProvider {
|
||||
|
||||
let webhookURL = url + "/alerts/event/uptimekuma";
|
||||
|
||||
config = this.getAxiosConfigWithProxy(config);
|
||||
|
||||
await axios.post(webhookURL, data, config);
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
|
||||
@ -22,6 +22,7 @@ class Kook extends NotificationProvider {
|
||||
},
|
||||
};
|
||||
try {
|
||||
config = this.getAxiosConfigWithProxy(config);
|
||||
await axios.post(url, data, config);
|
||||
return okMsg;
|
||||
|
||||
|
||||
@ -19,6 +19,7 @@ class Line extends NotificationProvider {
|
||||
"Authorization": "Bearer " + notification.lineChannelAccessToken
|
||||
}
|
||||
};
|
||||
config = this.getAxiosConfigWithProxy(config);
|
||||
if (heartbeatJSON == null) {
|
||||
let testMessage = {
|
||||
"to": notification.lineUserID,
|
||||
|
||||
@ -20,6 +20,7 @@ class LineNotify extends NotificationProvider {
|
||||
"Authorization": "Bearer " + notification.lineNotifyAccessToken
|
||||
}
|
||||
};
|
||||
config = this.getAxiosConfigWithProxy(config);
|
||||
if (heartbeatJSON == null) {
|
||||
let testMessage = {
|
||||
"message": msg,
|
||||
|
||||
@ -13,13 +13,14 @@ class LunaSea extends NotificationProvider {
|
||||
const url = "https://notify.lunasea.app/v1";
|
||||
|
||||
try {
|
||||
let config = this.getAxiosConfigWithProxy({});
|
||||
const target = this.getTarget(notification);
|
||||
if (heartbeatJSON == null) {
|
||||
let testdata = {
|
||||
"title": "Uptime Kuma Alert",
|
||||
"body": msg,
|
||||
};
|
||||
await axios.post(`${url}/custom/${target}`, testdata);
|
||||
await axios.post(`${url}/custom/${target}`, testdata, config);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
@ -30,7 +31,7 @@ class LunaSea extends NotificationProvider {
|
||||
heartbeatJSON["msg"] +
|
||||
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
|
||||
};
|
||||
await axios.post(`${url}/custom/${target}`, downdata);
|
||||
await axios.post(`${url}/custom/${target}`, downdata, config);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
@ -41,7 +42,7 @@ class LunaSea extends NotificationProvider {
|
||||
heartbeatJSON["msg"] +
|
||||
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
|
||||
};
|
||||
await axios.post(`${url}/custom/${target}`, updata);
|
||||
await axios.post(`${url}/custom/${target}`, updata, config);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
|
||||
@ -37,6 +37,7 @@ class Matrix extends NotificationProvider {
|
||||
"body": msg
|
||||
};
|
||||
|
||||
config = this.getAxiosConfigWithProxy(config);
|
||||
await axios.put(`${notification.homeserverUrl}/_matrix/client/r0/rooms/${roomId}/send/m.room.message/${randomString}`, data, config);
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
|
||||
@ -12,6 +12,7 @@ class Mattermost extends NotificationProvider {
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
let config = this.getAxiosConfigWithProxy({});
|
||||
const mattermostUserName = notification.mattermostusername || "Uptime Kuma";
|
||||
// If heartbeatJSON is null, assume non monitoring notification (Certificate warning) or testing.
|
||||
if (heartbeatJSON == null) {
|
||||
@ -19,7 +20,7 @@ class Mattermost extends NotificationProvider {
|
||||
username: mattermostUserName,
|
||||
text: msg,
|
||||
};
|
||||
await axios.post(notification.mattermostWebhookUrl, mattermostTestData);
|
||||
await axios.post(notification.mattermostWebhookUrl, mattermostTestData, config);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
@ -79,13 +80,11 @@ class Mattermost extends NotificationProvider {
|
||||
fallback:
|
||||
"Your " +
|
||||
monitorJSON.pathName +
|
||||
monitorJSON.name +
|
||||
" service went " +
|
||||
statusText,
|
||||
color: color,
|
||||
title:
|
||||
monitorJSON.pathName +
|
||||
monitorJSON.name +
|
||||
" service went " +
|
||||
statusText,
|
||||
title_link: monitorJSON.url,
|
||||
@ -100,7 +99,7 @@ class Mattermost extends NotificationProvider {
|
||||
},
|
||||
],
|
||||
};
|
||||
await axios.post(notification.mattermostWebhookUrl, mattermostdata);
|
||||
await axios.post(notification.mattermostWebhookUrl, mattermostdata, config);
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
|
||||
66
server/notification-providers/nextcloudtalk.js
Normal file
66
server/notification-providers/nextcloudtalk.js
Normal file
@ -0,0 +1,66 @@
|
||||
const { UP, DOWN } = require("../../src/util");
|
||||
const Crypto = require("crypto");
|
||||
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class NextcloudTalk extends NotificationProvider {
|
||||
name = "nextcloudtalk";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
// See documentation at https://nextcloud-talk.readthedocs.io/en/latest/bots/#sending-a-chat-message
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
// Create a random string
|
||||
const talkRandom = encodeURIComponent(
|
||||
Crypto
|
||||
.randomBytes(64)
|
||||
.toString("hex")
|
||||
.slice(0, 64)
|
||||
);
|
||||
|
||||
// Create the signature over random and message
|
||||
const talkSignature = Crypto
|
||||
.createHmac("sha256", Buffer.from(notification.botSecret, "utf8"))
|
||||
.update(Buffer.from(`${talkRandom}${msg}`, "utf8"))
|
||||
.digest("hex");
|
||||
|
||||
let silentUp = (heartbeatJSON?.status === UP && notification.sendSilentUp);
|
||||
let silentDown = (heartbeatJSON?.status === DOWN && notification.sendSilentDown);
|
||||
let silent = (silentUp || silentDown);
|
||||
|
||||
let url = `${notification.host}/ocs/v2.php/apps/spreed/api/v1/bot/${notification.conversationToken}/message`;
|
||||
let config = this.getAxiosConfigWithProxy({});
|
||||
|
||||
const data = {
|
||||
message: msg,
|
||||
silent
|
||||
};
|
||||
|
||||
const options = {
|
||||
...config,
|
||||
headers: {
|
||||
"X-Nextcloud-Talk-Bot-Random": talkRandom,
|
||||
"X-Nextcloud-Talk-Bot-Signature": talkSignature,
|
||||
"OCS-APIRequest": true,
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
let result = await axios.post(url, data, options);
|
||||
|
||||
if (result?.status === 201) {
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
throw new Error("Nextcloud Talk Error " + (result?.status ?? "Unknown"));
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = NextcloudTalk;
|
||||
54
server/notification-providers/notifery.js
Normal file
54
server/notification-providers/notifery.js
Normal file
@ -0,0 +1,54 @@
|
||||
const { getMonitorRelativeURL, UP } = require("../../src/util");
|
||||
const { setting } = require("../util-server");
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class Notifery extends NotificationProvider {
|
||||
name = "notifery";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
const url = "https://api.notifery.com/event";
|
||||
|
||||
let data = {
|
||||
title: notification.notiferyTitle || "Uptime Kuma Alert",
|
||||
message: msg,
|
||||
};
|
||||
|
||||
if (notification.notiferyGroup) {
|
||||
data.group = notification.notiferyGroup;
|
||||
}
|
||||
|
||||
// Link to the monitor
|
||||
const baseURL = await setting("primaryBaseURL");
|
||||
if (baseURL && monitorJSON) {
|
||||
data.message += `\n\nMonitor: ${baseURL}${getMonitorRelativeURL(monitorJSON.id)}`;
|
||||
}
|
||||
|
||||
if (heartbeatJSON) {
|
||||
data.code = heartbeatJSON.status === UP ? 0 : 1;
|
||||
|
||||
if (heartbeatJSON.ping) {
|
||||
data.duration = heartbeatJSON.ping;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": notification.notiferyApiKey,
|
||||
};
|
||||
|
||||
let config = this.getAxiosConfigWithProxy({ headers });
|
||||
await axios.post(url, data, config);
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Notifery;
|
||||
@ -1,5 +1,7 @@
|
||||
const { Liquid } = require("liquidjs");
|
||||
const { DOWN } = require("../../src/util");
|
||||
const { HttpProxyAgent } = require("http-proxy-agent");
|
||||
const { HttpsProxyAgent } = require("https-proxy-agent");
|
||||
|
||||
class NotificationProvider {
|
||||
|
||||
@ -61,7 +63,11 @@ class NotificationProvider {
|
||||
* @returns {Promise<string>} rendered template
|
||||
*/
|
||||
async renderTemplate(template, msg, monitorJSON, heartbeatJSON) {
|
||||
const engine = new Liquid();
|
||||
const engine = new Liquid({
|
||||
root: "./no-such-directory-uptime-kuma",
|
||||
relativeReference: false,
|
||||
dynamicPartials: false,
|
||||
});
|
||||
const parsedTpl = engine.parse(template);
|
||||
|
||||
// Let's start with dummy values to simplify code
|
||||
@ -115,6 +121,30 @@ class NotificationProvider {
|
||||
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns axios config with proxy agent if proxy env is set.
|
||||
* @param {object} axiosConfig - Axios config containing params
|
||||
* @returns {object} Axios config
|
||||
*/
|
||||
getAxiosConfigWithProxy(axiosConfig = {}) {
|
||||
const proxyEnv = process.env.notification_proxy || process.env.NOTIFICATION_PROXY;
|
||||
if (proxyEnv) {
|
||||
const proxyUrl = new URL(proxyEnv);
|
||||
|
||||
if (proxyUrl.protocol === "http:") {
|
||||
axiosConfig.httpAgent = new HttpProxyAgent(proxyEnv);
|
||||
axiosConfig.httpsAgent = new HttpsProxyAgent(proxyEnv);
|
||||
} else if (proxyUrl.protocol === "https:") {
|
||||
const agent = new HttpsProxyAgent(proxyEnv);
|
||||
axiosConfig.httpAgent = agent;
|
||||
axiosConfig.httpsAgent = agent;
|
||||
}
|
||||
|
||||
axiosConfig.proxy = false;
|
||||
}
|
||||
return axiosConfig;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = NotificationProvider;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user