| @ -1,46 +0,0 @@ | |||||
| /.idea |  | ||||
| /dist |  | ||||
| /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 |  | ||||
| @ -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, |  | ||||
|             }, |  | ||||
|         } |  | ||||
|     ] |  | ||||
| }; |  | ||||
| @ -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'] |  | ||||
| @ -1,21 +0,0 @@ | |||||
| --- |  | ||||
| name: Ask for help |  | ||||
| about: You can ask any question related to Uptime Kuma. |  | ||||
| title: '' |  | ||||
| labels: help |  | ||||
| assignees: '' |  | ||||
| 
 |  | ||||
| --- |  | ||||
| **Is it a duplicate question?** |  | ||||
| Please search in Issues without filters: https://github.com/louislam/uptime-kuma/issues?q= |  | ||||
| 
 |  | ||||
| **Describe your problem** |  | ||||
| Please describe what you are asking for |  | ||||
| 
 |  | ||||
| **Info** |  | ||||
| Uptime Kuma Version: |  | ||||
| Using Docker?: Yes/No |  | ||||
| Docker Version: |  | ||||
| Node.js Version (Without Docker only): |  | ||||
| OS: |  | ||||
| Browser: |  | ||||
| @ -1,42 +0,0 @@ | |||||
| --- |  | ||||
| name: Bug report |  | ||||
| about: Create a report to help us improve |  | ||||
| title: '' |  | ||||
| labels: bug |  | ||||
| assignees: '' |  | ||||
| 
 |  | ||||
| --- |  | ||||
| 
 |  | ||||
| **Is it a duplicate question?** |  | ||||
| Please search in Issues without filters: https://github.com/louislam/uptime-kuma/issues?q= |  | ||||
| 
 |  | ||||
| **Describe the bug** |  | ||||
| A clear and concise description of what the bug is. |  | ||||
| 
 |  | ||||
| **To Reproduce** |  | ||||
| Steps to reproduce the behavior: |  | ||||
| 
 |  | ||||
| 1. Go to '...' |  | ||||
| 2. Click on '....' |  | ||||
| 3. Scroll down to '....' |  | ||||
| 4. See error |  | ||||
| 
 |  | ||||
| **Expected behavior** |  | ||||
| A clear and concise description of what you expected to happen. |  | ||||
| 
 |  | ||||
| **Info** |  | ||||
| Uptime Kuma Version: |  | ||||
| Using Docker?: Yes/No |  | ||||
| Docker Version: |  | ||||
| Node.js Version (Without Docker only): |  | ||||
| OS: |  | ||||
| Browser: |  | ||||
| 
 |  | ||||
| **Screenshots** |  | ||||
| If applicable, add screenshots to help explain your problem. |  | ||||
| 
 |  | ||||
| **Error Log** |  | ||||
| It is easier for us to find out the problem. |  | ||||
| 
 |  | ||||
| Docker: `docker logs <container id>` |  | ||||
| PM2: `~/.pm2/logs/` (e.g. `/home/ubuntu/.pm2/logs`) |  | ||||
| @ -1,22 +0,0 @@ | |||||
| --- |  | ||||
| name: Feature request |  | ||||
| about: Suggest an idea for this project |  | ||||
| title: '' |  | ||||
| labels: enhancement |  | ||||
| assignees: '' |  | ||||
| 
 |  | ||||
| --- |  | ||||
| **Is it a duplicate question?** |  | ||||
| Please search in Issues without filters: https://github.com/louislam/uptime-kuma/issues?q= |  | ||||
| 
 |  | ||||
| **Is your feature request related to a problem? Please describe.** |  | ||||
| A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] |  | ||||
| 
 |  | ||||
| **Describe the solution you'd like** |  | ||||
| A clear and concise description of what you want to happen. |  | ||||
| 
 |  | ||||
| **Describe alternatives you've considered** |  | ||||
| A clear and concise description of any alternative solutions or features you've considered. |  | ||||
| 
 |  | ||||
| **Additional context** |  | ||||
| Add any other context or screenshots about the feature request here. |  | ||||
| @ -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] |  | ||||
|         # 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 }} |  | ||||
| @ -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 +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 |  | ||||
| louis@uptimekuma.louislam.net. |  | ||||
| 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. |  | ||||
| @ -1,180 +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 structed and commented so well, lol. Sorry about that. |  | ||||
| 
 |  | ||||
| The project was created with vite.js (vue3). Then I created a sub-directory 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 do not affect any existing logic, workflow and perfomance, I will merge into the master branch once it is tested. |  | ||||
| 
 |  | ||||
| If you are not sure, feel free to create an empty pull request draft first. |  | ||||
| 
 |  | ||||
| ### Pull Request Examples |  | ||||
| 
 |  | ||||
| #### ✅ High - Medium Priority |  | ||||
| 
 |  | ||||
| - Add a new notification |  | ||||
| - Add a chart |  | ||||
| - Fix a bug |  | ||||
| - Translations |  | ||||
| 
 |  | ||||
| #### *️⃣ Requires one more reviewer |  | ||||
| 
 |  | ||||
| I do not have such knowledge to test it. |  | ||||
| 
 |  | ||||
| - Add k8s supports |  | ||||
| 
 |  | ||||
| #### *️⃣ Low Priority |  | ||||
| 
 |  | ||||
| It changed my current workflow and require further studies. |  | ||||
| 
 |  | ||||
| - Change my release approach |  | ||||
| 
 |  | ||||
| #### ❌ Won't Merge |  | ||||
| 
 |  | ||||
| - 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 Intellji 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/ (indivdual notification logic) |  | ||||
| - routers/ (Express Routers) |  | ||||
| - scoket-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 |  | ||||
| @ -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. |  | ||||
| @ -1,138 +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=Backers&color=brightgreen" /></a> |  | ||||
| 
 |  | ||||
