commit
f05651d235
@ -1,22 +0,0 @@
|
|||||||
---
|
|
||||||
name: Ask for help
|
|
||||||
about: You can ask any question related to Uptime Kuma.
|
|
||||||
title: ''
|
|
||||||
labels: help
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
**Is it a duplicate question?**
|
|
||||||
Please search in Issues without filters: https://github.com/louislam/uptime-kuma/issues?q=
|
|
||||||
|
|
||||||
|
|
||||||
**Describe your problem**
|
|
||||||
|
|
||||||
|
|
||||||
**Info**
|
|
||||||
Uptime Kuma Version:
|
|
||||||
Using Docker?: Yes/No
|
|
||||||
Docker Version:
|
|
||||||
Node.js Version (Without Docker only):
|
|
||||||
OS:
|
|
||||||
Browser:
|
|
@ -0,0 +1,68 @@
|
|||||||
|
name: "❓ Ask for help"
|
||||||
|
description: "Submit any question related to Uptime Kuma"
|
||||||
|
#title: "[Help] "
|
||||||
|
labels: [help]
|
||||||
|
body:
|
||||||
|
- type: checkboxes
|
||||||
|
id: no-duplicate-issues
|
||||||
|
attributes:
|
||||||
|
label: "⚠️ Please verify that this bug has NOT been raised before."
|
||||||
|
description: "Search in the issues sections by clicking [HERE](https://github.com/louislam/uptime-kuma/issues?q=)"
|
||||||
|
options:
|
||||||
|
- label: "I checked and didn't find similar issue"
|
||||||
|
required: true
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: "🛡️ Security Policy"
|
||||||
|
description: Please review the security policy before reporting security related issues/bugs.
|
||||||
|
options:
|
||||||
|
- label: I agree to have read this project [Security Policy](https://github.com/louislam/uptime-kuma/security/policy)
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: steps-to-reproduce
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: "📝 Describe your problem"
|
||||||
|
description: "Please walk us through it step by step."
|
||||||
|
placeholder: "Describe what are you asking for..."
|
||||||
|
- type: input
|
||||||
|
id: uptime-kuma-version
|
||||||
|
attributes:
|
||||||
|
label: "🐻 Uptime-Kuma Version"
|
||||||
|
description: "Which version of Uptime-Kuma are you running? Please do NOT provide the docker tag such as latest or 1"
|
||||||
|
placeholder: "Ex. 1.10.0"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: operating-system
|
||||||
|
attributes:
|
||||||
|
label: "💻 Operating System and Arch"
|
||||||
|
description: "Which OS is your server/device running on?"
|
||||||
|
placeholder: "Ex. Ubuntu 20.04 x86"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: browser-vendor
|
||||||
|
attributes:
|
||||||
|
label: "🌐 Browser"
|
||||||
|
description: "Which browser are you running on?"
|
||||||
|
placeholder: "Ex. Google Chrome 95.0.4638.69"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: docker-version
|
||||||
|
attributes:
|
||||||
|
label: "🐋 Docker Version"
|
||||||
|
description: "If running with Docker, which version are you running?"
|
||||||
|
placeholder: "Ex. Docker 20.10.9 / K8S / Podman"
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: input
|
||||||
|
id: nodejs-version
|
||||||
|
attributes:
|
||||||
|
label: "🟩 NodeJS Version"
|
||||||
|
description: "If running with Node.js? which version are you running?"
|
||||||
|
placeholder: "Ex. 14.18.0"
|
||||||
|
validations:
|
||||||
|
required: false
|
@ -1,42 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: Create a report to help us improve
|
|
||||||
title: ''
|
|
||||||
labels: bug
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Is it a duplicate question?**
|
|
||||||
Please search in Issues without filters: https://github.com/louislam/uptime-kuma/issues?q=
|
|
||||||
|
|
||||||
**Describe the bug**
|
|
||||||
A clear and concise description of what the bug is.
|
|
||||||
|
|
||||||
**To Reproduce**
|
|
||||||
Steps to reproduce the behavior:
|
|
||||||
|
|
||||||
1. Go to '...'
|
|
||||||
2. Click on '....'
|
|
||||||
3. Scroll down to '....'
|
|
||||||
4. See error
|
|
||||||
|
|
||||||
**Expected behavior**
|
|
||||||
A clear and concise description of what you expected to happen.
|
|
||||||
|
|
||||||
**Info**
|
|
||||||
Uptime Kuma Version:
|
|
||||||
Using Docker?: Yes/No
|
|
||||||
Docker Version:
|
|
||||||
Node.js Version (Without Docker only):
|
|
||||||
OS:
|
|
||||||
Browser:
|
|
||||||
|
|
||||||
**Screenshots**
|
|
||||||
If applicable, add screenshots to help explain your problem.
|
|
||||||
|
|
||||||
**Error Log**
|
|
||||||
It is easier for us to find out the problem.
|
|
||||||
|
|
||||||
Docker: `docker logs <container id>`
|
|
||||||
PM2: `~/.pm2/logs/` (e.g. `/home/ubuntu/.pm2/logs`)
|
|
@ -0,0 +1,99 @@
|
|||||||
|
name: "🐛 Bug Report"
|
||||||
|
description: "Submit a bug report to help us improve"
|
||||||
|
#title: "[Bug] "
|
||||||
|
labels: [bug]
|
||||||
|
body:
|
||||||
|
- type: checkboxes
|
||||||
|
id: no-duplicate-issues
|
||||||
|
attributes:
|
||||||
|
label: "⚠️ Please verify that this bug has NOT been raised before."
|
||||||
|
description: "Search in the issues sections by clicking [HERE](https://github.com/louislam/uptime-kuma/issues?q=)"
|
||||||
|
options:
|
||||||
|
- label: "I checked and didn't find similar issue"
|
||||||
|
required: true
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: "🛡️ Security Policy"
|
||||||
|
description: Please review the security policy before reporting security related issues/bugs.
|
||||||
|
options:
|
||||||
|
- label: I agree to have read this project [Security Policy](https://github.com/louislam/uptime-kuma/security/policy)
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
attributes:
|
||||||
|
label: "Description"
|
||||||
|
description: "You could also upload screenshots"
|
||||||
|
- type: textarea
|
||||||
|
id: steps-to-reproduce
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: "👟 Reproduction steps"
|
||||||
|
description: "How do you trigger this bug? Please walk us through it step by step."
|
||||||
|
placeholder: "..."
|
||||||
|
- type: textarea
|
||||||
|
id: expected-behavior
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: "👀 Expected behavior"
|
||||||
|
description: "What did you think would happen?"
|
||||||
|
placeholder: "..."
|
||||||
|
- type: textarea
|
||||||
|
id: actual-behavior
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: "😓 Actual Behavior"
|
||||||
|
description: "What actually happen?"
|
||||||
|
placeholder: "..."
|
||||||
|
- type: input
|
||||||
|
id: uptime-kuma-version
|
||||||
|
attributes:
|
||||||
|
label: "🐻 Uptime-Kuma Version"
|
||||||
|
description: "Which version of Uptime-Kuma are you running? Please do NOT provide the docker tag such as latest or 1"
|
||||||
|
placeholder: "Ex. 1.10.0"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: operating-system
|
||||||
|
attributes:
|
||||||
|
label: "💻 Operating System and Arch"
|
||||||
|
description: "Which OS is your server/device running on?"
|
||||||
|
placeholder: "Ex. Ubuntu 20.04 x86"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: browser-vendor
|
||||||
|
attributes:
|
||||||
|
label: "🌐 Browser"
|
||||||
|
description: "Which browser are you running on?"
|
||||||
|
placeholder: "Ex. Google Chrome 95.0.4638.69"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: docker-version
|
||||||
|
attributes:
|
||||||
|
label: "🐋 Docker Version"
|
||||||
|
description: "If running with Docker, which version are you running?"
|
||||||
|
placeholder: "Ex. Docker 20.10.9 / K8S / Podman"
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: input
|
||||||
|
id: nodejs-version
|
||||||
|
attributes:
|
||||||
|
label: "🟩 NodeJS Version"
|
||||||
|
description: "If running with Node.js? which version are you running?"
|
||||||
|
placeholder: "Ex. 14.18.0"
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
id: logs
|
||||||
|
attributes:
|
||||||
|
label: "📝 Relevant log output"
|
||||||
|
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
||||||
|
render: shell
|
||||||
|
validations:
|
||||||
|
required: false
|
@ -1,22 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: Suggest an idea for this project
|
|
||||||
title: ''
|
|
||||||
labels: enhancement
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
**Is it a duplicate question?**
|
|
||||||
Please search in Issues without filters: https://github.com/louislam/uptime-kuma/issues?q=
|
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe.**
|
|
||||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
|
||||||
|
|
||||||
**Describe the solution you'd like**
|
|
||||||
A clear and concise description of what you want to happen.
|
|
||||||
|
|
||||||
**Describe alternatives you've considered**
|
|
||||||
A clear and concise description of any alternative solutions or features you've considered.
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context or screenshots about the feature request here.
|
|
@ -0,0 +1,59 @@
|
|||||||
|
name: 🚀 Feature Request
|
||||||
|
description: "Submit a proposal for a new feature"
|
||||||
|
#title: "[Feature] "
|
||||||
|
labels: [feature-request]
|
||||||
|
body:
|
||||||
|
- type: checkboxes
|
||||||
|
id: no-duplicate-issues
|
||||||
|
attributes:
|
||||||
|
label: "⚠️ Please verify that this feature request has NOT been suggested before."
|
||||||
|
description: "Search in the issues sections by clicking [HERE](https://github.com/louislam/uptime-kuma/issues?q=)"
|
||||||
|
options:
|
||||||
|
- label: "I checked and didn't find similar feature request"
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: feature-area
|
||||||
|
attributes:
|
||||||
|
label: "🏷️ Feature Request Type"
|
||||||
|
description: "What kind of feature request is this?"
|
||||||
|
multiple: true
|
||||||
|
options:
|
||||||
|
- API
|
||||||
|
- New Notification
|
||||||
|
- New Monitor
|
||||||
|
- UI Feature
|
||||||
|
- Other
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: feature-description
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: "🔖 Feature description"
|
||||||
|
description: "A clear and concise description of what the feature request is."
|
||||||
|
placeholder: "You should add ..."
|
||||||
|
- type: textarea
|
||||||
|
id: solution
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: "✔️ Solution"
|
||||||
|
description: "A clear and concise description of what you want to happen."
|
||||||
|
placeholder: "In my use-case, ..."
|
||||||
|
- type: textarea
|
||||||
|
id: alternatives
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
attributes:
|
||||||
|
label: "❓ Alternatives"
|
||||||
|
description: "A clear and concise description of any alternative solutions or features you've considered."
|
||||||
|
placeholder: "I have considered ..."
|
||||||
|
- type: textarea
|
||||||
|
id: additional-context
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
attributes:
|
||||||
|
label: "📝 Additional Context"
|
||||||
|
description: "Add any other context or screenshots about the feature request here."
|
||||||
|
placeholder: "..."
|
@ -0,0 +1,28 @@
|
|||||||
|
# Description
|
||||||
|
|
||||||
|
Fixes #(issue)
|
||||||
|
|
||||||
|
## Type of change
|
||||||
|
|
||||||
|
Please delete options that are not relevant.
|
||||||
|
|
||||||
|
- Bug fix (non-breaking change which fixes an issue)
|
||||||
|
- User Interface
|
||||||
|
- New feature (non-breaking change which adds functionality)
|
||||||
|
- Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||||
|
- Translation update
|
||||||
|
- Other
|
||||||
|
- This change requires a documentation update
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [ ] My code follows the style guidelines of this project
|
||||||
|
- [ ] I ran ESLint and other linters for modified files
|
||||||
|
- [ ] I have performed a self-review of my own code and test it
|
||||||
|
- [ ] I have commented my code, particularly in hard-to-understand areas
|
||||||
|
- [ ] My changes generate no new warnings
|
||||||
|
- [ ] My code needed automated testing. I have added them (this is optional task)
|
||||||
|
|
||||||
|
## Screenshots (if any)
|
||||||
|
|
||||||
|
Please do not use any external image service. Instead, just paste in or drag and drop the image here, and it will be uploaded automatically.
|
@ -0,0 +1,26 @@
|
|||||||
|
|
||||||
|
name: Close Incorrect Issue
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
close-incorrect-issue:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest]
|
||||||
|
node-version: [16.x]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
cache: 'npm'
|
||||||
|
- run: npm ci
|
||||||
|
- run: node extra/close-incorrect-issue.js ${{ secrets.GITHUB_TOKEN }} ${{ github.event.issue.number }} ${{ github.event.issue.user.login }}
|
@ -0,0 +1,22 @@
|
|||||||
|
name: 'Automatically close stale issues and PRs'
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * *'
|
||||||
|
#Run once a day at midnight
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
stale:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/stale@v4
|
||||||
|
with:
|
||||||
|
stale-issue-message: 'We are clearing up our old issues and your ticket has been open for 6 months with no activity. Remove stale label or comment or this will be closed in 7 days.'
|
||||||
|
stale-pr-message: 'We are clearing up our old Pull Requests and yours has been open for 6 months with no activity. Remove stale label or comment or this will be closed in 7 days.'
|
||||||
|
close-issue-message: 'This issue was closed because it has been stalled for 7 days with no activity.'
|
||||||
|
close-pr-message: 'This PR was closed because it has been stalled for 7 days with no activity.'
|
||||||
|
days-before-stale: 180
|
||||||
|
days-before-close: 7
|
||||||
|
exempt-issue-labels: 'News,Medium,High,discussion,bug,doc,'
|
||||||
|
exempt-pr-labels: 'awaiting-approval,work-in-progress,enhancement,'
|
||||||
|
exempt-issue-assignees: 'louislam'
|
||||||
|
exempt-pr-assignees: 'louislam'
|
@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
"rootDir": "..",
|
||||||
|
"testRegex": "./test/backend.spec.js",
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,33 @@
|
|||||||
|
const PuppeteerEnvironment = require("jest-environment-puppeteer");
|
||||||
|
const util = require("util");
|
||||||
|
|
||||||
|
class DebugEnv extends PuppeteerEnvironment {
|
||||||
|
async handleTestEvent(event, state) {
|
||||||
|
const ignoredEvents = [
|
||||||
|
"setup",
|
||||||
|
"add_hook",
|
||||||
|
"start_describe_definition",
|
||||||
|
"add_test",
|
||||||
|
"finish_describe_definition",
|
||||||
|
"run_start",
|
||||||
|
"run_describe_start",
|
||||||
|
"test_start",
|
||||||
|
"hook_start",
|
||||||
|
"hook_success",
|
||||||
|
"test_fn_start",
|
||||||
|
"test_fn_success",
|
||||||
|
"test_done",
|
||||||
|
"run_describe_finish",
|
||||||
|
"run_finish",
|
||||||
|
"teardown",
|
||||||
|
"test_fn_failure",
|
||||||
|
];
|
||||||
|
if (!ignoredEvents.includes(event.name)) {
|
||||||
|
console.log(
|
||||||
|
new Date().toString() + ` Unhandled event [${event.name}] ` + util.inspect(event)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = DebugEnv;
|
@ -1,5 +1,5 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
"rootDir": ".",
|
"rootDir": "..",
|
||||||
"testRegex": "./test/frontend.spec.js",
|
"testRegex": "./test/frontend.spec.js",
|
||||||
};
|
};
|
||||||
|
|
@ -0,0 +1,20 @@
|
|||||||
|
module.exports = {
|
||||||
|
"launch": {
|
||||||
|
"dumpio": true,
|
||||||
|
"slowMo": 500,
|
||||||
|
"headless": process.env.HEADLESS_TEST || false,
|
||||||
|
"userDataDir": "./data/test-chrome-profile",
|
||||||
|
args: [
|
||||||
|
"--disable-setuid-sandbox",
|
||||||
|
"--disable-gpu",
|
||||||
|
"--disable-dev-shm-usage",
|
||||||
|
"--no-default-browser-check",
|
||||||
|
"--no-experiments",
|
||||||
|
"--no-first-run",
|
||||||
|
"--no-pings",
|
||||||
|
"--no-sandbox",
|
||||||
|
"--no-zygote",
|
||||||
|
"--single-process",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,7 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE user
|
||||||
|
ADD twofa_last_token VARCHAR(6);
|
||||||
|
|
||||||
|
COMMIT;
|
@ -0,0 +1,13 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD method TEXT default 'GET' not null;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD body TEXT default null;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD headers TEXT default null;
|
||||||
|
|
||||||
|
COMMIT;
|
@ -0,0 +1,10 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD basic_auth_user TEXT default null;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD basic_auth_pass TEXT default null;
|
||||||
|
|
||||||
|
COMMIT;
|
@ -0,0 +1,18 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
CREATE TABLE [notification_sent_history] (
|
||||||
|
[id] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
[type] VARCHAR(50) NOT NULL,
|
||||||
|
[monitor_id] INTEGER NOT NULL,
|
||||||
|
[days] INTEGER NOT NULL,
|
||||||
|
UNIQUE([type], [monitor_id], [days])
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX [good_index] ON [notification_sent_history] (
|
||||||
|
[type],
|
||||||
|
[monitor_id],
|
||||||
|
[days]
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMIT;
|
@ -0,0 +1,57 @@
|
|||||||
|
const github = require("@actions/github");
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const token = process.argv[2];
|
||||||
|
const issueNumber = process.argv[3];
|
||||||
|
const username = process.argv[4];
|
||||||
|
|
||||||
|
const client = github.getOctokit(token).rest;
|
||||||
|
|
||||||
|
const issue = {
|
||||||
|
owner: "louislam",
|
||||||
|
repo: "uptime-kuma",
|
||||||
|
number: issueNumber,
|
||||||
|
};
|
||||||
|
|
||||||
|
const labels = (
|
||||||
|
await client.issues.listLabelsOnIssue({
|
||||||
|
owner: issue.owner,
|
||||||
|
repo: issue.repo,
|
||||||
|
issue_number: issue.number
|
||||||
|
})
|
||||||
|
).data.map(({ name }) => name);
|
||||||
|
|
||||||
|
if (labels.length === 0) {
|
||||||
|
console.log("Bad format here");
|
||||||
|
|
||||||
|
await client.issues.addLabels({
|
||||||
|
owner: issue.owner,
|
||||||
|
repo: issue.repo,
|
||||||
|
issue_number: issue.number,
|
||||||
|
labels: ["invalid-format"]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add the issue closing comment
|
||||||
|
await client.issues.createComment({
|
||||||
|
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 a blank issue`
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close the issue
|
||||||
|
await client.issues.update({
|
||||||
|
owner: issue.owner,
|
||||||
|
repo: issue.repo,
|
||||||
|
issue_number: issue.number,
|
||||||
|
state: "closed"
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log("Pass!");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
})();
|
@ -0,0 +1,60 @@
|
|||||||
|
console.log("== Uptime Kuma Remove 2FA Tool ==");
|
||||||
|
console.log("Loading the database");
|
||||||
|
|
||||||
|
const Database = require("../server/database");
|
||||||
|
const { R } = require("redbean-node");
|
||||||
|
const readline = require("readline");
|
||||||
|
const TwoFA = require("../server/2fa");
|
||||||
|
const args = require("args-parser")(process.argv);
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout
|
||||||
|
});
|
||||||
|
|
||||||
|
const main = async () => {
|
||||||
|
Database.init(args);
|
||||||
|
await Database.connect();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// No need to actually reset the password for testing, just make sure no connection problem. It is ok for now.
|
||||||
|
if (!process.env.TEST_BACKEND) {
|
||||||
|
const user = await R.findOne("user");
|
||||||
|
if (! user) {
|
||||||
|
throw new Error("user not found, have you installed?");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Found user: " + user.username);
|
||||||
|
|
||||||
|
let ans = await question("Are you sure want to remove 2FA? [y/N]");
|
||||||
|
|
||||||
|
if (ans.toLowerCase() === "y") {
|
||||||
|
await TwoFA.disable2FA(user.id);
|
||||||
|
console.log("2FA has been removed successfully.");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error: " + e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Database.close();
|
||||||
|
rl.close();
|
||||||
|
|
||||||
|
console.log("Finished.");
|
||||||
|
};
|
||||||
|
|
||||||
|
function question(question) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
rl.question(question, (answer) => {
|
||||||
|
resolve(answer);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!process.env.TEST_BACKEND) {
|
||||||
|
main();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
main,
|
||||||
|
};
|
@ -1,6 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
"launch": {
|
|
||||||
"headless": process.env.HEADLESS_TEST || false,
|
|
||||||
"userDataDir": "./data/test-chrome-profile",
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,32 +0,0 @@
|
|||||||
# Uptime-Kuma K8s Deployment
|
|
||||||
|
|
||||||
⚠ Warning: K8s deployment is provided by contributors. I have no experience with K8s and I can't fix error in the future. I only test Docker and Node.js. Use at your own risk.
|
|
||||||
|
|
||||||
## How does it work?
|
|
||||||
|
|
||||||
Kustomize is a tool which builds a complete deployment file for all config elements.
|
|
||||||
You can edit the files in the ```uptime-kuma``` folder except the ```kustomization.yml``` until you know what you're doing.
|
|
||||||
If you want to choose another namespace you can edit the ```kustomization.yml``` in the ```kubernetes```-Folder and change the ```namespace: uptime-kuma``` to something you like.
|
|
||||||
|
|
||||||
It creates a certificate with the specified Issuer and creates the Ingress for the Uptime-Kuma ClusterIP-Service.
|
|
||||||
|
|
||||||
## What do I have to edit?
|
|
||||||
|
|
||||||
You have to edit the ```ingressroute.yml``` to your needs.
|
|
||||||
This ingressroute.yml is for the [nginx-ingress-controller](https://kubernetes.github.io/ingress-nginx/) in combination with the [cert-manager](https://cert-manager.io/).
|
|
||||||
|
|
||||||
- Host
|
|
||||||
- Secrets and secret names
|
|
||||||
- (Cluster)Issuer (optional)
|
|
||||||
- The Version in the Deployment-File
|
|
||||||
- Update:
|
|
||||||
- Change to newer version and run the above commands, it will update the pods one after another
|
|
||||||
|
|
||||||
## How To use
|
|
||||||
|
|
||||||
- Install [kustomize](https://kubectl.docs.kubernetes.io/installation/kustomize/)
|
|
||||||
- Edit files mentioned above to your needs
|
|
||||||
- Run ```kustomize build > apply.yml```
|
|
||||||
- Run ```kubectl apply -f apply.yml```
|
|
||||||
|
|
||||||
Now you should see some k8s magic and Uptime-Kuma should be available at the specified address.
|
|
@ -1,10 +0,0 @@
|
|||||||
namespace: uptime-kuma
|
|
||||||
namePrefix: uptime-kuma-
|
|
||||||
|
|
||||||
commonLabels:
|
|
||||||
app: uptime-kuma
|
|
||||||
|
|
||||||
bases:
|
|
||||||
- uptime-kuma
|
|
||||||
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
|||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
component: uptime-kuma
|
|
||||||
name: deployment
|
|
||||||
spec:
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
component: uptime-kuma
|
|
||||||
replicas: 1
|
|
||||||
strategy:
|
|
||||||
type: Recreate
|
|
||||||
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
component: uptime-kuma
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: app
|
|
||||||
image: louislam/uptime-kuma:1
|
|
||||||
ports:
|
|
||||||
- containerPort: 3001
|
|
||||||
volumeMounts:
|
|
||||||
- mountPath: /app/data
|
|
||||||
name: storage
|
|
||||||
livenessProbe:
|
|
||||||
exec:
|
|
||||||
command:
|
|
||||||
- node
|
|
||||||
- extra/healthcheck.js
|
|
||||||
initialDelaySeconds: 180
|
|
||||||
periodSeconds: 60
|
|
||||||
timeoutSeconds: 30
|
|
||||||
readinessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /
|
|
||||||
port: 3001
|
|
||||||
scheme: HTTP
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
- name: storage
|
|
||||||
persistentVolumeClaim:
|
|
||||||
claimName: pvc
|
|
@ -1,39 +0,0 @@
|
|||||||
apiVersion: networking.k8s.io/v1
|
|
||||||
kind: Ingress
|
|
||||||
metadata:
|
|
||||||
annotations:
|
|
||||||
kubernetes.io/ingress.class: nginx
|
|
||||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
|
||||||
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
|
|
||||||
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
|
|
||||||
nginx.ingress.kubernetes.io/server-snippets: |
|
|
||||||
location / {
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header X-Forwarded-Host $http_host;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_set_header X-Forwarded-For $remote_addr;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_cache_bypass $http_upgrade;
|
|
||||||
}
|
|
||||||
name: ingress
|
|
||||||
spec:
|
|
||||||
tls:
|
|
||||||
- hosts:
|
|
||||||
- example.com
|
|
||||||
secretName: example-com-tls
|
|
||||||
rules:
|
|
||||||
- host: example.com
|
|
||||||
http:
|
|
||||||
paths:
|
|
||||||
- path: /
|
|
||||||
pathType: Prefix
|
|
||||||
backend:
|
|
||||||
service:
|
|
||||||
name: service
|
|
||||||
port:
|
|
||||||
number: 3001
|
|
@ -1,5 +0,0 @@
|
|||||||
resources:
|
|
||||||
- deployment.yml
|
|
||||||
- service.yml
|
|
||||||
- ingressroute.yml
|
|
||||||
- pvc.yml
|
|
@ -1,10 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: PersistentVolumeClaim
|
|
||||||
metadata:
|
|
||||||
name: pvc
|
|
||||||
spec:
|
|
||||||
accessModes:
|
|
||||||
- ReadWriteOnce
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: 4Gi
|
|
@ -1,13 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: service
|
|
||||||
spec:
|
|
||||||
selector:
|
|
||||||
component: uptime-kuma
|
|
||||||
type: ClusterIP
|
|
||||||
ports:
|
|
||||||
- name: http
|
|
||||||
port: 3001
|
|
||||||
targetPort: 3001
|
|
||||||
protocol: TCP
|
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,14 @@
|
|||||||
|
const { checkLogin } = require("./util-server");
|
||||||
|
const { R } = require("redbean-node");
|
||||||
|
|
||||||
|
class TwoFA {
|
||||||
|
|
||||||
|
static async disable2FA(userID) {
|
||||||
|
return await R.exec("UPDATE `user` SET twofa_status = 0 WHERE id = ? ", [
|
||||||
|
userID,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = TwoFA;
|
@ -0,0 +1,7 @@
|
|||||||
|
const args = require("args-parser")(process.argv);
|
||||||
|
const demoMode = args["demo"] || false;
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
args,
|
||||||
|
demoMode
|
||||||
|
};
|
@ -0,0 +1,31 @@
|
|||||||
|
const path = require("path");
|
||||||
|
const Bree = require("bree");
|
||||||
|
const { SHARE_ENV } = require("worker_threads");
|
||||||
|
|
||||||
|
const jobs = [
|
||||||
|
{
|
||||||
|
name: "clear-old-data",
|
||||||
|
interval: "at 03:14",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const initBackgroundJobs = function (args) {
|
||||||
|
const bree = new Bree({
|
||||||
|
root: path.resolve("server", "jobs"),
|
||||||
|
jobs,
|
||||||
|
worker: {
|
||||||
|
env: SHARE_ENV,
|
||||||
|
workerData: args,
|
||||||
|
},
|
||||||
|
workerMessageHandler: (message) => {
|
||||||
|
console.log("[Background Job]:", message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
bree.start();
|
||||||
|
return bree;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
initBackgroundJobs
|
||||||
|
};
|
@ -0,0 +1,40 @@
|
|||||||
|
const { log, exit, connectDb } = require("./util-worker");
|
||||||
|
const { R } = require("redbean-node");
|
||||||
|
const { setSetting, setting } = require("../util-server");
|
||||||
|
|
||||||
|
const DEFAULT_KEEP_PERIOD = 180;
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
await connectDb();
|
||||||
|
|
||||||
|
let period = await setting("keepDataPeriodDays");
|
||||||
|
|
||||||
|
// Set Default Period
|
||||||
|
if (period == null) {
|
||||||
|
await setSetting("keepDataPeriodDays", DEFAULT_KEEP_PERIOD, "general");
|
||||||
|
period = DEFAULT_KEEP_PERIOD;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try parse setting
|
||||||
|
let parsedPeriod;
|
||||||
|
try {
|
||||||
|
parsedPeriod = parseInt(period);
|
||||||
|
} catch (_) {
|
||||||
|
log("Failed to parse setting, resetting to default..");
|
||||||
|
await setSetting("keepDataPeriodDays", DEFAULT_KEEP_PERIOD, "general");
|
||||||
|
parsedPeriod = DEFAULT_KEEP_PERIOD;
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`Clearing Data older than ${parsedPeriod} days...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await R.exec(
|
||||||
|
"DELETE FROM heartbeat WHERE time < DATETIME('now', '-' || ? || ' days') ",
|
||||||
|
[parsedPeriod]
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
log(`Failed to clear old data: ${e.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
exit();
|
||||||
|
})();
|
@ -0,0 +1,39 @@
|
|||||||
|
const { parentPort, workerData } = require("worker_threads");
|
||||||
|
const Database = require("../database");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const log = function (any) {
|
||||||
|
if (parentPort) {
|
||||||
|
parentPort.postMessage(any);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const exit = function (error) {
|
||||||
|
if (error && error != 0) {
|
||||||
|
process.exit(error);
|
||||||
|
} else {
|
||||||
|
if (parentPort) {
|
||||||
|
parentPort.postMessage("done");
|
||||||
|
} else {
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const connectDb = async function () {
|
||||||
|
const dbPath = path.join(
|
||||||
|
process.env.DATA_DIR || workerData["data-dir"] || "./data/"
|
||||||
|
);
|
||||||
|
|
||||||
|
Database.init({
|
||||||
|
"data-dir": dbPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
await Database.connect();
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
log,
|
||||||
|
exit,
|
||||||
|
connectDb,
|
||||||
|
};
|
@ -0,0 +1,108 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const { DOWN, UP } = require("../../src/util");
|
||||||
|
const { default: axios } = require("axios");
|
||||||
|
const Crypto = require("crypto");
|
||||||
|
const qs = require("qs");
|
||||||
|
|
||||||
|
class AliyunSMS extends NotificationProvider {
|
||||||
|
name = "AliyunSMS";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully.";
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (heartbeatJSON != null) {
|
||||||
|
let msgBody = JSON.stringify({
|
||||||
|
name: monitorJSON["name"],
|
||||||
|
time: heartbeatJSON["time"],
|
||||||
|
status: this.statusToString(heartbeatJSON["status"]),
|
||||||
|
msg: heartbeatJSON["msg"],
|
||||||
|
});
|
||||||
|
if (this.sendSms(notification, msgBody)) {
|
||||||
|
return okMsg;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let msgBody = JSON.stringify({
|
||||||
|
name: "",
|
||||||
|
time: "",
|
||||||
|
status: "",
|
||||||
|
msg: msg,
|
||||||
|
});
|
||||||
|
if (this.sendSms(notification, msgBody)) {
|
||||||
|
return okMsg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendSms(notification, msgbody) {
|
||||||
|
let params = {
|
||||||
|
PhoneNumbers: notification.phonenumber,
|
||||||
|
TemplateCode: notification.templateCode,
|
||||||
|
SignName: notification.signName,
|
||||||
|
TemplateParam: msgbody,
|
||||||
|
AccessKeyId: notification.accessKeyId,
|
||||||
|
Format: "JSON",
|
||||||
|
SignatureMethod: "HMAC-SHA1",
|
||||||
|
SignatureVersion: "1.0",
|
||||||
|
SignatureNonce: Math.random().toString(),
|
||||||
|
Timestamp: new Date().toISOString(),
|
||||||
|
Action: "SendSms",
|
||||||
|
Version: "2017-05-25",
|
||||||
|
};
|
||||||
|
|
||||||
|
params.Signature = this.sign(params, notification.secretAccessKey);
|
||||||
|
let config = {
|
||||||
|
method: "POST",
|
||||||
|
url: "http://dysmsapi.aliyuncs.com/",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
data: qs.stringify(params),
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = await axios(config);
|
||||||
|
if (result.data.Message == "OK") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Aliyun request sign */
|
||||||
|
sign(param, AccessKeySecret) {
|
||||||
|
let param2 = {};
|
||||||
|
let data = [];
|
||||||
|
|
||||||
|
let oa = Object.keys(param).sort();
|
||||||
|
|
||||||
|
for (let i = 0; i < oa.length; i++) {
|
||||||
|
let key = oa[i];
|
||||||
|
param2[key] = param[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let key in param2) {
|
||||||
|
data.push(`${encodeURIComponent(key)}=${encodeURIComponent(param2[key])}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let StringToSign = `POST&${encodeURIComponent("/")}&${encodeURIComponent(data.join("&"))}`;
|
||||||
|
return Crypto
|
||||||
|
.createHmac("sha1", `${AccessKeySecret}&`)
|
||||||
|
.update(Buffer.from(StringToSign))
|
||||||
|
.digest("base64");
|
||||||
|
}
|
||||||
|
|
||||||
|
statusToString(status) {
|
||||||
|
switch (status) {
|
||||||
|
case DOWN:
|
||||||
|
return "DOWN";
|
||||||
|
case UP:
|
||||||
|
return "UP";
|
||||||
|
default:
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = AliyunSMS;
|
@ -0,0 +1,89 @@
|
|||||||
|
//
|
||||||
|
// bark.js
|
||||||
|
// UptimeKuma
|
||||||
|
//
|
||||||
|
// Created by Lakr Aream on 2021/10/24.
|
||||||
|
// Copyright © 2021 Lakr Aream. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const { DOWN, UP } = require("../../src/util");
|
||||||
|
const { default: axios } = require("axios");
|
||||||
|
|
||||||
|
// bark is an APN bridge that sends notifications to Apple devices.
|
||||||
|
|
||||||
|
const barkNotificationGroup = "UptimeKuma";
|
||||||
|
const barkNotificationAvatar = "https://github.com/louislam/uptime-kuma/raw/master/public/icon.png";
|
||||||
|
const barkNotificationSound = "telegraph";
|
||||||
|
const successMessage = "Successes!";
|
||||||
|
|
||||||
|
class Bark extends NotificationProvider {
|
||||||
|
name = "Bark";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
try {
|
||||||
|
var barkEndpoint = notification.barkEndpoint;
|
||||||
|
|
||||||
|
// check if the endpoint has a "/" suffix, if so, delete it first
|
||||||
|
if (barkEndpoint.endsWith("/")) {
|
||||||
|
barkEndpoint = barkEndpoint.substring(0, barkEndpoint.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] == UP) {
|
||||||
|
let title = "UptimeKuma Monitor Up";
|
||||||
|
return await this.postNotification(title, msg, barkEndpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] == DOWN) {
|
||||||
|
let title = "UptimeKuma Monitor Down";
|
||||||
|
return await this.postNotification(title, msg, barkEndpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg != null) {
|
||||||
|
let title = "UptimeKuma Message";
|
||||||
|
return await this.postNotification(title, msg, barkEndpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add additional parameter for better on device styles (iOS 15 optimized)
|
||||||
|
appendAdditionalParameters(postUrl) {
|
||||||
|
// grouping all our notifications
|
||||||
|
postUrl += "?group=" + barkNotificationGroup;
|
||||||
|
// set icon to uptime kuma icon, 11kb should be fine
|
||||||
|
postUrl += "&icon=" + barkNotificationAvatar;
|
||||||
|
// picked a sound, this should follow system's mute status when arrival
|
||||||
|
postUrl += "&sound=" + barkNotificationSound;
|
||||||
|
return postUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// thrown if failed to check result, result code should be in range 2xx
|
||||||
|
checkResult(result) {
|
||||||
|
if (result.status == null) {
|
||||||
|
throw new Error("Bark notification failed with invalid response!");
|
||||||
|
}
|
||||||
|
if (result.status < 200 || result.status >= 300) {
|
||||||
|
throw new Error("Bark notification failed with status code " + result.status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async postNotification(title, subtitle, endpoint) {
|
||||||
|
// url encode title and subtitle
|
||||||
|
title = encodeURIComponent(title);
|
||||||
|
subtitle = encodeURIComponent(subtitle);
|
||||||
|
let postUrl = endpoint + "/" + title + "/" + subtitle;
|
||||||
|
postUrl = this.appendAdditionalParameters(postUrl);
|
||||||
|
let result = await axios.get(postUrl);
|
||||||
|
this.checkResult(result);
|
||||||
|
if (result.statusText != null) {
|
||||||
|
return "Bark notification succeed: " + result.statusText;
|
||||||
|
}
|
||||||
|
// because returned in range 200 ..< 300
|
||||||
|
return successMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Bark;
|
@ -0,0 +1,42 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
|
||||||
|
class ClickSendSMS extends NotificationProvider {
|
||||||
|
|
||||||
|
name = "clicksendsms";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully.";
|
||||||
|
try {
|
||||||
|
console.log({ notification });
|
||||||
|
let config = {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": "Basic " + Buffer.from(notification.clicksendsmsLogin + ":" + notification.clicksendsmsPassword).toString('base64'),
|
||||||
|
"Accept": "text/json",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let data = {
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
"body": msg.replace(/[^\x00-\x7F]/g, ""),
|
||||||
|
"to": notification.clicksendsmsToNumber,
|
||||||
|
"source": "uptime-kuma",
|
||||||
|
"from": notification.clicksendsmsSenderName,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
let resp = await axios.post("https://rest.clicksend.com/v3/sms/send", data, config);
|
||||||
|
if (resp.data.data.messages[0].status !== "SUCCESS") {
|
||||||
|
let error = "Something gone wrong. Api returned " + resp.data.data.messages[0].status + ".";
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return okMsg;
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ClickSendSMS;
|
@ -0,0 +1,79 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const { DOWN, UP } = require("../../src/util");
|
||||||
|
const { default: axios } = require("axios");
|
||||||
|
const Crypto = require("crypto");
|
||||||
|
|
||||||
|
class DingDing extends NotificationProvider {
|
||||||
|
name = "DingDing";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully.";
|
||||||
|
|
||||||
|
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(UTC):${heartbeatJSON["time"]}`,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (this.sendToDingDing(notification, params)) {
|
||||||
|
return okMsg;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let params = {
|
||||||
|
msgtype: "text",
|
||||||
|
text: {
|
||||||
|
content: msg
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (this.sendToDingDing(notification, params)) {
|
||||||
|
return okMsg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendToDingDing(notification, params) {
|
||||||
|
let timestamp = Date.now();
|
||||||
|
|
||||||
|
let config = {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
url: `${notification.webHookUrl}×tamp=${timestamp}&sign=${encodeURIComponent(this.sign(timestamp, notification.secretKey))}`,
|
||||||
|
data: JSON.stringify(params),
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = await axios(config);
|
||||||
|
if (result.data.errmsg == "ok") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** DingDing sign */
|
||||||
|
sign(timestamp, secretKey) {
|
||||||
|
return Crypto
|
||||||
|
.createHmac("sha256", Buffer.from(secretKey, "utf8"))
|
||||||
|
.update(Buffer.from(`${timestamp}\n${secretKey}`, "utf8"))
|
||||||
|
.digest("base64");
|
||||||
|
}
|
||||||
|
|
||||||
|
statusToString(status) {
|
||||||
|
switch (status) {
|
||||||
|
case DOWN:
|
||||||
|
return "DOWN";
|
||||||
|
case UP:
|
||||||
|
return "UP";
|
||||||
|
default:
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = DingDing;
|
@ -0,0 +1,44 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
|
||||||
|
class SerwerSMS extends NotificationProvider {
|
||||||
|
|
||||||
|
name = "serwersms";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully.";
|
||||||
|
|
||||||
|
try {
|
||||||
|
let config = {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let data = {
|
||||||
|
"username": notification.serwersmsUsername,
|
||||||
|
"password": notification.serwersmsPassword,
|
||||||
|
"phone": notification.serwersmsPhoneNumber,
|
||||||
|
"text": msg.replace(/[^\x00-\x7F]/g, ""),
|
||||||
|
"sender": notification.serwersmsSenderName,
|
||||||
|
};
|
||||||
|
|
||||||
|
let resp = await axios.post("https://api2.serwersms.pl/messages/send_sms", data, config);
|
||||||
|
|
||||||
|
if (!resp.data.success) {
|
||||||
|
if (resp.data.error) {
|
||||||
|
let error = `SerwerSMS.pl API returned error code ${resp.data.error.code} (${resp.data.error.type}) with error message: ${resp.data.error.message}`;
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
} else {
|
||||||
|
let error = "SerwerSMS.pl API returned an unexpected response";
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return okMsg;
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = SerwerSMS;
|
@ -0,0 +1,41 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
const { setting } = require("../util-server");
|
||||||
|
const { getMonitorRelativeURL } = require("../../src/util");
|
||||||
|
|
||||||
|
class Stackfield extends NotificationProvider {
|
||||||
|
|
||||||
|
name = "stackfield";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully.";
|
||||||
|
try {
|
||||||
|
// Stackfield message formatting: https://www.stackfield.com/help/formatting-messages-2001
|
||||||
|
|
||||||
|
let textMsg = "+Uptime Kuma Alert+";
|
||||||
|
|
||||||
|
if (monitorJSON && monitorJSON.name) {
|
||||||
|
textMsg += `\n*${monitorJSON.name}*`;
|
||||||
|
}
|
||||||
|
|
||||||
|
textMsg += `\n${msg}`;
|
||||||
|
|
||||||
|
const baseURL = await setting("primaryBaseURL");
|
||||||
|
if (baseURL) {
|
||||||
|
textMsg += `\n${baseURL + getMonitorRelativeURL(monitorJSON.id)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
"Title": textMsg,
|
||||||
|
};
|
||||||
|
|
||||||
|
await axios.post(notification.stackfieldwebhookURL, data);
|
||||||
|
return okMsg;
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Stackfield;
|
@ -0,0 +1,39 @@
|
|||||||
|
const { RateLimiter } = require("limiter");
|
||||||
|
const { debug } = require("../src/util");
|
||||||
|
|
||||||
|
class KumaRateLimiter {
|
||||||
|
constructor(config) {
|
||||||
|
this.errorMessage = config.errorMessage;
|
||||||
|
this.rateLimiter = new RateLimiter(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
async pass(callback, num = 1) {
|
||||||
|
const remainingRequests = await this.removeTokens(num);
|
||||||
|
debug("Rate Limit (remainingRequests):" + remainingRequests);
|
||||||
|
if (remainingRequests < 0) {
|
||||||
|
if (callback) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: this.errorMessage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeTokens(num = 1) {
|
||||||
|
return await this.rateLimiter.removeTokens(num);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loginRateLimiter = new KumaRateLimiter({
|
||||||
|
tokensPerInterval: 20,
|
||||||
|
interval: "minute",
|
||||||
|
fireImmediately: true,
|
||||||
|
errorMessage: "Too frequently, try again later."
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
loginRateLimiter
|
||||||
|
};
|
@ -0,0 +1,37 @@
|
|||||||
|
const { checkLogin } = require("../util-server");
|
||||||
|
const Database = require("../database");
|
||||||
|
|
||||||
|
module.exports = (socket) => {
|
||||||
|
|
||||||
|
// Post or edit incident
|
||||||
|
socket.on("getDatabaseSize", async (callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
size: Database.getSize(),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("shrinkDatabase", async (callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
Database.shrink();
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
@ -0,0 +1,25 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="accessKeyId" class="form-label">{{ $t("AccessKeyId") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||||
|
<input id="accessKeyId" v-model="$parent.notification.accessKeyId" type="text" class="form-control" required>
|
||||||
|
|
||||||
|
<label for="secretAccessKey" class="form-label">{{ $t("SecretAccessKey") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||||
|
<input id="secretAccessKey" v-model="$parent.notification.secretAccessKey" type="text" class="form-control" required>
|
||||||
|
|
||||||
|
<label for="phonenumber" class="form-label">{{ $t("Phonenumber") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||||
|
<input id="phonenumber" v-model="$parent.notification.phonenumber" type="text" class="form-control" required>
|
||||||
|
|
||||||
|
<label for="templateCode" class="form-label">{{ $t("TemplateCode") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||||
|
<input id="templateCode" v-model="$parent.notification.templateCode" type="text" class="form-control" required>
|
||||||
|
|
||||||
|
<label for="signName" class="form-label">{{ $t("SignName") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||||
|
<input id="signName" v-model="$parent.notification.signName" type="text" class="form-control" required>
|
||||||
|
|
||||||
|
<div class="form-text">
|
||||||
|
<p>Sms template must contain parameters: <br> <code>${name} ${time} ${status} ${msg}</code></p>
|
||||||
|
<i18n-t tag="p" keypath="Read more:">
|
||||||
|
<a href="https://help.aliyun.com/document_detail/101414.html" target="_blank">https://help.aliyun.com/document_detail/101414.html</a>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
@ -0,0 +1,15 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="Bark Endpoint" class="form-label">{{ $t("Bark Endpoint") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||||
|
<input id="Bark Endpoint" v-model="$parent.notification.barkEndpoint" type="text" class="form-control" required>
|
||||||
|
<div class="form-text">
|
||||||
|
<p><span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}</p>
|
||||||
|
</div>
|
||||||
|
<i18n-t tag="div" keypath="wayToGetTeamsURL" class="form-text">
|
||||||
|
<a
|
||||||
|
href="https://github.com/Finb/Bark"
|
||||||
|
target="_blank"
|
||||||
|
>{{ $t("here") }}</a>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
</template>
|
@ -0,0 +1,38 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="clicksendsms-login" class="form-label">API Username</label>
|
||||||
|
<div class="form-text">
|
||||||
|
{{ $t("apiCredentials") }}
|
||||||
|
<a href="http://dashboard.clicksend.com/account/subaccounts" target="_blank">{{ $t("here") }}</a>
|
||||||
|
</div>
|
||||||
|
<input id="clicksendsms-login" v-model="$parent.notification.clicksendsmsLogin" type="text" class="form-control" required>
|
||||||
|
<label for="clicksendsms-key" class="form-label">API Key</label>
|
||||||
|
<HiddenInput id="clicksendsms-key" v-model="$parent.notification.clicksendsmsPassword" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="form-text">
|
||||||
|
{{ $t("checkPrice", [$t("clicksendsms")]) }}
|
||||||
|
<a href="https://www.clicksend.com/us/pricing" target="_blank">https://clicksend.com/us/pricing</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="clicksendsms-to-number" class="form-label">Recipient Number</label>
|
||||||
|
<input id="clicksendsms-to-number" v-model="$parent.notification.clicksendsmsToNumber" type="text" minlength="8" maxlength="14" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="clicksendsms-sender-name" class="form-label">From Name/Number -
|
||||||
|
<a href="https://help.clicksend.com/article/4kgj7krx00-what-is-a-sender-id-or-sender-number" target="_blank">More Info</a>
|
||||||
|
</label>
|
||||||
|
<input id="clicksendsms-sender-name" v-model="$parent.notification.clicksendsmsSenderName" type="text" minlength="3" maxlength="11" class="form-control">
|
||||||
|
<div class="form-text">Leave blank to use a shared sender number.</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import HiddenInput from "../HiddenInput.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
HiddenInput,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="WebHookUrl" class="form-label">{{ $t("WebHookUrl") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||||
|
<input id="WebHookUrl" v-model="$parent.notification.webHookUrl" type="text" class="form-control" required>
|
||||||
|
|
||||||
|
<label for="secretKey" class="form-label">{{ $t("SecretKey") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||||
|
<input id="secretKey" v-model="$parent.notification.secretKey" type="text" class="form-control" required>
|
||||||
|
|
||||||
|
<div class="form-text">
|
||||||
|
<p>For safety, must use secret key</p>
|
||||||
|
<i18n-t tag="p" keypath="Read more:">
|
||||||
|
<a href="https://developers.dingtalk.com/document/robots/custom-robot-access" target="_blank">https://developers.dingtalk.com/document/robots/custom-robot-access</a>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
@ -0,0 +1,28 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="serwersms-username" class="form-label">{{ $t('serwersmsAPIUser') }}</label>
|
||||||
|
<input id="serwersms-username" v-model="$parent.notification.serwersmsUsername" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="serwersms-key" class="form-label">{{ $t('serwersmsAPIPassword') }}</label>
|
||||||
|
<HiddenInput id="serwersms-key" v-model="$parent.notification.serwersmsPassword" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="serwersms-phone-number" class="form-label">{{ $t("serwersmsPhoneNumber") }}</label>
|
||||||
|
<input id="serwersms-phone-number" v-model="$parent.notification.serwersmsPhoneNumber" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="serwersms-sender-name" class="form-label">{{ $t("serwersmsSenderName") }}</label>
|
||||||
|
<input id="serwersms-sender-name" v-model="$parent.notification.serwersmsSenderName" type="text" minlength="3" maxlength="11" class="form-control">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HiddenInput from "../HiddenInput.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
HiddenInput,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="stackfield-webhook-url" class="form-label">{{ $t("Webhook URL") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||||
|
<input id="stackfield-webhook-url" v-model="$parent.notification.stackfieldwebhookURL" type="text" class="form-control" required>
|
||||||
|
|
||||||
|
<div class="form-text">
|
||||||
|
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
|
||||||
|
<i18n-t tag="p" keypath="aboutWebhooks" style="margin-top: 8px;">
|
||||||
|
<a href="https://www.stackfield.com/developer-api#AnchorAPI2" target="_blank">https://www.stackfield.com/developer-api#AnchorAPI2</a>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
@ -0,0 +1,25 @@
|
|||||||
|
<template>
|
||||||
|
<div class="d-flex justify-content-center align-items-center">
|
||||||
|
<div class="logo d-flex flex-column justify-content-center align-items-center">
|
||||||
|
<object class="my-4" width="200" height="200" data="/icon.svg" />
|
||||||
|
<div class="fs-4 fw-bold">Uptime Kuma</div>
|
||||||
|
<div>{{ $t("Version") }}: {{ $root.info.version }}</div>
|
||||||
|
<div class="my-1 update-link"><a href="https://github.com/louislam/uptime-kuma/releases" target="_blank" rel="noopener">{{ $t("Check Update On GitHub") }}</a></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.logo {
|
||||||
|
margin: 4em 1em;
|
||||||
|
}
|
||||||
|
.update-link {
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,143 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="my-4">
|
||||||
|
<label for="language" class="form-label">
|
||||||
|
{{ $t("Language") }}
|
||||||
|
</label>
|
||||||
|
<select id="language" v-model="$root.language" class="form-select">
|
||||||
|
<option
|
||||||
|
v-for="(lang, i) in $i18n.availableLocales"
|
||||||
|
:key="`Lang${i}`"
|
||||||
|
:value="lang"
|
||||||
|
>
|
||||||
|
{{ $i18n.messages[lang].languageName }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="my-4">
|
||||||
|
<label for="timezone" class="form-label">{{ $t("Theme") }}</label>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="btn-group"
|
||||||
|
role="group"
|
||||||
|
aria-label="Basic checkbox toggle button group"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="btncheck1"
|
||||||
|
v-model="$root.userTheme"
|
||||||
|
type="radio"
|
||||||
|
class="btn-check"
|
||||||
|
name="theme"
|
||||||
|
autocomplete="off"
|
||||||
|
value="light"
|
||||||
|
/>
|
||||||
|
<label class="btn btn-outline-primary" for="btncheck1">
|
||||||
|
{{ $t("Light") }}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
id="btncheck2"
|
||||||
|
v-model="$root.userTheme"
|
||||||
|
type="radio"
|
||||||
|
class="btn-check"
|
||||||
|
name="theme"
|
||||||
|
autocomplete="off"
|
||||||
|
value="dark"
|
||||||
|
/>
|
||||||
|
<label class="btn btn-outline-primary" for="btncheck2">
|
||||||
|
{{ $t("Dark") }}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
id="btncheck3"
|
||||||
|
v-model="$root.userTheme"
|
||||||
|
type="radio"
|
||||||
|
class="btn-check"
|
||||||
|
name="theme"
|
||||||
|
autocomplete="off"
|
||||||
|
value="auto"
|
||||||
|
/>
|
||||||
|
<label class="btn btn-outline-primary" for="btncheck3">
|
||||||
|
{{ $t("Auto") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="my-4">
|
||||||
|
<label class="form-label">{{ $t("Theme - Heartbeat Bar") }}</label>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="btn-group"
|
||||||
|
role="group"
|
||||||
|
aria-label="Basic checkbox toggle button group"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="btncheck4"
|
||||||
|
v-model="$root.userHeartbeatBar"
|
||||||
|
type="radio"
|
||||||
|
class="btn-check"
|
||||||
|
name="heartbeatBarTheme"
|
||||||
|
autocomplete="off"
|
||||||
|
value="normal"
|
||||||
|
/>
|
||||||
|
<label class="btn btn-outline-primary" for="btncheck4">
|
||||||
|
{{ $t("Normal") }}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
id="btncheck5"
|
||||||
|
v-model="$root.userHeartbeatBar"
|
||||||
|
type="radio"
|
||||||
|
class="btn-check"
|
||||||
|
name="heartbeatBarTheme"
|
||||||
|
autocomplete="off"
|
||||||
|
value="bottom"
|
||||||
|
/>
|
||||||
|
<label class="btn btn-outline-primary" for="btncheck5">
|
||||||
|
{{ $t("Bottom") }}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
id="btncheck6"
|
||||||
|
v-model="$root.userHeartbeatBar"
|
||||||
|
type="radio"
|
||||||
|
class="btn-check"
|
||||||
|
name="heartbeatBarTheme"
|
||||||
|
autocomplete="off"
|
||||||
|
value="none"
|
||||||
|
/>
|
||||||
|
<label class="btn btn-outline-primary" for="btncheck6">
|
||||||
|
{{ $t("None") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "../../assets/vars.scss";
|
||||||
|
|
||||||
|
.btn-check:active + .btn-outline-primary,
|
||||||
|
.btn-check:checked + .btn-outline-primary,
|
||||||
|
.btn-check:hover + .btn-outline-primary {
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
.list-group-item {
|
||||||
|
background-color: $dark-bg2;
|
||||||
|
color: $dark-font-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,213 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="my-4">
|
||||||
|
<h4 class="mt-4 mb-2">{{ $t("Export Backup") }}</h4>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{{ $t("backupDescription") }} <br />
|
||||||
|
({{ $t("backupDescription2") }}) <br />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="mb-2">
|
||||||
|
<button class="btn btn-primary" @click="downloadBackup">
|
||||||
|
{{ $t("Export") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>{{ $t("backupDescription3") }}</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-4">
|
||||||
|
<h4 class="mt-4 mb-2">{{ $t("Import Backup") }}</h4>
|
||||||
|
|
||||||
|
<label class="form-label">{{ $t("Options") }}:</label>
|
||||||
|
<br />
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input
|
||||||
|
id="radioKeep"
|
||||||
|
v-model="importHandle"
|
||||||
|
class="form-check-input"
|
||||||
|
type="radio"
|
||||||
|
name="radioImportHandle"
|
||||||
|
value="keep"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="radioKeep">
|
||||||
|
{{ $t("Keep both") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input
|
||||||
|
id="radioSkip"
|
||||||
|
v-model="importHandle"
|
||||||
|
class="form-check-input"
|
||||||
|
type="radio"
|
||||||
|
name="radioImportHandle"
|
||||||
|
value="skip"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="radioSkip">
|
||||||
|
{{ $t("Skip existing") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input
|
||||||
|
id="radioOverwrite"
|
||||||
|
v-model="importHandle"
|
||||||
|
class="form-check-input"
|
||||||
|
type="radio"
|
||||||
|
name="radioImportHandle"
|
||||||
|
value="overwrite"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="radioOverwrite">
|
||||||
|
{{ $t("Overwrite") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-text mb-2">
|
||||||
|
{{ $t("importHandleDescription") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-2">
|
||||||
|
<input
|
||||||
|
id="importBackup"
|
||||||
|
type="file"
|
||||||
|
class="form-control"
|
||||||
|
accept="application/json"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group mb-2 justify-content-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-primary"
|
||||||
|
:disabled="processing"
|
||||||
|
@click="confirmImport"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="processing"
|
||||||
|
class="spinner-border spinner-border-sm me-1"
|
||||||
|
></div>
|
||||||
|
{{ $t("Import") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="importAlert"
|
||||||
|
class="alert alert-danger mt-3"
|
||||||
|
style="padding: 6px 16px"
|
||||||
|
>
|
||||||
|
{{ importAlert }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Confirm
|
||||||
|
ref="confirmImport"
|
||||||
|
btn-style="btn-danger"
|
||||||
|
:yes-text="$t('Yes')"
|
||||||
|
:no-text="$t('No')"
|
||||||
|
@yes="importBackup"
|
||||||
|
>
|
||||||
|
{{ $t("confirmImportMsg") }}
|
||||||
|
</Confirm>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Confirm from "../../components/Confirm.vue";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { useToast } from "vue-toastification";
|
||||||
|
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Confirm,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
processing: false,
|
||||||
|
importHandle: "skip",
|
||||||
|
importAlert: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
confirmImport() {
|
||||||
|
this.$refs.confirmImport.show();
|
||||||
|
},
|
||||||
|
|
||||||
|
downloadBackup() {
|
||||||
|
let time = dayjs().format("YYYY_MM_DD-hh_mm_ss");
|
||||||
|
let fileName = `Uptime_Kuma_Backup_${time}.json`;
|
||||||
|
let monitorList = Object.values(this.$root.monitorList);
|
||||||
|
let exportData = {
|
||||||
|
version: this.$root.info.version,
|
||||||
|
notificationList: this.$root.notificationList,
|
||||||
|
monitorList: monitorList,
|
||||||
|
};
|
||||||
|
exportData = JSON.stringify(exportData, null, 4);
|
||||||
|
let downloadItem = document.createElement("a");
|
||||||
|
downloadItem.setAttribute(
|
||||||
|
"href",
|
||||||
|
"data:application/json;charset=utf-8," +
|
||||||
|
encodeURIComponent(exportData)
|
||||||
|
);
|
||||||
|
downloadItem.setAttribute("download", fileName);
|
||||||
|
downloadItem.click();
|
||||||
|
},
|
||||||
|
|
||||||
|
importBackup() {
|
||||||
|
this.processing = true;
|
||||||
|
let uploadItem = document.getElementById("importBackup").files;
|
||||||
|
|
||||||
|
if (uploadItem.length <= 0) {
|
||||||
|
this.processing = false;
|
||||||
|
return (this.importAlert = this.$t("alertNoFile"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uploadItem.item(0).type !== "application/json") {
|
||||||
|
this.processing = false;
|
||||||
|
return (this.importAlert = this.$t("alertWrongFileType"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let fileReader = new FileReader();
|
||||||
|
fileReader.readAsText(uploadItem.item(0));
|
||||||
|
|
||||||
|
fileReader.onload = (item) => {
|
||||||
|
this.$root.uploadBackup(
|
||||||
|
item.target.result,
|
||||||
|
this.importHandle,
|
||||||
|
(res) => {
|
||||||
|
this.processing = false;
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
toast.success(res.msg);
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "../../assets/vars.scss";
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
#importBackup {
|
||||||
|
&::file-selector-button {
|
||||||
|
color: $primary;
|
||||||
|
background-color: $dark-bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(:disabled):not([readonly])::file-selector-button {
|
||||||
|
color: $dark-font-color2;
|
||||||
|
background-color: $primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue