only for ansible
|
@ -1,45 +0,0 @@
|
||||||
/.idea
|
|
||||||
/node_modules
|
|
||||||
/data
|
|
||||||
/out
|
|
||||||
/test
|
|
||||||
/kubernetes
|
|
||||||
/.do
|
|
||||||
**/.dockerignore
|
|
||||||
/private
|
|
||||||
**/.git
|
|
||||||
**/.gitignore
|
|
||||||
**/docker-compose*
|
|
||||||
**/[Dd]ockerfile*
|
|
||||||
LICENSE
|
|
||||||
README.md
|
|
||||||
.editorconfig
|
|
||||||
.vscode
|
|
||||||
.eslint*
|
|
||||||
.stylelint*
|
|
||||||
/.github
|
|
||||||
yarn.lock
|
|
||||||
app.json
|
|
||||||
CODE_OF_CONDUCT.md
|
|
||||||
CONTRIBUTING.md
|
|
||||||
CNAME
|
|
||||||
install.sh
|
|
||||||
SECURITY.md
|
|
||||||
tsconfig.json
|
|
||||||
.env
|
|
||||||
/tmp
|
|
||||||
|
|
||||||
### .gitignore content (commented rules are duplicated)
|
|
||||||
|
|
||||||
#node_modules
|
|
||||||
.DS_Store
|
|
||||||
#dist
|
|
||||||
dist-ssr
|
|
||||||
*.local
|
|
||||||
#.idea
|
|
||||||
|
|
||||||
#/data
|
|
||||||
#!/data/.gitkeep
|
|
||||||
#.vscode
|
|
||||||
|
|
||||||
### End of .gitignore content
|
|
|
@ -1,21 +0,0 @@
|
||||||
root = true
|
|
||||||
|
|
||||||
[*]
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 4
|
|
||||||
end_of_line = lf
|
|
||||||
charset = utf-8
|
|
||||||
trim_trailing_whitespace = true
|
|
||||||
insert_final_newline = true
|
|
||||||
|
|
||||||
[*.md]
|
|
||||||
trim_trailing_whitespace = false
|
|
||||||
|
|
||||||
[*.yaml]
|
|
||||||
indent_size = 2
|
|
||||||
|
|
||||||
[*.yml]
|
|
||||||
indent_size = 2
|
|
||||||
|
|
||||||
[*.vue]
|
|
||||||
trim_trailing_whitespace = false
|
|
113
.eslintrc.js
|
@ -1,113 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
env: {
|
|
||||||
browser: true,
|
|
||||||
commonjs: true,
|
|
||||||
es2020: true,
|
|
||||||
node: true,
|
|
||||||
},
|
|
||||||
extends: [
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:vue/vue3-recommended",
|
|
||||||
],
|
|
||||||
parser: "vue-eslint-parser",
|
|
||||||
parserOptions: {
|
|
||||||
parser: "@babel/eslint-parser",
|
|
||||||
sourceType: "module",
|
|
||||||
requireConfigFile: false,
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
"linebreak-style": ["error", "unix"],
|
|
||||||
"camelcase": ["warn", {
|
|
||||||
"properties": "never",
|
|
||||||
"ignoreImports": true
|
|
||||||
}],
|
|
||||||
// override/add rules settings here, such as:
|
|
||||||
// 'vue/no-unused-vars': 'error'
|
|
||||||
"no-unused-vars": "warn",
|
|
||||||
indent: [
|
|
||||||
"error",
|
|
||||||
4,
|
|
||||||
{
|
|
||||||
ignoredNodes: ["TemplateLiteral"],
|
|
||||||
SwitchCase: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
quotes: ["warn", "double"],
|
|
||||||
semi: "warn",
|
|
||||||
"vue/html-indent": ["warn", 4], // default: 2
|
|
||||||
"vue/max-attributes-per-line": "off",
|
|
||||||
"vue/singleline-html-element-content-newline": "off",
|
|
||||||
"vue/html-self-closing": "off",
|
|
||||||
"vue/attribute-hyphenation": "off", // This change noNL to "no-n-l" unexpectedly
|
|
||||||
"no-multi-spaces": ["error", {
|
|
||||||
ignoreEOLComments: true,
|
|
||||||
}],
|
|
||||||
"space-before-function-paren": ["error", {
|
|
||||||
"anonymous": "always",
|
|
||||||
"named": "never",
|
|
||||||
"asyncArrow": "always"
|
|
||||||
}],
|
|
||||||
"curly": "error",
|
|
||||||
"object-curly-spacing": ["error", "always"],
|
|
||||||
"object-curly-newline": "off",
|
|
||||||
"object-property-newline": "error",
|
|
||||||
"comma-spacing": "error",
|
|
||||||
"brace-style": "error",
|
|
||||||
"no-var": "error",
|
|
||||||
"key-spacing": "warn",
|
|
||||||
"keyword-spacing": "warn",
|
|
||||||
"space-infix-ops": "warn",
|
|
||||||
"arrow-spacing": "warn",
|
|
||||||
"no-trailing-spaces": "warn",
|
|
||||||
"no-constant-condition": ["error", {
|
|
||||||
"checkLoops": false,
|
|
||||||
}],
|
|
||||||
"space-before-blocks": "warn",
|
|
||||||
//'no-console': 'warn',
|
|
||||||
"no-extra-boolean-cast": "off",
|
|
||||||
"no-multiple-empty-lines": ["warn", {
|
|
||||||
"max": 1,
|
|
||||||
"maxBOF": 0,
|
|
||||||
}],
|
|
||||||
"lines-between-class-members": ["warn", "always", {
|
|
||||||
exceptAfterSingleLine: true,
|
|
||||||
}],
|
|
||||||
"no-unneeded-ternary": "error",
|
|
||||||
"array-bracket-newline": ["error", "consistent"],
|
|
||||||
"eol-last": ["error", "always"],
|
|
||||||
//'prefer-template': 'error',
|
|
||||||
"comma-dangle": ["warn", "only-multiline"],
|
|
||||||
"no-empty": ["error", {
|
|
||||||
"allowEmptyCatch": true
|
|
||||||
}],
|
|
||||||
"no-control-regex": "off",
|
|
||||||
"one-var": ["error", "never"],
|
|
||||||
"max-statements-per-line": ["error", { "max": 1 }]
|
|
||||||
},
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"files": [ "src/languages/*.js", "src/icon.js" ],
|
|
||||||
"rules": {
|
|
||||||
"comma-dangle": ["error", "always-multiline"],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Override for jest puppeteer
|
|
||||||
{
|
|
||||||
"files": [
|
|
||||||
"**/*.spec.js",
|
|
||||||
"**/*.spec.jsx"
|
|
||||||
],
|
|
||||||
env: {
|
|
||||||
jest: true,
|
|
||||||
},
|
|
||||||
globals: {
|
|
||||||
page: true,
|
|
||||||
browser: true,
|
|
||||||
context: true,
|
|
||||||
jestPuppeteer: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
12
.github/FUNDING.yml
vendored
|
@ -1,12 +0,0 @@
|
||||||
# These are supported funding model platforms
|
|
||||||
|
|
||||||
github: louislam # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
|
||||||
#patreon: # Replace with a single Patreon username
|
|
||||||
open_collective: uptime-kuma # Replace with a single Open Collective username
|
|
||||||
#ko_fi: # Replace with a single Ko-fi username
|
|
||||||
#tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
|
||||||
#community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
|
||||||
#liberapay: # Replace with a single Liberapay username
|
|
||||||
#issuehunt: # Replace with a single IssueHunt username
|
|
||||||
#otechie: # Replace with a single Otechie username
|
|
||||||
#custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
|
68
.github/ISSUE_TEMPLATE/ask-for-help.yaml
vendored
|
@ -1,68 +0,0 @@
|
||||||
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
|
|
99
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
|
@ -1,99 +0,0 @@
|
||||||
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
|
|
59
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
|
@ -1,59 +0,0 @@
|
||||||
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: "..."
|
|
28
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
@ -1,28 +0,0 @@
|
||||||
# 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.
|
|
35
.github/workflows/auto-test.yml
vendored
|
@ -1,35 +0,0 @@
|
||||||
# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
|
|
||||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
|
||||||
|
|
||||||
name: Auto Test
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ master ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ master ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
auto-test:
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
|
||||||
node-version: [14.x, 16.x, 17.x]
|
|
||||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
|
||||||
|
|
||||||
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 run install-legacy
|
|
||||||
- run: npm run build
|
|
||||||
- run: npm test
|
|
||||||
env:
|
|
||||||
HEADLESS_TEST: 1
|
|
||||||
JUST_FOR_TEST: ${{ secrets.JUST_FOR_TEST }}
|
|
26
.github/workflows/close-incorrect-issue.yml
vendored
|
@ -1,26 +0,0 @@
|
||||||
|
|
||||||
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 }}
|
|
22
.github/workflows/stale-bot.yml
vendored
|
@ -1,22 +0,0 @@
|
||||||
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'
|
|
15
.gitignore
vendored
|
@ -1,15 +0,0 @@
|
||||||
node_modules
|
|
||||||
.DS_Store
|
|
||||||
dist
|
|
||||||
dist-ssr
|
|
||||||
*.local
|
|
||||||
.idea
|
|
||||||
|
|
||||||
/data
|
|
||||||
!/data/.gitkeep
|
|
||||||
.vscode
|
|
||||||
|
|
||||||
/private
|
|
||||||
/out
|
|
||||||
/tmp
|
|
||||||
.env
|
|
|
@ -1,9 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "stylelint-config-standard",
|
|
||||||
"rules": {
|
|
||||||
"indentation": 4,
|
|
||||||
"no-descending-specificity": null,
|
|
||||||
"selector-list-comma-newline-after": null,
|
|
||||||
"declaration-empty-line-before": null
|
|
||||||
}
|
|
||||||
}
|
|
1
CNAME
|
@ -1 +0,0 @@
|
||||||
git.kuma.pet
|
|
|
@ -1,128 +0,0 @@
|
||||||
# Contributor Covenant Code of Conduct
|
|
||||||
|
|
||||||
## Our Pledge
|
|
||||||
|
|
||||||
We as members, contributors, and leaders pledge to make participation in our
|
|
||||||
community a harassment-free experience for everyone, regardless of age, body
|
|
||||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
|
||||||
identity and expression, level of experience, education, socio-economic status,
|
|
||||||
nationality, personal appearance, race, religion, or sexual identity
|
|
||||||
and orientation.
|
|
||||||
|
|
||||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
|
||||||
diverse, inclusive, and healthy community.
|
|
||||||
|
|
||||||
## Our Standards
|
|
||||||
|
|
||||||
Examples of behavior that contributes to a positive environment for our
|
|
||||||
community include:
|
|
||||||
|
|
||||||
* Demonstrating empathy and kindness toward other people
|
|
||||||
* Being respectful of differing opinions, viewpoints, and experiences
|
|
||||||
* Giving and gracefully accepting constructive feedback
|
|
||||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
|
||||||
and learning from the experience
|
|
||||||
* Focusing on what is best not just for us as individuals, but for the
|
|
||||||
overall community
|
|
||||||
|
|
||||||
Examples of unacceptable behavior include:
|
|
||||||
|
|
||||||
* The use of sexualized language or imagery, and sexual attention or
|
|
||||||
advances of any kind
|
|
||||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
|
||||||
* Public or private harassment
|
|
||||||
* Publishing others' private information, such as a physical or email
|
|
||||||
address, without their explicit permission
|
|
||||||
* Other conduct which could reasonably be considered inappropriate in a
|
|
||||||
professional setting
|
|
||||||
|
|
||||||
## Enforcement Responsibilities
|
|
||||||
|
|
||||||
Community leaders are responsible for clarifying and enforcing our standards of
|
|
||||||
acceptable behavior and will take appropriate and fair corrective action in
|
|
||||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
|
||||||
or harmful.
|
|
||||||
|
|
||||||
Community leaders have the right and responsibility to remove, edit, or reject
|
|
||||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
|
||||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
|
||||||
decisions when appropriate.
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
This Code of Conduct applies within all community spaces, and also applies when
|
|
||||||
an individual is officially representing the community in public spaces.
|
|
||||||
Examples of representing our community include using an official e-mail address,
|
|
||||||
posting via an official social media account, or acting as an appointed
|
|
||||||
representative at an online or offline event.
|
|
||||||
|
|
||||||
## Enforcement
|
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
|
||||||
reported to the community leaders responsible for enforcement at
|
|
||||||
uptime@kuma.pet.
|
|
||||||
All complaints will be reviewed and investigated promptly and fairly.
|
|
||||||
|
|
||||||
All community leaders are obligated to respect the privacy and security of the
|
|
||||||
reporter of any incident.
|
|
||||||
|
|
||||||
## Enforcement Guidelines
|
|
||||||
|
|
||||||
Community leaders will follow these Community Impact Guidelines in determining
|
|
||||||
the consequences for any action they deem in violation of this Code of Conduct:
|
|
||||||
|
|
||||||
### 1. Correction
|
|
||||||
|
|
||||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
|
||||||
unprofessional or unwelcome in the community.
|
|
||||||
|
|
||||||
**Consequence**: A private, written warning from community leaders, providing
|
|
||||||
clarity around the nature of the violation and an explanation of why the
|
|
||||||
behavior was inappropriate. A public apology may be requested.
|
|
||||||
|
|
||||||
### 2. Warning
|
|
||||||
|
|
||||||
**Community Impact**: A violation through a single incident or series
|
|
||||||
of actions.
|
|
||||||
|
|
||||||
**Consequence**: A warning with consequences for continued behavior. No
|
|
||||||
interaction with the people involved, including unsolicited interaction with
|
|
||||||
those enforcing the Code of Conduct, for a specified period of time. This
|
|
||||||
includes avoiding interactions in community spaces as well as external channels
|
|
||||||
like social media. Violating these terms may lead to a temporary or
|
|
||||||
permanent ban.
|
|
||||||
|
|
||||||
### 3. Temporary Ban
|
|
||||||
|
|
||||||
**Community Impact**: A serious violation of community standards, including
|
|
||||||
sustained inappropriate behavior.
|
|
||||||
|
|
||||||
**Consequence**: A temporary ban from any sort of interaction or public
|
|
||||||
communication with the community for a specified period of time. No public or
|
|
||||||
private interaction with the people involved, including unsolicited interaction
|
|
||||||
with those enforcing the Code of Conduct, is allowed during this period.
|
|
||||||
Violating these terms may lead to a permanent ban.
|
|
||||||
|
|
||||||
### 4. Permanent Ban
|
|
||||||
|
|
||||||
**Community Impact**: Demonstrating a pattern of violation of community
|
|
||||||
standards, including sustained inappropriate behavior, harassment of an
|
|
||||||
individual, or aggression toward or disparagement of classes of individuals.
|
|
||||||
|
|
||||||
**Consequence**: A permanent ban from any sort of public interaction within
|
|
||||||
the community.
|
|
||||||
|
|
||||||
## Attribution
|
|
||||||
|
|
||||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
|
||||||
version 2.0, available at
|
|
||||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
|
||||||
|
|
||||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
|
||||||
enforcement ladder](https://github.com/mozilla/diversity).
|
|
||||||
|
|
||||||
[homepage]: https://www.contributor-covenant.org
|
|
||||||
|
|
||||||
For answers to common questions about this code of conduct, see the FAQ at
|
|
||||||
https://www.contributor-covenant.org/faq. Translations are available at
|
|
||||||
https://www.contributor-covenant.org/translations.
|
|
254
CONTRIBUTING.md
|
@ -1,254 +0,0 @@
|
||||||
# Project Info
|
|
||||||
|
|
||||||
First of all, thank you everyone who made pull requests for Uptime Kuma, I never thought GitHub Community can be that nice! And also because of this, I also never thought other people actually read my code and edit my code. It is not structured and commented so well, lol. Sorry about that.
|
|
||||||
|
|
||||||
The project was created with vite.js (vue3). Then I created a subdirectory called "server" for server part. Both frontend and backend share the same package.json.
|
|
||||||
|
|
||||||
The frontend code build into "dist" directory. The server (express.js) exposes the "dist" directory as root of the endpoint. This is how production is working.
|
|
||||||
|
|
||||||
## Key Technical Skills
|
|
||||||
|
|
||||||
- Node.js (You should know what are promise, async/await and arrow function etc.)
|
|
||||||
- Socket.io
|
|
||||||
- SCSS
|
|
||||||
- Vue.js
|
|
||||||
- Bootstrap
|
|
||||||
- SQLite
|
|
||||||
|
|
||||||
## Directories
|
|
||||||
|
|
||||||
- data (App data)
|
|
||||||
- dist (Frontend build)
|
|
||||||
- extra (Extra useful scripts)
|
|
||||||
- public (Frontend resources for dev only)
|
|
||||||
- server (Server source code)
|
|
||||||
- src (Frontend source code)
|
|
||||||
- test (unit test)
|
|
||||||
|
|
||||||
## Can I create a pull request for Uptime Kuma?
|
|
||||||
|
|
||||||
Generally, if the pull request is working fine, and it does not affect any existing logic, workflow and performance, I will merge into the master branch once it is tested.
|
|
||||||
|
|
||||||
If you are not sure whether I will accept your pull request, feel free to create an empty pull request draft first.
|
|
||||||
|
|
||||||
### Recommended Pull Request Guideline
|
|
||||||
|
|
||||||
1. Fork the project
|
|
||||||
1. Clone your fork repo to local
|
|
||||||
1. Create a new branch
|
|
||||||
1. Create an empty commit
|
|
||||||
`git commit -m "[empty commit] pull request for <YOUR TASK NAME>" --allow-empty`
|
|
||||||
1. Push to your fork repo
|
|
||||||
1. Create a pull request: https://github.com/louislam/uptime-kuma/compare
|
|
||||||
1. Write a proper description
|
|
||||||
1. Click "Change to draft"
|
|
||||||
|
|
||||||
### Pull Request Examples
|
|
||||||
|
|
||||||
Here are some example situations in the past.
|
|
||||||
|
|
||||||
#### ✅ High - Medium Priority
|
|
||||||
|
|
||||||
Easy to review, no breaking change and not touching the existing code
|
|
||||||
|
|
||||||
- Add a new notification
|
|
||||||
- Add a chart
|
|
||||||
- Fix a bug
|
|
||||||
- Translations
|
|
||||||
- Add a independent new feature
|
|
||||||
|
|
||||||
#### *️⃣ Requires one more reviewer
|
|
||||||
|
|
||||||
I do not have such knowledge to test it.
|
|
||||||
|
|
||||||
- Add k8s supports
|
|
||||||
|
|
||||||
#### ⚠ Low Priority - Harsh Mode
|
|
||||||
|
|
||||||
Some pull requests are required to modify the core. To be honest, I do not want anyone to try to do that, because it would spend a lot of your time. I will review your pull request harshly. Also, you may need to write a lot of unit tests to ensure that there is no breaking change.
|
|
||||||
|
|
||||||
- Touch large parts of code of any very important features
|
|
||||||
- Touch monitoring logic
|
|
||||||
- Drop a table or drop a column for any reason
|
|
||||||
- Touch the entry point of Docker or Node.js
|
|
||||||
- Modify auth
|
|
||||||
|
|
||||||
#### *️⃣ Low Priority
|
|
||||||
|
|
||||||
It changed my current workflow and require further studies.
|
|
||||||
|
|
||||||
- Change my release approach
|
|
||||||
|
|
||||||
#### ❌ Won't Merge
|
|
||||||
|
|
||||||
- Any breaking changes
|
|
||||||
- Duplicated pull request
|
|
||||||
- Buggy
|
|
||||||
- Existing logic is completely modified or deleted
|
|
||||||
- A function that is completely out of scope
|
|
||||||
|
|
||||||
## Project Styles
|
|
||||||
|
|
||||||
I personally do not like something need to learn so much and need to config so much before you can finally start the app.
|
|
||||||
|
|
||||||
- Easy to install for non-Docker users, no native build dependency is needed (at least for x86_64), no extra config, no extra effort to get it run
|
|
||||||
- Single container for Docker users, no very complex docker-compose file. Just map the volume and expose the port, then good to go
|
|
||||||
- Settings should be configurable in the frontend. Env var is not encouraged.
|
|
||||||
- Easy to use
|
|
||||||
|
|
||||||
## Coding Styles
|
|
||||||
|
|
||||||
- 4 spaces indentation
|
|
||||||
- Follow `.editorconfig`
|
|
||||||
- Follow ESLint
|
|
||||||
|
|
||||||
## Name convention
|
|
||||||
|
|
||||||
- Javascript/Typescript: camelCaseType
|
|
||||||
- SQLite: underscore_type
|
|
||||||
- CSS/SCSS: dash-type
|
|
||||||
|
|
||||||
## Tools
|
|
||||||
|
|
||||||
- Node.js >= 14
|
|
||||||
- Git
|
|
||||||
- IDE that supports ESLint and EditorConfig (I am using IntelliJ IDEA)
|
|
||||||
- A SQLite tool (SQLite Expert Personal is suggested)
|
|
||||||
|
|
||||||
## Install dependencies
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm ci
|
|
||||||
```
|
|
||||||
|
|
||||||
## How to start the Backend Dev Server
|
|
||||||
|
|
||||||
(2021-09-23 Update)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run start-server-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
It binds to `0.0.0.0:3001` by default.
|
|
||||||
|
|
||||||
### Backend Details
|
|
||||||
|
|
||||||
It is mainly a socket.io app + express.js.
|
|
||||||
|
|
||||||
express.js is just used for serving the frontend built files (index.html, .js and .css etc.)
|
|
||||||
|
|
||||||
- model/ (Object model, auto mapping to the database table name)
|
|
||||||
- modules/ (Modified 3rd-party modules)
|
|
||||||
- notification-providers/ (individual notification logic)
|
|
||||||
- routers/ (Express Routers)
|
|
||||||
- socket-handler (Socket.io Handlers)
|
|
||||||
- server.js (Server main logic)
|
|
||||||
|
|
||||||
## How to start the Frontend Dev Server
|
|
||||||
|
|
||||||
1. Set the env var `NODE_ENV` to "development".
|
|
||||||
2. Start the frontend dev server by the following command.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
It binds to `0.0.0.0:3000` by default.
|
|
||||||
|
|
||||||
You can use Vue.js devtools Chrome extension for debugging.
|
|
||||||
|
|
||||||
### Build the frontend
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
### Frontend Details
|
|
||||||
|
|
||||||
Uptime Kuma Frontend is a single page application (SPA). Most paths are handled by Vue Router.
|
|
||||||
|
|
||||||
The router is in `src/router.js`
|
|
||||||
|
|
||||||
As you can see, most data in frontend is stored in root level, even though you changed the current router to any other pages.
|
|
||||||
|
|
||||||
The data and socket logic are in `src/mixins/socket.js`.
|
|
||||||
|
|
||||||
## Database Migration
|
|
||||||
|
|
||||||
1. Create `patch-{name}.sql` in `./db/`
|
|
||||||
2. Add your patch filename in the `patchList` list in `./server/database.js`
|
|
||||||
|
|
||||||
## Unit Test
|
|
||||||
|
|
||||||
It is an end-to-end testing. It is using Jest and Puppeteer.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run build
|
|
||||||
npm test
|
|
||||||
```
|
|
||||||
|
|
||||||
By default, the Chromium window will be shown up during the test. Specifying `HEADLESS_TEST=1` for terminal environments.
|
|
||||||
|
|
||||||
## Update Dependencies
|
|
||||||
|
|
||||||
Install `ncu`
|
|
||||||
https://github.com/raineorshine/npm-check-updates
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ncu -u -t patch
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
Since previously updating Vite 2.5.10 to 2.6.0 broke the application completely, from now on, it should update patch release version only.
|
|
||||||
|
|
||||||
Patch release = the third digit ([Semantic Versioning](https://semver.org/))
|
|
||||||
|
|
||||||
## Translations
|
|
||||||
|
|
||||||
Please read: https://github.com/louislam/uptime-kuma/tree/master/src/languages
|
|
||||||
|
|
||||||
## Wiki
|
|
||||||
|
|
||||||
Since there is no way to make a pull request to wiki's repo, I have set up another repo to do that.
|
|
||||||
|
|
||||||
https://github.com/louislam/uptime-kuma-wiki
|
|
||||||
|
|
||||||
## Maintainer
|
|
||||||
|
|
||||||
Check the latest issues and pull requests:
|
|
||||||
https://github.com/louislam/uptime-kuma/issues?q=sort%3Aupdated-desc
|
|
||||||
|
|
||||||
### Release Procedures
|
|
||||||
|
|
||||||
1. Draft a release note
|
|
||||||
1. Make sure the repo is cleared
|
|
||||||
1. `npm run update-version 1.X.X`
|
|
||||||
1. `npm run build`
|
|
||||||
1. `npm run build-docker`
|
|
||||||
1. `git push`
|
|
||||||
1. Publish the release note as 1.X.X
|
|
||||||
1. `npm run upload-artifacts`
|
|
||||||
1. SSH to demo site server and update to 1.X.X
|
|
||||||
|
|
||||||
Checking:
|
|
||||||
|
|
||||||
- Check all tags is fine on https://hub.docker.com/r/louislam/uptime-kuma/tags
|
|
||||||
- Try the Docker image with tag 1.X.X (Clean install / amd64 / arm64 / armv7)
|
|
||||||
- Try clean installation with Node.js
|
|
||||||
|
|
||||||
### Release Wiki
|
|
||||||
|
|
||||||
#### Setup Repo
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/louislam/uptime-kuma-wiki.git
|
|
||||||
cd uptime-kuma-wiki
|
|
||||||
git remote add production https://github.com/louislam/uptime-kuma.wiki.git
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Push to Production Wiki
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git pull
|
|
||||||
git push production master
|
|
||||||
```
|
|
21
LICENSE
|
@ -1,21 +0,0 @@
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2021 Louis Lam
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
141
README.md
|
@ -1,141 +0,0 @@
|
||||||
# Uptime Kuma
|
|
||||||
|
|
||||||
<a target="_blank" href="https://github.com/louislam/uptime-kuma"><img src="https://img.shields.io/github/stars/louislam/uptime-kuma" /></a> <a target="_blank" href="https://hub.docker.com/r/louislam/uptime-kuma"><img src="https://img.shields.io/docker/pulls/louislam/uptime-kuma" /></a> <a target="_blank" href="https://hub.docker.com/r/louislam/uptime-kuma"><img src="https://img.shields.io/docker/v/louislam/uptime-kuma/latest?label=docker%20image%20ver." /></a> <a target="_blank" href="https://github.com/louislam/uptime-kuma"><img src="https://img.shields.io/github/last-commit/louislam/uptime-kuma" /></a> <a target="_blank" href="https://opencollective.com/uptime-kuma"><img src="https://opencollective.com/uptime-kuma/total/badge.svg?label=Open%20Collective%20Backers&color=brightgreen" /></a>
|
|
||||||
[![GitHub Sponsors](https://img.shields.io/github/sponsors/louislam?label=GitHub%20Sponsors)](https://github.com/sponsors/louislam)
|
|
||||||
|
|
||||||
<div align="center" width="100%">
|
|
||||||
<img src="./public/icon.svg" width="128" alt="" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
It is a self-hosted monitoring tool like "Uptime Robot".
|
|
||||||
|
|
||||||
<img src="https://uptime.kuma.pet/img/dark.jpg" width="700" alt="" />
|
|
||||||
|
|
||||||
## 🥔 Live Demo
|
|
||||||
|
|
||||||
Try it!
|
|
||||||
|
|
||||||
https://demo.uptime.kuma.pet
|
|
||||||
|
|
||||||
It is a temporary live demo, all data will be deleted after 10 minutes. The server is located in Tokyo, so if you live far from there, it may affect your experience. I suggest that you should install and try it out for the best demo experience.
|
|
||||||
|
|
||||||
VPS is sponsored by Uptime Kuma sponsors on [Open Collective](https://opencollective.com/uptime-kuma)! Thank you so much!
|
|
||||||
|
|
||||||
## ⭐ Features
|
|
||||||
|
|
||||||
* Monitoring uptime for HTTP(s) / TCP / HTTP(s) Keyword / Ping / DNS Record / Push / Steam Game Server.
|
|
||||||
* Fancy, Reactive, Fast UI/UX.
|
|
||||||
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [70+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/tree/master/src/components/notifications).
|
|
||||||
* 20 second intervals.
|
|
||||||
* [Multi Languages](https://github.com/louislam/uptime-kuma/tree/master/src/languages)
|
|
||||||
* Simple Status Page
|
|
||||||
* Ping Chart
|
|
||||||
* Certificate Info
|
|
||||||
|
|
||||||
## 🔧 How to Install
|
|
||||||
|
|
||||||
### 🐳 Docker
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker volume create uptime-kuma
|
|
||||||
docker run -d --restart=always -p 3001:3001 -v uptime-kuma:/app/data --name uptime-kuma louislam/uptime-kuma:1
|
|
||||||
```
|
|
||||||
|
|
||||||
⚠️ Please use a **local volume** only. Other types such as NFS are not supported.
|
|
||||||
|
|
||||||
Browse to http://localhost:3001 after starting.
|
|
||||||
|
|
||||||
### 💪🏻 Non-Docker
|
|
||||||
|
|
||||||
Required Tools: Node.js >= 14, git and pm2.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Update your npm to the latest version
|
|
||||||
npm install npm -g
|
|
||||||
|
|
||||||
git clone https://github.com/louislam/uptime-kuma.git
|
|
||||||
cd uptime-kuma
|
|
||||||
npm run setup
|
|
||||||
|
|
||||||
# Option 1. Try it
|
|
||||||
node server/server.js
|
|
||||||
|
|
||||||
# (Recommended) Option 2. Run in background using PM2
|
|
||||||
# Install PM2 if you don't have it: npm install pm2 -g
|
|
||||||
pm2 start server/server.js --name uptime-kuma
|
|
||||||
```
|
|
||||||
|
|
||||||
Browse to http://localhost:3001 after starting.
|
|
||||||
|
|
||||||
### 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
|
|
||||||
|
|
||||||
## 🆙 How to Update
|
|
||||||
|
|
||||||
Please read:
|
|
||||||
|
|
||||||
https://github.com/louislam/uptime-kuma/wiki/%F0%9F%86%99-How-to-Update
|
|
||||||
|
|
||||||
## 🆕 What's Next?
|
|
||||||
|
|
||||||
I will mark requests/issues to the next milestone.
|
|
||||||
|
|
||||||
https://github.com/louislam/uptime-kuma/milestones
|
|
||||||
|
|
||||||
Project Plan:
|
|
||||||
|
|
||||||
https://github.com/louislam/uptime-kuma/projects/1
|
|
||||||
|
|
||||||
## 🖼 More Screenshots
|
|
||||||
|
|
||||||
Light Mode:
|
|
||||||
|
|
||||||
<img src="https://uptime.kuma.pet/img/light.jpg" width="512" alt="" />
|
|
||||||
|
|
||||||
Status Page:
|
|
||||||
|
|
||||||
<img src="https://user-images.githubusercontent.com/1336778/134628766-a3fe0981-0926-4285-ab46-891a21c3e4cb.png" width="512" alt="" />
|
|
||||||
|
|
||||||
Settings Page:
|
|
||||||
|
|
||||||
<img src="https://louislam.net/uptimekuma/2.jpg" width="400" alt="" />
|
|
||||||
|
|
||||||
Telegram Notification Sample:
|
|
||||||
|
|
||||||
<img src="https://louislam.net/uptimekuma/3.jpg" width="400" alt="" />
|
|
||||||
|
|
||||||
## Motivation
|
|
||||||
|
|
||||||
* I was looking for a self-hosted monitoring tool like "Uptime Robot", but it is hard to find a suitable one. One of the close ones is statping. Unfortunately, it is not stable and unmaintained.
|
|
||||||
* Want to build a fancy UI.
|
|
||||||
* Learn Vue 3 and vite.js.
|
|
||||||
* Show the power of Bootstrap 5.
|
|
||||||
* Try to use WebSocket with SPA instead of REST API.
|
|
||||||
* Deploy my first Docker image to Docker Hub.
|
|
||||||
|
|
||||||
If you love this project, please consider giving me a ⭐.
|
|
||||||
|
|
||||||
## 🗣️ Discussion
|
|
||||||
|
|
||||||
### Issues Page
|
|
||||||
|
|
||||||
You can discuss or ask for help in [issues](https://github.com/louislam/uptime-kuma/issues).
|
|
||||||
|
|
||||||
### Subreddit
|
|
||||||
|
|
||||||
My Reddit account: louislamlam
|
|
||||||
You can mention me if you ask a question on Reddit.
|
|
||||||
https://www.reddit.com/r/UptimeKuma/
|
|
||||||
|
|
||||||
## Contribute
|
|
||||||
|
|
||||||
If you want to report a bug or request a new feature. Free feel to open a [new issue](https://github.com/louislam/uptime-kuma/issues).
|
|
||||||
|
|
||||||
If you want to translate Uptime Kuma into your language, please read: https://github.com/louislam/uptime-kuma/tree/master/src/languages
|
|
||||||
|
|
||||||
If you want to modify Uptime Kuma, this guideline may be useful for you: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md
|
|
||||||
|
|
||||||
English proofreading is needed too because my grammar is not that great, sadly. Feel free to correct my grammar in this README, source code, or wiki.
|
|
31
SECURITY.md
|
@ -1,31 +0,0 @@
|
||||||
# Security Policy
|
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
|
||||||
|
|
||||||
Please report security issues to uptime@kuma.pet.
|
|
||||||
|
|
||||||
Do not use the issue tracker or discuss it in the public as it will cause more damage.
|
|
||||||
|
|
||||||
## Supported Versions
|
|
||||||
|
|
||||||
Use this section to tell people about which versions of your project are
|
|
||||||
currently being supported with security updates.
|
|
||||||
|
|
||||||
### Uptime Kuma Versions
|
|
||||||
|
|
||||||
| Version | Supported |
|
|
||||||
| ------- | ------------------ |
|
|
||||||
| 1.9.X | :white_check_mark: |
|
|
||||||
| <= 1.8.X | ❌ |
|
|
||||||
|
|
||||||
### Upgradable Docker Tags
|
|
||||||
|
|
||||||
| Tag | Supported |
|
|
||||||
| ------- | ------------------ |
|
|
||||||
| 1 | :white_check_mark: |
|
|
||||||
| 1-debian | :white_check_mark: |
|
|
||||||
| 1-alpine | :white_check_mark: |
|
|
||||||
| latest | :white_check_mark: |
|
|
||||||
| debian | :white_check_mark: |
|
|
||||||
| alpine | :white_check_mark: |
|
|
||||||
| All other tags | ❌ |
|
|
|
@ -1,11 +0,0 @@
|
||||||
const config = {};
|
|
||||||
|
|
||||||
if (process.env.TEST_FRONTEND) {
|
|
||||||
config.presets = ["@babel/preset-env"];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.TEST_BACKEND) {
|
|
||||||
config.plugins = ["babel-plugin-rewire"];
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = config;
|
|
|
@ -1,5 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
"rootDir": "..",
|
|
||||||
"testRegex": "./test/backend.spec.js",
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
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 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
"rootDir": "..",
|
|
||||||
"testRegex": "./test/frontend.spec.js",
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
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",
|
|
||||||
],
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,12 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
"verbose": true,
|
|
||||||
"preset": "jest-puppeteer",
|
|
||||||
"globals": {
|
|
||||||
"__DEV__": true
|
|
||||||
},
|
|
||||||
"testRegex": "./test/e2e.spec.js",
|
|
||||||
"testEnvironment": "./config/jest-debug-env.js",
|
|
||||||
"rootDir": "..",
|
|
||||||
"testTimeout": 30000,
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
import legacy from "@vitejs/plugin-legacy";
|
|
||||||
import vue from "@vitejs/plugin-vue";
|
|
||||||
import { defineConfig } from "vite";
|
|
||||||
|
|
||||||
const postCssScss = require("postcss-scss");
|
|
||||||
const postcssRTLCSS = require("postcss-rtlcss");
|
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [
|
|
||||||
vue(),
|
|
||||||
legacy({
|
|
||||||
targets: ["ie > 11"],
|
|
||||||
additionalLegacyPolyfills: ["regenerator-runtime/runtime"]
|
|
||||||
})
|
|
||||||
],
|
|
||||||
css: {
|
|
||||||
postcss: {
|
|
||||||
"parser": postCssScss,
|
|
||||||
"map": false,
|
|
||||||
"plugins": [postcssRTLCSS]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
BIN
db/kuma.db
|
@ -1,7 +0,0 @@
|
||||||
-- 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;
|
|
|
@ -1,10 +0,0 @@
|
||||||
-- 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_secret VARCHAR(64);
|
|
||||||
|
|
||||||
ALTER TABLE user
|
|
||||||
ADD twofa_status BOOLEAN default 0 NOT NULL;
|
|
||||||
|
|
||||||
COMMIT;
|
|
|
@ -1,7 +0,0 @@
|
||||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
|
||||||
BEGIN TRANSACTION;
|
|
||||||
|
|
||||||
ALTER TABLE monitor
|
|
||||||
ADD retry_interval INTEGER default 0 not null;
|
|
||||||
|
|
||||||
COMMIT;
|
|
|
@ -1,30 +0,0 @@
|
||||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
|
||||||
BEGIN TRANSACTION;
|
|
||||||
|
|
||||||
create table `group`
|
|
||||||
(
|
|
||||||
id INTEGER not null
|
|
||||||
constraint group_pk
|
|
||||||
primary key autoincrement,
|
|
||||||
name VARCHAR(255) not null,
|
|
||||||
created_date DATETIME default (DATETIME('now')) not null,
|
|
||||||
public BOOLEAN default 0 not null,
|
|
||||||
active BOOLEAN default 1 not null,
|
|
||||||
weight BOOLEAN NOT NULL DEFAULT 1000
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE [monitor_group]
|
|
||||||
(
|
|
||||||
[id] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
||||||
[monitor_id] INTEGER NOT NULL REFERENCES [monitor] ([id]) ON DELETE CASCADE ON UPDATE CASCADE,
|
|
||||||
[group_id] INTEGER NOT NULL REFERENCES [group] ([id]) ON DELETE CASCADE ON UPDATE CASCADE,
|
|
||||||
weight BOOLEAN NOT NULL DEFAULT 1000
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX [fk]
|
|
||||||
ON [monitor_group] (
|
|
||||||
[monitor_id],
|
|
||||||
[group_id]);
|
|
||||||
|
|
||||||
|
|
||||||
COMMIT;
|
|
|
@ -1,13 +0,0 @@
|
||||||
-- 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;
|
|
|
@ -1,10 +0,0 @@
|
||||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
|
||||||
BEGIN TRANSACTION;
|
|
||||||
|
|
||||||
-- For sendHeartbeatList
|
|
||||||
CREATE INDEX monitor_time_index ON heartbeat (monitor_id, time);
|
|
||||||
|
|
||||||
-- For sendImportantHeartbeatList
|
|
||||||
CREATE INDEX monitor_important_time_index ON heartbeat (monitor_id, important,time);
|
|
||||||
|
|
||||||
COMMIT;
|
|
|
@ -1,18 +0,0 @@
|
||||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
|
||||||
BEGIN TRANSACTION;
|
|
||||||
|
|
||||||
create table incident
|
|
||||||
(
|
|
||||||
id INTEGER not null
|
|
||||||
constraint incident_pk
|
|
||||||
primary key autoincrement,
|
|
||||||
title VARCHAR(255) not null,
|
|
||||||
content TEXT not null,
|
|
||||||
style VARCHAR(30) default 'warning' not null,
|
|
||||||
created_date DATETIME default (DATETIME('now')) not null,
|
|
||||||
last_updated_date DATETIME,
|
|
||||||
pin BOOLEAN default 1 not null,
|
|
||||||
active BOOLEAN default 1 not null
|
|
||||||
);
|
|
||||||
|
|
||||||
COMMIT;
|
|
|
@ -1,10 +0,0 @@
|
||||||
-- 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;
|
|
|
@ -1,7 +0,0 @@
|
||||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
|
||||||
BEGIN TRANSACTION;
|
|
||||||
|
|
||||||
ALTER TABLE monitor
|
|
||||||
ADD push_token VARCHAR(20) DEFAULT NULL;
|
|
||||||
|
|
||||||
COMMIT;
|
|
|
@ -1,18 +0,0 @@
|
||||||
-- 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;
|
|
|
@ -1,22 +0,0 @@
|
||||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
|
||||||
BEGIN TRANSACTION;
|
|
||||||
|
|
||||||
-- Generated by Intellij IDEA
|
|
||||||
create table setting_dg_tmp
|
|
||||||
(
|
|
||||||
id INTEGER
|
|
||||||
primary key autoincrement,
|
|
||||||
key VARCHAR(200) not null
|
|
||||||
unique,
|
|
||||||
value TEXT,
|
|
||||||
type VARCHAR(20)
|
|
||||||
);
|
|
||||||
|
|
||||||
insert into setting_dg_tmp(id, key, value, type) select id, key, value, type from setting;
|
|
||||||
|
|
||||||
drop table setting;
|
|
||||||
|
|
||||||
alter table setting_dg_tmp rename to setting;
|
|
||||||
|
|
||||||
|
|
||||||
COMMIT;
|
|
|
@ -1,37 +0,0 @@
|
||||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
|
||||||
-- Change Monitor.created_date from "TIMESTAMP" to "DATETIME"
|
|
||||||
-- SQL Generated by Intellij Idea
|
|
||||||
PRAGMA foreign_keys=off;
|
|
||||||
|
|
||||||
BEGIN TRANSACTION;
|
|
||||||
|
|
||||||
create table monitor_dg_tmp
|
|
||||||
(
|
|
||||||
id INTEGER not null
|
|
||||||
primary key autoincrement,
|
|
||||||
name VARCHAR(150),
|
|
||||||
active BOOLEAN default 1 not null,
|
|
||||||
user_id INTEGER
|
|
||||||
references user
|
|
||||||
on update cascade on delete set null,
|
|
||||||
interval INTEGER default 20 not null,
|
|
||||||
url TEXT,
|
|
||||||
type VARCHAR(20),
|
|
||||||
weight INTEGER default 2000,
|
|
||||||
hostname VARCHAR(255),
|
|
||||||
port INTEGER,
|
|
||||||
created_date DATETIME,
|
|
||||||
keyword VARCHAR(255)
|
|
||||||
);
|
|
||||||
|
|
||||||
insert into monitor_dg_tmp(id, name, active, user_id, interval, url, type, weight, hostname, port, created_date, keyword) select id, name, active, user_id, interval, url, type, weight, hostname, port, created_date, keyword from monitor;
|
|
||||||
|
|
||||||
drop table monitor;
|
|
||||||
|
|
||||||
alter table monitor_dg_tmp rename to monitor;
|
|
||||||
|
|
||||||
create index user_id on monitor (user_id);
|
|
||||||
|
|
||||||
COMMIT;
|
|
||||||
|
|
||||||
PRAGMA foreign_keys=on;
|
|
|
@ -1,19 +0,0 @@
|
||||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
|
||||||
CREATE TABLE tag (
|
|
||||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
||||||
name VARCHAR(255) NOT NULL,
|
|
||||||
color VARCHAR(255) NOT NULL,
|
|
||||||
created_date DATETIME DEFAULT (DATETIME('now')) NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE monitor_tag (
|
|
||||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
||||||
monitor_id INTEGER NOT NULL,
|
|
||||||
tag_id INTEGER NOT NULL,
|
|
||||||
value TEXT,
|
|
||||||
CONSTRAINT FK_tag FOREIGN KEY (tag_id) REFERENCES tag(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
|
||||||
CONSTRAINT FK_monitor FOREIGN KEY (monitor_id) REFERENCES monitor(id) ON DELETE CASCADE ON UPDATE CASCADE
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX monitor_tag_monitor_id_index ON monitor_tag (monitor_id);
|
|
||||||
CREATE INDEX monitor_tag_tag_id_index ON monitor_tag (tag_id);
|
|
|
@ -1,9 +0,0 @@
|
||||||
BEGIN TRANSACTION;
|
|
||||||
|
|
||||||
CREATE TABLE monitor_tls_info (
|
|
||||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
||||||
monitor_id INTEGER NOT NULL,
|
|
||||||
info_json TEXT
|
|
||||||
);
|
|
||||||
|
|
||||||
COMMIT;
|
|
|
@ -1,37 +0,0 @@
|
||||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
|
||||||
-- Add maxretries column to monitor
|
|
||||||
PRAGMA foreign_keys=off;
|
|
||||||
|
|
||||||
BEGIN TRANSACTION;
|
|
||||||
|
|
||||||
create table monitor_dg_tmp
|
|
||||||
(
|
|
||||||
id INTEGER not null
|
|
||||||
primary key autoincrement,
|
|
||||||
name VARCHAR(150),
|
|
||||||
active BOOLEAN default 1 not null,
|
|
||||||
user_id INTEGER
|
|
||||||
references user
|
|
||||||
on update cascade on delete set null,
|
|
||||||
interval INTEGER default 20 not null,
|
|
||||||
url TEXT,
|
|
||||||
type VARCHAR(20),
|
|
||||||
weight INTEGER default 2000,
|
|
||||||
hostname VARCHAR(255),
|
|
||||||
port INTEGER,
|
|
||||||
created_date DATETIME,
|
|
||||||
keyword VARCHAR(255),
|
|
||||||
maxretries INTEGER NOT NULL DEFAULT 0
|
|
||||||
);
|
|
||||||
|
|
||||||
insert into monitor_dg_tmp(id, name, active, user_id, interval, url, type, weight, hostname, port, created_date, keyword) select id, name, active, user_id, interval, url, type, weight, hostname, port, created_date, keyword from monitor;
|
|
||||||
|
|
||||||
drop table monitor;
|
|
||||||
|
|
||||||
alter table monitor_dg_tmp rename to monitor;
|
|
||||||
|
|
||||||
create index user_id on monitor (user_id);
|
|
||||||
|
|
||||||
COMMIT;
|
|
||||||
|
|
||||||
PRAGMA foreign_keys=on;
|
|
|
@ -1,40 +0,0 @@
|
||||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
|
||||||
-- OK.... serious wrong, missing maxretries column
|
|
||||||
-- Developers should patch it manually if you have missing the maxretries column
|
|
||||||
PRAGMA foreign_keys=off;
|
|
||||||
|
|
||||||
BEGIN TRANSACTION;
|
|
||||||
|
|
||||||
create table monitor_dg_tmp
|
|
||||||
(
|
|
||||||
id INTEGER not null
|
|
||||||
primary key autoincrement,
|
|
||||||
name VARCHAR(150),
|
|
||||||
active BOOLEAN default 1 not null,
|
|
||||||
user_id INTEGER
|
|
||||||
references user
|
|
||||||
on update cascade on delete set null,
|
|
||||||
interval INTEGER default 20 not null,
|
|
||||||
url TEXT,
|
|
||||||
type VARCHAR(20),
|
|
||||||
weight INTEGER default 2000,
|
|
||||||
hostname VARCHAR(255),
|
|
||||||
port INTEGER,
|
|
||||||
created_date DATETIME,
|
|
||||||
keyword VARCHAR(255),
|
|
||||||
maxretries INTEGER NOT NULL DEFAULT 0,
|
|
||||||
ignore_tls BOOLEAN default 0 not null,
|
|
||||||
upside_down BOOLEAN default 0 not null
|
|
||||||
);
|
|
||||||
|
|
||||||
insert into monitor_dg_tmp(id, name, active, user_id, interval, url, type, weight, hostname, port, created_date, keyword, maxretries) select id, name, active, user_id, interval, url, type, weight, hostname, port, created_date, keyword, maxretries from monitor;
|
|
||||||
|
|
||||||
drop table monitor;
|
|
||||||
|
|
||||||
alter table monitor_dg_tmp rename to monitor;
|
|
||||||
|
|
||||||
create index user_id on monitor (user_id);
|
|
||||||
|
|
||||||
COMMIT;
|
|
||||||
|
|
||||||
PRAGMA foreign_keys=on;
|
|
|
@ -1,70 +0,0 @@
|
||||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
|
||||||
PRAGMA foreign_keys = off;
|
|
||||||
|
|
||||||
BEGIN TRANSACTION;
|
|
||||||
|
|
||||||
create table monitor_dg_tmp (
|
|
||||||
id INTEGER not null primary key autoincrement,
|
|
||||||
name VARCHAR(150),
|
|
||||||
active BOOLEAN default 1 not null,
|
|
||||||
user_id INTEGER references user on update cascade on delete
|
|
||||||
set
|
|
||||||
null,
|
|
||||||
interval INTEGER default 20 not null,
|
|
||||||
url TEXT,
|
|
||||||
type VARCHAR(20),
|
|
||||||
weight INTEGER default 2000,
|
|
||||||
hostname VARCHAR(255),
|
|
||||||
port INTEGER,
|
|
||||||
created_date DATETIME default (DATETIME('now')) not null,
|
|
||||||
keyword VARCHAR(255),
|
|
||||||
maxretries INTEGER NOT NULL DEFAULT 0,
|
|
||||||
ignore_tls BOOLEAN default 0 not null,
|
|
||||||
upside_down BOOLEAN default 0 not null
|
|
||||||
);
|
|
||||||
|
|
||||||
insert into
|
|
||||||
monitor_dg_tmp(
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
active,
|
|
||||||
user_id,
|
|
||||||
interval,
|
|
||||||
url,
|
|
||||||
type,
|
|
||||||
weight,
|
|
||||||
hostname,
|
|
||||||
port,
|
|
||||||
keyword,
|
|
||||||
maxretries,
|
|
||||||
ignore_tls,
|
|
||||||
upside_down
|
|
||||||
)
|
|
||||||
select
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
active,
|
|
||||||
user_id,
|
|
||||||
interval,
|
|
||||||
url,
|
|
||||||
type,
|
|
||||||
weight,
|
|
||||||
hostname,
|
|
||||||
port,
|
|
||||||
keyword,
|
|
||||||
maxretries,
|
|
||||||
ignore_tls,
|
|
||||||
upside_down
|
|
||||||
from
|
|
||||||
monitor;
|
|
||||||
|
|
||||||
drop table monitor;
|
|
||||||
|
|
||||||
alter table
|
|
||||||
monitor_dg_tmp rename to monitor;
|
|
||||||
|
|
||||||
create index user_id on monitor (user_id);
|
|
||||||
|
|
||||||
COMMIT;
|
|
||||||
|
|
||||||
PRAGMA foreign_keys = on;
|
|
|
@ -1,74 +0,0 @@
|
||||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
|
||||||
PRAGMA foreign_keys = off;
|
|
||||||
|
|
||||||
BEGIN TRANSACTION;
|
|
||||||
|
|
||||||
create table monitor_dg_tmp (
|
|
||||||
id INTEGER not null primary key autoincrement,
|
|
||||||
name VARCHAR(150),
|
|
||||||
active BOOLEAN default 1 not null,
|
|
||||||
user_id INTEGER references user on update cascade on delete
|
|
||||||
set
|
|
||||||
null,
|
|
||||||
interval INTEGER default 20 not null,
|
|
||||||
url TEXT,
|
|
||||||
type VARCHAR(20),
|
|
||||||
weight INTEGER default 2000,
|
|
||||||
hostname VARCHAR(255),
|
|
||||||
port INTEGER,
|
|
||||||
created_date DATETIME default (DATETIME('now')) not null,
|
|
||||||
keyword VARCHAR(255),
|
|
||||||
maxretries INTEGER NOT NULL DEFAULT 0,
|
|
||||||
ignore_tls BOOLEAN default 0 not null,
|
|
||||||
upside_down BOOLEAN default 0 not null,
|
|
||||||
maxredirects INTEGER default 10 not null,
|
|
||||||
accepted_statuscodes_json TEXT default '["200-299"]' not null
|
|
||||||
);
|
|
||||||
|
|
||||||
insert into
|
|
||||||
monitor_dg_tmp(
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
active,
|
|
||||||
user_id,
|
|
||||||
interval,
|
|
||||||
url,
|
|
||||||
type,
|
|
||||||
weight,
|
|
||||||
hostname,
|
|
||||||
port,
|
|
||||||
created_date,
|
|
||||||
keyword,
|
|
||||||
maxretries,
|
|
||||||
ignore_tls,
|
|
||||||
upside_down
|
|
||||||
)
|
|
||||||
select
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
active,
|
|
||||||
user_id,
|
|
||||||
interval,
|
|
||||||
url,
|
|
||||||
type,
|
|
||||||
weight,
|
|
||||||
hostname,
|
|
||||||
port,
|
|
||||||
created_date,
|
|
||||||
keyword,
|
|
||||||
maxretries,
|
|
||||||
ignore_tls,
|
|
||||||
upside_down
|
|
||||||
from
|
|
||||||
monitor;
|
|
||||||
|
|
||||||
drop table monitor;
|
|
||||||
|
|
||||||
alter table
|
|
||||||
monitor_dg_tmp rename to monitor;
|
|
||||||
|
|
||||||
create index user_id on monitor (user_id);
|
|
||||||
|
|
||||||
COMMIT;
|
|
||||||
|
|
||||||
PRAGMA foreign_keys = on;
|
|
|
@ -1,10 +0,0 @@
|
||||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
|
||||||
BEGIN TRANSACTION;
|
|
||||||
|
|
||||||
ALTER TABLE monitor
|
|
||||||
ADD dns_resolve_type VARCHAR(5);
|
|
||||||
|
|
||||||
ALTER TABLE monitor
|
|
||||||
ADD dns_resolve_server VARCHAR(255);
|
|
||||||
|
|
||||||
COMMIT;
|
|
|
@ -1,7 +0,0 @@
|
||||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
|
||||||
BEGIN TRANSACTION;
|
|
||||||
|
|
||||||
ALTER TABLE monitor
|
|
||||||
ADD dns_last_result VARCHAR(255);
|
|
||||||
|
|
||||||
COMMIT;
|
|
|
@ -1,7 +0,0 @@
|
||||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
|
||||||
BEGIN TRANSACTION;
|
|
||||||
|
|
||||||
ALTER TABLE notification
|
|
||||||
ADD is_default BOOLEAN default 0 NOT NULL;
|
|
||||||
|
|
||||||
COMMIT;
|
|
|
@ -1,8 +0,0 @@
|
||||||
# DON'T UPDATE TO alpine3.13, 1.14, see #41.
|
|
||||||
FROM node:14-alpine3.12
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Install apprise, iputils for non-root ping, setpriv
|
|
||||||
RUN apk add --no-cache iputils setpriv dumb-init python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib && \
|
|
||||||
pip3 --no-cache-dir install apprise==0.9.6 && \
|
|
||||||
rm -rf /root/.cache
|
|
|
@ -1,12 +0,0 @@
|
||||||
# DON'T UPDATE TO node:14-bullseye-slim, see #372.
|
|
||||||
# If the image changed, the second stage image should be changed too
|
|
||||||
FROM node:14-buster-slim
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Install Apprise, add sqlite3 cli for debugging in the future, iputils-ping for ping, util-linux for setpriv
|
|
||||||
# Stupid python3 and python3-pip actually install a lot of useless things into Debian, specify --no-install-recommends to skip them, make the base even smaller than alpine!
|
|
||||||
RUN apt update && \
|
|
||||||
apt --yes --no-install-recommends install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \
|
|
||||||
sqlite3 iputils-ping util-linux dumb-init && \
|
|
||||||
pip3 --no-cache-dir install apprise==0.9.6 && \
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
|
@ -1,13 +0,0 @@
|
||||||
# Simple docker-composer.yml
|
|
||||||
# You can change your port or volume location
|
|
||||||
|
|
||||||
version: '3.3'
|
|
||||||
|
|
||||||
services:
|
|
||||||
uptime-kuma:
|
|
||||||
image: louislam/uptime-kuma
|
|
||||||
container_name: uptime-kuma
|
|
||||||
volumes:
|
|
||||||
- ./uptime-kuma:/app/data
|
|
||||||
ports:
|
|
||||||
- 3001:3001
|
|
|
@ -1,52 +0,0 @@
|
||||||
FROM louislam/uptime-kuma:base-debian AS build
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
RUN npm ci --production && \
|
|
||||||
chmod +x /app/extra/entrypoint.sh
|
|
||||||
|
|
||||||
|
|
||||||
FROM louislam/uptime-kuma:base-debian AS release
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy app files from build layer
|
|
||||||
COPY --from=build /app /app
|
|
||||||
|
|
||||||
EXPOSE 3001
|
|
||||||
VOLUME ["/app/data"]
|
|
||||||
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js
|
|
||||||
ENTRYPOINT ["/usr/bin/dumb-init", "--", "extra/entrypoint.sh"]
|
|
||||||
CMD ["node", "server/server.js"]
|
|
||||||
|
|
||||||
|
|
||||||
FROM release AS nightly
|
|
||||||
RUN npm run mark-as-nightly
|
|
||||||
|
|
||||||
|
|
||||||
# Upload the artifact to Github
|
|
||||||
FROM louislam/uptime-kuma:base-debian AS upload-artifact
|
|
||||||
WORKDIR /
|
|
||||||
RUN apt update && \
|
|
||||||
apt --yes install curl file
|
|
||||||
|
|
||||||
COPY --from=build /app /app
|
|
||||||
|
|
||||||
ARG VERSION
|
|
||||||
ARG GITHUB_TOKEN
|
|
||||||
ARG TARGETARCH
|
|
||||||
ARG PLATFORM=debian
|
|
||||||
ARG FILE=$PLATFORM-$TARGETARCH-$VERSION.tar.gz
|
|
||||||
ARG DIST=dist.tar.gz
|
|
||||||
|
|
||||||
RUN chmod +x /app/extra/upload-github-release-asset.sh
|
|
||||||
|
|
||||||
# Full Build
|
|
||||||
# RUN tar -zcvf $FILE app
|
|
||||||
# RUN /app/extra/upload-github-release-asset.sh github_api_token=$GITHUB_TOKEN owner=louislam repo=uptime-kuma tag=$VERSION filename=$FILE
|
|
||||||
|
|
||||||
# Dist only
|
|
||||||
RUN cd /app && tar -zcvf $DIST dist
|
|
||||||
RUN /app/extra/upload-github-release-asset.sh github_api_token=$GITHUB_TOKEN owner=louislam repo=uptime-kuma tag=$VERSION filename=/app/$DIST
|
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
FROM louislam/uptime-kuma:base-alpine AS build
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
RUN npm ci --production && \
|
|
||||||
chmod +x /app/extra/entrypoint.sh
|
|
||||||
|
|
||||||
|
|
||||||
FROM louislam/uptime-kuma:base-alpine AS release
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy app files from build layer
|
|
||||||
COPY --from=build /app /app
|
|
||||||
|
|
||||||
EXPOSE 3001
|
|
||||||
VOLUME ["/app/data"]
|
|
||||||
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js
|
|
||||||
ENTRYPOINT ["/usr/bin/dumb-init", "--", "extra/entrypoint.sh"]
|
|
||||||
CMD ["node", "server/server.js"]
|
|
||||||
|
|
||||||
|
|
||||||
FROM release AS nightly
|
|
||||||
RUN npm run mark-as-nightly
|
|
|
@ -1,6 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
apps: [{
|
|
||||||
name: "uptime-kuma",
|
|
||||||
script: "./server/server.js",
|
|
||||||
}]
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
})();
|
|
|
@ -1,2 +0,0 @@
|
||||||
# Must enable File Sharing in Docker Desktop
|
|
||||||
docker run -it --rm -v ${pwd}:/app louislam/batsh /usr/bin/batsh bash --output ./install.sh ./extra/install.batsh
|
|
|
@ -1,59 +0,0 @@
|
||||||
console.log("Downloading dist");
|
|
||||||
const https = require("https");
|
|
||||||
const tar = require("tar");
|
|
||||||
|
|
||||||
const packageJSON = require("../package.json");
|
|
||||||
const fs = require("fs");
|
|
||||||
const version = packageJSON.version;
|
|
||||||
|
|
||||||
const filename = "dist.tar.gz";
|
|
||||||
|
|
||||||
const url = `https://github.com/louislam/uptime-kuma/releases/download/${version}/${filename}`;
|
|
||||||
download(url);
|
|
||||||
|
|
||||||
function download(url) {
|
|
||||||
console.log(url);
|
|
||||||
|
|
||||||
https.get(url, (response) => {
|
|
||||||
if (response.statusCode === 200) {
|
|
||||||
console.log("Extracting dist...");
|
|
||||||
|
|
||||||
if (fs.existsSync("./dist")) {
|
|
||||||
|
|
||||||
if (fs.existsSync("./dist-backup")) {
|
|
||||||
fs.rmdirSync("./dist-backup", {
|
|
||||||
recursive: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.renameSync("./dist", "./dist-backup");
|
|
||||||
}
|
|
||||||
|
|
||||||
const tarStream = tar.x({
|
|
||||||
cwd: "./",
|
|
||||||
});
|
|
||||||
|
|
||||||
tarStream.on("close", () => {
|
|
||||||
if (fs.existsSync("./dist-backup")) {
|
|
||||||
fs.rmdirSync("./dist-backup", {
|
|
||||||
recursive: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("Done");
|
|
||||||
});
|
|
||||||
|
|
||||||
tarStream.on("error", () => {
|
|
||||||
if (fs.existsSync("./dist-backup")) {
|
|
||||||
fs.renameSync("./dist-backup", "./dist");
|
|
||||||
}
|
|
||||||
console.error("Error from tarStream");
|
|
||||||
});
|
|
||||||
|
|
||||||
response.pipe(tarStream);
|
|
||||||
} else if (response.statusCode === 302) {
|
|
||||||
download(response.headers.location);
|
|
||||||
} else {
|
|
||||||
console.log("dist not found");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
#!/usr/bin/env sh
|
|
||||||
|
|
||||||
# set -e Exit the script if an error happens
|
|
||||||
set -e
|
|
||||||
PUID=${PUID=0}
|
|
||||||
PGID=${PGID=0}
|
|
||||||
|
|
||||||
files_ownership () {
|
|
||||||
# -h Changes the ownership of an encountered symbolic link and not that of the file or directory pointed to by the symbolic link.
|
|
||||||
# -R Recursively descends the specified directories
|
|
||||||
# -c Like verbose but report only when a change is made
|
|
||||||
chown -hRc "$PUID":"$PGID" /app/data
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "==> Performing startup jobs and maintenance tasks"
|
|
||||||
files_ownership
|
|
||||||
|
|
||||||
echo "==> Starting application with user $PUID group $PGID"
|
|
||||||
|
|
||||||
# --clear-groups Clear supplementary groups.
|
|
||||||
exec setpriv --reuid "$PUID" --regid "$PGID" --clear-groups "$@"
|
|
|
@ -1,50 +0,0 @@
|
||||||
/*
|
|
||||||
* This script should be run after a period of time (180s), because the server may need some time to prepare.
|
|
||||||
*/
|
|
||||||
const { FBSD } = require("../server/util-server");
|
|
||||||
|
|
||||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
||||||
|
|
||||||
let client;
|
|
||||||
|
|
||||||
const sslKey = process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || undefined;
|
|
||||||
const sslCert = process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || undefined;
|
|
||||||
|
|
||||||
if (sslKey && sslCert) {
|
|
||||||
client = require("https");
|
|
||||||
} else {
|
|
||||||
client = require("http");
|
|
||||||
}
|
|
||||||
|
|
||||||
// If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise.
|
|
||||||
// Dual-stack support for (::)
|
|
||||||
let hostname = process.env.UPTIME_KUMA_HOST;
|
|
||||||
|
|
||||||
// Also read HOST if not FreeBSD, as HOST is a system environment variable in FreeBSD
|
|
||||||
if (!hostname && !FBSD) {
|
|
||||||
hostname = process.env.HOST;
|
|
||||||
}
|
|
||||||
|
|
||||||
const port = parseInt(process.env.UPTIME_KUMA_PORT || process.env.PORT || 3001);
|
|
||||||
|
|
||||||
let options = {
|
|
||||||
host: hostname || "127.0.0.1",
|
|
||||||
port: port,
|
|
||||||
timeout: 28 * 1000,
|
|
||||||
};
|
|
||||||
|
|
||||||
let request = client.request(options, (res) => {
|
|
||||||
console.log(`Health Check OK [Res Code: ${res.statusCode}]`);
|
|
||||||
if (res.statusCode === 302) {
|
|
||||||
process.exit(0);
|
|
||||||
} else {
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
request.on("error", function (err) {
|
|
||||||
console.error("Health Check ERROR");
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
request.end();
|
|
|
@ -1,245 +0,0 @@
|
||||||
// install.sh is generated by ./extra/install.batsh, do not modify it directly.
|
|
||||||
// "npm run compile-install-script" to compile install.sh
|
|
||||||
// The command is working on Windows PowerShell and Docker for Windows only.
|
|
||||||
|
|
||||||
|
|
||||||
// curl -o kuma_install.sh https://raw.githubusercontent.com/louislam/uptime-kuma/master/install.sh && sudo bash kuma_install.sh
|
|
||||||
println("=====================");
|
|
||||||
println("Uptime Kuma Installer");
|
|
||||||
println("=====================");
|
|
||||||
println("Supported OS: CentOS 7/8, Ubuntu >= 16.04 and Debian");
|
|
||||||
println("---------------------------------------");
|
|
||||||
println("This script is designed for Linux and basic usage.");
|
|
||||||
println("For advanced usage, please go to https://github.com/louislam/uptime-kuma/wiki/Installation");
|
|
||||||
println("---------------------------------------");
|
|
||||||
println("");
|
|
||||||
println("Local - Install Uptime Kuma in your current machine with git, Node.js 14 and pm2");
|
|
||||||
println("Docker - Install Uptime Kuma Docker container");
|
|
||||||
println("");
|
|
||||||
|
|
||||||
if ("$1" != "") {
|
|
||||||
type = "$1";
|
|
||||||
} else {
|
|
||||||
call("read", "-p", "Which installation method do you prefer? [DOCKER/local]: ", "type");
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultPort = "3001";
|
|
||||||
|
|
||||||
function checkNode() {
|
|
||||||
bash("nodeVersion=$(node -e 'console.log(process.versions.node.split(`.`)[0])')");
|
|
||||||
println("Node Version: " ++ nodeVersion);
|
|
||||||
|
|
||||||
if (nodeVersion < "12") {
|
|
||||||
println("Error: Required Node.js 14");
|
|
||||||
call("exit", "1");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nodeVersion == "12") {
|
|
||||||
println("Warning: NodeJS " ++ nodeVersion ++ " is not tested.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function deb() {
|
|
||||||
bash("nodeCheck=$(node -v)");
|
|
||||||
bash("apt --yes update");
|
|
||||||
|
|
||||||
if (nodeCheck != "") {
|
|
||||||
checkNode();
|
|
||||||
} else {
|
|
||||||
|
|
||||||
// Old nodejs binary name is "nodejs"
|
|
||||||
bash("check=$(nodejs --version)");
|
|
||||||
if (check != "") {
|
|
||||||
println("Error: 'node' command is not found, but 'nodejs' command is found. Your NodeJS should be too old.");
|
|
||||||
bash("exit 1");
|
|
||||||
}
|
|
||||||
|
|
||||||
bash("curlCheck=$(curl --version)");
|
|
||||||
if (curlCheck == "") {
|
|
||||||
println("Installing Curl");
|
|
||||||
bash("apt --yes install curl");
|
|
||||||
}
|
|
||||||
|
|
||||||
println("Installing Node.js 14");
|
|
||||||
bash("curl -sL https://deb.nodesource.com/setup_14.x | bash - > log.txt");
|
|
||||||
bash("apt --yes install nodejs");
|
|
||||||
bash("node -v");
|
|
||||||
|
|
||||||
bash("nodeCheckAgain=$(node -v)");
|
|
||||||
|
|
||||||
if (nodeCheckAgain == "") {
|
|
||||||
println("Error during Node.js installation");
|
|
||||||
bash("exit 1");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bash("check=$(git --version)");
|
|
||||||
if (check == "") {
|
|
||||||
println("Installing Git");
|
|
||||||
bash("apt --yes install git");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type == "local") {
|
|
||||||
defaultInstallPath = "/opt/uptime-kuma";
|
|
||||||
|
|
||||||
if (exists("/etc/redhat-release")) {
|
|
||||||
os = call("cat", "/etc/redhat-release");
|
|
||||||
distribution = "rhel";
|
|
||||||
|
|
||||||
} else if (exists("/etc/issue")) {
|
|
||||||
bash("os=$(head -n1 /etc/issue | cut -f 1 -d ' ')");
|
|
||||||
if (os == "Ubuntu") {
|
|
||||||
distribution = "ubuntu";
|
|
||||||
}
|
|
||||||
if (os == "Debian") {
|
|
||||||
distribution = "debian";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bash("arch=$(uname -i)");
|
|
||||||
|
|
||||||
println("Your OS: " ++ os);
|
|
||||||
println("Distribution: " ++ distribution);
|
|
||||||
println("Arch: " ++ arch);
|
|
||||||
|
|
||||||
if ("$3" != "") {
|
|
||||||
port = "$3";
|
|
||||||
} else {
|
|
||||||
call("read", "-p", "Listening Port [$defaultPort]: ", "port");
|
|
||||||
|
|
||||||
if (port == "") {
|
|
||||||
port = defaultPort;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("$2" != "") {
|
|
||||||
installPath = "$2";
|
|
||||||
} else {
|
|
||||||
call("read", "-p", "Installation Path [$defaultInstallPath]: ", "installPath");
|
|
||||||
|
|
||||||
if (installPath == "") {
|
|
||||||
installPath = defaultInstallPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CentOS
|
|
||||||
if (distribution == "rhel") {
|
|
||||||
bash("nodeCheck=$(node -v)");
|
|
||||||
|
|
||||||
if (nodeCheck != "") {
|
|
||||||
checkNode();
|
|
||||||
} else {
|
|
||||||
|
|
||||||
bash("curlCheck=$(curl --version)");
|
|
||||||
if (curlCheck == "") {
|
|
||||||
println("Installing Curl");
|
|
||||||
bash("yum -y -q install curl");
|
|
||||||
}
|
|
||||||
|
|
||||||
println("Installing Node.js 14");
|
|
||||||
bash("curl -sL https://rpm.nodesource.com/setup_14.x | bash - > log.txt");
|
|
||||||
bash("yum install -y -q nodejs");
|
|
||||||
bash("node -v");
|
|
||||||
|
|
||||||
bash("nodeCheckAgain=$(node -v)");
|
|
||||||
|
|
||||||
if (nodeCheckAgain == "") {
|
|
||||||
println("Error during Node.js installation");
|
|
||||||
bash("exit 1");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bash("check=$(git --version)");
|
|
||||||
if (check == "") {
|
|
||||||
println("Installing Git");
|
|
||||||
bash("yum -y -q install git");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ubuntu
|
|
||||||
} else if (distribution == "ubuntu") {
|
|
||||||
deb();
|
|
||||||
|
|
||||||
// Debian
|
|
||||||
} else if (distribution == "debian") {
|
|
||||||
deb();
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// Unknown distribution
|
|
||||||
error = 0;
|
|
||||||
|
|
||||||
bash("check=$(git --version)");
|
|
||||||
if (check == "") {
|
|
||||||
error = 1;
|
|
||||||
println("Error: git is missing");
|
|
||||||
}
|
|
||||||
|
|
||||||
bash("check=$(node -v)");
|
|
||||||
if (check == "") {
|
|
||||||
error = 1;
|
|
||||||
println("Error: node is missing");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error > 0) {
|
|
||||||
println("Please install above missing software");
|
|
||||||
bash("exit 1");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bash("check=$(pm2 --version)");
|
|
||||||
if (check == "") {
|
|
||||||
println("Installing PM2");
|
|
||||||
bash("npm install pm2 -g");
|
|
||||||
bash("pm2 startup");
|
|
||||||
}
|
|
||||||
|
|
||||||
bash("mkdir -p $installPath");
|
|
||||||
bash("cd $installPath");
|
|
||||||
bash("git clone https://github.com/louislam/uptime-kuma.git .");
|
|
||||||
bash("npm run setup");
|
|
||||||
|
|
||||||
bash("pm2 start server/server.js --name uptime-kuma -- --port=$port");
|
|
||||||
|
|
||||||
} else {
|
|
||||||
defaultVolume = "uptime-kuma";
|
|
||||||
|
|
||||||
bash("check=$(docker -v)");
|
|
||||||
if (check == "") {
|
|
||||||
println("Error: docker is not found!");
|
|
||||||
bash("exit 1");
|
|
||||||
}
|
|
||||||
|
|
||||||
bash("check=$(docker info)");
|
|
||||||
|
|
||||||
bash("if [[ \"$check\" == *\"Is the docker daemon running\"* ]]; then
|
|
||||||
\"echo\" \"Error: docker is not running\"
|
|
||||||
\"exit\" \"1\"
|
|
||||||
fi");
|
|
||||||
|
|
||||||
if ("$3" != "") {
|
|
||||||
port = "$3";
|
|
||||||
} else {
|
|
||||||
call("read", "-p", "Expose Port [$defaultPort]: ", "port");
|
|
||||||
|
|
||||||
if (port == "") {
|
|
||||||
port = defaultPort;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("$2" != "") {
|
|
||||||
volume = "$2";
|
|
||||||
} else {
|
|
||||||
call("read", "-p", "Volume Name [$defaultVolume]: ", "volume");
|
|
||||||
|
|
||||||
if (volume == "") {
|
|
||||||
volume = defaultVolume;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
println("Port: $port");
|
|
||||||
println("Volume: $volume");
|
|
||||||
bash("docker volume create $volume");
|
|
||||||
bash("docker run -d --restart=always -p $port:3001 -v $volume:/app/data --name uptime-kuma louislam/uptime-kuma:1");
|
|
||||||
}
|
|
||||||
|
|
||||||
println("http://localhost:$port");
|
|
|
@ -1,24 +0,0 @@
|
||||||
const pkg = require("../package.json");
|
|
||||||
const fs = require("fs");
|
|
||||||
const util = require("../src/util");
|
|
||||||
|
|
||||||
util.polyfill();
|
|
||||||
|
|
||||||
const oldVersion = pkg.version
|
|
||||||
const newVersion = oldVersion + "-nightly"
|
|
||||||
|
|
||||||
console.log("Old Version: " + oldVersion)
|
|
||||||
console.log("New Version: " + newVersion)
|
|
||||||
|
|
||||||
if (newVersion) {
|
|
||||||
// Process package.json
|
|
||||||
pkg.version = newVersion
|
|
||||||
pkg.scripts.setup = pkg.scripts.setup.replaceAll(oldVersion, newVersion)
|
|
||||||
pkg.scripts["build-docker"] = pkg.scripts["build-docker"].replaceAll(oldVersion, newVersion)
|
|
||||||
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n")
|
|
||||||
|
|
||||||
// Process README.md
|
|
||||||
if (fs.existsSync("README.md")) {
|
|
||||||
fs.writeFileSync("README.md", fs.readFileSync("README.md", "utf8").replaceAll(oldVersion, newVersion))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,60 +0,0 @@
|
||||||
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,70 +0,0 @@
|
||||||
console.log("== Uptime Kuma Reset Password Tool ==");
|
|
||||||
|
|
||||||
console.log("Loading the database");
|
|
||||||
|
|
||||||
const Database = require("../server/database");
|
|
||||||
const { R } = require("redbean-node");
|
|
||||||
const readline = require("readline");
|
|
||||||
const { initJWTSecret } = require("../server/util-server");
|
|
||||||
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);
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
let password = await question("New Password: ");
|
|
||||||
let confirmPassword = await question("Confirm New Password: ");
|
|
||||||
|
|
||||||
if (password === confirmPassword) {
|
|
||||||
await user.resetPassword(password);
|
|
||||||
|
|
||||||
// Reset all sessions by reset jwt secret
|
|
||||||
await initJWTSecret();
|
|
||||||
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
console.log("Passwords do not match, please try again.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log("Password reset 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,144 +0,0 @@
|
||||||
/*
|
|
||||||
* Simple DNS Server
|
|
||||||
* For testing DNS monitoring type, dev only
|
|
||||||
*/
|
|
||||||
const dns2 = require("dns2");
|
|
||||||
|
|
||||||
const { Packet } = dns2;
|
|
||||||
|
|
||||||
const server = dns2.createServer({
|
|
||||||
udp: true
|
|
||||||
});
|
|
||||||
|
|
||||||
server.on("request", (request, send, rinfo) => {
|
|
||||||
for (let question of request.questions) {
|
|
||||||
console.log(question.name, type(question.type), question.class);
|
|
||||||
|
|
||||||
const response = Packet.createResponseFromRequest(request);
|
|
||||||
|
|
||||||
if (question.name === "existing.com") {
|
|
||||||
|
|
||||||
if (question.type === Packet.TYPE.A) {
|
|
||||||
response.answers.push({
|
|
||||||
name: question.name,
|
|
||||||
type: question.type,
|
|
||||||
class: question.class,
|
|
||||||
ttl: 300,
|
|
||||||
address: "1.2.3.4"
|
|
||||||
});
|
|
||||||
} if (question.type === Packet.TYPE.AAAA) {
|
|
||||||
response.answers.push({
|
|
||||||
name: question.name,
|
|
||||||
type: question.type,
|
|
||||||
class: question.class,
|
|
||||||
ttl: 300,
|
|
||||||
address: "fe80::::1234:5678:abcd:ef00",
|
|
||||||
});
|
|
||||||
} else if (question.type === Packet.TYPE.CNAME) {
|
|
||||||
response.answers.push({
|
|
||||||
name: question.name,
|
|
||||||
type: question.type,
|
|
||||||
class: question.class,
|
|
||||||
ttl: 300,
|
|
||||||
domain: "cname1.existing.com",
|
|
||||||
});
|
|
||||||
} else if (question.type === Packet.TYPE.MX) {
|
|
||||||
response.answers.push({
|
|
||||||
name: question.name,
|
|
||||||
type: question.type,
|
|
||||||
class: question.class,
|
|
||||||
ttl: 300,
|
|
||||||
exchange: "mx1.existing.com",
|
|
||||||
priority: 5
|
|
||||||
});
|
|
||||||
} else if (question.type === Packet.TYPE.NS) {
|
|
||||||
response.answers.push({
|
|
||||||
name: question.name,
|
|
||||||
type: question.type,
|
|
||||||
class: question.class,
|
|
||||||
ttl: 300,
|
|
||||||
ns: "ns1.existing.com",
|
|
||||||
});
|
|
||||||
} else if (question.type === Packet.TYPE.SOA) {
|
|
||||||
response.answers.push({
|
|
||||||
name: question.name,
|
|
||||||
type: question.type,
|
|
||||||
class: question.class,
|
|
||||||
ttl: 300,
|
|
||||||
primary: "existing.com",
|
|
||||||
admin: "admin@existing.com",
|
|
||||||
serial: 2021082701,
|
|
||||||
refresh: 300,
|
|
||||||
retry: 3,
|
|
||||||
expiration: 10,
|
|
||||||
minimum: 10,
|
|
||||||
});
|
|
||||||
} else if (question.type === Packet.TYPE.SRV) {
|
|
||||||
response.answers.push({
|
|
||||||
name: question.name,
|
|
||||||
type: question.type,
|
|
||||||
class: question.class,
|
|
||||||
ttl: 300,
|
|
||||||
priority: 5,
|
|
||||||
weight: 5,
|
|
||||||
port: 8080,
|
|
||||||
target: "srv1.existing.com",
|
|
||||||
});
|
|
||||||
} else if (question.type === Packet.TYPE.TXT) {
|
|
||||||
response.answers.push({
|
|
||||||
name: question.name,
|
|
||||||
type: question.type,
|
|
||||||
class: question.class,
|
|
||||||
ttl: 300,
|
|
||||||
data: "#v=spf1 include:_spf.existing.com ~all",
|
|
||||||
});
|
|
||||||
} else if (question.type === Packet.TYPE.CAA) {
|
|
||||||
response.answers.push({
|
|
||||||
name: question.name,
|
|
||||||
type: question.type,
|
|
||||||
class: question.class,
|
|
||||||
ttl: 300,
|
|
||||||
flags: 0,
|
|
||||||
tag: "issue",
|
|
||||||
value: "ca.existing.com",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if (question.name === "4.3.2.1.in-addr.arpa") {
|
|
||||||
if (question.type === Packet.TYPE.PTR) {
|
|
||||||
response.answers.push({
|
|
||||||
name: question.name,
|
|
||||||
type: question.type,
|
|
||||||
class: question.class,
|
|
||||||
ttl: 300,
|
|
||||||
domain: "ptr1.existing.com",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
send(response);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
server.on("listening", () => {
|
|
||||||
console.log("Listening");
|
|
||||||
console.log(server.addresses());
|
|
||||||
});
|
|
||||||
|
|
||||||
server.on("close", () => {
|
|
||||||
console.log("server closed");
|
|
||||||
});
|
|
||||||
|
|
||||||
server.listen({
|
|
||||||
udp: 5300
|
|
||||||
});
|
|
||||||
|
|
||||||
function type(code) {
|
|
||||||
for (let name in Packet.TYPE) {
|
|
||||||
if (Packet.TYPE[name] === code) {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
3
extra/update-language-files/.gitignore
vendored
|
@ -1,3 +0,0 @@
|
||||||
package-lock.json
|
|
||||||
test.js
|
|
||||||
languages/
|
|
|
@ -1,86 +0,0 @@
|
||||||
// Need to use ES6 to read language files
|
|
||||||
|
|
||||||
import fs from "fs";
|
|
||||||
import path from "path";
|
|
||||||
import util from "util";
|
|
||||||
|
|
||||||
// https://stackoverflow.com/questions/13786160/copy-folder-recursively-in-node-js
|
|
||||||
/**
|
|
||||||
* Look ma, it's cp -R.
|
|
||||||
* @param {string} src The path to the thing to copy.
|
|
||||||
* @param {string} dest The path to the new copy.
|
|
||||||
*/
|
|
||||||
const copyRecursiveSync = function (src, dest) {
|
|
||||||
let exists = fs.existsSync(src);
|
|
||||||
let stats = exists && fs.statSync(src);
|
|
||||||
let isDirectory = exists && stats.isDirectory();
|
|
||||||
|
|
||||||
if (isDirectory) {
|
|
||||||
fs.mkdirSync(dest);
|
|
||||||
fs.readdirSync(src).forEach(function (childItemName) {
|
|
||||||
copyRecursiveSync(path.join(src, childItemName),
|
|
||||||
path.join(dest, childItemName));
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
fs.copyFileSync(src, dest);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log("Arguments:", process.argv);
|
|
||||||
const baseLangCode = process.argv[2] || "en";
|
|
||||||
console.log("Base Lang: " + baseLangCode);
|
|
||||||
if (fs.existsSync("./languages")) {
|
|
||||||
fs.rmdirSync("./languages", { recursive: true });
|
|
||||||
}
|
|
||||||
copyRecursiveSync("../../src/languages", "./languages");
|
|
||||||
|
|
||||||
const en = (await import("./languages/en.js")).default;
|
|
||||||
const baseLang = (await import(`./languages/${baseLangCode}.js`)).default;
|
|
||||||
const files = fs.readdirSync("./languages");
|
|
||||||
console.log("Files:", files);
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
if (!file.endsWith(".js")) {
|
|
||||||
console.log("Skipping " + file);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Processing " + file);
|
|
||||||
const lang = await import("./languages/" + file);
|
|
||||||
|
|
||||||
let obj;
|
|
||||||
|
|
||||||
if (lang.default) {
|
|
||||||
obj = lang.default;
|
|
||||||
} else {
|
|
||||||
console.log("Empty file");
|
|
||||||
obj = {
|
|
||||||
languageName: "<Your Language name in your language (not in English)>"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// En first
|
|
||||||
for (const key in en) {
|
|
||||||
if (! obj[key]) {
|
|
||||||
obj[key] = en[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (baseLang !== en) {
|
|
||||||
// Base second
|
|
||||||
for (const key in baseLang) {
|
|
||||||
if (! obj[key]) {
|
|
||||||
obj[key] = key;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const code = "export default " + util.inspect(obj, {
|
|
||||||
depth: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
fs.writeFileSync(`../../src/languages/${file}`, code);
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.rmdirSync("./languages", { recursive: true });
|
|
||||||
console.log("Done. Fixing formatting by ESLint...");
|
|
|
@ -1,12 +0,0 @@
|
||||||
{
|
|
||||||
"name": "update-language-files",
|
|
||||||
"type": "module",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "",
|
|
||||||
"main": "index.js",
|
|
||||||
"scripts": {
|
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
|
||||||
},
|
|
||||||
"author": "",
|
|
||||||
"license": "ISC"
|
|
||||||
}
|
|
|
@ -1,100 +0,0 @@
|
||||||
const pkg = require("../package.json");
|
|
||||||
const fs = require("fs");
|
|
||||||
const child_process = require("child_process");
|
|
||||||
const util = require("../src/util");
|
|
||||||
|
|
||||||
util.polyfill();
|
|
||||||
|
|
||||||
const oldVersion = pkg.version;
|
|
||||||
const newVersion = process.argv[2];
|
|
||||||
|
|
||||||
console.log("Old Version: " + oldVersion);
|
|
||||||
console.log("New Version: " + newVersion);
|
|
||||||
|
|
||||||
if (! newVersion) {
|
|
||||||
console.error("invalid version");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const exists = tagExists(newVersion);
|
|
||||||
|
|
||||||
if (! exists) {
|
|
||||||
|
|
||||||
// Process package.json
|
|
||||||
pkg.version = newVersion;
|
|
||||||
pkg.scripts.setup = pkg.scripts.setup.replaceAll(oldVersion, newVersion);
|
|
||||||
pkg.scripts["build-docker"] = pkg.scripts["build-docker"].replaceAll(oldVersion, newVersion);
|
|
||||||
pkg.scripts["build-docker-alpine"] = pkg.scripts["build-docker-alpine"].replaceAll(oldVersion, newVersion);
|
|
||||||
pkg.scripts["build-docker-debian"] = pkg.scripts["build-docker-debian"].replaceAll(oldVersion, newVersion);
|
|
||||||
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
|
|
||||||
|
|
||||||
commit(newVersion);
|
|
||||||
tag(newVersion);
|
|
||||||
|
|
||||||
updateWiki(oldVersion, newVersion);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
console.log("version exists");
|
|
||||||
}
|
|
||||||
|
|
||||||
function commit(version) {
|
|
||||||
let msg = "update to " + version;
|
|
||||||
|
|
||||||
let res = child_process.spawnSync("git", ["commit", "-m", msg, "-a"]);
|
|
||||||
let stdout = res.stdout.toString().trim();
|
|
||||||
console.log(stdout);
|
|
||||||
|
|
||||||
if (stdout.includes("no changes added to commit")) {
|
|
||||||
throw new Error("commit error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function tag(version) {
|
|
||||||
let res = child_process.spawnSync("git", ["tag", version]);
|
|
||||||
console.log(res.stdout.toString().trim());
|
|
||||||
}
|
|
||||||
|
|
||||||
function tagExists(version) {
|
|
||||||
if (! version) {
|
|
||||||
throw new Error("invalid version");
|
|
||||||
}
|
|
||||||
|
|
||||||
let res = child_process.spawnSync("git", ["tag", "-l", version]);
|
|
||||||
|
|
||||||
return res.stdout.toString().trim() === version;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateWiki(oldVersion, newVersion) {
|
|
||||||
const wikiDir = "./tmp/wiki";
|
|
||||||
const howToUpdateFilename = "./tmp/wiki/🆙-How-to-Update.md";
|
|
||||||
|
|
||||||
safeDelete(wikiDir);
|
|
||||||
|
|
||||||
child_process.spawnSync("git", ["clone", "https://github.com/louislam/uptime-kuma.wiki.git", wikiDir]);
|
|
||||||
let content = fs.readFileSync(howToUpdateFilename).toString();
|
|
||||||
content = content.replaceAll(`git checkout ${oldVersion}`, `git checkout ${newVersion}`);
|
|
||||||
fs.writeFileSync(howToUpdateFilename, content);
|
|
||||||
|
|
||||||
child_process.spawnSync("git", ["add", "-A"], {
|
|
||||||
cwd: wikiDir,
|
|
||||||
});
|
|
||||||
|
|
||||||
child_process.spawnSync("git", ["commit", "-m", `Update to ${newVersion} from ${oldVersion}`], {
|
|
||||||
cwd: wikiDir,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("Pushing to Github");
|
|
||||||
child_process.spawnSync("git", ["push"], {
|
|
||||||
cwd: wikiDir,
|
|
||||||
});
|
|
||||||
|
|
||||||
safeDelete(wikiDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
function safeDelete(dir) {
|
|
||||||
if (fs.existsSync(dir)) {
|
|
||||||
fs.rmdirSync(dir, {
|
|
||||||
recursive: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,64 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
#
|
|
||||||
# Author: Stefan Buck
|
|
||||||
# License: MIT
|
|
||||||
# https://gist.github.com/stefanbuck/ce788fee19ab6eb0b4447a85fc99f447
|
|
||||||
#
|
|
||||||
#
|
|
||||||
# This script accepts the following parameters:
|
|
||||||
#
|
|
||||||
# * owner
|
|
||||||
# * repo
|
|
||||||
# * tag
|
|
||||||
# * filename
|
|
||||||
# * github_api_token
|
|
||||||
#
|
|
||||||
# Script to upload a release asset using the GitHub API v3.
|
|
||||||
#
|
|
||||||
# Example:
|
|
||||||
#
|
|
||||||
# upload-github-release-asset.sh github_api_token=TOKEN owner=stefanbuck repo=playground tag=v0.1.0 filename=./build.zip
|
|
||||||
#
|
|
||||||
|
|
||||||
# Check dependencies.
|
|
||||||
set -e
|
|
||||||
xargs=$(which gxargs || which xargs)
|
|
||||||
|
|
||||||
# Validate settings.
|
|
||||||
[ "$TRACE" ] && set -x
|
|
||||||
|
|
||||||
CONFIG=$@
|
|
||||||
|
|
||||||
for line in $CONFIG; do
|
|
||||||
eval "$line"
|
|
||||||
done
|
|
||||||
|
|
||||||
# Define variables.
|
|
||||||
GH_API="https://api.github.com"
|
|
||||||
GH_REPO="$GH_API/repos/$owner/$repo"
|
|
||||||
GH_TAGS="$GH_REPO/releases/tags/$tag"
|
|
||||||
AUTH="Authorization: token $github_api_token"
|
|
||||||
WGET_ARGS="--content-disposition --auth-no-challenge --no-cookie"
|
|
||||||
CURL_ARGS="-LJO#"
|
|
||||||
|
|
||||||
if [[ "$tag" == 'LATEST' ]]; then
|
|
||||||
GH_TAGS="$GH_REPO/releases/latest"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Validate token.
|
|
||||||
curl -o /dev/null -sH "$AUTH" $GH_REPO || { echo "Error: Invalid repo, token or network issue!"; exit 1; }
|
|
||||||
|
|
||||||
# Read asset tags.
|
|
||||||
response=$(curl -sH "$AUTH" $GH_TAGS)
|
|
||||||
|
|
||||||
# Get ID of the asset based on given filename.
|
|
||||||
eval $(echo "$response" | grep -m 1 "id.:" | grep -w id | tr : = | tr -cd '[[:alnum:]]=')
|
|
||||||
[ "$id" ] || { echo "Error: Failed to get release id for tag: $tag"; echo "$response" | awk 'length($0)<100' >&2; exit 1; }
|
|
||||||
|
|
||||||
# Upload asset
|
|
||||||
echo "Uploading asset... "
|
|
||||||
|
|
||||||
# Construct url
|
|
||||||
GH_ASSET="https://uploads.github.com/repos/$owner/$repo/releases/$id/assets?name=$(basename $filename)"
|
|
||||||
|
|
||||||
curl "$GITHUB_OAUTH_BASIC" --data-binary @"$filename" -H "Authorization: token $github_api_token" -H "Content-Type: application/octet-stream" $GH_ASSET
|
|
17
index.html
|
@ -1,17 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
|
||||||
<link rel="manifest" href="/manifest.json" />
|
|
||||||
<meta name="theme-color" id="theme-color" content="" />
|
|
||||||
<meta name="description" content="Uptime Kuma monitoring tool" />
|
|
||||||
<title>Uptime Kuma</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
<script type="module" src="/src/main.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
203
install.sh
|
@ -1,203 +0,0 @@
|
||||||
# install.sh is generated by ./extra/install.batsh, do not modify it directly.
|
|
||||||
# "npm run compile-install-script" to compile install.sh
|
|
||||||
# The command is working on Windows PowerShell and Docker for Windows only.
|
|
||||||
# curl -o kuma_install.sh https://raw.githubusercontent.com/louislam/uptime-kuma/master/install.sh && sudo bash kuma_install.sh
|
|
||||||
"echo" "-e" "====================="
|
|
||||||
"echo" "-e" "Uptime Kuma Installer"
|
|
||||||
"echo" "-e" "====================="
|
|
||||||
"echo" "-e" "Supported OS: CentOS 7/8, Ubuntu >= 16.04 and Debian"
|
|
||||||
"echo" "-e" "---------------------------------------"
|
|
||||||
"echo" "-e" "This script is designed for Linux and basic usage."
|
|
||||||
"echo" "-e" "For advanced usage, please go to https://github.com/louislam/uptime-kuma/wiki/Installation"
|
|
||||||
"echo" "-e" "---------------------------------------"
|
|
||||||
"echo" "-e" ""
|
|
||||||
"echo" "-e" "Local - Install Uptime Kuma in your current machine with git, Node.js 14 and pm2"
|
|
||||||
"echo" "-e" "Docker - Install Uptime Kuma Docker container"
|
|
||||||
"echo" "-e" ""
|
|
||||||
if [ "$1" != "" ]; then
|
|
||||||
type="$1"
|
|
||||||
else
|
|
||||||
"read" "-p" "Which installation method do you prefer? [DOCKER/local]: " "type"
|
|
||||||
fi
|
|
||||||
defaultPort="3001"
|
|
||||||
function checkNode {
|
|
||||||
local _0
|
|
||||||
nodeVersion=$(node -e 'console.log(process.versions.node.split(`.`)[0])')
|
|
||||||
"echo" "-e" "Node Version: ""$nodeVersion"
|
|
||||||
_0="12"
|
|
||||||
if [ $(($nodeVersion < $_0)) == 1 ]; then
|
|
||||||
"echo" "-e" "Error: Required Node.js 14"
|
|
||||||
"exit" "1"
|
|
||||||
fi
|
|
||||||
if [ "$nodeVersion" == "12" ]; then
|
|
||||||
"echo" "-e" "Warning: NodeJS ""$nodeVersion"" is not tested."
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
function deb {
|
|
||||||
nodeCheck=$(node -v)
|
|
||||||
apt --yes update
|
|
||||||
if [ "$nodeCheck" != "" ]; then
|
|
||||||
"checkNode"
|
|
||||||
else
|
|
||||||
# Old nodejs binary name is "nodejs"
|
|
||||||
check=$(nodejs --version)
|
|
||||||
if [ "$check" != "" ]; then
|
|
||||||
"echo" "-e" "Error: 'node' command is not found, but 'nodejs' command is found. Your NodeJS should be too old."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
curlCheck=$(curl --version)
|
|
||||||
if [ "$curlCheck" == "" ]; then
|
|
||||||
"echo" "-e" "Installing Curl"
|
|
||||||
apt --yes install curl
|
|
||||||
fi
|
|
||||||
"echo" "-e" "Installing Node.js 14"
|
|
||||||
curl -sL https://deb.nodesource.com/setup_14.x | bash - > log.txt
|
|
||||||
apt --yes install nodejs
|
|
||||||
node -v
|
|
||||||
nodeCheckAgain=$(node -v)
|
|
||||||
if [ "$nodeCheckAgain" == "" ]; then
|
|
||||||
"echo" "-e" "Error during Node.js installation"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
check=$(git --version)
|
|
||||||
if [ "$check" == "" ]; then
|
|
||||||
"echo" "-e" "Installing Git"
|
|
||||||
apt --yes install git
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
if [ "$type" == "local" ]; then
|
|
||||||
defaultInstallPath="/opt/uptime-kuma"
|
|
||||||
if [ -e "/etc/redhat-release" ]; then
|
|
||||||
os=$("cat" "/etc/redhat-release")
|
|
||||||
distribution="rhel"
|
|
||||||
else
|
|
||||||
if [ -e "/etc/issue" ]; then
|
|
||||||
os=$(head -n1 /etc/issue | cut -f 1 -d ' ')
|
|
||||||
if [ "$os" == "Ubuntu" ]; then
|
|
||||||
distribution="ubuntu"
|
|
||||||
fi
|
|
||||||
if [ "$os" == "Debian" ]; then
|
|
||||||
distribution="debian"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
arch=$(uname -i)
|
|
||||||
"echo" "-e" "Your OS: ""$os"
|
|
||||||
"echo" "-e" "Distribution: ""$distribution"
|
|
||||||
"echo" "-e" "Arch: ""$arch"
|
|
||||||
if [ "$3" != "" ]; then
|
|
||||||
port="$3"
|
|
||||||
else
|
|
||||||
"read" "-p" "Listening Port [$defaultPort]: " "port"
|
|
||||||
if [ "$port" == "" ]; then
|
|
||||||
port="$defaultPort"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
if [ "$2" != "" ]; then
|
|
||||||
installPath="$2"
|
|
||||||
else
|
|
||||||
"read" "-p" "Installation Path [$defaultInstallPath]: " "installPath"
|
|
||||||
if [ "$installPath" == "" ]; then
|
|
||||||
installPath="$defaultInstallPath"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
# CentOS
|
|
||||||
if [ "$distribution" == "rhel" ]; then
|
|
||||||
nodeCheck=$(node -v)
|
|
||||||
if [ "$nodeCheck" != "" ]; then
|
|
||||||
"checkNode"
|
|
||||||
else
|
|
||||||
curlCheck=$(curl --version)
|
|
||||||
if [ "$curlCheck" == "" ]; then
|
|
||||||
"echo" "-e" "Installing Curl"
|
|
||||||
yum -y -q install curl
|
|
||||||
fi
|
|
||||||
"echo" "-e" "Installing Node.js 14"
|
|
||||||
curl -sL https://rpm.nodesource.com/setup_14.x | bash - > log.txt
|
|
||||||
yum install -y -q nodejs
|
|
||||||
node -v
|
|
||||||
nodeCheckAgain=$(node -v)
|
|
||||||
if [ "$nodeCheckAgain" == "" ]; then
|
|
||||||
"echo" "-e" "Error during Node.js installation"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
check=$(git --version)
|
|
||||||
if [ "$check" == "" ]; then
|
|
||||||
"echo" "-e" "Installing Git"
|
|
||||||
yum -y -q install git
|
|
||||||
fi
|
|
||||||
# Ubuntu
|
|
||||||
else
|
|
||||||
if [ "$distribution" == "ubuntu" ]; then
|
|
||||||
"deb"
|
|
||||||
# Debian
|
|
||||||
else
|
|
||||||
if [ "$distribution" == "debian" ]; then
|
|
||||||
"deb"
|
|
||||||
else
|
|
||||||
# Unknown distribution
|
|
||||||
error=$((0))
|
|
||||||
check=$(git --version)
|
|
||||||
if [ "$check" == "" ]; then
|
|
||||||
error=$((1))
|
|
||||||
"echo" "-e" "Error: git is missing"
|
|
||||||
fi
|
|
||||||
check=$(node -v)
|
|
||||||
if [ "$check" == "" ]; then
|
|
||||||
error=$((1))
|
|
||||||
"echo" "-e" "Error: node is missing"
|
|
||||||
fi
|
|
||||||
if [ $(($error > 0)) == 1 ]; then
|
|
||||||
"echo" "-e" "Please install above missing software"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
check=$(pm2 --version)
|
|
||||||
if [ "$check" == "" ]; then
|
|
||||||
"echo" "-e" "Installing PM2"
|
|
||||||
npm install pm2 -g
|
|
||||||
pm2 startup
|
|
||||||
fi
|
|
||||||
mkdir -p $installPath
|
|
||||||
cd $installPath
|
|
||||||
git clone https://github.com/louislam/uptime-kuma.git .
|
|
||||||
npm run setup
|
|
||||||
pm2 start server/server.js --name uptime-kuma -- --port=$port
|
|
||||||
else
|
|
||||||
defaultVolume="uptime-kuma"
|
|
||||||
check=$(docker -v)
|
|
||||||
if [ "$check" == "" ]; then
|
|
||||||
"echo" "-e" "Error: docker is not found!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
check=$(docker info)
|
|
||||||
if [[ "$check" == *"Is the docker daemon running"* ]]; then
|
|
||||||
"echo" "Error: docker is not running"
|
|
||||||
"exit" "1"
|
|
||||||
fi
|
|
||||||
if [ "$3" != "" ]; then
|
|
||||||
port="$3"
|
|
||||||
else
|
|
||||||
"read" "-p" "Expose Port [$defaultPort]: " "port"
|
|
||||||
if [ "$port" == "" ]; then
|
|
||||||
port="$defaultPort"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
if [ "$2" != "" ]; then
|
|
||||||
volume="$2"
|
|
||||||
else
|
|
||||||
"read" "-p" "Volume Name [$defaultVolume]: " "volume"
|
|
||||||
if [ "$volume" == "" ]; then
|
|
||||||
volume="$defaultVolume"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
"echo" "-e" "Port: $port"
|
|
||||||
"echo" "-e" "Volume: $volume"
|
|
||||||
docker volume create $volume
|
|
||||||
docker run -d --restart=always -p $port:3001 -v $volume:/app/data --name uptime-kuma louislam/uptime-kuma:1
|
|
||||||
fi
|
|
||||||
"echo" "-e" "http://localhost:$port"
|
|
25652
package-lock.json
generated
134
package.json
|
@ -1,134 +0,0 @@
|
||||||
{
|
|
||||||
"name": "uptime-kuma",
|
|
||||||
"version": "1.11.1",
|
|
||||||
"license": "MIT",
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/louislam/uptime-kuma.git"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "14.* || >=16.*"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"install-legacy": "npm install --legacy-peer-deps",
|
|
||||||
"update-legacy": "npm update --legacy-peer-deps",
|
|
||||||
"lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .",
|
|
||||||
"lint:style": "stylelint \"**/*.{vue,css,scss}\" --ignore-path .gitignore",
|
|
||||||
"lint": "npm run lint:js && npm run lint:style",
|
|
||||||
"dev": "vite --host --config ./config/vite.config.js",
|
|
||||||
"start": "npm run start-server",
|
|
||||||
"start-server": "node server/server.js",
|
|
||||||
"start-server-dev": "cross-env NODE_ENV=development node server/server.js",
|
|
||||||
"build": "vite build --config ./config/vite.config.js",
|
|
||||||
"test": "node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/ --test",
|
|
||||||
"test-with-build": "npm run build && npm test",
|
|
||||||
"jest": "node test/prepare-jest.js && npm run jest-frontend && npm run jest-backend",
|
|
||||||
"jest-frontend": "cross-env TEST_FRONTEND=1 jest --config=./config/jest-frontend.config.js",
|
|
||||||
"jest-backend": "cross-env TEST_BACKEND=1 jest --config=./config/jest-backend.config.js",
|
|
||||||
"tsc": "tsc",
|
|
||||||
"vite-preview-dist": "vite preview --host --config ./config/vite.config.js",
|
|
||||||
"build-docker": "npm run build-docker-debian && npm run build-docker-alpine",
|
|
||||||
"build-docker-alpine-base": "docker buildx build -f docker/alpine-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base-alpine . --push",
|
|
||||||
"build-docker-debian-base": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base-debian . --push",
|
|
||||||
"build-docker-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:1.11.1-alpine --target release . --push",
|
|
||||||
"build-docker-debian": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.11.1 -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:1.11.1-debian --target release . --push",
|
|
||||||
"build-docker-nightly": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
|
|
||||||
"build-docker-nightly-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push",
|
|
||||||
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
|
|
||||||
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
|
|
||||||
"setup": "git checkout 1.11.1 && npm ci --production && npm run download-dist",
|
|
||||||
"download-dist": "node extra/download-dist.js",
|
|
||||||
"update-version": "node extra/update-version.js",
|
|
||||||
"mark-as-nightly": "node extra/mark-as-nightly.js",
|
|
||||||
"reset-password": "node extra/reset-password.js",
|
|
||||||
"remove-2fa": "node extra/remove-2fa.js",
|
|
||||||
"compile-install-script": "@powershell -NoProfile -ExecutionPolicy Unrestricted -Command ./extra/compile-install-script.ps1",
|
|
||||||
"test-install-script-centos7": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/centos7.dockerfile .",
|
|
||||||
"test-install-script-alpine3": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/alpine3.dockerfile .",
|
|
||||||
"test-install-script-ubuntu": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu.dockerfile .",
|
|
||||||
"test-install-script-ubuntu1604": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu1604.dockerfile .",
|
|
||||||
"test-nodejs16": "docker build --progress plain -f test/ubuntu-nodejs16.dockerfile .",
|
|
||||||
"simple-dns-server": "node extra/simple-dns-server.js",
|
|
||||||
"update-language-files-with-base-lang": "cd extra/update-language-files && node index.js %npm_config_base_lang% && eslint ../../src/languages/**.js --fix",
|
|
||||||
"update-language-files": "cd extra/update-language-files && node index.js && eslint ../../src/languages/**.js --fix",
|
|
||||||
"ncu-patch": "ncu -u -t patch"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@fortawesome/fontawesome-svg-core": "~1.2.36",
|
|
||||||
"@fortawesome/free-regular-svg-icons": "~5.15.4",
|
|
||||||
"@fortawesome/free-solid-svg-icons": "~5.15.4",
|
|
||||||
"@fortawesome/vue-fontawesome": "~3.0.0-5",
|
|
||||||
"@louislam/sqlite3": "~6.0.1",
|
|
||||||
"@popperjs/core": "~2.10.2",
|
|
||||||
"args-parser": "~1.3.0",
|
|
||||||
"axios": "~0.21.4",
|
|
||||||
"bcryptjs": "~2.4.3",
|
|
||||||
"bootstrap": "5.1.3",
|
|
||||||
"bree": "~7.1.0",
|
|
||||||
"chardet": "^1.3.0",
|
|
||||||
"chart.js": "~3.6.0",
|
|
||||||
"chartjs-adapter-dayjs": "~1.0.0",
|
|
||||||
"check-password-strength": "^2.0.3",
|
|
||||||
"command-exists": "~1.2.9",
|
|
||||||
"compare-versions": "~3.6.0",
|
|
||||||
"dayjs": "~1.10.7",
|
|
||||||
"express": "~4.17.1",
|
|
||||||
"express-basic-auth": "~1.2.0",
|
|
||||||
"form-data": "~4.0.0",
|
|
||||||
"http-graceful-shutdown": "~3.1.5",
|
|
||||||
"iconv-lite": "^0.6.3",
|
|
||||||
"jsonwebtoken": "~8.5.1",
|
|
||||||
"jwt-decode": "^3.1.2",
|
|
||||||
"limiter": "^2.1.0",
|
|
||||||
"nodemailer": "~6.6.5",
|
|
||||||
"notp": "~2.0.3",
|
|
||||||
"password-hash": "~1.2.2",
|
|
||||||
"postcss-rtlcss": "~3.4.1",
|
|
||||||
"postcss-scss": "~4.0.2",
|
|
||||||
"prom-client": "~13.2.0",
|
|
||||||
"prometheus-api-metrics": "~3.2.0",
|
|
||||||
"qrcode": "~1.5.0",
|
|
||||||
"redbean-node": "0.1.3",
|
|
||||||
"socket.io": "~4.2.0",
|
|
||||||
"socket.io-client": "~4.2.0",
|
|
||||||
"tar": "^6.1.11",
|
|
||||||
"tcp-ping": "~0.1.1",
|
|
||||||
"thirty-two": "~1.0.2",
|
|
||||||
"timezones-list": "~3.0.1",
|
|
||||||
"v-pagination-3": "~0.1.7",
|
|
||||||
"vue": "next",
|
|
||||||
"vue-chart-3": "~0.5.11",
|
|
||||||
"vue-confirm-dialog": "~1.0.2",
|
|
||||||
"vue-contenteditable": "~3.0.4",
|
|
||||||
"vue-i18n": "~9.1.9",
|
|
||||||
"vue-image-crop-upload": "~3.0.3",
|
|
||||||
"vue-multiselect": "~3.0.0-alpha.2",
|
|
||||||
"vue-qrcode": "~1.0.0",
|
|
||||||
"vue-router": "~4.0.12",
|
|
||||||
"vue-toastification": "~2.0.0-rc.5",
|
|
||||||
"vuedraggable": "~4.1.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@actions/github": "~5.0.0",
|
|
||||||
"@babel/eslint-parser": "~7.15.8",
|
|
||||||
"@babel/preset-env": "^7.15.8",
|
|
||||||
"@types/bootstrap": "~5.1.6",
|
|
||||||
"@vitejs/plugin-legacy": "~1.6.3",
|
|
||||||
"@vitejs/plugin-vue": "~1.9.4",
|
|
||||||
"@vue/compiler-sfc": "~3.2.22",
|
|
||||||
"babel-plugin-rewire": "~1.2.0",
|
|
||||||
"core-js": "~3.18.3",
|
|
||||||
"cross-env": "~7.0.3",
|
|
||||||
"dns2": "~2.0.1",
|
|
||||||
"eslint": "~7.32.0",
|
|
||||||
"eslint-plugin-vue": "~7.18.0",
|
|
||||||
"jest": "~27.2.5",
|
|
||||||
"jest-puppeteer": "~6.0.0",
|
|
||||||
"puppeteer": "~10.4.0",
|
|
||||||
"sass": "~1.42.1",
|
|
||||||
"stylelint": "~13.13.1",
|
|
||||||
"stylelint-config-standard": "~22.0.0",
|
|
||||||
"typescript": "~4.4.4",
|
|
||||||
"vite": "~2.6.14"
|
|
||||||
}
|
|
||||||
}
|
|
Before Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 9.5 KiB |
BIN
public/icon.png
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 6.4 KiB |
|
@ -1,19 +0,0 @@
|
||||||
{
|
|
||||||
"name": "Uptime Kuma",
|
|
||||||
"short_name": "Uptime Kuma",
|
|
||||||
"start_url": "/",
|
|
||||||
"background_color": "#fff",
|
|
||||||
"display": "standalone",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "icon-192x192.png",
|
|
||||||
"sizes": "192x192",
|
|
||||||
"type": "image/png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "icon-512x512.png",
|
|
||||||
"sizes": "512x512",
|
|
||||||
"type": "image/png"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
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;
|
|
|
@ -1,61 +0,0 @@
|
||||||
const basicAuth = require("express-basic-auth");
|
|
||||||
const passwordHash = require("./password-hash");
|
|
||||||
const { R } = require("redbean-node");
|
|
||||||
const { setting } = require("./util-server");
|
|
||||||
const { debug } = require("../src/util");
|
|
||||||
const { loginRateLimiter } = require("./rate-limiter");
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param username : string
|
|
||||||
* @param password : string
|
|
||||||
* @returns {Promise<Bean|null>}
|
|
||||||
*/
|
|
||||||
exports.login = async function (username, password) {
|
|
||||||
let user = await R.findOne("user", " username = ? AND active = 1 ", [
|
|
||||||
username,
|
|
||||||
]);
|
|
||||||
|
|
||||||
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),
|
|
||||||
user.id,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
function myAuthorizer(username, password, callback) {
|
|
||||||
setting("disableAuth").then((result) => {
|
|
||||||
if (result) {
|
|
||||||
callback(null, true);
|
|
||||||
} else {
|
|
||||||
// Login Rate Limit
|
|
||||||
loginRateLimiter.pass(null, 0).then((pass) => {
|
|
||||||
if (pass) {
|
|
||||||
exports.login(username, password).then((user) => {
|
|
||||||
callback(null, user != null);
|
|
||||||
|
|
||||||
if (user == null) {
|
|
||||||
loginRateLimiter.removeTokens(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
callback(null, false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.basicAuth = basicAuth({
|
|
||||||
authorizer: myAuthorizer,
|
|
||||||
authorizeAsync: true,
|
|
||||||
challenge: true,
|
|
||||||
});
|
|
|
@ -1,41 +0,0 @@
|
||||||
const { setSetting } = require("./util-server");
|
|
||||||
const axios = require("axios");
|
|
||||||
|
|
||||||
exports.version = require("../package.json").version;
|
|
||||||
exports.latestVersion = null;
|
|
||||||
|
|
||||||
let interval;
|
|
||||||
|
|
||||||
exports.startInterval = () => {
|
|
||||||
let check = async () => {
|
|
||||||
try {
|
|
||||||
const res = await axios.get("https://uptime.kuma.pet/version");
|
|
||||||
|
|
||||||
// For debug
|
|
||||||
if (process.env.TEST_CHECK_VERSION === "1") {
|
|
||||||
res.data.slow = "1000.0.0";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res.data.slow) {
|
|
||||||
exports.latestVersion = res.data.slow;
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (_) { }
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
check();
|
|
||||||
interval = setInterval(check, 3600 * 1000 * 48);
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.enableCheckUpdate = async (value) => {
|
|
||||||
await setSetting("checkUpdate", value);
|
|
||||||
|
|
||||||
clearInterval(interval);
|
|
||||||
|
|
||||||
if (value) {
|
|
||||||
exports.startInterval();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.socket = null;
|
|
100
server/client.js
|
@ -1,100 +0,0 @@
|
||||||
/*
|
|
||||||
* For Client Socket
|
|
||||||
*/
|
|
||||||
const { TimeLogger } = require("../src/util");
|
|
||||||
const { R } = require("redbean-node");
|
|
||||||
const { io } = require("./server");
|
|
||||||
const { setting } = require("./util-server");
|
|
||||||
const checkVersion = require("./check-version");
|
|
||||||
|
|
||||||
async function sendNotificationList(socket) {
|
|
||||||
const timeLogger = new TimeLogger();
|
|
||||||
|
|
||||||
let result = [];
|
|
||||||
let list = await R.find("notification", " user_id = ? ", [
|
|
||||||
socket.userID,
|
|
||||||
]);
|
|
||||||
|
|
||||||
for (let bean of list) {
|
|
||||||
result.push(bean.export());
|
|
||||||
}
|
|
||||||
|
|
||||||
io.to(socket.userID).emit("notificationList", result);
|
|
||||||
|
|
||||||
timeLogger.print("Send Notification List");
|
|
||||||
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send Heartbeat History list to socket
|
|
||||||
* @param toUser True = send to all browsers with the same user id, False = send to the current browser only
|
|
||||||
* @param overwrite Overwrite client-side's heartbeat list
|
|
||||||
*/
|
|
||||||
async function sendHeartbeatList(socket, monitorID, toUser = false, overwrite = false) {
|
|
||||||
const timeLogger = new TimeLogger();
|
|
||||||
|
|
||||||
let list = await R.getAll(`
|
|
||||||
SELECT * FROM heartbeat
|
|
||||||
WHERE monitor_id = ?
|
|
||||||
ORDER BY time DESC
|
|
||||||
LIMIT 100
|
|
||||||
`, [
|
|
||||||
monitorID,
|
|
||||||
]);
|
|
||||||
|
|
||||||
let result = list.reverse();
|
|
||||||
|
|
||||||
if (toUser) {
|
|
||||||
io.to(socket.userID).emit("heartbeatList", monitorID, result, overwrite);
|
|
||||||
} else {
|
|
||||||
socket.emit("heartbeatList", monitorID, result, overwrite);
|
|
||||||
}
|
|
||||||
|
|
||||||
timeLogger.print(`[Monitor: ${monitorID}] sendHeartbeatList`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Important Heart beat list (aka event list)
|
|
||||||
* @param socket
|
|
||||||
* @param monitorID
|
|
||||||
* @param toUser True = send to all browsers with the same user id, False = send to the current browser only
|
|
||||||
* @param overwrite Overwrite client-side's heartbeat list
|
|
||||||
*/
|
|
||||||
async function sendImportantHeartbeatList(socket, monitorID, toUser = false, overwrite = false) {
|
|
||||||
const timeLogger = new TimeLogger();
|
|
||||||
|
|
||||||
let list = await R.find("heartbeat", `
|
|
||||||
monitor_id = ?
|
|
||||||
AND important = 1
|
|
||||||
ORDER BY time DESC
|
|
||||||
LIMIT 500
|
|
||||||
`, [
|
|
||||||
monitorID,
|
|
||||||
]);
|
|
||||||
|
|
||||||
timeLogger.print(`[Monitor: ${monitorID}] sendImportantHeartbeatList`);
|
|
||||||
|
|
||||||
if (toUser) {
|
|
||||||
io.to(socket.userID).emit("importantHeartbeatList", monitorID, list, overwrite);
|
|
||||||
} else {
|
|
||||||
socket.emit("importantHeartbeatList", monitorID, list, overwrite);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
async function sendInfo(socket) {
|
|
||||||
socket.emit("info", {
|
|
||||||
version: checkVersion.version,
|
|
||||||
latestVersion: checkVersion.latestVersion,
|
|
||||||
primaryBaseURL: await setting("primaryBaseURL")
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
sendNotificationList,
|
|
||||||
sendImportantHeartbeatList,
|
|
||||||
sendHeartbeatList,
|
|
||||||
sendInfo
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
const args = require("args-parser")(process.argv);
|
|
||||||
const demoMode = args["demo"] || false;
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
args,
|
|
||||||
demoMode
|
|
||||||
};
|
|
|
@ -1,398 +0,0 @@
|
||||||
const fs = require("fs");
|
|
||||||
const { R } = require("redbean-node");
|
|
||||||
const { setSetting, setting } = require("./util-server");
|
|
||||||
const { debug, sleep } = require("../src/util");
|
|
||||||
const dayjs = require("dayjs");
|
|
||||||
const knex = require("knex");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Database & App Data Folder
|
|
||||||
*/
|
|
||||||
class Database {
|
|
||||||
|
|
||||||
static templatePath = "./db/kuma.db";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Data Dir (Default: ./data)
|
|
||||||
*/
|
|
||||||
static dataDir;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* User Upload Dir (Default: ./data/upload)
|
|
||||||
*/
|
|
||||||
static uploadDir;
|
|
||||||
|
|
||||||
static path;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
|
||||||
static patched = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For Backup only
|
|
||||||
*/
|
|
||||||
static backupPath = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add patch filename in key
|
|
||||||
* Values:
|
|
||||||
* true: Add it regardless of order
|
|
||||||
* false: Do nothing
|
|
||||||
* { parents: []}: Need parents before add it
|
|
||||||
*/
|
|
||||||
static patchList = {
|
|
||||||
"patch-setting-value-type.sql": true,
|
|
||||||
"patch-improve-performance.sql": true,
|
|
||||||
"patch-2fa.sql": true,
|
|
||||||
"patch-add-retry-interval-monitor.sql": true,
|
|
||||||
"patch-incident-table.sql": true,
|
|
||||||
"patch-group-table.sql": true,
|
|
||||||
"patch-monitor-push_token.sql": true,
|
|
||||||
"patch-http-monitor-method-body-and-headers.sql": true,
|
|
||||||
"patch-2fa-invalidate-used-token.sql": true,
|
|
||||||
"patch-notification_sent_history.sql": true,
|
|
||||||
"patch-monitor-basic-auth.sql": true,
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The final version should be 10 after merged tag feature
|
|
||||||
* @deprecated Use patchList for any new feature
|
|
||||||
*/
|
|
||||||
static latestVersion = 10;
|
|
||||||
|
|
||||||
static noReject = true;
|
|
||||||
|
|
||||||
static init(args) {
|
|
||||||
// Data Directory (must be end with "/")
|
|
||||||
Database.dataDir = process.env.DATA_DIR || args["data-dir"] || "./data/";
|
|
||||||
Database.path = Database.dataDir + "kuma.db";
|
|
||||||
if (! fs.existsSync(Database.dataDir)) {
|
|
||||||
fs.mkdirSync(Database.dataDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
Database.uploadDir = Database.dataDir + "upload/";
|
|
||||||
|
|
||||||
if (! fs.existsSync(Database.uploadDir)) {
|
|
||||||
fs.mkdirSync(Database.uploadDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Data Dir: ${Database.dataDir}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
static async connect(testMode = false) {
|
|
||||||
const acquireConnectionTimeout = 120 * 1000;
|
|
||||||
|
|
||||||
const Dialect = require("knex/lib/dialects/sqlite3/index.js");
|
|
||||||
Dialect.prototype._driver = () => require("@louislam/sqlite3");
|
|
||||||
|
|
||||||
const knexInstance = knex({
|
|
||||||
client: Dialect,
|
|
||||||
connection: {
|
|
||||||
filename: Database.path,
|
|
||||||
acquireConnectionTimeout: acquireConnectionTimeout,
|
|
||||||
},
|
|
||||||
useNullAsDefault: true,
|
|
||||||
pool: {
|
|
||||||
min: 1,
|
|
||||||
max: 1,
|
|
||||||
idleTimeoutMillis: 120 * 1000,
|
|
||||||
propagateCreateError: false,
|
|
||||||
acquireTimeoutMillis: acquireConnectionTimeout,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
R.setup(knexInstance);
|
|
||||||
|
|
||||||
if (process.env.SQL_LOG === "1") {
|
|
||||||
R.debug(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto map the model to a bean object
|
|
||||||
R.freeze(true);
|
|
||||||
await R.autoloadModels("./server/model");
|
|
||||||
|
|
||||||
await R.exec("PRAGMA foreign_keys = ON");
|
|
||||||
if (testMode) {
|
|
||||||
// Change to MEMORY
|
|
||||||
await R.exec("PRAGMA journal_mode = MEMORY");
|
|
||||||
} else {
|
|
||||||
// Change to WAL
|
|
||||||
await R.exec("PRAGMA journal_mode = WAL");
|
|
||||||
}
|
|
||||||
await R.exec("PRAGMA cache_size = -12000");
|
|
||||||
await R.exec("PRAGMA auto_vacuum = FULL");
|
|
||||||
|
|
||||||
console.log("SQLite config:");
|
|
||||||
console.log(await R.getAll("PRAGMA journal_mode"));
|
|
||||||
console.log(await R.getAll("PRAGMA cache_size"));
|
|
||||||
console.log("SQLite Version: " + await R.getCell("SELECT sqlite_version()"));
|
|
||||||
}
|
|
||||||
|
|
||||||
static async patch() {
|
|
||||||
let version = parseInt(await setting("database_version"));
|
|
||||||
|
|
||||||
if (! version) {
|
|
||||||
version = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.info("Your database version: " + version);
|
|
||||||
console.info("Latest database version: " + this.latestVersion);
|
|
||||||
|
|
||||||
if (version === this.latestVersion) {
|
|
||||||
console.info("Database patch not needed");
|
|
||||||
} else if (version > this.latestVersion) {
|
|
||||||
console.info("Warning: Database version is newer than expected");
|
|
||||||
} else {
|
|
||||||
console.info("Database patch is needed");
|
|
||||||
|
|
||||||
this.backup(version);
|
|
||||||
|
|
||||||
// Try catch anything here, if gone wrong, restore the backup
|
|
||||||
try {
|
|
||||||
for (let i = version + 1; i <= this.latestVersion; i++) {
|
|
||||||
const sqlFile = `./db/patch${i}.sql`;
|
|
||||||
console.info(`Patching ${sqlFile}`);
|
|
||||||
await Database.importSQLFile(sqlFile);
|
|
||||||
console.info(`Patched ${sqlFile}`);
|
|
||||||
await setSetting("database_version", i);
|
|
||||||
}
|
|
||||||
} catch (ex) {
|
|
||||||
await Database.close();
|
|
||||||
|
|
||||||
console.error(ex);
|
|
||||||
console.error("Start Uptime-Kuma failed due to issue patching the database");
|
|
||||||
console.error("Please submit a bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
|
|
||||||
|
|
||||||
this.restore();
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.patch2();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Call it from patch() only
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
static async patch2() {
|
|
||||||
console.log("Database Patch 2.0 Process");
|
|
||||||
let databasePatchedFiles = await setting("databasePatchedFiles");
|
|
||||||
|
|
||||||
if (! databasePatchedFiles) {
|
|
||||||
databasePatchedFiles = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
debug("Patched files:");
|
|
||||||
debug(databasePatchedFiles);
|
|
||||||
|
|
||||||
try {
|
|
||||||
for (let sqlFilename in this.patchList) {
|
|
||||||
await this.patch2Recursion(sqlFilename, databasePatchedFiles);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.patched) {
|
|
||||||
console.log("Database Patched Successfully");
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (ex) {
|
|
||||||
await Database.close();
|
|
||||||
|
|
||||||
console.error(ex);
|
|
||||||
console.error("Start Uptime-Kuma failed due to issue patching the database");
|
|
||||||
console.error("Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
|
|
||||||
|
|
||||||
this.restore();
|
|
||||||
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
await setSetting("databasePatchedFiles", databasePatchedFiles);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used it patch2() only
|
|
||||||
* @param sqlFilename
|
|
||||||
* @param databasePatchedFiles
|
|
||||||
*/
|
|
||||||
static async patch2Recursion(sqlFilename, databasePatchedFiles) {
|
|
||||||
let value = this.patchList[sqlFilename];
|
|
||||||
|
|
||||||
if (! value) {
|
|
||||||
console.log(sqlFilename + " skip");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if patched
|
|
||||||
if (! databasePatchedFiles[sqlFilename]) {
|
|
||||||
console.log(sqlFilename + " is not patched");
|
|
||||||
|
|
||||||
if (value.parents) {
|
|
||||||
console.log(sqlFilename + " need parents");
|
|
||||||
for (let parentSQLFilename of value.parents) {
|
|
||||||
await this.patch2Recursion(parentSQLFilename, databasePatchedFiles);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.backup(dayjs().format("YYYYMMDDHHmmss"));
|
|
||||||
|
|
||||||
console.log(sqlFilename + " is patching");
|
|
||||||
this.patched = true;
|
|
||||||
await this.importSQLFile("./db/" + sqlFilename);
|
|
||||||
databasePatchedFiles[sqlFilename] = true;
|
|
||||||
console.log(sqlFilename + " was patched successfully");
|
|
||||||
|
|
||||||
} else {
|
|
||||||
debug(sqlFilename + " is already patched, skip");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sadly, multi sql statements is not supported by many sqlite libraries, I have to implement it myself
|
|
||||||
* @param filename
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
static async importSQLFile(filename) {
|
|
||||||
|
|
||||||
await R.getCell("SELECT 1");
|
|
||||||
|
|
||||||
let text = fs.readFileSync(filename).toString();
|
|
||||||
|
|
||||||
// Remove all comments (--)
|
|
||||||
let lines = text.split("\n");
|
|
||||||
lines = lines.filter((line) => {
|
|
||||||
return ! line.startsWith("--");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Split statements by semicolon
|
|
||||||
// Filter out empty line
|
|
||||||
text = lines.join("\n");
|
|
||||||
|
|
||||||
let statements = text.split(";")
|
|
||||||
.map((statement) => {
|
|
||||||
return statement.trim();
|
|
||||||
})
|
|
||||||
.filter((statement) => {
|
|
||||||
return statement !== "";
|
|
||||||
});
|
|
||||||
|
|
||||||
for (let statement of statements) {
|
|
||||||
await R.exec(statement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static getBetterSQLite3Database() {
|
|
||||||
return R.knex.client.acquireConnection();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Special handle, because tarn.js throw a promise reject that cannot be caught
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
static async close() {
|
|
||||||
const listener = (reason, p) => {
|
|
||||||
Database.noReject = false;
|
|
||||||
};
|
|
||||||
process.addListener("unhandledRejection", listener);
|
|
||||||
|
|
||||||
console.log("Closing the database");
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
Database.noReject = true;
|
|
||||||
await R.close();
|
|
||||||
await sleep(2000);
|
|
||||||
|
|
||||||
if (Database.noReject) {
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
console.log("Waiting to close the database");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log("SQLite closed");
|
|
||||||
|
|
||||||
process.removeListener("unhandledRejection", listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* One backup one time in this process.
|
|
||||||
* Reset this.backupPath if you want to backup again
|
|
||||||
* @param version
|
|
||||||
*/
|
|
||||||
static backup(version) {
|
|
||||||
if (! this.backupPath) {
|
|
||||||
console.info("Backing up the database");
|
|
||||||
this.backupPath = this.dataDir + "kuma.db.bak" + version;
|
|
||||||
fs.copyFileSync(Database.path, this.backupPath);
|
|
||||||
|
|
||||||
const shmPath = Database.path + "-shm";
|
|
||||||
if (fs.existsSync(shmPath)) {
|
|
||||||
this.backupShmPath = shmPath + ".bak" + version;
|
|
||||||
fs.copyFileSync(shmPath, this.backupShmPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
const walPath = Database.path + "-wal";
|
|
||||||
if (fs.existsSync(walPath)) {
|
|
||||||
this.backupWalPath = walPath + ".bak" + version;
|
|
||||||
fs.copyFileSync(walPath, this.backupWalPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
static restore() {
|
|
||||||
if (this.backupPath) {
|
|
||||||
console.error("Patching the database failed!!! Restoring the backup");
|
|
||||||
|
|
||||||
const shmPath = Database.path + "-shm";
|
|
||||||
const walPath = Database.path + "-wal";
|
|
||||||
|
|
||||||
// Delete patch failed db
|
|
||||||
try {
|
|
||||||
if (fs.existsSync(Database.path)) {
|
|
||||||
fs.unlinkSync(Database.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fs.existsSync(shmPath)) {
|
|
||||||
fs.unlinkSync(shmPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fs.existsSync(walPath)) {
|
|
||||||
fs.unlinkSync(walPath);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log("Restore failed; you may need to restore the backup manually");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore backup
|
|
||||||
fs.copyFileSync(this.backupPath, Database.path);
|
|
||||||
|
|
||||||
if (this.backupShmPath) {
|
|
||||||
fs.copyFileSync(this.backupShmPath, shmPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.backupWalPath) {
|
|
||||||
fs.copyFileSync(this.backupWalPath, walPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
console.log("Nothing to restore");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static getSize() {
|
|
||||||
debug("Database.getSize()");
|
|
||||||
let stats = fs.statSync(Database.path);
|
|
||||||
debug(stats);
|
|
||||||
return stats.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async shrink() {
|
|
||||||
await R.exec("VACUUM");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = Database;
|
|
|
@ -1,57 +0,0 @@
|
||||||
/*
|
|
||||||
From https://github.com/DiegoZoracKy/image-data-uri/blob/master/lib/image-data-uri.js
|
|
||||||
Modified with 0 dependencies
|
|
||||||
*/
|
|
||||||
let fs = require("fs");
|
|
||||||
|
|
||||||
let ImageDataURI = (() => {
|
|
||||||
|
|
||||||
function decode(dataURI) {
|
|
||||||
if (!/data:image\//.test(dataURI)) {
|
|
||||||
console.log("ImageDataURI :: Error :: It seems that it is not an Image Data URI. Couldn't match \"data:image/\"");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let regExMatches = dataURI.match("data:(image/.*);base64,(.*)");
|
|
||||||
return {
|
|
||||||
imageType: regExMatches[1],
|
|
||||||
dataBase64: regExMatches[2],
|
|
||||||
dataBuffer: new Buffer(regExMatches[2], "base64")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function encode(data, mediaType) {
|
|
||||||
if (!data || !mediaType) {
|
|
||||||
console.log("ImageDataURI :: Error :: Missing some of the required params: data, mediaType ");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
mediaType = (/\//.test(mediaType)) ? mediaType : "image/" + mediaType;
|
|
||||||
let dataBase64 = (Buffer.isBuffer(data)) ? data.toString("base64") : new Buffer(data).toString("base64");
|
|
||||||
let dataImgBase64 = "data:" + mediaType + ";base64," + dataBase64;
|
|
||||||
|
|
||||||
return dataImgBase64;
|
|
||||||
}
|
|
||||||
|
|
||||||
function outputFile(dataURI, filePath) {
|
|
||||||
filePath = filePath || "./";
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let imageDecoded = decode(dataURI);
|
|
||||||
|
|
||||||
fs.writeFile(filePath, imageDecoded.dataBuffer, err => {
|
|
||||||
if (err) {
|
|
||||||
return reject("ImageDataURI :: Error :: " + JSON.stringify(err, null, 4));
|
|
||||||
}
|
|
||||||
resolve(filePath);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
decode: decode,
|
|
||||||
encode: encode,
|
|
||||||
outputFile: outputFile,
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
||||||
module.exports = ImageDataURI;
|
|
|
@ -1,31 +0,0 @@
|
||||||
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
|
|
||||||
};
|
|
|
@ -1,40 +0,0 @@
|
||||||
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();
|
|
||||||
})();
|
|
|
@ -1,39 +0,0 @@
|
||||||
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,
|
|
||||||
};
|
|
|
@ -1,34 +0,0 @@
|
||||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
|
||||||
const { R } = require("redbean-node");
|
|
||||||
|
|
||||||
class Group extends BeanModel {
|
|
||||||
|
|
||||||
async toPublicJSON() {
|
|
||||||
let monitorBeanList = await this.getMonitorList();
|
|
||||||
let monitorList = [];
|
|
||||||
|
|
||||||
for (let bean of monitorBeanList) {
|
|
||||||
monitorList.push(await bean.toPublicJSON());
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: this.id,
|
|
||||||
name: this.name,
|
|
||||||
weight: this.weight,
|
|
||||||
monitorList,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async getMonitorList() {
|
|
||||||
return R.convertToBeans("monitor", await R.getAll(`
|
|
||||||
SELECT monitor.* FROM monitor, monitor_group
|
|
||||||
WHERE monitor.id = monitor_group.monitor_id
|
|
||||||
AND group_id = ?
|
|
||||||
ORDER BY monitor_group.weight
|
|
||||||
`, [
|
|
||||||
this.id,
|
|
||||||
]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = Group;
|
|
|
@ -1,39 +0,0 @@
|
||||||
const dayjs = require("dayjs");
|
|
||||||
const utc = require("dayjs/plugin/utc");
|
|
||||||
let timezone = require("dayjs/plugin/timezone");
|
|
||||||
dayjs.extend(utc);
|
|
||||||
dayjs.extend(timezone);
|
|
||||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* status:
|
|
||||||
* 0 = DOWN
|
|
||||||
* 1 = UP
|
|
||||||
* 2 = PENDING
|
|
||||||
*/
|
|
||||||
class Heartbeat extends BeanModel {
|
|
||||||
|
|
||||||
toPublicJSON() {
|
|
||||||
return {
|
|
||||||
status: this.status,
|
|
||||||
time: this.time,
|
|
||||||
msg: "", // Hide for public
|
|
||||||
ping: this.ping,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
toJSON() {
|
|
||||||
return {
|
|
||||||
monitorID: this.monitor_id,
|
|
||||||
status: this.status,
|
|
||||||
time: this.time,
|
|
||||||
msg: this.msg,
|
|
||||||
ping: this.ping,
|
|
||||||
important: this.important,
|
|
||||||
duration: this.duration,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = Heartbeat;
|
|
|
@ -1,18 +0,0 @@
|
||||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
|
||||||
|
|
||||||
class Incident extends BeanModel {
|
|
||||||
|
|
||||||
toPublicJSON() {
|
|
||||||
return {
|
|
||||||
id: this.id,
|
|
||||||
style: this.style,
|
|
||||||
title: this.title,
|
|
||||||
content: this.content,
|
|
||||||
pin: this.pin,
|
|
||||||
createdDate: this.createdDate,
|
|
||||||
lastUpdatedDate: this.lastUpdatedDate,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = Incident;
|
|
|
@ -1,768 +0,0 @@
|
||||||
const https = require("https");
|
|
||||||
const dayjs = require("dayjs");
|
|
||||||
const utc = require("dayjs/plugin/utc");
|
|
||||||
let timezone = require("dayjs/plugin/timezone");
|
|
||||||
dayjs.extend(utc);
|
|
||||||
dayjs.extend(timezone);
|
|
||||||
const axios = require("axios");
|
|
||||||
const { Prometheus } = require("../prometheus");
|
|
||||||
const { debug, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
|
|
||||||
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, errorLog } = require("../util-server");
|
|
||||||
const { R } = require("redbean-node");
|
|
||||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
|
||||||
const { Notification } = require("../notification");
|
|
||||||
const { demoMode } = require("../config");
|
|
||||||
const version = require("../../package.json").version;
|
|
||||||
const apicache = require("../modules/apicache");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* status:
|
|
||||||
* 0 = DOWN
|
|
||||||
* 1 = UP
|
|
||||||
* 2 = PENDING
|
|
||||||
*/
|
|
||||||
class Monitor extends BeanModel {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a object that ready to parse to JSON for public
|
|
||||||
* Only show necessary data to public
|
|
||||||
*/
|
|
||||||
async toPublicJSON() {
|
|
||||||
return {
|
|
||||||
id: this.id,
|
|
||||||
name: this.name,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a object that ready to parse to JSON
|
|
||||||
*/
|
|
||||||
async toJSON() {
|
|
||||||
|
|
||||||
let notificationIDList = {};
|
|
||||||
|
|
||||||
let list = await R.find("monitor_notification", " monitor_id = ? ", [
|
|
||||||
this.id,
|
|
||||||
]);
|
|
||||||
|
|
||||||
for (let bean of list) {
|
|
||||||
notificationIDList[bean.notification_id] = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tags = await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ?", [this.id]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: this.id,
|
|
||||||
name: this.name,
|
|
||||||
url: this.url,
|
|
||||||
method: this.method,
|
|
||||||
body: this.body,
|
|
||||||
headers: this.headers,
|
|
||||||
basic_auth_user: this.basic_auth_user,
|
|
||||||
basic_auth_pass: this.basic_auth_pass,
|
|
||||||
hostname: this.hostname,
|
|
||||||
port: this.port,
|
|
||||||
maxretries: this.maxretries,
|
|
||||||
weight: this.weight,
|
|
||||||
active: this.active,
|
|
||||||
type: this.type,
|
|
||||||
interval: this.interval,
|
|
||||||
retryInterval: this.retryInterval,
|
|
||||||
keyword: this.keyword,
|
|
||||||
ignoreTls: this.getIgnoreTls(),
|
|
||||||
upsideDown: this.isUpsideDown(),
|
|
||||||
maxredirects: this.maxredirects,
|
|
||||||
accepted_statuscodes: this.getAcceptedStatuscodes(),
|
|
||||||
dns_resolve_type: this.dns_resolve_type,
|
|
||||||
dns_resolve_server: this.dns_resolve_server,
|
|
||||||
dns_last_result: this.dns_last_result,
|
|
||||||
pushToken: this.pushToken,
|
|
||||||
notificationIDList,
|
|
||||||
tags: tags,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encode user and password to Base64 encoding
|
|
||||||
* for HTTP "basic" auth, as per RFC-7617
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
encodeBase64(user, pass) {
|
|
||||||
return Buffer.from(user + ":" + pass).toString("base64");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse to boolean
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
getIgnoreTls() {
|
|
||||||
return Boolean(this.ignoreTls);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse to boolean
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
isUpsideDown() {
|
|
||||||
return Boolean(this.upsideDown);
|
|
||||||
}
|
|
||||||
|
|
||||||
getAcceptedStatuscodes() {
|
|
||||||
return JSON.parse(this.accepted_statuscodes_json);
|
|
||||||
}
|
|
||||||
|
|
||||||
start(io) {
|
|
||||||
let previousBeat = null;
|
|
||||||
let retries = 0;
|
|
||||||
|
|
||||||
let prometheus = new Prometheus(this);
|
|
||||||
|
|
||||||
const beat = async () => {
|
|
||||||
|
|
||||||
// Expose here for prometheus update
|
|
||||||
// undefined if not https
|
|
||||||
let tlsInfo = undefined;
|
|
||||||
|
|
||||||
if (! previousBeat) {
|
|
||||||
previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [
|
|
||||||
this.id,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isFirstBeat = !previousBeat;
|
|
||||||
|
|
||||||
let bean = R.dispense("heartbeat");
|
|
||||||
bean.monitor_id = this.id;
|
|
||||||
bean.time = R.isoDateTime(dayjs.utc());
|
|
||||||
bean.status = DOWN;
|
|
||||||
|
|
||||||
if (this.isUpsideDown()) {
|
|
||||||
bean.status = flipStatus(bean.status);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Duration
|
|
||||||
if (! isFirstBeat) {
|
|
||||||
bean.duration = dayjs(bean.time).diff(dayjs(previousBeat.time), "second");
|
|
||||||
} else {
|
|
||||||
bean.duration = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (this.type === "http" || this.type === "keyword") {
|
|
||||||
// Do not do any queries/high loading things before the "bean.ping"
|
|
||||||
let startTime = dayjs().valueOf();
|
|
||||||
|
|
||||||
// HTTP basic auth
|
|
||||||
let basicAuthHeader = {};
|
|
||||||
if (this.basic_auth_user) {
|
|
||||||
basicAuthHeader = {
|
|
||||||
"Authorization": "Basic " + this.encodeBase64(this.basic_auth_user, this.basic_auth_pass),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
debug(`[${this.name}] Prepare Options for axios`);
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
url: this.url,
|
|
||||||
method: (this.method || "get").toLowerCase(),
|
|
||||||
...(this.body ? { data: JSON.parse(this.body) } : {}),
|
|
||||||
timeout: this.interval * 1000 * 0.8,
|
|
||||||
headers: {
|
|
||||||
"Accept": "*/*",
|
|
||||||
"User-Agent": "Uptime-Kuma/" + version,
|
|
||||||
...(this.headers ? JSON.parse(this.headers) : {}),
|
|
||||||
...(basicAuthHeader),
|
|
||||||
},
|
|
||||||
httpsAgent: new https.Agent({
|
|
||||||
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
|
||||||
rejectUnauthorized: ! this.getIgnoreTls(),
|
|
||||||
}),
|
|
||||||
maxRedirects: this.maxredirects,
|
|
||||||
validateStatus: (status) => {
|
|
||||||
return checkStatusCode(status, this.getAcceptedStatuscodes());
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
debug(`[${this.name}] Axios Request`);
|
|
||||||
let res = await axios.request(options);
|
|
||||||
bean.msg = `${res.status} - ${res.statusText}`;
|
|
||||||
bean.ping = dayjs().valueOf() - startTime;
|
|
||||||
|
|
||||||
// Check certificate if https is used
|
|
||||||
let certInfoStartTime = dayjs().valueOf();
|
|
||||||
if (this.getUrl()?.protocol === "https:") {
|
|
||||||
debug(`[${this.name}] Check cert`);
|
|
||||||
try {
|
|
||||||
let tlsInfoObject = checkCertificate(res);
|
|
||||||
tlsInfo = await this.updateTlsInfo(tlsInfoObject);
|
|
||||||
|
|
||||||
if (!this.getIgnoreTls()) {
|
|
||||||
debug(`[${this.name}] call sendCertNotification`);
|
|
||||||
await this.sendCertNotification(tlsInfoObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
if (e.message !== "No TLS certificate in response") {
|
|
||||||
console.error(e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.TIMELOGGER === "1") {
|
|
||||||
debug("Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.UPTIME_KUMA_LOG_RESPONSE_BODY_MONITOR_ID == this.id) {
|
|
||||||
console.log(res.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.type === "http") {
|
|
||||||
bean.status = UP;
|
|
||||||
} else {
|
|
||||||
|
|
||||||
let data = res.data;
|
|
||||||
|
|
||||||
// Convert to string for object/array
|
|
||||||
if (typeof data !== "string") {
|
|
||||||
data = JSON.stringify(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.includes(this.keyword)) {
|
|
||||||
bean.msg += ", keyword is found";
|
|
||||||
bean.status = UP;
|
|
||||||
} else {
|
|
||||||
throw new Error(bean.msg + ", but keyword is not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} 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);
|
|
||||||
bean.msg = "";
|
|
||||||
bean.status = UP;
|
|
||||||
} else if (this.type === "dns") {
|
|
||||||
let startTime = dayjs().valueOf();
|
|
||||||
let dnsMessage = "";
|
|
||||||
|
|
||||||
let dnsRes = await dnsResolve(this.hostname, this.dns_resolve_server, this.dns_resolve_type);
|
|
||||||
bean.ping = dayjs().valueOf() - startTime;
|
|
||||||
|
|
||||||
if (this.dns_resolve_type == "A" || this.dns_resolve_type == "AAAA" || this.dns_resolve_type == "TXT") {
|
|
||||||
dnsMessage += "Records: ";
|
|
||||||
dnsMessage += dnsRes.join(" | ");
|
|
||||||
} else if (this.dns_resolve_type == "CNAME" || this.dns_resolve_type == "PTR") {
|
|
||||||
dnsMessage = dnsRes[0];
|
|
||||||
} else if (this.dns_resolve_type == "CAA") {
|
|
||||||
dnsMessage = dnsRes[0].issue;
|
|
||||||
} else if (this.dns_resolve_type == "MX") {
|
|
||||||
dnsRes.forEach(record => {
|
|
||||||
dnsMessage += `Hostname: ${record.exchange} - Priority: ${record.priority} | `;
|
|
||||||
});
|
|
||||||
dnsMessage = dnsMessage.slice(0, -2);
|
|
||||||
} else if (this.dns_resolve_type == "NS") {
|
|
||||||
dnsMessage += "Servers: ";
|
|
||||||
dnsMessage += dnsRes.join(" | ");
|
|
||||||
} else if (this.dns_resolve_type == "SOA") {
|
|
||||||
dnsMessage += `NS-Name: ${dnsRes.nsname} | Hostmaster: ${dnsRes.hostmaster} | Serial: ${dnsRes.serial} | Refresh: ${dnsRes.refresh} | Retry: ${dnsRes.retry} | Expire: ${dnsRes.expire} | MinTTL: ${dnsRes.minttl}`;
|
|
||||||
} else if (this.dns_resolve_type == "SRV") {
|
|
||||||
dnsRes.forEach(record => {
|
|
||||||
dnsMessage += `Name: ${record.name} | Port: ${record.port} | Priority: ${record.priority} | Weight: ${record.weight} | `;
|
|
||||||
});
|
|
||||||
dnsMessage = dnsMessage.slice(0, -2);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.dnsLastResult !== dnsMessage) {
|
|
||||||
R.exec("UPDATE `monitor` SET dns_last_result = ? WHERE id = ? ", [
|
|
||||||
dnsMessage,
|
|
||||||
this.id
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
bean.msg = dnsMessage;
|
|
||||||
bean.status = UP;
|
|
||||||
} else if (this.type === "push") { // Type: Push
|
|
||||||
const time = R.isoDateTime(dayjs.utc().subtract(this.interval, "second"));
|
|
||||||
|
|
||||||
let heartbeatCount = await R.count("heartbeat", " monitor_id = ? AND time > ? ", [
|
|
||||||
this.id,
|
|
||||||
time
|
|
||||||
]);
|
|
||||||
|
|
||||||
debug("heartbeatCount" + heartbeatCount + " " + time);
|
|
||||||
|
|
||||||
if (heartbeatCount <= 0) {
|
|
||||||
// Fix #922, since previous heartbeat could be inserted by api, it should get from database
|
|
||||||
previousBeat = await Monitor.getPreviousHeartbeat(this.id);
|
|
||||||
|
|
||||||
throw new Error("No heartbeat in the time window");
|
|
||||||
} else {
|
|
||||||
// No need to insert successful heartbeat for push type, so end here
|
|
||||||
retries = 0;
|
|
||||||
this.heartbeatInterval = setTimeout(beat, this.interval * 1000);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (this.type === "steam") {
|
|
||||||
const steamApiUrl = "https://api.steampowered.com/IGameServersService/GetServerList/v1/";
|
|
||||||
const steamAPIKey = await setting("steamAPIKey");
|
|
||||||
const filter = `addr\\${this.hostname}:${this.port}`;
|
|
||||||
|
|
||||||
if (!steamAPIKey) {
|
|
||||||
throw new Error("Steam API Key not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
let res = await axios.get(steamApiUrl, {
|
|
||||||
timeout: this.interval * 1000 * 0.8,
|
|
||||||
headers: {
|
|
||||||
"Accept": "*/*",
|
|
||||||
"User-Agent": "Uptime-Kuma/" + version,
|
|
||||||
},
|
|
||||||
httpsAgent: new https.Agent({
|
|
||||||
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
|
||||||
rejectUnauthorized: ! this.getIgnoreTls(),
|
|
||||||
}),
|
|
||||||
maxRedirects: this.maxredirects,
|
|
||||||
validateStatus: (status) => {
|
|
||||||
return checkStatusCode(status, this.getAcceptedStatuscodes());
|
|
||||||
},
|
|
||||||
params: {
|
|
||||||
filter: filter,
|
|
||||||
key: steamAPIKey,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.data.response && res.data.response.servers && res.data.response.servers.length > 0) {
|
|
||||||
bean.status = UP;
|
|
||||||
bean.msg = res.data.response.servers[0].name;
|
|
||||||
|
|
||||||
try {
|
|
||||||
bean.ping = await ping(this.hostname);
|
|
||||||
} catch (_) { }
|
|
||||||
} else {
|
|
||||||
throw new Error("Server not found on Steam");
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
bean.msg = "Unknown Monitor Type";
|
|
||||||
bean.status = PENDING;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isUpsideDown()) {
|
|
||||||
bean.status = flipStatus(bean.status);
|
|
||||||
|
|
||||||
if (bean.status === DOWN) {
|
|
||||||
throw new Error("Flip UP to DOWN");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
retries = 0;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
|
|
||||||
bean.msg = error.message;
|
|
||||||
|
|
||||||
// If UP come in here, it must be upside down mode
|
|
||||||
// Just reset the retries
|
|
||||||
if (this.isUpsideDown() && bean.status === UP) {
|
|
||||||
retries = 0;
|
|
||||||
|
|
||||||
} else if ((this.maxretries > 0) && (retries < this.maxretries)) {
|
|
||||||
retries++;
|
|
||||||
bean.status = PENDING;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let beatInterval = this.interval;
|
|
||||||
|
|
||||||
debug(`[${this.name}] Check isImportant`);
|
|
||||||
let isImportant = Monitor.isImportantBeat(isFirstBeat, previousBeat?.status, bean.status);
|
|
||||||
|
|
||||||
// Mark as important if status changed, ignore pending pings,
|
|
||||||
// Don't notify if disrupted changes to up
|
|
||||||
if (isImportant) {
|
|
||||||
bean.important = true;
|
|
||||||
|
|
||||||
debug(`[${this.name}] sendNotification`);
|
|
||||||
await Monitor.sendNotification(isFirstBeat, this, bean);
|
|
||||||
|
|
||||||
// Clear Status Page Cache
|
|
||||||
debug(`[${this.name}] apicache clear`);
|
|
||||||
apicache.clear();
|
|
||||||
|
|
||||||
} else {
|
|
||||||
bean.important = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bean.status === UP) {
|
|
||||||
console.info(`Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
|
||||||
} else if (bean.status === PENDING) {
|
|
||||||
if (this.retryInterval > 0) {
|
|
||||||
beatInterval = this.retryInterval;
|
|
||||||
}
|
|
||||||
console.warn(`Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Max retries: ${this.maxretries} | Retry: ${retries} | Retry Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
|
||||||
} else {
|
|
||||||
console.warn(`Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
debug(`[${this.name}] Send to socket`);
|
|
||||||
io.to(this.user_id).emit("heartbeat", bean.toJSON());
|
|
||||||
Monitor.sendStats(io, this.id, this.user_id);
|
|
||||||
|
|
||||||
debug(`[${this.name}] Store`);
|
|
||||||
await R.store(bean);
|
|
||||||
|
|
||||||
debug(`[${this.name}] prometheus.update`);
|
|
||||||
prometheus.update(bean, tlsInfo);
|
|
||||||
|
|
||||||
previousBeat = bean;
|
|
||||||
|
|
||||||
if (! this.isStop) {
|
|
||||||
|
|
||||||
if (demoMode) {
|
|
||||||
if (beatInterval < 20) {
|
|
||||||
console.log("beat interval too low, reset to 20s");
|
|
||||||
beatInterval = 20;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
debug(`[${this.name}] SetTimeout for next check.`);
|
|
||||||
this.heartbeatInterval = setTimeout(safeBeat, beatInterval * 1000);
|
|
||||||
} else {
|
|
||||||
console.log(`[${this.name}] isStop = true, no next check.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
const safeBeat = async () => {
|
|
||||||
try {
|
|
||||||
await beat();
|
|
||||||
} catch (e) {
|
|
||||||
console.trace(e);
|
|
||||||
errorLog(e, false);
|
|
||||||
console.error("Please report to https://github.com/louislam/uptime-kuma/issues");
|
|
||||||
|
|
||||||
if (! this.isStop) {
|
|
||||||
console.log("Try to restart the monitor");
|
|
||||||
this.heartbeatInterval = setTimeout(safeBeat, this.interval * 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Delay Push Type
|
|
||||||
if (this.type === "push") {
|
|
||||||
setTimeout(() => {
|
|
||||||
safeBeat();
|
|
||||||
}, this.interval * 1000);
|
|
||||||
} else {
|
|
||||||
safeBeat();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
clearTimeout(this.heartbeatInterval);
|
|
||||||
this.isStop = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper Method:
|
|
||||||
* returns URL object for further usage
|
|
||||||
* returns null if url is invalid
|
|
||||||
* @returns {null|URL}
|
|
||||||
*/
|
|
||||||
getUrl() {
|
|
||||||
try {
|
|
||||||
return new URL(this.url);
|
|
||||||
} catch (_) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Store TLS info to database
|
|
||||||
* @param checkCertificateResult
|
|
||||||
* @returns {Promise<object>}
|
|
||||||
*/
|
|
||||||
async updateTlsInfo(checkCertificateResult) {
|
|
||||||
let tls_info_bean = await R.findOne("monitor_tls_info", "monitor_id = ?", [
|
|
||||||
this.id,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (tls_info_bean == null) {
|
|
||||||
tls_info_bean = R.dispense("monitor_tls_info");
|
|
||||||
tls_info_bean.monitor_id = this.id;
|
|
||||||
} else {
|
|
||||||
|
|
||||||
// Clear sent history if the cert changed.
|
|
||||||
try {
|
|
||||||
let oldCertInfo = JSON.parse(tls_info_bean.info_json);
|
|
||||||
|
|
||||||
let isValidObjects = oldCertInfo && oldCertInfo.certInfo && checkCertificateResult && checkCertificateResult.certInfo;
|
|
||||||
|
|
||||||
if (isValidObjects) {
|
|
||||||
if (oldCertInfo.certInfo.fingerprint256 !== checkCertificateResult.certInfo.fingerprint256) {
|
|
||||||
debug("Resetting sent_history");
|
|
||||||
await R.exec("DELETE FROM notification_sent_history WHERE type = 'certificate' AND monitor_id = ?", [
|
|
||||||
this.id
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
debug("No need to reset sent_history");
|
|
||||||
debug(oldCertInfo.certInfo.fingerprint256);
|
|
||||||
debug(checkCertificateResult.certInfo.fingerprint256);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
debug("Not valid object");
|
|
||||||
}
|
|
||||||
} catch (e) { }
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
tls_info_bean.info_json = JSON.stringify(checkCertificateResult);
|
|
||||||
await R.store(tls_info_bean);
|
|
||||||
|
|
||||||
return checkCertificateResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async sendStats(io, monitorID, userID) {
|
|
||||||
const hasClients = getTotalClientInRoom(io, userID) > 0;
|
|
||||||
|
|
||||||
if (hasClients) {
|
|
||||||
await Monitor.sendAvgPing(24, io, monitorID, userID);
|
|
||||||
await Monitor.sendUptime(24, io, monitorID, userID);
|
|
||||||
await Monitor.sendUptime(24 * 30, io, monitorID, userID);
|
|
||||||
await Monitor.sendCertInfo(io, monitorID, userID);
|
|
||||||
} else {
|
|
||||||
debug("No clients in the room, no need to send stats");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param duration : int Hours
|
|
||||||
*/
|
|
||||||
static async sendAvgPing(duration, io, monitorID, userID) {
|
|
||||||
const timeLogger = new TimeLogger();
|
|
||||||
|
|
||||||
let avgPing = parseInt(await R.getCell(`
|
|
||||||
SELECT AVG(ping)
|
|
||||||
FROM heartbeat
|
|
||||||
WHERE time > DATETIME('now', ? || ' hours')
|
|
||||||
AND ping IS NOT NULL
|
|
||||||
AND monitor_id = ? `, [
|
|
||||||
-duration,
|
|
||||||
monitorID,
|
|
||||||
]));
|
|
||||||
|
|
||||||
timeLogger.print(`[Monitor: ${monitorID}] avgPing`);
|
|
||||||
|
|
||||||
io.to(userID).emit("avgPing", monitorID, avgPing);
|
|
||||||
}
|
|
||||||
|
|
||||||
static async sendCertInfo(io, monitorID, userID) {
|
|
||||||
let tls_info = await R.findOne("monitor_tls_info", "monitor_id = ?", [
|
|
||||||
monitorID,
|
|
||||||
]);
|
|
||||||
if (tls_info != null) {
|
|
||||||
io.to(userID).emit("certInfo", monitorID, tls_info.info_json);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Uptime with calculation
|
|
||||||
* Calculation based on:
|
|
||||||
* https://www.uptrends.com/support/kb/reporting/calculation-of-uptime-and-downtime
|
|
||||||
* @param duration : int Hours
|
|
||||||
*/
|
|
||||||
static async calcUptime(duration, monitorID) {
|
|
||||||
const timeLogger = new TimeLogger();
|
|
||||||
|
|
||||||
const startTime = R.isoDateTime(dayjs.utc().subtract(duration, "hour"));
|
|
||||||
|
|
||||||
// Handle if heartbeat duration longer than the target duration
|
|
||||||
// e.g. If the last beat's duration is bigger that the 24hrs window, it will use the duration between the (beat time - window margin) (THEN case in SQL)
|
|
||||||
let result = await R.getRow(`
|
|
||||||
SELECT
|
|
||||||
-- SUM all duration, also trim off the beat out of time window
|
|
||||||
SUM(
|
|
||||||
CASE
|
|
||||||
WHEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400 < duration
|
|
||||||
THEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400
|
|
||||||
ELSE duration
|
|
||||||
END
|
|
||||||
) AS total_duration,
|
|
||||||
|
|
||||||
-- SUM all uptime duration, also trim off the beat out of time window
|
|
||||||
SUM(
|
|
||||||
CASE
|
|
||||||
WHEN (status = 1)
|
|
||||||
THEN
|
|
||||||
CASE
|
|
||||||
WHEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400 < duration
|
|
||||||
THEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400
|
|
||||||
ELSE duration
|
|
||||||
END
|
|
||||||
END
|
|
||||||
) AS uptime_duration
|
|
||||||
FROM heartbeat
|
|
||||||
WHERE time > ?
|
|
||||||
AND monitor_id = ?
|
|
||||||
`, [
|
|
||||||
startTime, startTime, startTime, startTime, startTime,
|
|
||||||
monitorID,
|
|
||||||
]);
|
|
||||||
|
|
||||||
timeLogger.print(`[Monitor: ${monitorID}][${duration}] sendUptime`);
|
|
||||||
|
|
||||||
let totalDuration = result.total_duration;
|
|
||||||
let uptimeDuration = result.uptime_duration;
|
|
||||||
let uptime = 0;
|
|
||||||
|
|
||||||
if (totalDuration > 0) {
|
|
||||||
uptime = uptimeDuration / totalDuration;
|
|
||||||
if (uptime < 0) {
|
|
||||||
uptime = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// Handle new monitor with only one beat, because the beat's duration = 0
|
|
||||||
let status = parseInt(await R.getCell("SELECT `status` FROM heartbeat WHERE monitor_id = ?", [ monitorID ]));
|
|
||||||
|
|
||||||
if (status === UP) {
|
|
||||||
uptime = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return uptime;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send Uptime
|
|
||||||
* @param duration : int Hours
|
|
||||||
*/
|
|
||||||
static async sendUptime(duration, io, monitorID, userID) {
|
|
||||||
const uptime = await this.calcUptime(duration, monitorID);
|
|
||||||
io.to(userID).emit("uptime", monitorID, duration, uptime);
|
|
||||||
}
|
|
||||||
|
|
||||||
static isImportantBeat(isFirstBeat, previousBeatStatus, currentBeatStatus) {
|
|
||||||
// * ? -> ANY STATUS = important [isFirstBeat]
|
|
||||||
// UP -> PENDING = not important
|
|
||||||
// * UP -> DOWN = important
|
|
||||||
// UP -> UP = not important
|
|
||||||
// PENDING -> PENDING = not important
|
|
||||||
// * PENDING -> DOWN = important
|
|
||||||
// PENDING -> UP = not important
|
|
||||||
// DOWN -> PENDING = this case not exists
|
|
||||||
// DOWN -> DOWN = not important
|
|
||||||
// * DOWN -> UP = important
|
|
||||||
let isImportant = isFirstBeat ||
|
|
||||||
(previousBeatStatus === UP && currentBeatStatus === DOWN) ||
|
|
||||||
(previousBeatStatus === DOWN && currentBeatStatus === UP) ||
|
|
||||||
(previousBeatStatus === PENDING && currentBeatStatus === DOWN);
|
|
||||||
return isImportant;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async sendNotification(isFirstBeat, monitor, bean) {
|
|
||||||
if (!isFirstBeat || bean.status === DOWN) {
|
|
||||||
const notificationList = await Monitor.getNotificationList(monitor);
|
|
||||||
|
|
||||||
let text;
|
|
||||||
if (bean.status === UP) {
|
|
||||||
text = "✅ Up";
|
|
||||||
} else {
|
|
||||||
text = "🔴 Down";
|
|
||||||
}
|
|
||||||
|
|
||||||
let msg = `[${monitor.name}] [${text}] ${bean.msg}`;
|
|
||||||
|
|
||||||
for (let notification of notificationList) {
|
|
||||||
try {
|
|
||||||
await Notification.send(JSON.parse(notification.config), msg, await monitor.toJSON(), bean.toJSON());
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Cannot send notification to " + notification.name);
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async getNotificationList(monitor) {
|
|
||||||
let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [
|
|
||||||
monitor.id,
|
|
||||||
]);
|
|
||||||
return notificationList;
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendCertNotification(tlsInfoObject) {
|
|
||||||
if (tlsInfoObject && tlsInfoObject.certInfo && tlsInfoObject.certInfo.daysRemaining) {
|
|
||||||
const notificationList = await Monitor.getNotificationList(this);
|
|
||||||
|
|
||||||
debug("call sendCertNotificationByTargetDays");
|
|
||||||
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, 21, notificationList);
|
|
||||||
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, 14, notificationList);
|
|
||||||
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, 7, notificationList);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendCertNotificationByTargetDays(daysRemaining, targetDays, notificationList) {
|
|
||||||
|
|
||||||
if (daysRemaining > targetDays) {
|
|
||||||
debug(`No need to send cert notification. ${daysRemaining} > ${targetDays}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (notificationList.length > 0) {
|
|
||||||
|
|
||||||
let row = await R.getRow("SELECT * FROM notification_sent_history WHERE type = ? AND monitor_id = ? AND days = ?", [
|
|
||||||
"certificate",
|
|
||||||
this.id,
|
|
||||||
targetDays,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Sent already, no need to send again
|
|
||||||
if (row) {
|
|
||||||
debug("Sent already, no need to send again");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let sent = false;
|
|
||||||
debug("Send certificate notification");
|
|
||||||
|
|
||||||
for (let notification of notificationList) {
|
|
||||||
try {
|
|
||||||
debug("Sending to " + notification.name);
|
|
||||||
await Notification.send(JSON.parse(notification.config), `[${this.name}][${this.url}] Certificate will be expired in ${daysRemaining} days`);
|
|
||||||
sent = true;
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Cannot send cert notification to " + notification.name);
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sent) {
|
|
||||||
await R.exec("INSERT INTO notification_sent_history (type, monitor_id, days) VALUES(?, ?, ?)", [
|
|
||||||
"certificate",
|
|
||||||
this.id,
|
|
||||||
targetDays,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
debug("No notification, no need to send cert notification");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async getPreviousHeartbeat(monitorID) {
|
|
||||||
return await R.getRow(`
|
|
||||||
SELECT status, time FROM heartbeat
|
|
||||||
WHERE id = (select MAX(id) from heartbeat where monitor_id = ?)
|
|
||||||
`, [
|
|
||||||
monitorID
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = Monitor;
|
|
|
@ -1,13 +0,0 @@
|
||||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
|
||||||
|
|
||||||
class Tag extends BeanModel {
|
|
||||||
toJSON() {
|
|
||||||
return {
|
|
||||||
id: this._id,
|
|
||||||
name: this._name,
|
|
||||||
color: this._color,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = Tag;
|
|
|
@ -1,21 +0,0 @@
|
||||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
|
||||||
const passwordHash = require("../password-hash");
|
|
||||||
const { R } = require("redbean-node");
|
|
||||||
|
|
||||||
class User extends BeanModel {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Direct execute, no need R.store()
|
|
||||||
* @param newPassword
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async resetPassword(newPassword) {
|
|
||||||
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
|
|
||||||
passwordHash.generate(newPassword),
|
|
||||||
this.id
|
|
||||||
]);
|
|
||||||
this.password = newPassword;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = User;
|
|
|
@ -1,749 +0,0 @@
|
||||||
let url = require("url");
|
|
||||||
let MemoryCache = require("./memory-cache");
|
|
||||||
|
|
||||||
let t = {
|
|
||||||
ms: 1,
|
|
||||||
second: 1000,
|
|
||||||
minute: 60000,
|
|
||||||
hour: 3600000,
|
|
||||||
day: 3600000 * 24,
|
|
||||||
week: 3600000 * 24 * 7,
|
|
||||||
month: 3600000 * 24 * 30,
|
|
||||||
};
|
|
||||||
|
|
||||||
let instances = [];
|
|
||||||
|
|
||||||
let matches = function (a) {
|
|
||||||
return function (b) {
|
|
||||||
return a === b;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
let doesntMatch = function (a) {
|
|
||||||
return function (b) {
|
|
||||||
return !matches(a)(b);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
let logDuration = function (d, prefix) {
|
|
||||||
let str = d > 1000 ? (d / 1000).toFixed(2) + "sec" : d + "ms";
|
|
||||||
return "\x1b[33m- " + (prefix ? prefix + " " : "") + str + "\x1b[0m";
|
|
||||||
};
|
|
||||||
|
|
||||||
function getSafeHeaders(res) {
|
|
||||||
return res.getHeaders ? res.getHeaders() : res._headers;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ApiCache() {
|
|
||||||
let memCache = new MemoryCache();
|
|
||||||
|
|
||||||
let globalOptions = {
|
|
||||||
debug: false,
|
|
||||||
defaultDuration: 3600000,
|
|
||||||
enabled: true,
|
|
||||||
appendKey: [],
|
|
||||||
jsonp: false,
|
|
||||||
redisClient: false,
|
|
||||||
headerBlacklist: [],
|
|
||||||
statusCodes: {
|
|
||||||
include: [],
|
|
||||||
exclude: [],
|
|
||||||
},
|
|
||||||
events: {
|
|
||||||
expire: undefined,
|
|
||||||
},
|
|
||||||
headers: {
|
|
||||||
// 'cache-control': 'no-cache' // example of header overwrite
|
|
||||||
},
|
|
||||||
trackPerformance: false,
|
|
||||||
respectCacheControl: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
let middlewareOptions = [];
|
|
||||||
let instance = this;
|
|
||||||
let index = null;
|
|
||||||
let timers = {};
|
|
||||||
let performanceArray = []; // for tracking cache hit rate
|
|
||||||
|
|
||||||
instances.push(this);
|
|
||||||
this.id = instances.length;
|
|
||||||
|
|
||||||
function debug(a, b, c, d) {
|
|
||||||
let arr = ["\x1b[36m[apicache]\x1b[0m", a, b, c, d].filter(function (arg) {
|
|
||||||
return arg !== undefined;
|
|
||||||
});
|
|
||||||
let debugEnv = process.env.DEBUG && process.env.DEBUG.split(",").indexOf("apicache") !== -1;
|
|
||||||
|
|
||||||
return (globalOptions.debug || debugEnv) && console.log.apply(null, arr);
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldCacheResponse(request, response, toggle) {
|
|
||||||
let opt = globalOptions;
|
|
||||||
let codes = opt.statusCodes;
|
|
||||||
|
|
||||||
if (!response) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toggle && !toggle(request, response)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (codes.exclude && codes.exclude.length && codes.exclude.indexOf(response.statusCode) !== -1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (codes.include && codes.include.length && codes.include.indexOf(response.statusCode) === -1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function addIndexEntries(key, req) {
|
|
||||||
let groupName = req.apicacheGroup;
|
|
||||||
|
|
||||||
if (groupName) {
|
|
||||||
debug("group detected \"" + groupName + "\"");
|
|
||||||
let group = (index.groups[groupName] = index.groups[groupName] || []);
|
|
||||||
group.unshift(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
index.all.unshift(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
function filterBlacklistedHeaders(headers) {
|
|
||||||
return Object.keys(headers)
|
|
||||||
.filter(function (key) {
|
|
||||||
return globalOptions.headerBlacklist.indexOf(key) === -1;
|
|
||||||
})
|
|
||||||
.reduce(function (acc, header) {
|
|
||||||
acc[header] = headers[header];
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
function createCacheObject(status, headers, data, encoding) {
|
|
||||||
return {
|
|
||||||
status: status,
|
|
||||||
headers: filterBlacklistedHeaders(headers),
|
|
||||||
data: data,
|
|
||||||
encoding: encoding,
|
|
||||||
timestamp: new Date().getTime() / 1000, // seconds since epoch. This is used to properly decrement max-age headers in cached responses.
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function cacheResponse(key, value, duration) {
|
|
||||||
let redis = globalOptions.redisClient;
|
|
||||||
let expireCallback = globalOptions.events.expire;
|
|
||||||
|
|
||||||
if (redis && redis.connected) {
|
|
||||||
try {
|
|
||||||
redis.hset(key, "response", JSON.stringify(value));
|
|
||||||
redis.hset(key, "duration", duration);
|
|
||||||
redis.expire(key, duration / 1000, expireCallback || function () {});
|
|
||||||
} catch (err) {
|
|
||||||
debug("[apicache] error in redis.hset()");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
memCache.add(key, value, duration, expireCallback);
|
|
||||||
}
|
|
||||||
|
|
||||||
// add automatic cache clearing from duration, includes max limit on setTimeout
|
|
||||||
timers[key] = setTimeout(function () {
|
|
||||||
instance.clear(key, true);
|
|
||||||
}, Math.min(duration, 2147483647));
|
|
||||||
}
|
|
||||||
|
|
||||||
function accumulateContent(res, content) {
|
|
||||||
if (content) {
|
|
||||||
if (typeof content == "string") {
|
|
||||||
res._apicache.content = (res._apicache.content || "") + content;
|
|
||||||
} else if (Buffer.isBuffer(content)) {
|
|
||||||
let oldContent = res._apicache.content;
|
|
||||||
|
|
||||||
if (typeof oldContent === "string") {
|
|
||||||
oldContent = !Buffer.from ? new Buffer(oldContent) : Buffer.from(oldContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!oldContent) {
|
|
||||||
oldContent = !Buffer.alloc ? new Buffer(0) : Buffer.alloc(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
res._apicache.content = Buffer.concat(
|
|
||||||
[oldContent, content],
|
|
||||||
oldContent.length + content.length
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
res._apicache.content = content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeResponseCacheable(req, res, next, key, duration, strDuration, toggle) {
|
|
||||||
// monkeypatch res.end to create cache object
|
|
||||||
res._apicache = {
|
|
||||||
write: res.write,
|
|
||||||
writeHead: res.writeHead,
|
|
||||||
end: res.end,
|
|
||||||
cacheable: true,
|
|
||||||
content: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
// append header overwrites if applicable
|
|
||||||
Object.keys(globalOptions.headers).forEach(function (name) {
|
|
||||||
res.setHeader(name, globalOptions.headers[name]);
|
|
||||||
});
|
|
||||||
|
|
||||||
res.writeHead = function () {
|
|
||||||
// add cache control headers
|
|
||||||
if (!globalOptions.headers["cache-control"]) {
|
|
||||||
if (shouldCacheResponse(req, res, toggle)) {
|
|
||||||
res.setHeader("cache-control", "max-age=" + (duration / 1000).toFixed(0));
|
|
||||||
} else {
|
|
||||||
res.setHeader("cache-control", "no-cache, no-store, must-revalidate");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res._apicache.headers = Object.assign({}, getSafeHeaders(res));
|
|
||||||
return res._apicache.writeHead.apply(this, arguments);
|
|
||||||
};
|
|
||||||
|
|
||||||
// patch res.write
|
|
||||||
res.write = function (content) {
|
|
||||||
accumulateContent(res, content);
|
|
||||||
return res._apicache.write.apply(this, arguments);
|
|
||||||
};
|
|
||||||
|
|
||||||
// patch res.end
|
|
||||||
res.end = function (content, encoding) {
|
|
||||||
if (shouldCacheResponse(req, res, toggle)) {
|
|
||||||
accumulateContent(res, content);
|
|
||||||
|
|
||||||
if (res._apicache.cacheable && res._apicache.content) {
|
|
||||||
addIndexEntries(key, req);
|
|
||||||
let headers = res._apicache.headers || getSafeHeaders(res);
|
|
||||||
let cacheObject = createCacheObject(
|
|
||||||
res.statusCode,
|
|
||||||
headers,
|
|
||||||
res._apicache.content,
|
|
||||||
encoding
|
|
||||||
);
|
|
||||||
cacheResponse(key, cacheObject, duration);
|
|
||||||
|
|
||||||
// display log entry
|
|
||||||
let elapsed = new Date() - req.apicacheTimer;
|
|
||||||
debug("adding cache entry for \"" + key + "\" @ " + strDuration, logDuration(elapsed));
|
|
||||||
debug("_apicache.headers: ", res._apicache.headers);
|
|
||||||
debug("res.getHeaders(): ", getSafeHeaders(res));
|
|
||||||
debug("cacheObject: ", cacheObject);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res._apicache.end.apply(this, arguments);
|
|
||||||
};
|
|
||||||
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendCachedResponse(request, response, cacheObject, toggle, next, duration) {
|
|
||||||
if (toggle && !toggle(request, response)) {
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
|
|
||||||
let headers = getSafeHeaders(response);
|
|
||||||
|
|
||||||
// Modified by @louislam, removed Cache-control, since I don't need client side cache!
|
|
||||||
// Original Source: https://github.com/kwhitley/apicache/blob/0d5686cc21fad353c6dddee646288c2fca3e4f50/src/apicache.js#L254
|
|
||||||
Object.assign(headers, filterBlacklistedHeaders(cacheObject.headers || {}));
|
|
||||||
|
|
||||||
// only embed apicache headers when not in production environment
|
|
||||||
if (process.env.NODE_ENV !== "production") {
|
|
||||||
Object.assign(headers, {
|
|
||||||
"apicache-store": globalOptions.redisClient ? "redis" : "memory",
|
|
||||||
"apicache-version": "1.6.2-modified",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// unstringify buffers
|
|
||||||
let data = cacheObject.data;
|
|
||||||
if (data && data.type === "Buffer") {
|
|
||||||
data =
|
|
||||||
typeof data.data === "number" ? new Buffer.alloc(data.data) : new Buffer.from(data.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// test Etag against If-None-Match for 304
|
|
||||||
let cachedEtag = cacheObject.headers.etag;
|
|
||||||
let requestEtag = request.headers["if-none-match"];
|
|
||||||
|
|
||||||
if (requestEtag && cachedEtag === requestEtag) {
|
|
||||||
response.writeHead(304, headers);
|
|
||||||
return response.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
response.writeHead(cacheObject.status || 200, headers);
|
|
||||||
|
|
||||||
return response.end(data, cacheObject.encoding);
|
|
||||||
}
|
|
||||||
|
|
||||||
function syncOptions() {
|
|
||||||
for (let i in middlewareOptions) {
|
|
||||||
Object.assign(middlewareOptions[i].options, globalOptions, middlewareOptions[i].localOptions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.clear = function (target, isAutomatic) {
|
|
||||||
let group = index.groups[target];
|
|
||||||
let redis = globalOptions.redisClient;
|
|
||||||
|
|
||||||
if (group) {
|
|
||||||
debug("clearing group \"" + target + "\"");
|
|
||||||
|
|
||||||
group.forEach(function (key) {
|
|
||||||
debug("clearing cached entry for \"" + key + "\"");
|
|
||||||
clearTimeout(timers[key]);
|
|
||||||
delete timers[key];
|
|
||||||
if (!globalOptions.redisClient) {
|
|
||||||
memCache.delete(key);
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
redis.del(key);
|
|
||||||
} catch (err) {
|
|
||||||
console.log("[apicache] error in redis.del(\"" + key + "\")");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
index.all = index.all.filter(doesntMatch(key));
|
|
||||||
});
|
|
||||||
|
|
||||||
delete index.groups[target];
|
|
||||||
} else if (target) {
|
|
||||||
debug("clearing " + (isAutomatic ? "expired" : "cached") + " entry for \"" + target + "\"");
|
|
||||||
clearTimeout(timers[target]);
|
|
||||||
delete timers[target];
|
|
||||||
// clear actual cached entry
|
|
||||||
if (!redis) {
|
|
||||||
memCache.delete(target);
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
redis.del(target);
|
|
||||||
} catch (err) {
|
|
||||||
console.log("[apicache] error in redis.del(\"" + target + "\")");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove from global index
|
|
||||||
index.all = index.all.filter(doesntMatch(target));
|
|
||||||
|
|
||||||
// remove target from each group that it may exist in
|
|
||||||
Object.keys(index.groups).forEach(function (groupName) {
|
|
||||||
index.groups[groupName] = index.groups[groupName].filter(doesntMatch(target));
|
|
||||||
|
|
||||||
// delete group if now empty
|
|
||||||
if (!index.groups[groupName].length) {
|
|
||||||
delete index.groups[groupName];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
debug("clearing entire index");
|
|
||||||
|
|
||||||
if (!redis) {
|
|
||||||
memCache.clear();
|
|
||||||
} else {
|
|
||||||
// clear redis keys one by one from internal index to prevent clearing non-apicache entries
|
|
||||||
index.all.forEach(function (key) {
|
|
||||||
clearTimeout(timers[key]);
|
|
||||||
delete timers[key];
|
|
||||||
try {
|
|
||||||
redis.del(key);
|
|
||||||
} catch (err) {
|
|
||||||
console.log("[apicache] error in redis.del(\"" + key + "\")");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.resetIndex();
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.getIndex();
|
|
||||||
};
|
|
||||||
|
|
||||||
function parseDuration(duration, defaultDuration) {
|
|
||||||
if (typeof duration === "number") {
|
|
||||||
return duration;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof duration === "string") {
|
|
||||||
let split = duration.match(/^([\d\.,]+)\s?(\w+)$/);
|
|
||||||
|
|
||||||
if (split.length === 3) {
|
|
||||||
let len = parseFloat(split[1]);
|
|
||||||
let unit = split[2].replace(/s$/i, "").toLowerCase();
|
|
||||||
if (unit === "m") {
|
|
||||||
unit = "ms";
|
|
||||||
}
|
|
||||||
|
|
||||||
return (len || 1) * (t[unit] || 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaultDuration;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.getDuration = function (duration) {
|
|
||||||
return parseDuration(duration, globalOptions.defaultDuration);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return cache performance statistics (hit rate). Suitable for putting into a route:
|
|
||||||
* <code>
|
|
||||||
* app.get('/api/cache/performance', (req, res) => {
|
|
||||||
* res.json(apicache.getPerformance())
|
|
||||||
* })
|
|
||||||
* </code>
|
|
||||||
*/
|
|
||||||
this.getPerformance = function () {
|
|
||||||
return performanceArray.map(function (p) {
|
|
||||||
return p.report();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
this.getIndex = function (group) {
|
|
||||||
if (group) {
|
|
||||||
return index.groups[group];
|
|
||||||
} else {
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.middleware = function cache(strDuration, middlewareToggle, localOptions) {
|
|
||||||
let duration = instance.getDuration(strDuration);
|
|
||||||
let opt = {};
|
|
||||||
|
|
||||||
middlewareOptions.push({
|
|
||||||
options: opt,
|
|
||||||
});
|
|
||||||
|
|
||||||
let options = function (localOptions) {
|
|
||||||
if (localOptions) {
|
|
||||||
middlewareOptions.find(function (middleware) {
|
|
||||||
return middleware.options === opt;
|
|
||||||
}).localOptions = localOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
syncOptions();
|
|
||||||
|
|
||||||
return opt;
|
|
||||||
};
|
|
||||||
|
|
||||||
options(localOptions);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A Function for non tracking performance
|
|
||||||
*/
|
|
||||||
function NOOPCachePerformance() {
|
|
||||||
this.report = this.hit = this.miss = function () {}; // noop;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A function for tracking and reporting hit rate. These statistics are returned by the getPerformance() call above.
|
|
||||||
*/
|
|
||||||
function CachePerformance() {
|
|
||||||
/**
|
|
||||||
* Tracks the hit rate for the last 100 requests.
|
|
||||||
* If there have been fewer than 100 requests, the hit rate just considers the requests that have happened.
|
|
||||||
*/
|
|
||||||
this.hitsLast100 = new Uint8Array(100 / 4); // each hit is 2 bits
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tracks the hit rate for the last 1000 requests.
|
|
||||||
* If there have been fewer than 1000 requests, the hit rate just considers the requests that have happened.
|
|
||||||
*/
|
|
||||||
this.hitsLast1000 = new Uint8Array(1000 / 4); // each hit is 2 bits
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tracks the hit rate for the last 10000 requests.
|
|
||||||
* If there have been fewer than 10000 requests, the hit rate just considers the requests that have happened.
|
|
||||||
*/
|
|
||||||
this.hitsLast10000 = new Uint8Array(10000 / 4); // each hit is 2 bits
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tracks the hit rate for the last 100000 requests.
|
|
||||||
* If there have been fewer than 100000 requests, the hit rate just considers the requests that have happened.
|
|
||||||
*/
|
|
||||||
this.hitsLast100000 = new Uint8Array(100000 / 4); // each hit is 2 bits
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The number of calls that have passed through the middleware since the server started.
|
|
||||||
*/
|
|
||||||
this.callCount = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The total number of hits since the server started
|
|
||||||
*/
|
|
||||||
this.hitCount = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The key from the last cache hit. This is useful in identifying which route these statistics apply to.
|
|
||||||
*/
|
|
||||||
this.lastCacheHit = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The key from the last cache miss. This is useful in identifying which route these statistics apply to.
|
|
||||||
*/
|
|
||||||
this.lastCacheMiss = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return performance statistics
|
|
||||||
*/
|
|
||||||
this.report = function () {
|
|
||||||
return {
|
|
||||||
lastCacheHit: this.lastCacheHit,
|
|
||||||
lastCacheMiss: this.lastCacheMiss,
|
|
||||||
callCount: this.callCount,
|
|
||||||
hitCount: this.hitCount,
|
|
||||||
missCount: this.callCount - this.hitCount,
|
|
||||||
hitRate: this.callCount == 0 ? null : this.hitCount / this.callCount,
|
|
||||||
hitRateLast100: this.hitRate(this.hitsLast100),
|
|
||||||
hitRateLast1000: this.hitRate(this.hitsLast1000),
|
|
||||||
hitRateLast10000: this.hitRate(this.hitsLast10000),
|
|
||||||
hitRateLast100000: this.hitRate(this.hitsLast100000),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Computes a cache hit rate from an array of hits and misses.
|
|
||||||
* @param {Uint8Array} array An array representing hits and misses.
|
|
||||||
* @returns a number between 0 and 1, or null if the array has no hits or misses
|
|
||||||
*/
|
|
||||||
this.hitRate = function (array) {
|
|
||||||
let hits = 0;
|
|
||||||
let misses = 0;
|
|
||||||
for (let i = 0; i < array.length; i++) {
|
|
||||||
let n8 = array[i];
|
|
||||||
for (let j = 0; j < 4; j++) {
|
|
||||||
switch (n8 & 3) {
|
|
||||||
case 1:
|
|
||||||
hits++;
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
misses++;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
n8 >>= 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let total = hits + misses;
|
|
||||||
if (total == 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return hits / total;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Record a hit or miss in the given array. It will be recorded at a position determined
|
|
||||||
* by the current value of the callCount variable.
|
|
||||||
* @param {Uint8Array} array An array representing hits and misses.
|
|
||||||
* @param {boolean} hit true for a hit, false for a miss
|
|
||||||
* Each element in the array is 8 bits, and encodes 4 hit/miss records.
|
|
||||||
* Each hit or miss is encoded as to bits as follows:
|
|
||||||
* 00 means no hit or miss has been recorded in these bits
|
|
||||||
* 01 encodes a hit
|
|
||||||
* 10 encodes a miss
|
|
||||||
*/
|
|
||||||
this.recordHitInArray = function (array, hit) {
|
|
||||||
let arrayIndex = ~~(this.callCount / 4) % array.length;
|
|
||||||
let bitOffset = (this.callCount % 4) * 2; // 2 bits per record, 4 records per uint8 array element
|
|
||||||
let clearMask = ~(3 << bitOffset);
|
|
||||||
let record = (hit ? 1 : 2) << bitOffset;
|
|
||||||
array[arrayIndex] = (array[arrayIndex] & clearMask) | record;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Records the hit or miss in the tracking arrays and increments the call count.
|
|
||||||
* @param {boolean} hit true records a hit, false records a miss
|
|
||||||
*/
|
|
||||||
this.recordHit = function (hit) {
|
|
||||||
this.recordHitInArray(this.hitsLast100, hit);
|
|
||||||
this.recordHitInArray(this.hitsLast1000, hit);
|
|
||||||
this.recordHitInArray(this.hitsLast10000, hit);
|
|
||||||
this.recordHitInArray(this.hitsLast100000, hit);
|
|
||||||
if (hit) {
|
|
||||||
this.hitCount++;
|
|
||||||
}
|
|
||||||
this.callCount++;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Records a hit event, setting lastCacheMiss to the given key
|
|
||||||
* @param {string} key The key that had the cache hit
|
|
||||||
*/
|
|
||||||
this.hit = function (key) {
|
|
||||||
this.recordHit(true);
|
|
||||||
this.lastCacheHit = key;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Records a miss event, setting lastCacheMiss to the given key
|
|
||||||
* @param {string} key The key that had the cache miss
|
|
||||||
*/
|
|
||||||
this.miss = function (key) {
|
|
||||||
this.recordHit(false);
|
|
||||||
this.lastCacheMiss = key;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let perf = globalOptions.trackPerformance ? new CachePerformance() : new NOOPCachePerformance();
|
|
||||||
|
|
||||||
performanceArray.push(perf);
|
|
||||||
|
|
||||||
let cache = function (req, res, next) {
|
|
||||||
function bypass() {
|
|
||||||
debug("bypass detected, skipping cache.");
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
|
|
||||||
// initial bypass chances
|
|
||||||
if (!opt.enabled) {
|
|
||||||
return bypass();
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
req.headers["x-apicache-bypass"] ||
|
|
||||||
req.headers["x-apicache-force-fetch"] ||
|
|
||||||
(opt.respectCacheControl && req.headers["cache-control"] == "no-cache")
|
|
||||||
) {
|
|
||||||
return bypass();
|
|
||||||
}
|
|
||||||
|
|
||||||
// REMOVED IN 0.11.1 TO CORRECT MIDDLEWARE TOGGLE EXECUTE ORDER
|
|
||||||
// if (typeof middlewareToggle === 'function') {
|
|
||||||
// if (!middlewareToggle(req, res)) return bypass()
|
|
||||||
// } else if (middlewareToggle !== undefined && !middlewareToggle) {
|
|
||||||
// return bypass()
|
|
||||||
// }
|
|
||||||
|
|
||||||
// embed timer
|
|
||||||
req.apicacheTimer = new Date();
|
|
||||||
|
|
||||||
// In Express 4.x the url is ambigious based on where a router is mounted. originalUrl will give the full Url
|
|
||||||
let key = req.originalUrl || req.url;
|
|
||||||
|
|
||||||
// Remove querystring from key if jsonp option is enabled
|
|
||||||
if (opt.jsonp) {
|
|
||||||
key = url.parse(key).pathname;
|
|
||||||
}
|
|
||||||
|
|
||||||
// add appendKey (either custom function or response path)
|
|
||||||
if (typeof opt.appendKey === "function") {
|
|
||||||
key += "$$appendKey=" + opt.appendKey(req, res);
|
|
||||||
} else if (opt.appendKey.length > 0) {
|
|
||||||
let appendKey = req;
|
|
||||||
|
|
||||||
for (let i = 0; i < opt.appendKey.length; i++) {
|
|
||||||
appendKey = appendKey[opt.appendKey[i]];
|
|
||||||
}
|
|
||||||
key += "$$appendKey=" + appendKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
// attempt cache hit
|
|
||||||
let redis = opt.redisClient;
|
|
||||||
let cached = !redis ? memCache.getValue(key) : null;
|
|
||||||
|
|
||||||
// send if cache hit from memory-cache
|
|
||||||
if (cached) {
|
|
||||||
let elapsed = new Date() - req.apicacheTimer;
|
|
||||||
debug("sending cached (memory-cache) version of", key, logDuration(elapsed));
|
|
||||||
|
|
||||||
perf.hit(key);
|
|
||||||
return sendCachedResponse(req, res, cached, middlewareToggle, next, duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
// send if cache hit from redis
|
|
||||||
if (redis && redis.connected) {
|
|
||||||
try {
|
|
||||||
redis.hgetall(key, function (err, obj) {
|
|
||||||
if (!err && obj && obj.response) {
|
|
||||||
let elapsed = new Date() - req.apicacheTimer;
|
|
||||||
debug("sending cached (redis) version of", key, logDuration(elapsed));
|
|
||||||
|
|
||||||
perf.hit(key);
|
|
||||||
return sendCachedResponse(
|
|
||||||
req,
|
|
||||||
res,
|
|
||||||
JSON.parse(obj.response),
|
|
||||||
middlewareToggle,
|
|
||||||
next,
|
|
||||||
duration
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
perf.miss(key);
|
|
||||||
return makeResponseCacheable(
|
|
||||||
req,
|
|
||||||
res,
|
|
||||||
next,
|
|
||||||
key,
|
|
||||||
duration,
|
|
||||||
strDuration,
|
|
||||||
middlewareToggle
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
// bypass redis on error
|
|
||||||
perf.miss(key);
|
|
||||||
return makeResponseCacheable(req, res, next, key, duration, strDuration, middlewareToggle);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
perf.miss(key);
|
|
||||||
return makeResponseCacheable(req, res, next, key, duration, strDuration, middlewareToggle);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
cache.options = options;
|
|
||||||
|
|
||||||
return cache;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.options = function (options) {
|
|
||||||
if (options) {
|
|
||||||
Object.assign(globalOptions, options);
|
|
||||||
syncOptions();
|
|
||||||
|
|
||||||
if ("defaultDuration" in options) {
|
|
||||||
// Convert the default duration to a number in milliseconds (if needed)
|
|
||||||
globalOptions.defaultDuration = parseDuration(globalOptions.defaultDuration, 3600000);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (globalOptions.trackPerformance) {
|
|
||||||
debug("WARNING: using trackPerformance flag can cause high memory usage!");
|
|
||||||
}
|
|
||||||
|
|
||||||
return this;
|
|
||||||
} else {
|
|
||||||
return globalOptions;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.resetIndex = function () {
|
|
||||||
index = {
|
|
||||||
all: [],
|
|
||||||
groups: {},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
this.newInstance = function (config) {
|
|
||||||
let instance = new ApiCache();
|
|
||||||
|
|
||||||
if (config) {
|
|
||||||
instance.options(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
return instance;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.clone = function () {
|
|
||||||
return this.newInstance(this.options());
|
|
||||||
};
|
|
||||||
|
|
||||||
// initialize index
|
|
||||||
this.resetIndex();
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = new ApiCache();
|
|
|
@ -1,14 +0,0 @@
|
||||||
const apicache = require("./apicache");
|
|
||||||
|
|
||||||
apicache.options({
|
|
||||||
headerBlacklist: [
|
|
||||||
"cache-control"
|
|
||||||
],
|
|
||||||
headers: {
|
|
||||||
// Disable client side cache, only server side cache.
|
|
||||||
// BUG! Not working for the second request
|
|
||||||
"cache-control": "no-cache",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = apicache;
|
|