| <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 at 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 / Ping / DNS Record / Push. |  | ||||
| * 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 |  | ||||
| ``` |  | ||||
| 
 |  | ||||
| Browse to http://localhost:3001 after starting. |  | ||||
| 
 |  | ||||
| ### 💪🏻 Without 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 reserve 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 langauge, 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. |  | ||||
| @ -1,31 +0,0 @@ | |||||
| # Security Policy |  | ||||
| 
 |  | ||||
| ## 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.8.X  | :white_check_mark: | |  | ||||
| | <= 1.7.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  | ❌ | |  | ||||
| 
 |  | ||||
| ## 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. |  | ||||
| @ -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,5 +0,0 @@ | |||||
| module.exports = { |  | ||||
|     "rootDir": "..", |  | ||||
|     "testRegex": "./test/frontend.spec.js", |  | ||||
| }; |  | ||||
| 
 |  | ||||
| @ -1,6 +0,0 @@ | |||||
| module.exports = { |  | ||||
|     "launch": { |  | ||||
|         "headless": process.env.HEADLESS_TEST || false, |  | ||||
|         "userDataDir": "./data/test-chrome-profile", |  | ||||
|     } |  | ||||
| }; |  | ||||
| @ -1,11 +0,0 @@ | |||||
| module.exports = { |  | ||||
|     "verbose": true, |  | ||||
|     "preset": "jest-puppeteer", |  | ||||
|     "globals": { |  | ||||
|         "__DEV__": true |  | ||||
|     }, |  | ||||
|     "testRegex": "./test/e2e.spec.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] |  | ||||
|         } |  | ||||
|     }, |  | ||||
| }); |  | ||||
| @ -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,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,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 && \ |  | ||||
|     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, specific --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 && \ |  | ||||
|     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,51 +0,0 @@ | |||||
| FROM louislam/uptime-kuma:base-debian AS build |  | ||||
| WORKDIR /app |  | ||||
| 
 |  | ||||
| ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1 |  | ||||
| 
 |  | ||||
| COPY . . |  | ||||
| RUN npm ci && \ |  | ||||
|     npm run build && \ |  | ||||
|     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 |  | ||||
| 
 |  | ||||
| ARG GITHUB_TOKEN |  | ||||
| ARG TARGETARCH |  | ||||
| ARG PLATFORM=debian |  | ||||
| ARG VERSION=1.9.0 |  | ||||
| ARG FILE=$PLATFORM-$TARGETARCH-$VERSION.tar.gz |  | ||||
| ARG DIST=dist.tar.gz |  | ||||
| 
 |  | ||||
| COPY --from=build /app /app |  | ||||
| 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,26 +0,0 @@ | |||||
| FROM louislam/uptime-kuma:base-alpine AS build |  | ||||
| WORKDIR /app |  | ||||
| 
 |  | ||||
| ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1 |  | ||||
| 
 |  | ||||
| COPY . . |  | ||||
| RUN npm ci && \ |  | ||||
|     npm run build && \ |  | ||||
|     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,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,57 +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", () => { |  | ||||
|                 fs.rmdirSync("./dist-backup", { |  | ||||
|                     recursive: true |  | ||||
|                 }); |  | ||||
|                 console.log("Done"); |  | ||||
|             }); |  | ||||
| 
 |  | ||||
|             tarStream.on("error", () => { |  | ||||
|                 if (fs.existsSync("./dist-backup")) { |  | ||||
|                     fs.renameSync("./dist-backup", "./dist"); |  | ||||
|                 } |  | ||||
|                 console.log("Done"); |  | ||||
|             }); |  | ||||
| 
 |  | ||||
|             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,34 +0,0 @@ | |||||
| /* |  | ||||
|  * This script should be run after a period of time (180s), because the server may need some time to prepare. |  | ||||
|  */ |  | ||||
| process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; |  | ||||
| 
 |  | ||||
| let client; |  | ||||
| 
 |  | ||||
| if (process.env.SSL_KEY && process.env.SSL_CERT) { |  | ||||
|     client = require("https"); |  | ||||
| } else { |  | ||||
|     client = require("http"); |  | ||||
| } |  | ||||
| 
 |  | ||||
| let options = { |  | ||||
|     host: process.env.HOST || "127.0.0.1", |  | ||||
|     port: parseInt(process.env.PORT) || 3001, |  | ||||
|     timeout: 28 * 1000, |  | ||||
| }; |  | ||||
| 
 |  | ||||
| let request = client.request(options, (res) => { |  | ||||
|     console.log(`Health Check OK [Res Code: ${res.statusCode}]`); |  | ||||
|     if (res.statusCode === 200) { |  | ||||
|         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,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; |  | ||||
|         } |  | ||||
|     } |  | ||||
| } |  | ||||
| @ -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 |  | ||||
| @ -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> |  | ||||
| @ -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" |  | ||||
| @ -1,128 +0,0 @@ | |||||
| { |  | ||||
|     "name": "uptime-kuma", |  | ||||
|     "version": "1.9.0", |  | ||||
|     "license": "MIT", |  | ||||
|     "repository": { |  | ||||
|         "type": "git", |  | ||||
|         "url": "https://github.com/louislam/uptime-kuma.git" |  | ||||
|     }, |  | ||||
|     "engines": { |  | ||||
|         "node": "14.*" |  | ||||
|     }, |  | ||||
|     "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 --config=./config/jest.config.js", |  | ||||
|         "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.9.0-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.9.0 -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:1.9.0-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 GITHUB_TOKEN --target upload-artifact . --progress plain", |  | ||||
|         "setup": "git checkout 1.9.0 && 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", |  | ||||
|         "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" |  | ||||
|     }, |  | ||||
|     "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-4", |  | ||||
|         "@louislam/sqlite3": "~6.0.0", |  | ||||
|         "@popperjs/core": "~2.10.2", |  | ||||
|         "args-parser": "~1.3.0", |  | ||||
|         "axios": "~0.21.4", |  | ||||
|         "bcryptjs": "~2.4.3", |  | ||||
|         "bootstrap": "~5.1.1", |  | ||||
|         "chardet": "^1.3.0", |  | ||||
|         "bree": "~6.3.1", |  | ||||
|         "chart.js": "~3.5.1", |  | ||||
|         "chartjs-adapter-dayjs": "~1.0.0", |  | ||||
|         "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.4", |  | ||||
|         "iconv-lite": "^0.6.3", |  | ||||
|         "jsonwebtoken": "~8.5.1", |  | ||||
|         "nodemailer": "~6.6.5", |  | ||||
|         "notp": "~2.0.3", |  | ||||
|         "password-hash": "~1.2.2", |  | ||||
|         "postcss-rtlcss": "~3.4.1", |  | ||||
|         "postcss-scss": "~4.0.1", |  | ||||
|         "prom-client": "~13.2.0", |  | ||||
|         "prometheus-api-metrics": "~3.2.0", |  | ||||
|         "qrcode": "~1.4.4", |  | ||||
|         "redbean-node": "0.1.2", |  | ||||
|         "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.6", |  | ||||
|         "vue": "next", |  | ||||
|         "vue-chart-3": "~0.5.8", |  | ||||
|         "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.11", |  | ||||
|         "vue-toastification": "~2.0.0-rc.1", |  | ||||
|         "vuedraggable": "~4.1.0" |  | ||||
|     }, |  | ||||
|     "devDependencies": { |  | ||||
|         "@babel/eslint-parser": "~7.15.7", |  | ||||
|         "@babel/preset-env": "^7.15.8", |  | ||||
|         "@types/bootstrap": "~5.1.6", |  | ||||
|         "@vitejs/plugin-legacy": "~1.6.1", |  | ||||
|         "@vitejs/plugin-vue": "~1.9.2", |  | ||||
|         "@vue/compiler-sfc": "~3.2.19", |  | ||||
|         "babel-plugin-rewire": "~1.2.0", |  | ||||
|         "core-js": "~3.18.1", |  | ||||
|         "cross-env": "~7.0.3", |  | ||||
|         "dns2": "~2.0.1", |  | ||||
|         "eslint": "~7.32.0", |  | ||||
|         "eslint-plugin-vue": "~7.18.0", |  | ||||
|         "jest": "~27.2.4", |  | ||||
|         "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.3", |  | ||||
|         "vite": "~2.6.4" |  | ||||
|     } |  | ||||
| } |  | ||||
| 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 | 
| 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,51 +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"); |  | ||||
| 
 |  | ||||
| /** |  | ||||
|  * |  | ||||
|  * @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 { |  | ||||
|             exports.login(username, password).then((user) => { |  | ||||
|                 callback(null, user != null) |  | ||||
|             }) |  | ||||
|         } |  | ||||
|     }) |  | ||||
| 
 |  | ||||
| } |  | ||||
| 
 |  | ||||
| exports.basicAuth = basicAuth({ |  | ||||
|     authorizer: myAuthorizer, |  | ||||
|     authorizeAsync: true, |  | ||||
|     challenge: true, |  | ||||
| }); |  | ||||
| @ -1,42 +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://raw.githubusercontent.com/louislam/uptime-kuma/master/package.json"); |  | ||||
| 
 |  | ||||
|             if (typeof res.data === "string") { |  | ||||
|                 res.data = JSON.parse(res.data); |  | ||||
|             } |  | ||||
| 
 |  | ||||
|             // For debug
 |  | ||||
|             if (process.env.TEST_CHECK_VERSION === "1") { |  | ||||
|                 res.data.version = "1000.0.0"; |  | ||||
|             } |  | ||||
| 
 |  | ||||
|             exports.latestVersion = res.data.version; |  | ||||
|         } 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; |  | ||||
| @ -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,378 +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, |  | ||||
|     } |  | ||||
| 
 |  | ||||
|     /** |  | ||||
|      * 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() { |  | ||||
|         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"); |  | ||||
|         // Change to WAL
 |  | ||||
|         await R.exec("PRAGMA journal_mode = WAL"); |  | ||||
|         await R.exec("PRAGMA cache_size = -12000"); |  | ||||
| 
 |  | ||||
|         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 no need to patch"); |  | ||||
|         } 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 patch db failed"); |  | ||||
|                 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 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 patch db failed"); |  | ||||
|             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 + " is 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 DB"); |  | ||||
| 
 |  | ||||
|         while (true) { |  | ||||
|             Database.noReject = true; |  | ||||
|             await R.close(); |  | ||||
|             await sleep(2000); |  | ||||
| 
 |  | ||||
|             if (Database.noReject) { |  | ||||
|                 break; |  | ||||
|             } else { |  | ||||
|                 console.log("Waiting to close the db"); |  | ||||
|             } |  | ||||
|         } |  | ||||
|         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("Backup the db"); |  | ||||
|             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("Patch db 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"); |  | ||||
|         } |  | ||||
|     } |  | ||||
| } |  | ||||
| 
 |  | ||||
| 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,609 +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 } = 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, |  | ||||
|             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, |  | ||||
|         }; |  | ||||
|     } |  | ||||
| 
 |  | ||||
|     /** |  | ||||
|      * 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(); |  | ||||
| 
 |  | ||||
|                     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) : {}), |  | ||||
|                         }, |  | ||||
|                         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()); |  | ||||
|                         }, |  | ||||
|                     }; |  | ||||
|                     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:") { |  | ||||
|                         try { |  | ||||
|                             tlsInfo = await this.updateTlsInfo(checkCertificate(res)); |  | ||||
|                         } 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) { |  | ||||
|                         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; |  | ||||
| 
 |  | ||||
|             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; |  | ||||
|                 await Monitor.sendNotification(isFirstBeat, this, bean); |  | ||||
|             } 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}`); |  | ||||
|             } |  | ||||
| 
 |  | ||||
|             io.to(this.user_id).emit("heartbeat", bean.toJSON()); |  | ||||
|             Monitor.sendStats(io, this.id, this.user_id); |  | ||||
| 
 |  | ||||
|             await R.store(bean); |  | ||||
|             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; |  | ||||
|                     } |  | ||||
|                 } |  | ||||
| 
 |  | ||||
|                 this.heartbeatInterval = setTimeout(beat, beatInterval * 1000); |  | ||||
|             } |  | ||||
| 
 |  | ||||
|         }; |  | ||||
| 
 |  | ||||
|         // Delay Push Type
 |  | ||||
|         if (this.type === "push") { |  | ||||
|             setTimeout(() => { |  | ||||
|                 beat(); |  | ||||
|             }, this.interval * 1000); |  | ||||
|         } else { |  | ||||
|             beat(); |  | ||||
|         } |  | ||||
|     } |  | ||||
| 
 |  | ||||
|     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; |  | ||||
|         } |  | ||||
|         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) { |  | ||||
|             let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [ |  | ||||
|                 monitor.id, |  | ||||
|             ]); |  | ||||
| 
 |  | ||||
|             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); |  | ||||
|                 } |  | ||||
|             } |  | ||||
| 
 |  | ||||
|             // Clear Status Page Cache
 |  | ||||
|             apicache.clear(); |  | ||||
|         } |  | ||||
|     } |  | ||||
| 
 |  | ||||
| } |  | ||||
| 
 |  | ||||
| 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; |  | ||||
| @ -1,59 +0,0 @@ | |||||
| function MemoryCache() { |  | ||||
|     this.cache = {}; |  | ||||
|     this.size = 0; |  | ||||
| } |  | ||||
| 
 |  | ||||
| MemoryCache.prototype.add = function (key, value, time, timeoutCallback) { |  | ||||
|     let old = this.cache[key]; |  | ||||
|     let instance = this; |  | ||||
| 
 |  | ||||
|     let entry = { |  | ||||
|         value: value, |  | ||||
|         expire: time + Date.now(), |  | ||||
|         timeout: setTimeout(function () { |  | ||||
|             instance.delete(key); |  | ||||
|             return timeoutCallback && typeof timeoutCallback === "function" && timeoutCallback(value, key); |  | ||||
|         }, time) |  | ||||
|     }; |  | ||||
| 
 |  | ||||
|     this.cache[key] = entry; |  | ||||
|     this.size = Object.keys(this.cache).length; |  | ||||
| 
 |  | ||||
|     return entry; |  | ||||
| }; |  | ||||
| 
 |  | ||||
| MemoryCache.prototype.delete = function (key) { |  | ||||
|     let entry = this.cache[key]; |  | ||||
| 
 |  | ||||
|     if (entry) { |  | ||||
|         clearTimeout(entry.timeout); |  | ||||
|     } |  | ||||
| 
 |  | ||||
|     delete this.cache[key]; |  | ||||
| 
 |  | ||||
|     this.size = Object.keys(this.cache).length; |  | ||||
| 
 |  | ||||
|     return null; |  | ||||
| }; |  | ||||
| 
 |  | ||||
| MemoryCache.prototype.get = function (key) { |  | ||||
|     let entry = this.cache[key]; |  | ||||
| 
 |  | ||||
|     return entry; |  | ||||
| }; |  | ||||
| 
 |  | ||||
| MemoryCache.prototype.getValue = function (key) { |  | ||||
|     let entry = this.get(key); |  | ||||
| 
 |  | ||||
|     return entry && entry.value; |  | ||||
| }; |  | ||||
| 
 |  | ||||
| MemoryCache.prototype.clear = function () { |  | ||||
|     Object.keys(this.cache).forEach(function (key) { |  | ||||
|         this.delete(key); |  | ||||
|     }, this); |  | ||||
| 
 |  | ||||
|     return true; |  | ||||
| }; |  | ||||
| 
 |  | ||||
| module.exports = MemoryCache; |  | ||||
| @ -1,108 +0,0 @@ | |||||
| const NotificationProvider = require("./notification-provider"); |  | ||||
| const { DOWN, UP } = require("../../src/util"); |  | ||||
| const { default: axios } = require("axios"); |  | ||||
| const Crypto = require("crypto"); |  | ||||
| const qs = require("qs"); |  | ||||
| 
 |  | ||||
| class AliyunSMS extends NotificationProvider { |  | ||||
|     name = "AliyunSMS"; |  | ||||
| 
 |  | ||||
|     async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { |  | ||||
|         let okMsg = "Sent Successfully."; |  | ||||
| 
 |  | ||||
|         try { |  | ||||
|             if (heartbeatJSON != null) { |  | ||||
|                 let msgBody = JSON.stringify({ |  | ||||
|                     name: monitorJSON["name"], |  | ||||
|                     time: heartbeatJSON["time"], |  | ||||
|                     status: this.statusToString(heartbeatJSON["status"]), |  | ||||
|                     msg: heartbeatJSON["msg"], |  | ||||
|                 }); |  | ||||
|                 if (this.sendSms(notification, msgBody)) { |  | ||||
|                     return okMsg; |  | ||||
|                 } |  | ||||
|             } else { |  | ||||
|                 let msgBody = JSON.stringify({ |  | ||||
|                     name: "", |  | ||||
|                     time: "", |  | ||||
|                     status: "", |  | ||||
|                     msg: msg, |  | ||||
|                 }); |  | ||||
|                 if (this.sendSms(notification, msgBody)) { |  | ||||
|                     return okMsg; |  | ||||
|                 } |  | ||||
|             } |  | ||||
|         } catch (error) { |  | ||||
|             this.throwGeneralAxiosError(error); |  | ||||
|         } |  | ||||
|     } |  | ||||
| 
 |  | ||||
|     async sendSms(notification, msgbody) { |  | ||||
|         let params = { |  | ||||
|             PhoneNumbers: notification.phonenumber, |  | ||||
|             TemplateCode: notification.templateCode, |  | ||||
|             SignName: notification.signName, |  | ||||
|             TemplateParam: msgbody, |  | ||||
|             AccessKeyId: notification.accessKeyId, |  | ||||
|             Format: "JSON", |  | ||||
|             SignatureMethod: "HMAC-SHA1", |  | ||||
|             SignatureVersion: "1.0", |  | ||||
|             SignatureNonce: Math.random().toString(), |  | ||||
|             Timestamp: new Date().toISOString(), |  | ||||
|             Action: "SendSms", |  | ||||
|             Version: "2017-05-25", |  | ||||
|         }; |  | ||||
| 
 |  | ||||
|         params.Signature = this.sign(params, notification.secretAccessKey); |  | ||||
|         let config = { |  | ||||
|             method: "POST", |  | ||||
|             url: "http://dysmsapi.aliyuncs.com/", |  | ||||
|             headers: { |  | ||||
|                 "Content-Type": "application/x-www-form-urlencoded", |  | ||||
|             }, |  | ||||
|             data: qs.stringify(params), |  | ||||
|         }; |  | ||||
| 
 |  | ||||
|         let result = await axios(config); |  | ||||
|         if (result.data.Message == "OK") { |  | ||||
|             return true; |  | ||||
|         } |  | ||||
|         return false; |  | ||||
|     } |  | ||||
| 
 |  | ||||
|     /** Aliyun request sign */ |  | ||||
|     sign(param, AccessKeySecret) { |  | ||||
|         let param2 = {}; |  | ||||
|         let data = []; |  | ||||
| 
 |  | ||||
|         let oa = Object.keys(param).sort(); |  | ||||
| 
 |  | ||||
|         for (let i = 0; i < oa.length; i++) { |  | ||||
|             let key = oa[i]; |  | ||||
|             param2[key] = param[key]; |  | ||||
|         } |  | ||||
| 
 |  | ||||
|         for (let key in param2) { |  | ||||
|             data.push(`${encodeURIComponent(key)}=${encodeURIComponent(param2[key])}`); |  | ||||
|         } |  | ||||
| 
 |  | ||||
|         let StringToSign = `POST&${encodeURIComponent("/")}&${encodeURIComponent(data.join("&"))}`; |  | ||||
|         return Crypto |  | ||||
|             .createHmac("sha1", `${AccessKeySecret}&`) |  | ||||
|             .update(Buffer.from(StringToSign)) |  | ||||
|             .digest("base64"); |  | ||||
|     } |  | ||||
| 
 |  | ||||
|     statusToString(status) { |  | ||||
|         switch (status) { |  | ||||
|             case DOWN: |  | ||||
|                 return "DOWN"; |  | ||||
|             case UP: |  | ||||
|                 return "UP"; |  | ||||
|             default: |  | ||||
|                 return status; |  | ||||
|         } |  | ||||
|     } |  | ||||
| } |  | ||||
| 
 |  | ||||
| module.exports = AliyunSMS; |  | ||||
| @ -1,26 +0,0 @@ | |||||
| const NotificationProvider = require("./notification-provider"); |  | ||||
| const child_process = require("child_process"); |  | ||||
| 
 |  | ||||
| class Apprise extends NotificationProvider { |  | ||||
| 
 |  | ||||
|     name = "apprise"; |  | ||||
| 
 |  | ||||
|     async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { |  | ||||
|         let s = child_process.spawnSync("apprise", [ "-vv", "-b", msg, notification.appriseURL]) |  | ||||
| 
 |  | ||||
|         let output = (s.stdout) ? s.stdout.toString() : "ERROR: maybe apprise not found"; |  | ||||
| 
 |  | ||||
|         if (output) { |  | ||||
| 
 |  | ||||
|             if (! output.includes("ERROR")) { |  | ||||
|                 return "Sent Successfully"; |  | ||||
|             } |  | ||||
| 
 |  | ||||
|             throw new Error(output) |  | ||||
|         } else { |  | ||||
|             return "No output from apprise"; |  | ||||
|         } |  | ||||
|     } |  | ||||
| } |  | ||||
| 
 |  | ||||
| module.exports = Apprise; |  | ||||
| @ -1,79 +0,0 @@ | |||||
| const NotificationProvider = require("./notification-provider"); |  | ||||
| const { DOWN, UP } = require("../../src/util"); |  | ||||
| const { default: axios } = require("axios"); |  | ||||
| const Crypto = require("crypto"); |  | ||||
| 
 |  | ||||
| class DingDing extends NotificationProvider { |  | ||||
|     name = "DingDing"; |  | ||||
| 
 |  | ||||
|     async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { |  | ||||
|         let okMsg = "Sent Successfully."; |  | ||||
| 
 |  | ||||
|         try { |  | ||||
|             if (heartbeatJSON != null) { |  | ||||
|                 let params = { |  | ||||
|                     msgtype: "markdown", |  | ||||
|                     markdown: { |  | ||||
|                         title: monitorJSON["name"], |  | ||||
|                         text: `## [${this.statusToString(heartbeatJSON["status"])}] \n > ${heartbeatJSON["msg"]}  \n > Time(UTC):${heartbeatJSON["time"]}`, |  | ||||
|                     } |  | ||||
|                 }; |  | ||||
|                 if (this.sendToDingDing(notification, params)) { |  | ||||
|                     return okMsg; |  | ||||
|                 } |  | ||||
|             } else { |  | ||||
|                 let params = { |  | ||||
|                     msgtype: "text", |  | ||||
|                     text: { |  | ||||
|                         content: msg |  | ||||
|                     } |  | ||||
|                 }; |  | ||||
|                 if (this.sendToDingDing(notification, params)) { |  | ||||
|                     return okMsg; |  | ||||
|                 } |  | ||||
|             } |  | ||||
|         } catch (error) { |  | ||||
|             this.throwGeneralAxiosError(error); |  | ||||
|         } |  | ||||
|     } |  | ||||
| 
 |  | ||||
|     async sendToDingDing(notification, params) { |  | ||||
|         let timestamp = Date.now(); |  | ||||
| 
 |  | ||||
|         let config = { |  | ||||
|             method: "POST", |  | ||||
|             headers: { |  | ||||
|                 "Content-Type": "application/json", |  | ||||
|             }, |  | ||||
|             url: `${notification.webHookUrl}×tamp=${timestamp}&sign=${encodeURIComponent(this.sign(timestamp, notification.secretKey))}`, |  | ||||
|             data: JSON.stringify(params), |  | ||||
|         }; |  | ||||
| 
 |  | ||||
|         let result = await axios(config); |  | ||||
|         if (result.data.errmsg == "ok") { |  | ||||
|             return true; |  | ||||
|         } |  | ||||
|         return false; |  | ||||
|     } |  | ||||
| 
 |  | ||||
|     /** DingDing sign */ |  | ||||
|     sign(timestamp, secretKey) { |  | ||||
|         return Crypto |  | ||||
|             .createHmac("sha256", Buffer.from(secretKey, "utf8")) |  | ||||
|             .update(Buffer.from(`${timestamp}\n${secretKey}`, "utf8")) |  | ||||
|             .digest("base64"); |  | ||||
|     } |  | ||||
| 
 |  | ||||
|     statusToString(status) { |  | ||||
|         switch (status) { |  | ||||
|             case DOWN: |  | ||||
|                 return "DOWN"; |  | ||||
|             case UP: |  | ||||
|                 return "UP"; |  | ||||
|             default: |  | ||||
|                 return status; |  | ||||
|         } |  | ||||
|     } |  | ||||
| } |  | ||||
| 
 |  | ||||
| module.exports = DingDing; |  | ||||
| @ -1,115 +0,0 @@ | |||||
| const NotificationProvider = require("./notification-provider"); |  | ||||
| const axios = require("axios"); |  | ||||
| const { DOWN, UP } = require("../../src/util"); |  | ||||
| 
 |  | ||||
| class Discord extends NotificationProvider { |  | ||||
| 
 |  | ||||
|     name = "discord"; |  | ||||
| 
 |  | ||||
|     async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { |  | ||||
|         let okMsg = "Sent Successfully."; |  | ||||
| 
 |  | ||||
|         try { |  | ||||
|             const discordDisplayName = notification.discordUsername || "Uptime Kuma"; |  | ||||
| 
 |  | ||||
|             // If heartbeatJSON is null, assume we're testing.
 |  | ||||
|             if (heartbeatJSON == null) { |  | ||||
|                 let discordtestdata = { |  | ||||
|                     username: discordDisplayName, |  | ||||
|                     content: msg, |  | ||||
|                 } |  | ||||
|                 await axios.post(notification.discordWebhookUrl, discordtestdata) |  | ||||
|                 return okMsg; |  | ||||
|             } |  | ||||
| 
 |  | ||||
|             let url; |  | ||||
| 
 |  | ||||
|             if (monitorJSON["type"] === "port") { |  | ||||
|                 url = monitorJSON["hostname"]; |  | ||||
|                 if (monitorJSON["port"]) { |  | ||||
|                     url += ":" + monitorJSON["port"]; |  | ||||
|                 } |  | ||||
| 
 |  | ||||
|             } else { |  | ||||
|                 url = monitorJSON["url"]; |  | ||||
|             } |  | ||||
| 
 |  | ||||
|             // If heartbeatJSON is not null, we go into the normal alerting loop.
 |  | ||||
|             if (heartbeatJSON["status"] == DOWN) { |  | ||||
|                 let discorddowndata = { |  | ||||
|                     username: discordDisplayName, |  | ||||
|                     embeds: [{ |  | ||||
|                         title: "❌ Your service " + monitorJSON["name"] + " went down. ❌", |  | ||||
|                         color: 16711680, |  | ||||
|                         timestamp: heartbeatJSON["time"], |  | ||||
|                         fields: [ |  | ||||
|                             { |  | ||||
|                                 name: "Service Name", |  | ||||
|                                 value: monitorJSON["name"], |  | ||||
|                             }, |  | ||||
|                             { |  | ||||
|                                 name: "Service URL", |  | ||||
|                                 value: url, |  | ||||
|                             }, |  | ||||
|                             { |  | ||||
|                                 name: "Time (UTC)", |  | ||||
|                                 value: heartbeatJSON["time"], |  | ||||
|                             }, |  | ||||
|                             { |  | ||||
|                                 name: "Error", |  | ||||
|                                 value: heartbeatJSON["msg"], |  | ||||
|                             }, |  | ||||
|                         ], |  | ||||
|                     }], |  | ||||
|                 } |  | ||||
| 
 |  | ||||
|                 if (notification.discordPrefixMessage) { |  | ||||
|                     discorddowndata.content = notification.discordPrefixMessage; |  | ||||
|                 } |  | ||||
| 
 |  | ||||
|                 await axios.post(notification.discordWebhookUrl, discorddowndata) |  | ||||
|                 return okMsg; |  | ||||
| 
 |  | ||||
|             } else if (heartbeatJSON["status"] == UP) { |  | ||||
|                 let discordupdata = { |  | ||||
|                     username: discordDisplayName, |  | ||||
|                     embeds: [{ |  | ||||
|                         title: "✅ Your service " + monitorJSON["name"] + " is up! ✅", |  | ||||
|                         color: 65280, |  | ||||
|                         timestamp: heartbeatJSON["time"], |  | ||||
|                         fields: [ |  | ||||
|                             { |  | ||||
|                                 name: "Service Name", |  | ||||
|                                 value: monitorJSON["name"], |  | ||||
|                             }, |  | ||||
|                             { |  | ||||
|                                 name: "Service URL", |  | ||||
|                                 value: url.startsWith("http") ? "[Visit Service](" + url + ")" : url, |  | ||||
|                             }, |  | ||||
|                             { |  | ||||
|                                 name: "Time (UTC)", |  | ||||
|                                 value: heartbeatJSON["time"], |  | ||||
|                             }, |  | ||||
|                             { |  | ||||
|                                 name: "Ping", |  | ||||
|                                 value: heartbeatJSON["ping"] + "ms", |  | ||||
|                             }, |  | ||||
|                         ], |  | ||||
|                     }], |  | ||||
|                 } |  | ||||
| 
 |  | ||||
|                 if (notification.discordPrefixMessage) { |  | ||||
|                     discordupdata.content = notification.discordPrefixMessage; |  | ||||
|                 } |  | ||||
| 
 |  | ||||
|                 await axios.post(notification.discordWebhookUrl, discordupdata) |  | ||||
|                 return okMsg; |  | ||||
|             } |  | ||||
|         } catch (error) { |  | ||||
|             this.throwGeneralAxiosError(error) |  | ||||
|         } |  | ||||
|     } |  | ||||
| 
 |  | ||||
| } |  | ||||
| 
 |  | ||||
| module.exports = Discord; |  | ||||
| @ -1,83 +0,0 @@ | |||||
| const NotificationProvider = require("./notification-provider"); |  | ||||
| const axios = require("axios"); |  | ||||
| const { DOWN, UP } = require("../../src/util"); |  | ||||
| 
 |  | ||||
| class Feishu extends NotificationProvider { |  | ||||
|     name = "Feishu"; |  | ||||
| 
 |  | ||||
|     async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { |  | ||||
|         let okMsg = "Sent Successfully."; |  | ||||
|         let feishuWebHookUrl = notification.feishuWebHookUrl; |  | ||||
| 
 |  | ||||
|         try { |  | ||||
|             if (heartbeatJSON == null) { |  | ||||
|                 let testdata = { |  | ||||
|                     msg_type: "text", |  | ||||
|                     content: { |  | ||||
|                         text: msg, |  | ||||
|                     }, |  | ||||
|                 }; |  | ||||
|                 await axios.post(feishuWebHookUrl, testdata); |  | ||||
|                 return okMsg; |  | ||||
|             } |  | ||||
| 
 |  | ||||
|             if (heartbeatJSON["status"] == DOWN) { |  | ||||
|                 let downdata = { |  | ||||
|                     msg_type: "post", |  | ||||
|                     content: { |  | ||||
|                         post: { |  | ||||
|                             zh_cn: { |  | ||||
|                                 title: "UptimeKuma Alert: " + monitorJSON["name"], |  | ||||
|                                 content: [ |  | ||||
|                                     [ |  | ||||
|                                         { |  | ||||
|                                             tag: "text", |  | ||||
|                                             text: |  | ||||
|                                                 "[Down] " + |  | ||||
|                                                 heartbeatJSON["msg"] + |  | ||||
|                                                 "\nTime (UTC): " + |  | ||||
|                                                 heartbeatJSON["time"], |  | ||||
|                                         }, |  | ||||
|                                     ], |  | ||||
|                                 ], |  | ||||
|                             }, |  | ||||
|                         }, |  | ||||
|                     }, |  | ||||
|                 }; |  | ||||
|                 await axios.post(feishuWebHookUrl, downdata); |  | ||||
|                 return okMsg; |  | ||||
|             } |  | ||||
| 
 |  | ||||
|             if (heartbeatJSON["status"] == UP) { |  | ||||
|                 let updata = { |  | ||||
|                     msg_type: "post", |  | ||||
|                     content: { |  | ||||
|                         post: { |  | ||||
|                             zh_cn: { |  | ||||
|                                 title: "UptimeKuma Alert: " + monitorJSON["name"], |  | ||||
|                                 content: [ |  | ||||
|                                     [ |  | ||||
|                                         { |  | ||||
|                                             tag: "text", |  | ||||
|                                             text: |  | ||||
|                                                 "[Up] " + |  | ||||
|                                                 heartbeatJSON["msg"] + |  | ||||
|                                                 "\nTime (UTC): " + |  | ||||
|                                                 heartbeatJSON["time"], |  | ||||
|                                         }, |  | ||||
|                                     ], |  | ||||
|                                 ], |  | ||||
|                             }, |  | ||||
|                         }, |  | ||||
|                     }, |  | ||||
|                 }; |  | ||||
|                 await axios.post(feishuWebHookUrl, updata); |  | ||||
|                 return okMsg; |  | ||||
|             } |  | ||||
|         } catch (error) { |  | ||||
|             this.throwGeneralAxiosError(error); |  | ||||
|         } |  | ||||
|     } |  | ||||
| } |  | ||||
| 
 |  | ||||
| module.exports = Feishu; |  | ||||
| @ -1,28 +0,0 @@ | |||||
| const NotificationProvider = require("./notification-provider"); |  | ||||
| const axios = require("axios"); |  | ||||
| 
 |  | ||||
| class Gotify extends NotificationProvider { |  | ||||
| 
 |  | ||||
|     name = "gotify"; |  | ||||
| 
 |  | ||||
|     async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { |  | ||||
|         let okMsg = "Sent Successfully."; |  | ||||
|         try { |  | ||||
|             if (notification.gotifyserverurl && notification.gotifyserverurl.endsWith("/")) { |  | ||||
|                 notification.gotifyserverurl = notification.gotifyserverurl.slice(0, -1); |  | ||||
|             } |  | ||||
|             await axios.post(`${notification.gotifyserverurl}/message?token=${notification.gotifyapplicationToken}`, { |  | ||||
|                 "message": msg, |  | ||||
|                 "priority": notification.gotifyPriority || 8, |  | ||||
|                 "title": "Uptime-Kuma", |  | ||||
|             }) |  | ||||
| 
 |  | ||||
|             return okMsg; |  | ||||
| 
 |  | ||||
|         } catch (error) { |  | ||||
|             this.throwGeneralAxiosError(error); |  | ||||
|         } |  | ||||
|     } |  | ||||
| } |  | ||||
| 
 |  | ||||
| module.exports = Gotify; |  | ||||
| @ -1,60 +0,0 @@ | |||||
| const NotificationProvider = require("./notification-provider"); |  | ||||
| const axios = require("axios"); |  | ||||
| const { DOWN, UP } = require("../../src/util"); |  | ||||
| 
 |  | ||||
| class Line extends NotificationProvider { |  | ||||
| 
 |  | ||||
|     name = "line"; |  | ||||
| 
 |  | ||||
|     async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { |  | ||||
|         let okMsg = "Sent Successfully."; |  | ||||
|         try { |  | ||||
|             let lineAPIUrl = "https://api.line.me/v2/bot/message/push"; |  | ||||
|             let config = { |  | ||||
|                 headers: { |  | ||||
|                     "Content-Type": "application/json", |  | ||||
|                     "Authorization": "Bearer " + notification.lineChannelAccessToken |  | ||||
|                 } |  | ||||
|             }; |  | ||||
|             if (heartbeatJSON == null) { |  | ||||
|                 let testMessage = { |  | ||||
|                     "to": notification.lineUserID, |  | ||||
|                     "messages": [ |  | ||||
|                         { |  | ||||
|                             "type": "text", |  | ||||
|                             "text": "Test Successful!" |  | ||||
|                         } |  | ||||
|                     ] |  | ||||
|                 } |  | ||||
|                 await axios.post(lineAPIUrl, testMessage, config) |  | ||||
|             } else if (heartbeatJSON["status"] == DOWN) { |  | ||||
|                 let downMessage = { |  | ||||
|                     "to": notification.lineUserID, |  | ||||
|                     "messages": [ |  | ||||
|                         { |  | ||||
|                             "type": "text", |  | ||||
|                             "text": "UptimeKuma Alert: [🔴 Down]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"] |  | ||||
|                         } |  | ||||
|                     ] |  | ||||
|                 } |  | ||||
|                 await axios.post(lineAPIUrl, downMessage, config) |  | ||||
|             } else if (heartbeatJSON["status"] == UP) { |  | ||||
|                 let upMessage = { |  | ||||
|                     "to": notification.lineUserID, |  | ||||
|                     "messages": [ |  | ||||
|                         { |  | ||||
|                             "type": "text", |  | ||||
|                             "text": "UptimeKuma Alert: [✅ Up]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"] |  | ||||
|                         } |  | ||||
|                     ] |  | ||||
|                 } |  | ||||
|                 await axios.post(lineAPIUrl, upMessage, config) |  | ||||
|             } |  | ||||
|             return okMsg; |  | ||||
|         } catch (error) { |  | ||||
|             this.throwGeneralAxiosError(error) |  | ||||
|         } |  | ||||
|     } |  | ||||
| } |  | ||||
| 
 |  | ||||
| module.exports = Line; |  | ||||
| @ -1,48 +0,0 @@ | |||||
| const NotificationProvider = require("./notification-provider"); |  | ||||
| const axios = require("axios"); |  | ||||
| const { DOWN, UP } = require("../../src/util"); |  | ||||
| 
 |  | ||||
| class LunaSea extends NotificationProvider { |  | ||||
| 
 |  | ||||
|     name = "lunasea"; |  | ||||
| 
 |  | ||||
|     async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { |  | ||||
|         let okMsg = "Sent Successfully."; |  | ||||
|         let lunaseadevice = "https://notify.lunasea.app/v1/custom/device/" + notification.lunaseaDevice |  | ||||
| 
 |  | ||||
|         try { |  | ||||
|             if (heartbeatJSON == null) { |  | ||||
|                 let testdata = { |  | ||||
|                     "title": "Uptime Kuma Alert", |  | ||||
|                     "body": "Testing Successful.", |  | ||||
|                 } |  | ||||
|                 await axios.post(lunaseadevice, testdata) |  | ||||
|                 return okMsg; |  | ||||
|             } |  | ||||
| 
 |  | ||||
|             if (heartbeatJSON["status"] == DOWN) { |  | ||||
|                 let downdata = { |  | ||||
|                     "title": "UptimeKuma Alert: " + monitorJSON["name"], |  | ||||
|                     "body": "[🔴 Down] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"], |  | ||||
|                 } |  | ||||
|                 await axios.post(lunaseadevice, downdata) |  | ||||
|                 return okMsg; |  | ||||
|             } |  | ||||
| 
 |  | ||||
|             if (heartbeatJSON["status"] == UP) { |  | ||||
|                 let updata = { |  | ||||
|                     "title": "UptimeKuma Alert: " + monitorJSON["name"], |  | ||||
|                     "body": "[✅ Up] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"], |  | ||||
|                 } |  | ||||
|                 await axios.post(lunaseadevice, updata) |  | ||||
|                 return okMsg; |  | ||||
|             } |  | ||||
| 
 |  | ||||
|         } catch (error) { |  | ||||
|             this.throwGeneralAxiosError(error) |  | ||||
|         } |  | ||||
| 
 |  | ||||
|     } |  | ||||
| } |  | ||||
| 
 |  | ||||
| module.exports = LunaSea; |  | ||||
| @ -1,45 +0,0 @@ | |||||
| const NotificationProvider = require("./notification-provider"); |  | ||||
| const axios = require("axios"); |  | ||||
| const Crypto = require("crypto"); |  | ||||
| const { debug } = require("../../src/util"); |  | ||||
| 
 |  | ||||
| class Matrix extends NotificationProvider { |  | ||||
|     name = "matrix"; |  | ||||
| 
 |  | ||||
|     async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { |  | ||||
|         let okMsg = "Sent Successfully."; |  | ||||
| 
 |  | ||||
|         const size = 20; |  | ||||
|         const randomString = encodeURIComponent( |  | ||||
|             Crypto |  | ||||
|                 .randomBytes(size) |  | ||||
|                 .toString("base64") |  | ||||
|                 .slice(0, size) |  | ||||
|         ); |  | ||||
| 
 |  | ||||
|         debug("Random String: " + randomString); |  | ||||
| 
 |  | ||||
|         const roomId = encodeURIComponent(notification.internalRoomId); |  | ||||
| 
 |  | ||||
|         debug("Matrix Room ID: " + roomId); |  | ||||
| 
 |  | ||||
|         try { |  | ||||
|             let config = { |  | ||||
|                 headers: { |  | ||||
|                     "Authorization": `Bearer ${notification.accessToken}`, |  | ||||
|                 } |  | ||||
|             }; |  | ||||
|             let data = { |  | ||||
|                 "msgtype": "m.text", |  | ||||
|                 "body": msg |  | ||||
|             }; |  | ||||
| 
 |  | ||||
|             await axios.put(`${notification.homeserverUrl}/_matrix/client/r0/rooms/${roomId}/send/m.room.message/${randomString}`, data, config); |  | ||||
|             return okMsg; |  | ||||
|         } catch (error) { |  | ||||
|             this.throwGeneralAxiosError(error); |  | ||||
|         } |  | ||||
|     } |  | ||||
| } |  | ||||
| 
 |  | ||||
| module.exports = Matrix; |  | ||||