Compare commits

...

141 Commits

Author SHA1 Message Date
Louis Lam
5bb329fa0e Update to 1.23.16 2024-12-20 15:15:52 +08:00
Louis Lam
09dedc07fb [1.23.X] Update dependencies (#5455) 2024-12-20 15:11:24 +08:00
Louis Lam
6cfae01a0d Merge commit from fork
* [V1 Only] Change dev server's data path to ./data/v1

* Fix GHSA-2qgm-m29m-cj2h
2024-12-20 15:02:22 +08:00
Louis Lam
32dc76a085 Update to 1.23.15 2024-09-30 05:44:32 +08:00
Louis Lam
c6d6061a9f Pin cheerio to avoid the breaking change of undici (#5142) 2024-09-30 05:41:31 +08:00
Louis Lam
243726b03c Update to 1.23.14 2024-09-29 21:46:19 +08:00
Louis Lam
936665aac3 [1.23.X] Update dependencies (#5132) 2024-09-28 03:43:54 +08:00
Louis Lam
1185b259c2 Fix dayjs issue on frontend (#4881) 2024-06-25 18:08:02 +08:00
jmolnar-comparative
a81f949f98 chore: fixed a typo for internal, unused part of the file upload icon for status page (#4757) 2024-05-13 19:24:13 +02:00
Nelson Chan
59f10d542b Fix: Show API Keys disabled msg. when disabled Auth (#4723)
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2024-04-30 22:11:09 +02:00
Louis Lam
2778929f74 Update to 1.23.13 2024-04-25 15:27:28 +08:00
Louis Lam
f71d35e53e Update dependencies 2024-04-25 15:26:49 +08:00
Nelson Chan
1490443618 Fix: Getting TLS certificate through proxy & prometheus update (#4700) 2024-04-24 14:37:17 +08:00
Frank Elsinga
add5c128ce fix: Localisation-matching algorithm missing some edgecase (#4692) 2024-04-21 20:23:34 +08:00
Louis Lam
e797abd108 Update to 1.23.12 2024-04-19 01:17:13 +08:00
Louis Lam
7a9e2f5de6 Merge pull request from GHSA-23q2-5gf8-gjpp 2024-04-19 01:08:31 +08:00
Louis Lam
7b5d2a71ff Update dependencies 2024-04-18 20:48:07 +08:00
Nelson Chan
893278bd3d Feat: Use keylog event to obtain TLS certificate for better reliability [1.23.X] (#4630)
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2024-04-06 18:43:08 +08:00
Adam Stachowicz
0e30ea830d fix: Update nodemailer to fix GHSA-9h6g-pr28-7cqp [1.23.X] (#4653) 2024-04-05 17:38:24 +02:00
Louis Lam
c67a2070b8 Update deps 2024-04-05 12:12:36 +08:00
Adam Stachowicz
9863a10321 fix: Update axios, @actions/github and dompurify [1.23.X] (#4652) 2024-04-05 11:47:46 +08:00
Nelson Chan
ee7f8680c1 Fix: Add missing FK for monitor-tls-info table [1.23.X] (#4631) 2024-03-31 12:05:38 +08:00
Nelson Chan
c1301804d4 Fix: Fix CI on Windows Runner [1.23.X] (#4633) 2024-03-31 10:33:59 +08:00
Frank Elsinga
b385e81608 Improved helptext of how to send mail via the systems mail subsystem (#4477) 2024-03-05 19:40:45 +01:00
Frank Elsinga
f37f55e06c Fixed lining issues introduced by code reivew 2024-02-11 22:44:57 +01:00
Frank Elsinga
87d7a780e3 changed the helptext a bit to make it more usefull for novice users 2024-02-11 22:40:47 +01:00
apio-sys
0fc372f558 #2793 2024-02-11 20:20:52 +01:00
Joris Le Blansch
67a13e1259 #2793 2024-02-11 20:03:17 +01:00
Nelson Chan
2b8f55194f Fix: [JSON-Query] Prevent parsing string-only JSON (#4425) 2024-01-28 03:18:24 +08:00
Nelson Chan
288cab6dd7 Fix: Make sure browser is connected before returning (#4417) 2024-01-25 07:59:42 +08:00
AnnAngela
b4e45c7ce8 fix(notification-dingding): throw error when failed (#3598) 2024-01-20 03:29:13 +08:00
Frank Elsinga
7635ab54a0 made sure that the i18n does use navigator.languages instead of navigator.language for automatic language detection (#4244) 2024-01-07 23:55:10 +08:00
Adam Stachowicz
458cdf9f9b Fix encodeBase64 for empty password or user in HTTP Basic Authentication (#4326) 2024-01-07 02:06:06 +08:00
Louis Lam
f1e2ee74ea Update to 1.23.11 2023-12-31 05:46:54 +08:00
Louis Lam
8d847abf35 Update dependencies 2023-12-31 05:09:45 +08:00
Louis Lam
8151ac0e25 Fix Async child process output issue (#4231) 2023-12-14 04:54:34 +08:00
Nelson Chan
4185ec20b0 Fix: Origin undefined on error handling (#4224) 2023-12-13 01:35:39 +08:00
Louis Lam
4245ea86e7 Update to 1.23.10 2023-12-13 00:55:58 +08:00
Louis Lam
f861a48dfc Smoothing the update for origin check (#4216) 2023-12-12 16:23:41 +08:00
Louis Lam
fa1214ae5e Rebse #4213 (#4215)
Co-authored-by: Nelson Chan <chakflying@hotmail.com>
2023-12-11 19:30:01 +08:00
Louis Lam
621419e434 Update to 1.23.9 2023-12-10 20:43:29 +08:00
Louis Lam
482049c72b Merge pull request from GHSA-88j4-pcx8-q4q3
* WIP, still need to handle npm run reset-password

* Implement it for "npm run reset-password"

Bug fixes and change along with this commit
- Move `ssl`, `hostname`, `port` to ./server/config.js, so `reset-password` is able to read it
- Fix: FBSD is missing, no idea who dropped it.
- Fix: Frontend code should not require any backend code (./server/config.js), moved "badgeConstants" to the common util (./src/util.ts) and drop vite-common.js

* Minor
2023-12-10 20:40:40 +08:00
Louis Lam
2815cc73cf Merge pull request from GHSA-mj22-23ff-2hrr
* WIP

* WIP

* Handle parsing error

* Fix matching origin issue
2023-12-10 20:39:43 +08:00
Louis Lam
e1147c06aa Update denpendecies 2023-12-10 02:45:42 +08:00
Ritik Singh
abc8f2b131 Fix: Correct Maintenance Start/End Time Input to Use Explicitly Specified Timezone (#4186) 2023-12-09 18:27:07 +08:00
Frank Elsinga
777ef6bc7b chore: added a helptext for ntfy's priority field (#4175)
* added a helptext for `ntfy`'s `priority` field

* linting fixes

* removed an unnecessary `Math.max` call
2023-12-09 17:37:33 +08:00
Louis Lam
b244e8fcbb Re-export the icon on vectr.com, so it can be editable again. The current icon.svg was reduced size by a contributor previously, but the border is detached after that, which cannot edit by any svg editor anymore. 2023-12-09 04:24:19 +08:00
Frank Elsinga
031947319a Add an aria-label to the monitor search box (#4163)
* added the `Search monitored sites` label

* rebase
2023-12-05 01:15:28 +08:00
Adam Stachowicz
74a908a069 Max ESLint warnings 0 (#4158)
* Fix ESLint warnings. Update workflows. 0 ESLint warnings for auto-test

* json-yaml-validate: Fix `unable to find version `v2``
2023-12-04 18:19:18 +08:00
Frank Elsinga
9c56c9b346 Fixed the buttons of ActionsSelect and ActionsInput having a default type="submit" (#4162)
* fixed the buttons having a default type="submit"

* fixed linting issue
2023-12-04 01:15:40 +08:00
Louis Lam
37666bf35f Update to 1.23.8 2023-12-02 17:51:06 +08:00
Louis Lam
90badfabee Update dependencies 2023-12-02 15:41:59 +08:00
Frank Elsinga
e3396251a8 accessible domain selector (#4133)
* made the status domain selector more accessible

* linting fix

* implemented the suggested changes

---------

Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
2023-12-01 15:05:01 +08:00
Frank Elsinga
9c9a086788 accessible ActionSelect/ ActionInput (#4132)
* made sure that the ActionSelect'or has correct accessibiltiy tags

* fixed linting error

* improved the ActionInputs accessibility
2023-12-01 14:34:37 +08:00
Louis Lam
9fb95fe95e Add support for /snap/bin/chromium (#4141) 2023-12-01 14:25:41 +08:00
Louis Lam
1e75d81bcf Update apprise from 1.4.5 to 1.6.0 (#4140) 2023-12-01 14:16:35 +08:00
Louis Lam
cb3a104dc0 Default Retries from 1 to 0 (#4139)
* Default "Retries" from 1 to 0
2023-12-01 14:09:13 +08:00
Louis Lam
57a18958d6 Update gamedig from ~4.1.0 to ^4.2.0 (#4136) 2023-12-01 11:42:31 +08:00
Louis Lam
1708b67949 Change execSync/spawnSync to async (#4123)
* WIP

* Add missing await

* Update package-lock.json
2023-11-30 16:12:04 +08:00
Louis Lam
73239d441d Update to 1.23.7 2023-11-24 18:49:27 +08:00
Louis Lam
4ceeb304f1 Add a script to prepare a changelog 2023-11-24 18:44:54 +08:00
Louis Lam
711380bbbe Merge pull request #4095 from louislam/update-3
Rewrite Tailscale ping using spawnSync
2023-11-24 17:44:24 +08:00
Louis Lam
9536c6aa6a Minor 2023-11-24 17:33:13 +08:00
Louis Lam
4255496b11 Rewrite Tailscale ping using spawnSync 2023-11-24 17:29:42 +08:00
Louis Lam
f28dccf4e1 Merge pull request from GHSA-v4v2-8h88-65qj 2023-11-24 17:18:01 +08:00
Louis Lam
b689733d59 Fix getGameList, testChrome without checkLogin 2023-11-24 16:37:52 +08:00
Louis Lam
afaa7bb2f0 Do not process debug log for production 2023-11-24 16:03:35 +08:00
Louis Lam
121d1a11af Revert "Restart running monitors if no heartbeat (#3952)" (#4088)
This reverts commit c43223a16d.
2023-11-24 02:23:38 +08:00
Louis Lam
8e61158758 Close the client postgresql connection after rejection. (#4084)
Co-authored-by: Manuel Vázquez Acosta <manuel@merchise.org>
2023-11-22 19:50:03 +08:00
Louis Lam
bf58838b89 +10 seconds for Abort signal (#4053)
* Debug only

* Remove debug
2023-11-22 16:03:03 +08:00
Nelson Chan
33ce0ef02c Fix: Improve error message on timeout (#4054)
* Fix: Improve error message on timeout

* Chore: Format
2023-11-21 23:56:17 +08:00
Louis Lam
c1aaad0d85 Update to 1.23.6 2023-11-18 11:37:14 +08:00
Louis Lam
954e05b72f Fix #4051 2023-11-18 11:33:34 +08:00
Louis Lam
6d4a45f18c Update to 1.23.5 2023-11-18 01:23:53 +08:00
Louis Lam
f0975cd929 Should be a final ulitmate fix for request timeout issue (#4045)
* Try to fix timeout again

* Ops
2023-11-18 01:17:54 +08:00
Louis Lam
40d6a21453 Fix kafka migration script again (#4043) 2023-11-17 15:21:29 +08:00
Louis Lam
b383392e8f Remains Node.js 16' SSL behavior for 1.23.X (#4044) 2023-11-17 15:21:08 +08:00
Nelson Chan
9964b6c4d8 Fix: Update monitor object on pause (#4032) 2023-11-16 20:41:35 +08:00
Louis Lam
d56bf08cd7 Update to 1.23.4 2023-11-13 15:23:32 +08:00
Louis Lam
291d5d7c55 Update dependencies 2023-11-13 15:22:51 +08:00
Louis Lam
8e3ff25f7b Followup #3864, rebase for 1.23.x (#4016)
* Fix: Use ActionSelect Docker Host & validate input

* Fix: Handle docker host deleted while editing

* UI: Use add for ActionSelect & prevent delete instead

---------

Co-authored-by: Nelson Chan <chakflying@hotmail.com>
2023-11-12 20:32:40 +08:00
Louis Lam
6e80c850f4 Should be an ulitmate fix for request timeout issue (#4011) 2023-11-12 13:50:51 +08:00
Muhammed Hussein karimi
0608881954 🐛 fix: kafka producer booleans migration null values (#3984)
Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>
2023-11-10 00:32:54 +08:00
Nelson Chan
38efd97b28 Fix: Support float ping in push route (#3987) 2023-11-09 23:39:44 +08:00
Louis Lam
ce0ba6c0ca Fix/axios abort signal for 1.23.X (#3971)
* Fix: Add axios abort signal

* Chore: Fix comment

---------

Co-authored-by: Nelson Chan <chakflying@hotmail.com>
2023-11-01 10:10:48 +08:00
Louis Lam
c43223a16d Restart running monitors if no heartbeat (#3952) 2023-11-01 09:36:12 +08:00
Muhammed Hussein karimi
9f170a68d7 🐛 fix: boolean fields in kafka producer monitor (#3949)
* 🐛 fix: boolean fields in kafka producer monitor

Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>

* 🐛 fix: boolean fields db patch table modify

Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>

* ✏️  typo: remove `_old` COLUMNs in patch-fix-kafka-producer-booleans

Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>

---------

Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>
2023-10-28 14:42:55 +08:00
Louis Lam
1a862e47ab Check if the password changed when user is not null 2023-10-23 06:21:39 +08:00
Nelson Chan
e64bf0e3fe Fix: Stop notification check on root certs (#3874)
* Fix: Stop notification check on root certs

* Chore: Use Set for optimization

* Fix: Manually calculate SHA256 to support node v14
2023-10-16 02:20:38 +08:00
Louis Lam
523d137e2b Lint 2023-10-16 00:43:07 +08:00
Louis Lam
18169c59a1 [MySQL monitor] Split password into a standalone field (#3899) 2023-10-16 00:38:56 +08:00
Louis Lam
4ccf263481 Update docker image base from Node.js 16 to Node.js 18 for Uptime Kuma v1 (#3901) 2023-10-16 00:27:47 +08:00
Louis Lam
1c13a75970 Fix #3868 postgres monitor could possibly crash Uptime Kuma (#3880)
* Bump pg

* Handle uncaughtException

* Fix parsing issue of postgres connection and fix the query example
2023-10-13 02:50:10 +08:00
Louis Lam
c3e3f27457 Update to 1.23.3 2023-10-09 20:26:18 +08:00
Louis Lam
794f1810bf Minor 2023-10-09 20:09:29 +08:00
Louis Lam
168357d93c Update dependencies 2023-10-09 20:05:50 +08:00
Louis Lam
476deb9fec Pin npm@9 2023-10-09 07:33:52 +08:00
Louis Lam
a36f2a75ca Enable auto-test for 1.23.X branch 2023-10-09 07:23:30 +08:00
Louis Lam
88afab6571 Merge pull request from GHSA-g9v2-wqcj-j99g
* Fix attempt

* Update message
2023-10-09 07:01:54 +08:00
Nelson Chan
bd9c44cccf Fix: Disable status page saving before getData 2023-10-09 06:41:07 +08:00
前端小武
1b148786a5 Fix: Update x-forwarded-host field when using reverse proxy (#3726) 2023-10-09 06:31:52 +08:00
Louis Lam
66a10b8993 Pump gamedig from 4.0.X to 4.1.X and update dependencies 2023-10-03 04:51:02 +08:00
Muhammed Hussein karimi
2ab21ccf8a 🐛 fix: kafka producer bugs (#3771)
* 🐛 fix: missing Kafka Producer SSL option in frontend object

Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>

* ♻️  refactor: better error handling of kafka producer

Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>

---------

Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>
2023-09-24 03:30:15 +08:00
Marvin A. Ruder
90d0e8ccde Enable status page certificate expiry badge for all HTTP(s) monitors (#3649) 2023-09-24 03:18:18 +08:00
Louis Lam
16a396debb Similar to #3763, but for 1.23.3 2023-09-21 20:38:54 +08:00
Louis Lam
6b3d69e1d3 Ignore /extra/healthcheck-armv7 2023-09-18 03:59:56 +08:00
Louis Lam
b3b8e9f3a0 Update to 1.23.2 2023-09-18 03:04:29 +08:00
Louis Lam
e5345848a2 Update dependencies 2023-09-18 03:01:58 +08:00
FC (Fay) Stegerman
d8a8f6c08b EditMonitor: add missing config UI for json-query (#3751) 2023-09-17 20:39:07 +08:00
Louis Lam
f98a1ce077 Ignore some build files 2023-09-17 20:32:49 +08:00
Louis Lam
86fa57449e Fix #3596 2023-09-17 02:56:19 +08:00
Louis Lam
ff51704cdf Fix #3712 2023-09-17 02:40:08 +08:00
Henrik Gerdes
33804d8823 fix: respect the user defined oauth2 auth method (#3727) 2023-09-16 05:13:20 +08:00
Nelson Chan
1e12ca4786 Fix: Disable save button on submit (#3729) 2023-09-16 05:11:18 +08:00
Nelson Chan
0af4ee6c34 Fix: Missing await for isActive (#3717) 2023-09-10 01:54:03 +08:00
ZaneL1u
1f29fabe64 fix: fix the judgment condition of Tailscale (#3701) 2023-09-07 16:15:21 +08:00
Louis Lam
c4e222d1e6 Update README.md 2023-08-29 20:49:03 +08:00
Louis Lam
f2a1c26ef8 Update to 1.23.1 2023-08-29 03:52:53 +08:00
Louis Lam
8772baad9a Update dependencies 2023-08-29 03:39:47 +08:00
Louis Lam
215c89e8d3 Merge pull request #3612 from UptimeKumaBot/weblate-uptime-kuma-uptime-kuma
Translations Update from Weblate
2023-08-29 03:09:48 +08:00
Unai Tolosa Pontesta
e6a055af19 Translated using Weblate (Basque)
Currently translated at 68.9% (557 of 808 strings)

Co-authored-by: Unai Tolosa Pontesta <utolosa002@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/eu/
Translation: Uptime Kuma/Uptime Kuma
2023-08-28 08:15:57 +00:00
Yoswaris Lawpaiboon
88d71d2c7a Translated using Weblate (Thai)
Currently translated at 79.9% (646 of 808 strings)

Co-authored-by: Yoswaris Lawpaiboon <konglha19@outlook.co.th>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/th/
Translation: Uptime Kuma/Uptime Kuma
2023-08-28 08:15:57 +00:00
Mirinek
cd2d5325df Translated using Weblate (Slovak)
Currently translated at 26.7% (216 of 808 strings)

Co-authored-by: Mirinek <mirek.nozicka77@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/sk/
Translation: Uptime Kuma/Uptime Kuma
2023-08-28 08:15:57 +00:00
AlwaleedAlwabel
75a1245b70 Translated using Weblate (Arabic)
Currently translated at 83.9% (678 of 808 strings)

Co-authored-by: AlwaleedAlwabel <xomsd1@hotmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ar/
Translation: Uptime Kuma/Uptime Kuma
2023-08-28 08:15:57 +00:00
DevMirza
f666eb6d83 Translated using Weblate (Urdu)
Currently translated at 59.1% (478 of 808 strings)

Co-authored-by: DevMirza <pzhafeez@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ur/
Translation: Uptime Kuma/Uptime Kuma
2023-08-28 08:15:57 +00:00
Haim Cohen
cb10643f57 Translated using Weblate (Hebrew (Israel))
Currently translated at 87.6% (708 of 808 strings)

Co-authored-by: Haim Cohen <haim1979@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/he_IL/
Translation: Uptime Kuma/Uptime Kuma
2023-08-28 08:15:57 +00:00
Wishw
c9ba4e7e8b Translated using Weblate (Telugu)
Currently translated at 23.3% (189 of 808 strings)

Added translation using Weblate (Telugu)

Co-authored-by: Wishw <62600445+Wisw@users.noreply.github.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/te/
Translation: Uptime Kuma/Uptime Kuma
2023-08-28 08:15:57 +00:00
stanol
94187bca5d Translated using Weblate (Ukrainian)
Currently translated at 100.0% (808 of 808 strings)

Co-authored-by: stanol <stanol777@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/uk/
Translation: Uptime Kuma/Uptime Kuma
2023-08-28 08:15:57 +00:00
Matteo Mazzoni
39b4aa5966 Translated using Weblate (Italian)
Currently translated at 73.0% (590 of 808 strings)

Co-authored-by: Matteo Mazzoni <matteo@matteomazzoni.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/it/
Translation: Uptime Kuma/Uptime Kuma
2023-08-28 08:15:57 +00:00
Louis Lam
cd79df07e1 Add an ability to enable/disable nscd (#3652) 2023-08-28 16:15:48 +08:00
Louis Lam
0c40f02584 Put Monitor Group under Advanced section 2023-08-28 04:59:47 +08:00
Nelson Chan
db42c13e05 Fix: Remove legacy unused tags cleanup (#3651) 2023-08-27 18:56:50 +08:00
Nelson Chan
5f85d8f749 Fix: Focus & styling on safari (#3650) 2023-08-27 18:55:36 +08:00
Lior Slakman
c0e273df5b Show hostname:port for gamedig monitor on Discord notification (#3643) 2023-08-27 02:24:33 +08:00
Nelson Chan
4da1341aa5 Fix: Improve dnsMessage handling (#3614) 2023-08-26 21:27:32 +08:00
James Osborn
e765e6a1b8 Spelling & Grammar updates (#3638)
* Spelling & Grammar updates to SECURITY.md

No policy changes were made in this change, only improving readability.

* Spelling & Grammar updates to README.md

No policy changes were made in this change, only improving readability.

* Spelling & Grammar updates to CONTRIBUTING.md

No policy changes were made in this change, only improving readability.
2023-08-25 00:01:47 +08:00
bt90
eee9a1f004 Passwordmanager friendly inputs (#3593)
* Mark username/password inputs as required

* Mark 2FA input as required

* Add autocomplete attributes

* Add autocomplete suggestion to password change

* Add autocomplete to 2FA dialog

* Mark 2fa input as required
2023-08-21 02:25:35 +08:00
Frank Elsinga
4d07b65bdd fixed local docker not working anymore (#3606) 2023-08-20 04:45:58 +08:00
Frank Elsinga
1772158d62 fixed opsgenieRegion not being the same enum between the frontend and backend (#3616) 2023-08-20 04:41:42 +08:00
Frank Elsinga
7bfdb82f5d bug: fixed the curser being a pointer if hovering above the dropdown-item's (#3607)
* fixed the curser being a pointer if hovering above the `dropdown-item`'s

* formatting fix
2023-08-20 04:40:12 +08:00
Frank Elsinga
8945316ce6 added helptexs to the installation script (#3603) 2023-08-20 03:01:37 +08:00
81 changed files with 8411 additions and 6357 deletions

View File

@@ -34,7 +34,12 @@ tsconfig.json
/ecosystem.config.js
/extra/healthcheck.exe
/extra/healthcheck
extra/exe-builder
/extra/exe-builder
/extra/push-examples
/extra/uptime-kuma-push
# Comment the following line if you want to rebuild the healthcheck binary
/extra/healthcheck-armv7
### .gitignore content (commented rules are duplicated)

View File

@@ -78,7 +78,7 @@ module.exports = {
"checkLoops": false,
}],
"space-before-blocks": "warn",
//'no-console': 'warn',
//"no-console": "warn",
"no-extra-boolean-cast": "off",
"no-multiple-empty-lines": [ "warn", {
"max": 1,
@@ -90,7 +90,8 @@ module.exports = {
"no-unneeded-ternary": "error",
"array-bracket-newline": [ "error", "consistent" ],
"eol-last": [ "error", "always" ],
//'prefer-template': 'error',
//"prefer-template": "error",
"template-curly-spacing": [ "warn", "never" ],
"comma-dangle": [ "warn", "only-multiline" ],
"no-empty": [ "error", {
"allowEmptyCatch": true

View File

@@ -5,11 +5,11 @@ name: Auto Test
on:
push:
branches: [ master ]
branches: [ master, 1.23.X ]
paths-ignore:
- '*.md'
pull_request:
branches: [ master, 2.0.X ]
branches: [ master, 1.23.X ]
paths-ignore:
- '*.md'
@@ -22,19 +22,18 @@ jobs:
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest, ARM64]
node: [ 14, 20 ]
node: [ 16, 20.5 ]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- run: git config --global core.autocrlf false # Mainly for Windows
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node }}
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm install npm@latest -g
- run: npm install
- run: npm ci
- run: npm run build
- run: npm test
env:
@@ -50,18 +49,17 @@ jobs:
strategy:
matrix:
os: [ ARMv7 ]
node: [ 14, 20 ]
node: [ 16, 20.5 ]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- run: git config --global core.autocrlf false # Mainly for Windows
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node }}
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm install npm@latest -g
- run: npm ci --production
check-linters:
@@ -69,27 +67,27 @@ jobs:
steps:
- run: git config --global core.autocrlf false # Mainly for Windows
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Use Node.js 14
uses: actions/setup-node@v3
- name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: 14
- run: npm install
- run: npm run lint
node-version: 20.5
- run: npm ci
- run: npm run lint:prod
e2e-tests:
needs: [ check-linters ]
runs-on: ubuntu-latest
steps:
- run: git config --global core.autocrlf false # Mainly for Windows
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Use Node.js 14
uses: actions/setup-node@v3
- name: Use Node.js 16
uses: actions/setup-node@v4
with:
node-version: 14
- run: npm install
node-version: 16
- run: npm ci
- run: npm run build
- run: npm run cy:test
@@ -98,12 +96,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- run: git config --global core.autocrlf false # Mainly for Windows
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Use Node.js 14
uses: actions/setup-node@v3
- name: Use Node.js 16
uses: actions/setup-node@v4
with:
node-version: 14
- run: npm install
node-version: 16
- run: npm ci
- run: npm run build
- run: npm run cy:run:unit

View File

@@ -14,10 +14,10 @@ jobs:
node-version: [16]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'

View File

@@ -6,7 +6,7 @@ on:
pull_request:
branches:
- master
- 2.0.X
- 1.23.X
workflow_dispatch:
permissions:
@@ -17,11 +17,11 @@ jobs:
json-yaml-validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: json-yaml-validate
id: json-yaml-validate
uses: GrantBirki/json-yaml-validate@v1.3.0
uses: GrantBirki/json-yaml-validate@v2.4.0
with:
comment: "true" # enable comment mode
exclude_file: ".github/config/exclude.txt" # gitignore style file for exclusions

View File

@@ -9,7 +9,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v7
- uses: actions/stale@v8
with:
stale-issue-message: 'We are clearing up our old issues and your ticket has been open for 3 months with no activity. Remove stale label or comment or this will be closed in 2 days.'
close-issue-message: 'This issue was closed because it has been stalled for 2 days with no activity.'

View File

@@ -2,13 +2,13 @@
First of all, I want to thank everyone who made pull requests for Uptime Kuma. I never thought the GitHub Community would be so nice! Because of this, I also never thought that other people would actually read and edit my code. It is not very well structured or commented, sorry about that.
The project was created with vite.js (vue3). Then I created a subdirectory called "server" for server part. Both frontend and backend share the same package.json.
The project was created with vite.js (vue3). Then I created a subdirectory called "server" for the 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.
The frontend code builds into "dist" directory. The server (express.js) exposes the "dist" directory as the 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.)
- Node.js (You should know about promise, async/await and arrow function etc.)
- Socket.io
- SCSS
- Vue.js
@@ -30,7 +30,7 @@ The frontend code build into "dist" directory. The server (express.js) exposes t
## Can I create a pull request for Uptime Kuma?
Yes or no, it depends on what you will try to do. Since I don't want to waste your time, be sure to **create an empty draft pull request or open an issue, so we can have a discussion first**. Especially for a large pull request or you don't know it will be merged or not.
Yes or no, it depends on what you will try to do. Since I don't want to waste your time, be sure to **create an empty draft pull request or open an issue, so we can have a discussion first**. Especially for a large pull request or you don't know if it will be merged or not.
Here are some references:
@@ -46,8 +46,8 @@ Here are some references:
- New features
### ❌ Won't be merged:
- A dedicated pr for translating existing languages (see [these instructions](https://github.com/louislam/uptime-kuma/blob/master/src/lang/README.md))
- Do not pass the auto test
- A dedicated PR for translating existing languages (see [these instructions](https://github.com/louislam/uptime-kuma/blob/master/src/lang/README.md))
- Do not pass the auto-test
- Any breaking changes
- Duplicated pull requests
- Buggy
@@ -61,9 +61,9 @@ The above cases may not cover all possible situations.
I (@louislam) have the final say. If your pull request does not meet my expectations, I will reject it, no matter how much time you spend on it. Therefore, it is essential to have a discussion beforehand.
I will mark your pull request in the [milestones](https://github.com/louislam/uptime-kuma/milestones), if I am plan to review and merge it.
I will assign your pull request to a [milestone](https://github.com/louislam/uptime-kuma/milestones), if I plan to review and merge it.
Also, please don't rush or ask for ETA, because I have to understand the pull request, make sure it is no breaking changes and stick to my vision of this project, especially for large pull requests.
Also, please don't rush or ask for an ETA, because I have to understand the pull request, make sure it is no breaking changes and stick to my vision of this project, especially for large pull requests.
### Recommended Pull Request Guideline
@@ -83,11 +83,11 @@ Before deep into coding, discussion first is preferred. Creating an empty pull r
## Project Styles
I personally do not like something that requires so many configurations before you can finally start the app. I hope Uptime Kuma installation could be as easy as like installing a mobile app.
I personally do not like something that requires so many configurations before you can finally start the app. I hope Uptime Kuma installation will be as easy as like installing a mobile app.
- Easy to install for non-Docker users, no native build dependency is needed (for x86_64/armv7/arm64), no extra config, no extra effort required to get it running
- Easy to install for non-Docker users, no native build dependency is needed (for x86_64/armv7/arm64), no extra config, and no extra effort required to get it running
- 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. Environment variable is not encouraged, unless it is related to startup such as `DATA_DIR`
- Settings should be configurable in the frontend. Environment variables are discouraged, unless it is related to startup such as `DATA_DIR`
- Easy to use
- The web UI styling should be consistent and nice
@@ -130,7 +130,7 @@ Port `3000` and port `3001` will be used.
npm run dev
```
But sometimes, you would like to keep restart the server, but not the frontend, you can run these command in two terminals:
But sometimes, you would like to restart the server, but not the frontend, you can run these commands in two terminals:
```
npm run start-frontend-dev
npm run start-server-dev
@@ -146,13 +146,13 @@ It is mainly a socket.io app + express.js.
express.js is used for:
- entry point such as redirecting to a status page or the dashboard
- serving the frontend built files (index.html, .js and .css etc.)
- serving internal APIs of status page
- serving internal APIs of the status page
### Structure in /server/
- jobs/ (Jobs that are running in another process)
- model/ (Object model, auto mapping to the database table name)
- model/ (Object model, auto-mapping to the database table name)
- modules/ (Modified 3rd-party modules)
- monitor_types (Monitor Types)
- notification-providers/ (individual notification logic)
@@ -163,7 +163,7 @@ express.js is used for:
## Frontend Dev Server
It binds to `0.0.0.0:3000` by default. Frontend dev server is used for development only.
It binds to `0.0.0.0:3000` by default. The frontend dev server is used for development only.
For production, it is not used. It will be compiled to `dist` directory instead.
@@ -181,7 +181,7 @@ Uptime Kuma Frontend is a single page application (SPA). Most paths are handled
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.
As you can see, most data in the frontend is stored at the root level, even though you changed the current router to any other pages.
The data and socket logic are in `src/mixins/socket.js`.
@@ -210,7 +210,7 @@ Both frontend and backend share the same package.json. However, the frontend dep
### Update Dependencies
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.
Since previously updating Vite 2.5.10 to 2.6.0 broke the application completely, from now on, it should update the patch release version only.
Patch release = the third digit ([Semantic Versioning](https://semver.org/))
@@ -218,17 +218,17 @@ If for security / bug / other reasons, a library must be updated, breaking chang
## Translations
Please add **all** the strings which are translatable to `src/lang/en.json` (If translation keys are ommited, they can not be translated).
Please add **all** the strings which are translatable to `src/lang/en.json` (If translation keys are omitted, they can not be translated).
**Don't include any other languages in your inital Pull-Request** (even if this is your mother tounge), to avoid merge-conflicts between weblate and `master`.
The translations can then (after merging a PR into `master`) be translated by awesome people donating their language-skills.
**Don't include any other languages in your initial Pull-Request** (even if this is your mother tongue), to avoid merge-conflicts between weblate and `master`.
The translations can then (after merging a PR into `master`) be translated by awesome people donating their language skills.
If you want to help by translating Uptime Kuma into your language, please visit the [instructions on how to translate using weblate](https://github.com/louislam/uptime-kuma/blob/master/src/lang/README.md).
## Spelling & Grammar
Feel free to correct the grammar in the documentation or code.
My mother language is not english and my grammar is not that great.
My mother language is not English and my grammar is not that great.
## Wiki

View File

@@ -26,7 +26,7 @@ It is a temporary live demo, all data will be deleted after 10 minutes. Use the
* Monitoring uptime for HTTP(s) / TCP / HTTP(s) Keyword / HTTP(s) Json Query / Ping / DNS Record / Push / Steam Game Server / Docker Containers
* Fancy, Reactive, Fast UI/UX
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [90+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/tree/master/src/components/notifications)
* 20 second intervals
* 20-second intervals
* [Multi Languages](https://github.com/louislam/uptime-kuma/tree/master/src/lang)
* Multiple status pages
* Map status pages to specific domains
@@ -70,7 +70,7 @@ npm run setup
# Option 1. Try it
node server/server.js
# (Recommended) Option 2. Run in background using PM2
# (Recommended) Option 2. Run in the background using PM2
# Install PM2 if you don't have it:
npm install pm2 -g && pm2 install pm2-logrotate
@@ -93,7 +93,7 @@ pm2 save && pm2 startup
### Windows Portable (x64)
https://github.com/louislam/uptime-kuma/files/11886108/uptime-kuma-win64-portable-1.0.1.zip
https://github.com/louislam/uptime-kuma/releases/download/1.23.1/uptime-kuma-windows-x64-portable-1.23.1.zip
### Advanced Installation
@@ -109,7 +109,7 @@ 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.
I will assign requests/issues to the next milestone.
https://github.com/louislam/uptime-kuma/milestones

View File

@@ -3,19 +3,19 @@
## Reporting a Vulnerability
1. Please report security issues to https://github.com/louislam/uptime-kuma/security/advisories/new.
1. Please also create a empty security issues for alerting me, as GitHub Advisory do not send a notification, I probably will miss without this. https://github.com/louislam/uptime-kuma/issues/new?assignees=&labels=help&template=security.md
1. Please also create an empty security issue to alert me, as GitHub Advisories do not send a notification, I probably will miss it without this. https://github.com/louislam/uptime-kuma/issues/new?assignees=&labels=help&template=security.md
Do not use the public issue tracker or discuss it in the public as it will cause more damage.
Do not use the public issue tracker or discuss it in public as it will cause more damage.
## Do you accept other 3rd-party bug bounty platforms?
At this moment, I DO NOT accept other bug bounty platforms, because I am not familiar with these platforms and someone have tried to send a phishing link to me by this already. To minimize my own risk, please report through GitHub Advisories only. I will ignore all 3rd-party bug bounty platforms emails.
At this moment, I DO NOT accept other bug bounty platforms, because I am not familiar with these platforms and someone has tried to send a phishing link to me by doing this already. To minimize my own risk, please report through GitHub Advisories only. I will ignore all 3rd-party bug bounty platforms emails.
## Supported Versions
### Uptime Kuma Versions
You should use or upgrade to the latest version of Uptime Kuma. All `1.X.X` versions are upgradable to the lastest version.
You should use or upgrade to the latest version of Uptime Kuma. All `1.X.X` versions are upgradable to the latest version.
### Upgradable Docker Tags

View File

@@ -3,7 +3,6 @@ import vue from "@vitejs/plugin-vue";
import { defineConfig } from "vite";
import visualizer from "rollup-plugin-visualizer";
import viteCompression from "vite-plugin-compression";
import commonjs from "vite-plugin-commonjs";
const postCssScss = require("postcss-scss");
const postcssRTLCSS = require("postcss-rtlcss");
@@ -22,7 +21,6 @@ export default defineConfig({
"CODESPACE_NAME": JSON.stringify(process.env.CODESPACE_NAME),
},
plugins: [
commonjs(),
vue(),
legacy({
targets: [ "since 2015" ],

View File

@@ -0,0 +1,34 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
-- Rename COLUMNs to another one (suffixed by `_old`)
ALTER TABLE monitor
RENAME COLUMN kafka_producer_ssl TO kafka_producer_ssl_old;
ALTER TABLE monitor
RENAME COLUMN kafka_producer_allow_auto_topic_creation TO kafka_producer_allow_auto_topic_creation_old;
-- Add correct COLUMNs
ALTER TABLE monitor
ADD COLUMN kafka_producer_ssl BOOLEAN default 0 NOT NULL;
ALTER TABLE monitor
ADD COLUMN kafka_producer_allow_auto_topic_creation BOOLEAN default 0 NOT NULL;
-- These SQL is still not fully safe. See https://github.com/louislam/uptime-kuma/issues/4039.
-- Set bring old values from `_old` COLUMNs to correct ones
-- UPDATE monitor SET kafka_producer_allow_auto_topic_creation = monitor.kafka_producer_allow_auto_topic_creation_old
-- WHERE monitor.kafka_producer_allow_auto_topic_creation_old IS NOT NULL;
-- UPDATE monitor SET kafka_producer_ssl = monitor.kafka_producer_ssl_old
-- WHERE monitor.kafka_producer_ssl_old IS NOT NULL;
-- Remove old COLUMNs
ALTER TABLE monitor
DROP COLUMN kafka_producer_allow_auto_topic_creation_old;
ALTER TABLE monitor
DROP COLUMN kafka_producer_ssl_old;
COMMIT;

View File

@@ -0,0 +1,18 @@
BEGIN TRANSACTION;
PRAGMA writable_schema = TRUE;
UPDATE
SQLITE_MASTER
SET
sql = replace(sql,
'monitor_id INTEGER NOT NULL',
'monitor_id INTEGER NOT NULL REFERENCES [monitor] ([id]) ON DELETE CASCADE ON UPDATE CASCADE'
)
WHERE
name = 'monitor_tls_info'
AND type = 'table';
PRAGMA writable_schema = RESET;
COMMIT;

View File

@@ -0,0 +1,10 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
-- SQLite: Change the data type of the column "config" from VARCHAR to TEXT
ALTER TABLE notification RENAME COLUMN config TO config_old;
ALTER TABLE notification ADD COLUMN config TEXT;
UPDATE notification SET config = config_old;
ALTER TABLE notification DROP COLUMN config_old;
COMMIT;

7
db/patch-timeout.sql Normal file
View File

@@ -0,0 +1,7 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
UPDATE monitor SET timeout = (interval * 0.8)
WHERE timeout IS NULL OR timeout <= 0;
COMMIT;

View File

@@ -1,6 +1,6 @@
# DON'T UPDATE TO node:14-bullseye-slim, see #372.
# If the image changed, the second stage image should be changed too
FROM node:16-buster-slim
# DON'T UPDATE TO bullseye-slim, see #372.
# There is no 20-buster-slim for armv7 unfortunately, 18-buster-slim is the last one for Uptime Kuma v1.
FROM node:18-buster-slim
ARG TARGETPLATFORM
WORKDIR /app
@@ -27,7 +27,7 @@ RUN apt-get update && \
ca-certificates \
sudo \
nscd && \
pip3 --no-cache-dir install apprise==1.4.5 && \
pip3 --no-cache-dir install apprise==1.6.0 && \
rm -rf /var/lib/apt/lists/* && \
apt --yes autoremove

View File

@@ -6,7 +6,7 @@
* ⚠️ Deprecated: Changed to healthcheck.go, it will be deleted in the future.
* This script should be run after a period of time (180s), because the server may need some time to prepare.
*/
const { FBSD } = require("../server/util-server");
const FBSD = /^freebsd/.test(process.platform);
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";

View File

@@ -189,13 +189,15 @@ if (type == "local") {
bash("check=$(git --version)");
if (check == "") {
error = 1;
println("Error: git is missing");
println("Error: git is not found!");
println("help: an installation guide is available at https://git-scm.com/book/en/v2/Getting-Started-Installing-Git");
}
bash("check=$(node -v)");
if (check == "") {
error = 1;
println("Error: node is missing");
println("Error: node is not found");
println("help: an installation guide is available at https://nodejs.org/en/download");
}
if (error > 0) {
@@ -216,6 +218,7 @@ if (type == "local") {
bash("check=$(pm2 --version)");
if (check == "") {
println("Error: pm2 is not found!");
println("help: an installation guide is available at https://pm2.keymetrics.io/docs/usage/quick-start/");
bash("exit 1");
}
@@ -232,6 +235,7 @@ if (type == "local") {
bash("check=$(docker -v)");
if (check == "") {
println("Error: docker is not found!");
println("help: an installation guide is available at https://docs.docker.com/desktop/");
bash("exit 1");
}
@@ -239,6 +243,7 @@ if (type == "local") {
bash("if [[ \"$check\" == *\"Is the docker daemon running\"* ]]; then
\"echo\" \"Error: docker is not running\"
\"echo\" \"help: a troubleshooting guide is available at https://docs.docker.com/config/daemon/troubleshoot/\"
\"exit\" \"1\"
fi");

View File

@@ -0,0 +1,44 @@
// Generate on GitHub
const input = `
* Add Korean translation by @Alanimdeo in https://github.com/louislam/dockge/pull/86
`;
const template = `
### 🆕 New Features
### 💇‍♀️ Improvements
### 🐞 Bug Fixes
### ⬆️ Security Fixes
### 🦎 Translation Contributions
### Others
- Other small changes, code refactoring and comment/doc updates in this repo:
`;
const lines = input.split("\n").filter((line) => line.trim() !== "");
for (const line of lines) {
// Split the last " by "
const usernamePullRequesURL = line.split(" by ").pop();
if (!usernamePullRequesURL) {
console.log("Unable to parse", line);
continue;
}
const [ username, pullRequestURL ] = usernamePullRequesURL.split(" in ");
const pullRequestID = "#" + pullRequestURL.split("/").pop();
let message = line.split(" by ").shift();
if (!message) {
console.log("Unable to parse", line);
continue;
}
message = message.split("* ").pop();
console.log("-", pullRequestID, message, `(Thanks ${username})`);
}
console.log(template);

View File

@@ -5,6 +5,8 @@ const { R } = require("redbean-node");
const readline = require("readline");
const { initJWTSecret } = require("../server/util-server");
const User = require("../server/model/user");
const { io } = require("socket.io-client");
const { localWebSocketURL } = require("../server/config");
const args = require("args-parser")(process.argv);
const rl = readline.createInterface({
input: process.stdin,
@@ -36,12 +38,16 @@ const main = async () => {
// Reset all sessions by reset jwt secret
await initJWTSecret();
// Disconnect all other socket clients of the user
await disconnectAllSocketClients(user.username, password);
break;
} else {
console.log("Passwords do not match, please try again.");
}
}
console.log("Password reset successfully.");
}
} catch (e) {
console.error("Error: " + e.message);
@@ -66,6 +72,44 @@ function question(question) {
});
}
function disconnectAllSocketClients(username, password) {
return new Promise((resolve) => {
console.log("Connecting to " + localWebSocketURL + " to disconnect all other socket clients");
// Disconnect all socket connections
const socket = io(localWebSocketURL, {
reconnection: false,
timeout: 5000,
});
socket.on("connect", () => {
socket.emit("login", {
username,
password,
}, (res) => {
if (res.ok) {
console.log("Logged in.");
socket.emit("disconnectOtherSocketClients");
} else {
console.warn("Login failed.");
console.warn("Please restart the server to disconnect all sessions.");
}
socket.close();
});
});
socket.on("connect_error", function () {
// The localWebSocketURL is not guaranteed to be working for some complicated Uptime Kuma setup
// Ask the user to restart the server manually
console.warn("Failed to connect to " + localWebSocketURL);
console.warn("Please restart the server to disconnect all sessions manually.");
resolve();
});
socket.on("disconnect", () => {
resolve();
});
});
}
if (!process.env.TEST_BACKEND) {
main();
}

View File

@@ -156,12 +156,14 @@ fi
check=$(git --version)
if [ "$check" == "" ]; then
error=$((1))
"echo" "-e" "Error: git is missing"
"echo" "-e" "Error: git is not found!"
"echo" "-e" "help: an installation guide is available at https://git-scm.com/book/en/v2/Getting-Started-Installing-Git"
fi
check=$(node -v)
if [ "$check" == "" ]; then
error=$((1))
"echo" "-e" "Error: node is missing"
"echo" "-e" "Error: node is not found"
"echo" "-e" "help: an installation guide is available at https://nodejs.org/en/download"
fi
if [ $(($error > 0)) == 1 ]; then
"echo" "-e" "Please install above missing software"
@@ -180,6 +182,7 @@ fi
check=$(pm2 --version)
if [ "$check" == "" ]; then
"echo" "-e" "Error: pm2 is not found!"
"echo" "-e" "help: an installation guide is available at https://pm2.keymetrics.io/docs/usage/quick-start/"
exit 1
fi
mkdir -p $installPath
@@ -192,11 +195,13 @@ else
check=$(docker -v)
if [ "$check" == "" ]; then
"echo" "-e" "Error: docker is not found!"
"echo" "-e" "help: an installation guide is available at https://docs.docker.com/desktop/"
exit 1
fi
check=$(docker info)
if [[ "$check" == *"Is the docker daemon running"* ]]; then
"echo" "Error: docker is not running"
"echo" "help: a troubleshooting guide is available at https://docs.docker.com/config/daemon/troubleshoot/"
"exit" "1"
fi
if [ "$3" != "" ]; then

12568
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "uptime-kuma",
"version": "1.23.0",
"version": "1.23.16",
"license": "MIT",
"repository": {
"type": "git",
@@ -13,16 +13,18 @@
"install-legacy": "npm install",
"update-legacy": "npm update",
"lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .",
"lint:js-prod": "npm run lint:js -- --max-warnings 0",
"lint-fix:js": "eslint --ext \".js,.vue\" --fix --ignore-path .gitignore .",
"lint:style": "stylelint \"**/*.{vue,css,scss}\" --ignore-path .gitignore",
"lint-fix:style": "stylelint \"**/*.{vue,css,scss}\" --fix --ignore-path .gitignore",
"lint": "npm run lint:js && npm run lint:style",
"lint:prod": "npm run lint:js-prod && npm run lint:style",
"dev": "concurrently -k -r \"wait-on tcp:3000 && npm run start-server-dev \" \"npm run start-frontend-dev\"",
"start-frontend-dev": "cross-env NODE_ENV=development vite --host --config ./config/vite.config.js",
"start-frontend-devcontainer": "cross-env NODE_ENV=development DEVCONTAINER=1 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",
"start-server-dev": "cross-env NODE_ENV=development node server/server.js --data-dir=./data/v1/",
"build": "vite build --config ./config/vite.config.js",
"test": "node test/prepare-test-server.js && npm run jest-backend",
"test-with-build": "npm run build && npm test",
@@ -40,7 +42,7 @@
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
"build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test --target pr-test . --push",
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
"setup": "git checkout 1.23.0 && npm ci --production && npm run download-dist",
"setup": "git checkout 1.23.16 && npm ci --production && npm run download-dist",
"download-dist": "node extra/download-dist.js",
"mark-as-nightly": "node extra/mark-as-nightly.js",
"reset-password": "node extra/reset-password.js",
@@ -56,6 +58,9 @@
"test-install-script-ubuntu1604": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu1604.dockerfile .",
"simple-dns-server": "node extra/simple-dns-server.js",
"simple-mqtt-server": "node extra/simple-mqtt-server.js",
"simple-mongo": "docker run --rm -p 27017:27017 mongo",
"simple-postgres": "docker run --rm -p 5432:5432 -e POSTGRES_PASSWORD=postgres postgres",
"simple-mariadb": "docker run --rm -p 3306:3306 -e MYSQL_ROOT_PASSWORD=mariadb# mariadb",
"update-language-files": "cd extra/update-language-files && node index.js && cross-env-shell eslint ../../src/languages/$npm_config_language.js --fix",
"ncu-patch": "npm-check-updates -u -t patch",
"release-final": "node ./extra/test-docker.js && node extra/update-version.js && npm run build-docker && node ./extra/press-any-key.js && npm run upload-artifacts && node ./extra/update-wiki-version.js",
@@ -69,21 +74,22 @@
"cypress-open": "concurrently -k -r \"node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/\" \"cypress open --config-file ./config/cypress.config.js\"",
"build-healthcheck-armv7": "cross-env GOOS=linux GOARCH=arm GOARM=7 go build -x -o ./extra/healthcheck-armv7 ./extra/healthcheck.go",
"deploy-demo-server": "node extra/deploy-demo-server.js",
"sort-contributors": "node extra/sort-contributors.js"
"sort-contributors": "node extra/sort-contributors.js",
"start-server-node14-win": "private\\node14\\node.exe server/server.js"
},
"dependencies": {
"@grpc/grpc-js": "~1.7.3",
"@grpc/grpc-js": "~1.8.22",
"@louislam/ping": "~0.4.4-mod.1",
"@louislam/sqlite3": "15.1.6",
"args-parser": "~1.3.0",
"axios": "~0.27.0",
"axios": "~0.28.1",
"axios-ntlm": "1.3.0",
"badge-maker": "~3.3.1",
"bcryptjs": "~2.4.3",
"cacheable-lookup": "~6.0.4",
"chardet": "~1.4.0",
"check-password-strength": "^2.0.5",
"cheerio": "~1.0.0-rc.12",
"cheerio": "1.0.0-rc.12",
"chroma-js": "~2.4.2",
"command-exists": "~1.2.9",
"compare-versions": "~3.6.0",
@@ -91,11 +97,12 @@
"croner": "~6.0.5",
"dayjs": "~1.11.5",
"dotenv": "~16.0.3",
"express": "~4.17.3",
"express": "~4.21.0",
"express-basic-auth": "~1.2.1",
"express-static-gzip": "~2.1.7",
"form-data": "~4.0.0",
"gamedig": "~4.0.5",
"gamedig": "^4.2.0",
"html-escaper": "^3.0.3",
"http-graceful-shutdown": "~3.1.7",
"http-proxy-agent": "~5.0.0",
"https-proxy-agent": "~5.0.1",
@@ -108,38 +115,39 @@
"kafkajs": "^2.2.4",
"limiter": "~2.1.0",
"liquidjs": "^10.7.0",
"mongodb": "~4.14.0",
"mongodb": "~4.17.1",
"mqtt": "~4.3.7",
"mssql": "~8.1.4",
"mysql2": "~2.3.3",
"mysql2": "~3.9.6",
"nanoid": "~3.3.4",
"node-cloudflared-tunnel": "~1.0.9",
"node-radius-client": "~1.0.0",
"nodemailer": "~6.6.5",
"nodemailer": "~6.9.13",
"nostr-tools": "^1.13.1",
"notp": "~2.0.3",
"openid-client": "^5.4.2",
"password-hash": "~1.2.2",
"pg": "~8.8.0",
"pg-connection-string": "~2.5.0",
"pg": "~8.11.3",
"pg-connection-string": "~2.6.2",
"playwright-core": "~1.35.1",
"prom-client": "~13.2.0",
"prometheus-api-metrics": "~3.2.1",
"promisify-child-process": "~4.1.2",
"protobufjs": "~7.2.4",
"qs": "~6.10.4",
"redbean-node": "~0.3.0",
"redis": "~4.5.1",
"semver": "~7.5.4",
"socket.io": "~4.6.1",
"socket.io-client": "~4.6.1",
"socket.io": "~4.8.0",
"socket.io-client": "~4.8.0",
"socks-proxy-agent": "6.1.1",
"tar": "~6.1.11",
"tar": "~6.2.1",
"tcp-ping": "~0.1.1",
"thirty-two": "~1.0.2",
"ws": "^8.13.0"
},
"devDependencies": {
"@actions/github": "~5.0.1",
"@actions/github": "~5.1.1",
"@babel/eslint-parser": "^7.22.7",
"@babel/preset-env": "^7.15.8",
"@fortawesome/fontawesome-svg-core": "~1.2.36",
@@ -160,10 +168,10 @@
"core-js": "~3.26.1",
"cronstrue": "~2.24.0",
"cross-env": "~7.0.3",
"cypress": "^12.17.0",
"cypress": "^13.2.0",
"delay": "^5.0.0",
"dns2": "~2.0.1",
"dompurify": "~2.4.3",
"dompurify": "~3.1.7",
"eslint": "~8.14.0",
"eslint-plugin-vue": "~8.7.1",
"favico.js": "~0.3.10",
@@ -183,8 +191,7 @@
"timezones-list": "~3.0.1",
"typescript": "~4.4.4",
"v-pagination-3": "~0.1.7",
"vite": "~4.4.1",
"vite-plugin-commonjs": "^0.8.0",
"vite": "~5.2.8",
"vite-plugin-compression": "^0.5.1",
"vue": "~3.3.4",
"vue-chartjs": "~5.2.0",
@@ -198,7 +205,7 @@
"vue-router": "~4.0.14",
"vue-toastification": "~2.0.0-rc.5",
"vuedraggable": "~4.1.0",
"wait-on": "^6.0.1",
"wait-on": "^7.2.0",
"whatwg-url": "~12.0.1"
}
}

View File

@@ -1,10 +1,9 @@
<svg width="640" height="640" viewBox="0 0 640 640" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M490.4 235.64C544.09 358.38 544.09 435.34 490.4 466.5C409.85 513.24 199.96 527.49 139.54 455.64C99.2601 407.74 99.2601 334.4 139.54 235.64C180.5 168.18 238.71 134.45 314.17 134.45C389.64 134.45 448.38 168.18 490.4 235.64Z" fill="url(#paint0_linear_381_799)"/>
<path d="M490.4 235.64C544.09 358.38 544.09 435.34 490.4 466.5C409.85 513.24 199.96 527.49 139.54 455.64C99.2601 407.74 99.2601 334.4 139.54 235.64C180.5 168.18 238.71 134.45 314.17 134.45C389.64 134.45 448.38 168.18 490.4 235.64Z" stroke="#F2F2F2" stroke-opacity="0.51" stroke-width="200"/>
<defs>
<linearGradient id="paint0_linear_381_799" x1="259.78" y1="261.15" x2="463.85" y2="456.49" gradientUnits="userSpaceOnUse">
<svg width="640" height="640" viewBox="0 0 640 640" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1 0 0 1 320 320)">
<linearGradient id="S3" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1 0 0 1 -319.99875 -320.0001577393)" x1="259.78" y1="261.15" x2="463.85" y2="456.49">
<stop stop-color="#5CDD8B"/>
<stop offset="1" stop-color="#86E6A9"/>
</linearGradient>
</defs>
<path style="stroke: rgb(242,242,242); stroke-opacity: 0.51; stroke-width: 200; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: url(#S3); fill-rule: nonzero; opacity: 1;" transform=" translate(0, 0)" d="M 170.40125 -84.36016 C 224.09125 38.37984 224.09125 115.33984 170.40125 146.49984 C 89.85125000000001 193.23984000000002 -120.03875 207.48984000000002 -180.45875 135.63984 C -220.73875 87.73983999999999 -220.73875 14.399839999999998 -180.45875 -84.36016000000001 C -139.49875 -151.82016 -81.28875000000001 -185.55016 -5.828750000000014 -185.55016 C 69.64124999999999 -185.55016 128.38125 -151.82016000000002 170.40124999999998 -84.36016000000001 z" stroke-linecap="round" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 893 B

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -147,15 +147,18 @@ async function sendAPIKeyList(socket) {
async function sendInfo(socket, hideVersion = false) {
let version;
let latestVersion;
let isContainer;
if (!hideVersion) {
version = checkVersion.version;
latestVersion = checkVersion.latestVersion;
isContainer = (process.env.UPTIME_KUMA_IS_CONTAINER === "1");
}
socket.emit("info", {
version,
latestVersion,
isContainer,
primaryBaseURL: await setting("primaryBaseURL"),
serverTimezone: await server.getTimezone(),
serverTimezoneOffset: server.getTimezoneOffset(),

View File

@@ -1,29 +1,42 @@
const isFreeBSD = /^freebsd/.test(process.platform);
// Interop with browser
const args = (typeof process !== "undefined") ? require("args-parser")(process.argv) : {};
const demoMode = args["demo"] || false;
const badgeConstants = {
naColor: "#999",
defaultUpColor: "#66c20a",
defaultWarnColor: "#eed202",
defaultDownColor: "#c2290a",
defaultPendingColor: "#f8a306",
defaultMaintenanceColor: "#1747f5",
defaultPingColor: "blue", // as defined by badge-maker / shields.io
defaultStyle: "flat",
defaultPingValueSuffix: "ms",
defaultPingLabelSuffix: "h",
defaultUptimeValueSuffix: "%",
defaultUptimeLabelSuffix: "h",
defaultCertExpValueSuffix: " days",
defaultCertExpLabelSuffix: "h",
// Values Come From Default Notification Times
defaultCertExpireWarnDays: "14",
defaultCertExpireDownDays: "7"
};
// If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise.
// Dual-stack support for (::)
// Also read HOST if not FreeBSD, as HOST is a system environment variable in FreeBSD
let hostEnv = isFreeBSD ? null : process.env.HOST;
const hostname = args.host || process.env.UPTIME_KUMA_HOST || hostEnv;
const port = [ args.port, process.env.UPTIME_KUMA_PORT, process.env.PORT, 3001 ]
.map(portValue => parseInt(portValue))
.find(portValue => !isNaN(portValue));
const sslKey = args["ssl-key"] || process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || undefined;
const sslCert = args["ssl-cert"] || process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || undefined;
const sslKeyPassphrase = args["ssl-key-passphrase"] || process.env.UPTIME_KUMA_SSL_KEY_PASSPHRASE || process.env.SSL_KEY_PASSPHRASE || undefined;
const isSSL = sslKey && sslCert;
function getLocalWebSocketURL() {
const protocol = isSSL ? "wss" : "ws";
const host = hostname || "localhost";
return `${protocol}://${host}:${port}`;
}
const localWebSocketURL = getLocalWebSocketURL();
const demoMode = args["demo"] || false;
module.exports = {
args,
hostname,
port,
sslKey,
sslCert,
sslKeyPassphrase,
isSSL,
localWebSocketURL,
demoMode,
badgeConstants,
};

View File

@@ -81,6 +81,10 @@ class Database {
"patch-monitor-oauth-cc.sql": true,
"patch-add-timeout-monitor.sql": true,
"patch-add-gamedig-given-port.sql": true,
"patch-notification-config.sql": true,
"patch-fix-kafka-producer-booleans.sql": true,
"patch-timeout.sql": true,
"patch-monitor-tls-info-add-fk.sql": true,
};
/**

View File

@@ -80,8 +80,8 @@ class DockerHost {
options.socketPath = dockerHost.dockerDaemon;
} else if (dockerHost.dockerType === "tcp") {
options.baseURL = DockerHost.patchDockerURL(dockerHost.dockerDaemon);
options.httpsAgent = new https.Agent(DockerHost.getHttpsAgentOptions(dockerHost.dockerType, options.baseURL));
}
options.httpsAgent = new https.Agent(DockerHost.getHttpsAgentOptions(dockerHost.dockerType, options.baseURL));
let res = await axios.request(options);

View File

@@ -1,4 +1,5 @@
const jsesc = require("jsesc");
const { escape } = require("html-escaper");
/**
* Returns a string that represents the javascript that is required to insert the Google Analytics scripts
@@ -7,15 +8,18 @@ const jsesc = require("jsesc");
* @returns {string}
*/
function getGoogleAnalyticsScript(tagId) {
let escapedTagId = jsesc(tagId, { isScriptContext: true });
let escapedTagIdJS = jsesc(tagId, { isScriptContext: true });
if (escapedTagId) {
escapedTagId = escapedTagId.trim();
if (escapedTagIdJS) {
escapedTagIdJS = escapedTagIdJS.trim();
}
// Escape the tag ID for use in an HTML attribute.
let escapedTagIdHTMLAttribute = escape(tagId);
return `
<script async src="https://www.googletagmanager.com/gtag/js?id=${escapedTagId}"></script>
<script>window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date());gtag('config', '${escapedTagId}'); </script>
<script async src="https://www.googletagmanager.com/gtag/js?id=${escapedTagIdHTMLAttribute}"></script>
<script>window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date());gtag('config', '${escapedTagIdJS}'); </script>
`;
}

View File

@@ -6,7 +6,7 @@ const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, TimeLogger, MAX_INTERVA
SQL_DATETIME_FORMAT
} = require("../../src/util");
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, mqttAsync, setSetting, httpNtlm, radius, grpcQuery,
redisPingAsync, mongodbPing, kafkaProducerAsync, getOidcTokenClientCredentials,
redisPingAsync, mongodbPing, kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal
} = require("../util-server");
const { R } = require("redbean-node");
const { BeanModel } = require("redbean-node/dist/bean-model");
@@ -22,6 +22,9 @@ const { UptimeCacheList } = require("../uptime-cache-list");
const Gamedig = require("gamedig");
const jsonata = require("jsonata");
const jwt = require("jsonwebtoken");
const crypto = require("crypto");
const rootCertificates = rootCertificatesFingerprints();
/**
* status:
@@ -53,7 +56,7 @@ class Monitor extends BeanModel {
obj.tags = await this.getTags();
}
if (certExpiry && this.type === "http") {
if (certExpiry && (this.type === "http" || this.type === "keyword" || this.type === "json-query") && this.getURLProtocol() === "https:") {
const { certExpiryDaysRemaining, validCert } = await this.getCertExpiry(this.id);
obj.certExpiryDaysRemaining = certExpiryDaysRemaining;
obj.validCert = validCert;
@@ -141,8 +144,8 @@ class Monitor extends BeanModel {
expectedValue: this.expectedValue,
kafkaProducerTopic: this.kafkaProducerTopic,
kafkaProducerBrokers: JSON.parse(this.kafkaProducerBrokers),
kafkaProducerSsl: this.kafkaProducerSsl === "1" && true || false,
kafkaProducerAllowAutoTopicCreation: this.kafkaProducerAllowAutoTopicCreation === "1" && true || false,
kafkaProducerSsl: this.getKafkaProducerSsl(),
kafkaProducerAllowAutoTopicCreation: this.getKafkaProducerAllowAutoTopicCreation(),
kafkaProducerMessage: this.kafkaProducerMessage,
screenshot,
};
@@ -227,10 +230,12 @@ class Monitor extends BeanModel {
/**
* Encode user and password to Base64 encoding
* for HTTP "basic" auth, as per RFC-7617
* @param {string|null} user - The username (nullable if not changed by a user)
* @param {string|null} pass - The password (nullable if not changed by a user)
* @returns {string}
*/
encodeBase64(user, pass) {
return Buffer.from(user + ":" + pass).toString("base64");
return Buffer.from(`${user || ""}:${pass || ""}`).toString("base64");
}
/**
@@ -285,6 +290,22 @@ class Monitor extends BeanModel {
return Boolean(this.gamedigGivenPortOnly);
}
/**
* Parse to boolean
* @returns {boolean} Kafka Producer Ssl enabled?
*/
getKafkaProducerSsl() {
return Boolean(this.kafkaProducerSsl);
}
/**
* Parse to boolean
* @returns {boolean} Kafka Producer Allow Auto Topic Creation Enabled?
*/
getKafkaProducerAllowAutoTopicCreation() {
return Boolean(this.kafkaProducerAllowAutoTopicCreation);
}
/**
* Start monitor
* @param {Server} io Socket server instance
@@ -339,6 +360,12 @@ class Monitor extends BeanModel {
bean.duration = 0;
}
// Runtime patch timeout if it is 0
// See https://github.com/louislam/uptime-kuma/pull/3961#issuecomment-1804149144
if (!this.timeout || this.timeout <= 0) {
this.timeout = this.interval * 1000 * 0.8;
}
try {
if (await Monitor.isUnderMaintenance(this.id)) {
bean.msg = "Monitor under maintenance";
@@ -409,6 +436,7 @@ class Monitor extends BeanModel {
const httpsAgentOptions = {
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
rejectUnauthorized: !this.getIgnoreTls(),
secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT,
};
log.debug("monitor", `[${this.name}] Prepare Options for axios`);
@@ -447,6 +475,7 @@ class Monitor extends BeanModel {
validateStatus: (status) => {
return checkStatusCode(status, this.getAcceptedStatuscodes());
},
signal: axiosAbortSignal((this.timeout + 10) * 1000),
};
if (bodyValue) {
@@ -483,6 +512,18 @@ class Monitor extends BeanModel {
}
}
let tlsInfo = {};
// Store tlsInfo when secureConnect event is emitted
// The keylog event listener is a workaround to access the tlsSocket
options.httpsAgent.once("keylog", async (line, tlsSocket) => {
tlsSocket.once("secureConnect", async () => {
tlsInfo = checkCertificate(tlsSocket);
tlsInfo.valid = tlsSocket.authorized || false;
await this.handleTlsInfo(tlsInfo);
});
});
log.debug("monitor", `[${this.name}] Axios Options: ${JSON.stringify(options)}`);
log.debug("monitor", `[${this.name}] Axios Request`);
@@ -492,31 +533,19 @@ class Monitor extends BeanModel {
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:") {
log.debug("monitor", `[${this.name}] Check cert`);
try {
let tlsInfoObject = checkCertificate(res);
tlsInfo = await this.updateTlsInfo(tlsInfoObject);
// fallback for if kelog event is not emitted, but we may still have tlsInfo,
// e.g. if the connection is made through a proxy
if (this.getUrl()?.protocol === "https:" && tlsInfo.valid === undefined) {
const tlsSocket = res.request.res.socket;
if (!this.getIgnoreTls() && this.isEnabledExpiryNotification()) {
log.debug("monitor", `[${this.name}] call checkCertExpiryNotifications`);
await this.checkCertExpiryNotifications(tlsInfoObject);
}
if (tlsSocket) {
tlsInfo = checkCertificate(tlsSocket);
tlsInfo.valid = tlsSocket.authorized || false;
} catch (e) {
if (e.message !== "No TLS certificate in response") {
log.error("monitor", "Caught error");
log.error("monitor", e.message);
}
await this.handleTlsInfo(tlsInfo);
}
}
if (process.env.TIMELOGGER === "1") {
log.debug("monitor", "Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms");
}
if (process.env.UPTIME_KUMA_LOG_RESPONSE_BODY_MONITOR_ID === this.id) {
log.info("monitor", res.data);
}
@@ -549,8 +578,12 @@ class Monitor extends BeanModel {
let data = res.data;
// convert data to object
if (typeof data === "string") {
data = JSON.parse(data);
if (typeof data === "string" && res.headers["content-type"] !== "application/json") {
try {
data = JSON.parse(data);
} catch (_) {
// Failed to parse as JSON, just process it as a string
}
}
let expression = jsonata(this.jsonPath);
@@ -581,13 +614,13 @@ class Monitor extends BeanModel {
let dnsRes = await dnsResolve(this.hostname, this.dns_resolve_server, this.port, 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") {
if (this.dns_resolve_type === "A" || this.dns_resolve_type === "AAAA" || this.dns_resolve_type === "TXT" || this.dns_resolve_type === "PTR") {
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 === "CNAME") {
dnsMessage += dnsRes[0];
} else if (this.dns_resolve_type === "CAA") {
dnsMessage = dnsRes[0].issue;
dnsMessage += dnsRes[0].issue;
} else if (this.dns_resolve_type === "MX") {
dnsRes.forEach(record => {
dnsMessage += `Hostname: ${record.exchange} - Priority: ${record.priority} | `;
@@ -605,7 +638,7 @@ class Monitor extends BeanModel {
dnsMessage = dnsMessage.slice(0, -2);
}
if (this.dnsLastResult !== dnsMessage) {
if (this.dnsLastResult !== dnsMessage && dnsMessage !== undefined) {
R.exec("UPDATE `monitor` SET dns_last_result = ? WHERE id = ? ", [
dnsMessage,
this.id
@@ -662,6 +695,7 @@ class Monitor extends BeanModel {
httpsAgent: CacheableDnsHttpAgent.getHttpsAgent({
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
rejectUnauthorized: !this.getIgnoreTls(),
secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT,
}),
httpAgent: CacheableDnsHttpAgent.getHttpAgent({
maxCachedSessions: 0,
@@ -704,8 +738,6 @@ class Monitor extends BeanModel {
} else if (this.type === "docker") {
log.debug("monitor", `[${this.name}] Prepare Options for Axios`);
const dockerHost = await R.load("docker_host", this.docker_host);
const options = {
url: `/containers/${this.docker_container}/json`,
timeout: this.interval * 1000 * 0.8,
@@ -716,12 +748,19 @@ class Monitor extends BeanModel {
httpsAgent: CacheableDnsHttpAgent.getHttpsAgent({
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
rejectUnauthorized: !this.getIgnoreTls(),
secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT,
}),
httpAgent: CacheableDnsHttpAgent.getHttpAgent({
maxCachedSessions: 0,
}),
};
const dockerHost = await R.load("docker_host", this.docker_host);
if (!dockerHost) {
throw new Error("Failed to load docker host config");
}
if (dockerHost._dockerType === "socket") {
options.socketPath = dockerHost._dockerDaemon;
} else if (dockerHost._dockerType === "tcp") {
@@ -756,7 +795,7 @@ class Monitor extends BeanModel {
} else if (this.type === "sqlserver") {
let startTime = dayjs().valueOf();
await mssqlQuery(this.databaseConnectionString, this.databaseQuery);
await mssqlQuery(this.databaseConnectionString, this.databaseQuery || "SELECT 1");
bean.msg = "";
bean.status = UP;
@@ -795,7 +834,7 @@ class Monitor extends BeanModel {
} else if (this.type === "postgres") {
let startTime = dayjs().valueOf();
await postgresQuery(this.databaseConnectionString, this.databaseQuery);
await postgresQuery(this.databaseConnectionString, this.databaseQuery || "SELECT 1");
bean.msg = "";
bean.status = UP;
@@ -803,7 +842,11 @@ class Monitor extends BeanModel {
} else if (this.type === "mysql") {
let startTime = dayjs().valueOf();
bean.msg = await mysqlQuery(this.databaseConnectionString, this.databaseQuery);
// Use `radius_password` as `password` field, since there are too many unnecessary fields
// TODO: rename `radius_password` to `password` later for general use
let mysqlPassword = this.radiusPassword;
bean.msg = await mysqlQuery(this.databaseConnectionString, this.databaseQuery || "SELECT 1", mysqlPassword);
bean.status = UP;
bean.ping = dayjs().valueOf() - startTime;
} else if (this.type === "mongodb") {
@@ -891,7 +934,11 @@ class Monitor extends BeanModel {
} catch (error) {
bean.msg = error.message;
if (error?.name === "CanceledError") {
bean.msg = `timeout by AbortSignal (${this.timeout}s)`;
} else {
bean.msg = error.message;
}
// If UP come in here, it must be upside down mode
// Just reset the retries
@@ -1075,6 +1122,19 @@ class Monitor extends BeanModel {
}
}
/**
* Example: http: or https:
* @returns {(null|string)}
*/
getURLProtocol() {
const url = this.getUrl();
if (url) {
return this.getUrl().protocol;
} else {
return null;
}
}
/**
* Store TLS info to database
* @param checkCertificateResult
@@ -1411,7 +1471,10 @@ class Monitor extends BeanModel {
let certInfo = tlsInfoObject.certInfo;
while (certInfo) {
let subjectCN = certInfo.subject["CN"];
if (certInfo.daysRemaining > targetDays) {
if (rootCertificates.has(certInfo.fingerprint256)) {
log.debug("monitor", `Known root cert: ${certInfo.certType} certificate "${subjectCN}" (${certInfo.daysRemaining} days valid) on ${targetDays} deadline.`);
break;
} else if (certInfo.daysRemaining > targetDays) {
log.debug("monitor", `No need to send cert notification for ${certInfo.certType} certificate "${subjectCN}" (${certInfo.daysRemaining} days valid) on ${targetDays} deadline.`);
} else {
log.debug("monitor", `call sendCertNotificationByTargetDays for ${targetDays} deadline on certificate ${subjectCN}.`);
@@ -1619,6 +1682,21 @@ class Monitor extends BeanModel {
const parentActive = await Monitor.isParentActive(parent.id);
return parent.active && parentActive;
}
/**
* Store TLS certificate information and check for expiry
* @param {Object} tlsInfo Information about the TLS connection
* @returns {Promise<void>}
*/
async handleTlsInfo(tlsInfo) {
await this.updateTlsInfo(tlsInfo);
this.prometheus?.update(null, tlsInfo);
if (!this.getIgnoreTls() && this.isEnabledExpiryNotification()) {
log.debug("monitor", `[${this.name}] call checkCertExpiryNotifications`);
await this.checkCertExpiryNotifications(tlsInfo);
}
}
}
module.exports = Monitor;

View File

@@ -1,6 +1,8 @@
const { BeanModel } = require("redbean-node/dist/bean-model");
const passwordHash = require("../password-hash");
const { R } = require("redbean-node");
const jwt = require("jsonwebtoken");
const { shake256, SHAKE256_LENGTH } = require("../util-server");
class User extends BeanModel {
/**
@@ -27,6 +29,19 @@ class User extends BeanModel {
this.password = newPassword;
}
/**
* Create a new JWT for a user
* @param {User} user
* @param {string} jwtSecret
* @return {string}
*/
static createJWT(user, jwtSecret) {
return jwt.sign({
username: user.username,
h: shake256(user.password, SHAKE256_LENGTH),
}, jwtSecret);
}
}
module.exports = User;

View File

@@ -9,6 +9,10 @@ const Database = require("../database");
const jwt = require("jsonwebtoken");
const config = require("../config");
/**
* Cached instance of a browser
* @type {import ("playwright-core").Browser}
*/
let browser = null;
let allowedList = [];
@@ -40,6 +44,7 @@ if (process.platform === "win32") {
"/usr/bin/chromium",
"/usr/bin/chromium-browser",
"/usr/bin/google-chrome",
"/snap/bin/chromium", // Ubuntu
];
} else if (process.platform === "darwin") {
// TODO: Generated by GitHub Copilot, but not sure if it's correct
@@ -61,8 +66,15 @@ async function isAllowedChromeExecutable(executablePath) {
return allowedList.includes(executablePath);
}
/**
* Get the current instance of the browser. If there isn't one, create
* it.
* @returns {Promise<import ("playwright-core").Browser>} The browser
*/
async function getBrowser() {
if (!browser) {
if (browser && browser.isConnected()) {
return browser;
} else {
let executablePath = await Settings.get("chromeExecutable");
executablePath = await prepareChromeExecutable(executablePath);
@@ -71,8 +83,9 @@ async function getBrowser() {
//headless: false,
executablePath,
});
return browser;
}
return browser;
}
async function prepareChromeExecutable(executablePath) {
@@ -180,6 +193,14 @@ class RealBrowserMonitorType extends MonitorType {
const context = await browser.newContext();
const page = await context.newPage();
// Prevent Local File Inclusion
// Accept only http:// and https://
// https://github.com/louislam/uptime-kuma/security/advisories/GHSA-2qgm-m29m-cj2h
let url = new URL(monitor.url);
if (url.protocol !== "http:" && url.protocol !== "https:") {
throw new Error("Invalid url protocol, only http and https are allowed.");
}
const res = await page.goto(monitor.url, {
waitUntil: "networkidle",
timeout: monitor.interval * 1000 * 0.8,

View File

@@ -1,6 +1,6 @@
const { MonitorType } = require("./monitor-type");
const { UP, log } = require("../../src/util");
const exec = require("child_process").exec;
const { UP } = require("../../src/util");
const childProcessAsync = require("promisify-child-process");
/**
* A TailscalePing class extends the MonitorType.
@@ -23,7 +23,6 @@ class TailscalePing extends MonitorType {
let tailscaleOutput = await this.runTailscalePing(monitor.hostname, monitor.interval);
this.parseTailscaleOutput(tailscaleOutput, heartbeat);
} catch (err) {
log.debug("Tailscale", err);
// trigger log function somewhere to display a notification or alert to the user (but how?)
throw new Error(`Error checking Tailscale ping: ${err}`);
}
@@ -33,30 +32,24 @@ class TailscalePing extends MonitorType {
* Runs the Tailscale ping command to the given URL.
*
* @param {string} hostname - The hostname to ping.
* @param {number} interval
* @returns {Promise<string>} - A Promise that resolves to the output of the Tailscale ping command
* @throws Will throw an error if the command execution encounters any error.
*/
async runTailscalePing(hostname, interval) {
let cmd = `tailscale ping ${hostname}`;
log.debug("Tailscale", cmd);
return new Promise((resolve, reject) => {
let timeout = interval * 1000 * 0.8;
exec(cmd, { timeout: timeout }, (error, stdout, stderr) => {
// we may need to handle more cases if tailscale reports an error that isn't necessarily an error (such as not-logged in or DERP health-related issues)
if (error) {
reject(`Execution error: ${error.message}`);
return;
}
if (stderr) {
reject(`Error in output: ${stderr}`);
return;
}
resolve(stdout);
});
let timeout = interval * 1000 * 0.8;
let res = await childProcessAsync.spawn("tailscale", [ "ping", "--c", "1", hostname ], {
timeout: timeout,
encoding: "utf8",
});
if (res.stderr && res.stderr.toString()) {
throw new Error(`Error in output: ${res.stderr.toString()}`);
}
if (res.stdout && res.stdout.toString()) {
return res.stdout.toString();
} else {
throw new Error("No output from Tailscale ping");
}
}
/**
@@ -74,7 +67,7 @@ class TailscalePing extends MonitorType {
heartbeat.status = UP;
let time = line.split(" in ")[1].split(" ")[0];
heartbeat.ping = parseInt(time);
heartbeat.msg = line;
heartbeat.msg = "OK";
break;
} else if (line.includes("timed out")) {
throw new Error(`Ping timed out: "${line}"`);

View File

@@ -1,5 +1,5 @@
const NotificationProvider = require("./notification-provider");
const childProcess = require("child_process");
const childProcessAsync = require("promisify-child-process");
class Apprise extends NotificationProvider {
@@ -11,7 +11,9 @@ class Apprise extends NotificationProvider {
args.push("-t");
args.push(notification.title);
}
const s = childProcess.spawnSync("apprise", args);
const s = await childProcessAsync.spawn("apprise", args, {
encoding: "utf8",
});
const output = (s.stdout) ? s.stdout.toString() : "ERROR: maybe apprise not found";

View File

@@ -18,7 +18,7 @@ class DingDing extends NotificationProvider {
text: `## [${this.statusToString(heartbeatJSON["status"])}] ${monitorJSON["name"]} \n> ${heartbeatJSON["msg"]}\n> Time (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`,
}
};
if (this.sendToDingDing(notification, params)) {
if (await this.sendToDingDing(notification, params)) {
return okMsg;
}
} else {
@@ -28,7 +28,7 @@ class DingDing extends NotificationProvider {
content: msg
}
};
if (this.sendToDingDing(notification, params)) {
if (await this.sendToDingDing(notification, params)) {
return okMsg;
}
}
@@ -59,7 +59,7 @@ class DingDing extends NotificationProvider {
if (result.data.errmsg === "ok") {
return true;
}
return false;
throw new Error(result.data.errmsg);
}
/**

View File

@@ -30,6 +30,7 @@ class Discord extends NotificationProvider {
break;
case "port":
case "dns":
case "gamedig":
case "steam":
address = monitorJSON["hostname"];
if (monitorJSON["port"]) {

View File

@@ -20,10 +20,10 @@ class Opsgenie extends NotificationProvider {
try {
switch (notification.opsgenieRegion) {
case "US":
case "us":
opsgenieAlertsUrl = opsgenieAlertsUrlUS;
break;
case "EU":
case "eu":
opsgenieAlertsUrl = opsgenieAlertsUrlEU;
break;
default:

View File

@@ -79,23 +79,25 @@ class Prometheus {
}
}
try {
monitorStatus.set(this.monitorLabelValues, heartbeat.status);
} catch (e) {
log.error("prometheus", "Caught error");
log.error("prometheus", e);
}
try {
if (typeof heartbeat.ping === "number") {
monitorResponseTime.set(this.monitorLabelValues, heartbeat.ping);
} else {
// Is it good?
monitorResponseTime.set(this.monitorLabelValues, -1);
if (heartbeat) {
try {
monitorStatus.set(this.monitorLabelValues, heartbeat.status);
} catch (e) {
log.error("prometheus", "Caught error");
log.error("prometheus", e);
}
try {
if (typeof heartbeat.ping === "number") {
monitorResponseTime.set(this.monitorLabelValues, heartbeat.ping);
} else {
// Is it good?
monitorResponseTime.set(this.monitorLabelValues, -1);
}
} catch (e) {
log.error("prometheus", "Caught error");
log.error("prometheus", e);
}
} catch (e) {
log.error("prometheus", "Caught error");
log.error("prometheus", e);
}
}

View File

@@ -1,15 +1,21 @@
let express = require("express");
const { allowDevAllOrigin, allowAllOrigin, percentageToColor, filterAndJoin, sendHttpError } = require("../util-server");
const {
setting,
allowDevAllOrigin,
allowAllOrigin,
percentageToColor,
filterAndJoin,
sendHttpError,
} = require("../util-server");
const { R } = require("redbean-node");
const apicache = require("../modules/apicache");
const Monitor = require("../model/monitor");
const dayjs = require("dayjs");
const { UP, MAINTENANCE, DOWN, PENDING, flipStatus, log } = require("../../src/util");
const { UP, MAINTENANCE, DOWN, PENDING, flipStatus, log, badgeConstants } = require("../../src/util");
const StatusPage = require("../model/status_page");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const { UptimeCacheList } = require("../uptime-cache-list");
const { makeBadge } = require("badge-maker");
const { badgeConstants } = require("../config");
const { Prometheus } = require("../prometheus");
let router = express.Router();
@@ -22,10 +28,14 @@ router.get("/api/entry-page", async (request, response) => {
allowDevAllOrigin(response);
let result = { };
let hostname = request.hostname;
if ((await setting("trustProxy")) && request.headers["x-forwarded-host"]) {
hostname = request.headers["x-forwarded-host"];
}
if (request.hostname in StatusPage.domainMappingList) {
if (hostname in StatusPage.domainMappingList) {
result.type = "statusPageMatchedDomain";
result.statusPageSlug = StatusPage.domainMappingList[request.hostname];
result.statusPageSlug = StatusPage.domainMappingList[hostname];
} else {
result.type = "entryPage";
result.entryPage = server.entryPage;
@@ -38,7 +48,7 @@ router.get("/api/push/:pushToken", async (request, response) => {
let pushToken = request.params.pushToken;
let msg = request.query.msg || "OK";
let ping = parseInt(request.query.ping) || null;
let ping = parseFloat(request.query.ping) || null;
let statusString = request.query.status || "up";
let status = (statusString === "up") ? UP : DOWN;

View File

@@ -5,7 +5,7 @@ const StatusPage = require("../model/status_page");
const { allowDevAllOrigin, sendHttpError } = require("../util-server");
const { R } = require("redbean-node");
const Monitor = require("../model/monitor");
const { badgeConstants } = require("../config");
const { badgeConstants } = require("../../src/util");
const { makeBadge } = require("badge-maker");
let router = express.Router();

View File

@@ -48,9 +48,17 @@ if (! process.env.NODE_ENV) {
process.env.NODE_ENV = "production";
}
if (!process.env.UPTIME_KUMA_WS_ORIGIN_CHECK) {
process.env.UPTIME_KUMA_WS_ORIGIN_CHECK = "cors-like";
}
log.info("server", "Node Env: " + process.env.NODE_ENV);
log.info("server", "Inside Container: " + (process.env.UPTIME_KUMA_IS_CONTAINER === "1"));
if (process.env.UPTIME_KUMA_WS_ORIGIN_CHECK === "bypass") {
log.warn("server", "WebSocket Origin Check: " + process.env.UPTIME_KUMA_WS_ORIGIN_CHECK);
}
log.info("server", "Importing Node libraries");
const fs = require("fs");
@@ -76,15 +84,18 @@ const notp = require("notp");
const base32 = require("thirty-two");
const { UptimeKumaServer } = require("./uptime-kuma-server");
const server = UptimeKumaServer.getInstance(args);
const server = UptimeKumaServer.getInstance();
const io = module.exports.io = server.io;
const app = server.app;
log.info("server", "Importing this project modules");
log.debug("server", "Importing Monitor");
const Monitor = require("./model/monitor");
const User = require("./model/user");
log.debug("server", "Importing Settings");
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD, doubleCheckPassword, startE2eTests } = require("./util-server");
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, doubleCheckPassword, startE2eTests, shake256, SHAKE256_LENGTH
} = require("./util-server");
log.debug("server", "Importing Notification");
const { Notification } = require("./notification");
@@ -107,19 +118,13 @@ const passwordHash = require("./password-hash");
const checkVersion = require("./check-version");
log.info("server", "Version: " + checkVersion.version);
// If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise.
// Dual-stack support for (::)
// Also read HOST if not FreeBSD, as HOST is a system environment variable in FreeBSD
let hostEnv = FBSD ? null : process.env.HOST;
let hostname = args.host || process.env.UPTIME_KUMA_HOST || hostEnv;
const hostname = config.hostname;
if (hostname) {
log.info("server", "Custom hostname: " + hostname);
}
const port = [ args.port, process.env.UPTIME_KUMA_PORT, process.env.PORT, 3001 ]
.map(portValue => parseInt(portValue))
.find(portValue => !isNaN(portValue));
const port = config.port;
const disableFrameSameOrigin = !!process.env.UPTIME_KUMA_DISABLE_FRAME_SAMEORIGIN || args["disable-frame-sameorigin"] || false;
const cloudflaredToken = args["cloudflared-token"] || process.env.UPTIME_KUMA_CLOUDFLARED_TOKEN || undefined;
@@ -297,6 +302,11 @@ let needSetup = false;
]);
if (user) {
// Check if the password changed
if (decoded.h !== shake256(user.password, SHAKE256_LENGTH)) {
throw new Error("The token is invalid due to password change or old token");
}
log.debug("auth", "afterLogin");
afterLogin(socket, user);
log.debug("auth", "afterLogin ok");
@@ -316,9 +326,10 @@ let needSetup = false;
});
}
} catch (error) {
log.error("auth", `Invalid token. IP=${clientIP}`);
if (error.message) {
log.error("auth", error.message, `IP=${clientIP}`);
}
callback({
ok: false,
msg: "Invalid token.",
@@ -357,9 +368,7 @@ let needSetup = false;
callback({
ok: true,
token: jwt.sign({
username: data.username,
}, server.jwtSecret),
token: User.createJWT(user, server.jwtSecret),
});
}
@@ -387,9 +396,7 @@ let needSetup = false;
callback({
ok: true,
token: jwt.sign({
username: data.username,
}, server.jwtSecret),
token: User.createJWT(user, server.jwtSecret),
});
} else {
@@ -726,11 +733,11 @@ let needSetup = false;
bean.basic_auth_user = monitor.basic_auth_user;
bean.basic_auth_pass = monitor.basic_auth_pass;
bean.timeout = monitor.timeout;
bean.oauth_client_id = monitor.oauth_client_id,
bean.oauth_client_secret = monitor.oauth_client_secret,
bean.oauth_auth_method = this.oauth_auth_method,
bean.oauth_token_url = monitor.oauth_token_url,
bean.oauth_scopes = monitor.oauth_scopes,
bean.oauth_client_id = monitor.oauth_client_id;
bean.oauth_client_secret = monitor.oauth_client_secret;
bean.oauth_auth_method = monitor.oauth_auth_method;
bean.oauth_token_url = monitor.oauth_token_url;
bean.oauth_scopes = monitor.oauth_scopes;
bean.tlsCa = monitor.tlsCa;
bean.tlsCert = monitor.tlsCert;
bean.tlsKey = monitor.tlsKey;
@@ -784,6 +791,9 @@ let needSetup = false;
bean.kafkaProducerAllowAutoTopicCreation = monitor.kafkaProducerAllowAutoTopicCreation;
bean.kafkaProducerSaslOptions = JSON.stringify(monitor.kafkaProducerSaslOptions);
bean.kafkaProducerMessage = monitor.kafkaProducerMessage;
bean.kafkaProducerSsl = monitor.kafkaProducerSsl;
bean.kafkaProducerAllowAutoTopicCreation =
monitor.kafkaProducerAllowAutoTopicCreation;
bean.gamedigGivenPortOnly = monitor.gamedigGivenPortOnly;
bean.validate();
@@ -796,7 +806,7 @@ let needSetup = false;
await updateMonitorNotification(bean.id, monitor.notificationIDList);
if (bean.isActive()) {
if (await bean.isActive()) {
await restartMonitor(socket.userID, bean.id);
}
@@ -1116,9 +1126,6 @@ let needSetup = false;
value,
]);
// Cleanup unused Tags
await R.exec("delete from tag where ( select count(*) from monitor_tag mt where tag.id = mt.tag_id ) = 0");
callback({
ok: true,
msg: "Deleted Successfully.",
@@ -1147,6 +1154,8 @@ let needSetup = false;
let user = await doubleCheckPassword(socket, password.currentPassword);
await user.resetPassword(password.newPassword);
server.disconnectAllSocketClients(user.id, socket.id);
callback({
ok: true,
msg: "Password has been updated successfully.",
@@ -1196,7 +1205,14 @@ let needSetup = false;
await doubleCheckPassword(socket, currentPassword);
}
// Log out all clients if enabling auth
// GHSA-23q2-5gf8-gjpp
if (currentDisabledAuth && !data.disableAuth) {
server.disconnectAllSocketClients(socket.userID, socket.id);
}
const previousChromeExecutable = await Settings.get("chromeExecutable");
const previousNSCDStatus = await Settings.get("nscd");
await setSettings("general", data);
server.entryPage = data.entryPage;
@@ -1214,6 +1230,15 @@ let needSetup = false;
await resetChrome();
}
// Update nscd status
if (previousNSCDStatus !== data.nscd) {
if (data.nscd) {
await server.startNSCDServices();
} else {
await server.stopNSCDServices();
}
}
callback({
ok: true,
msg: "Saved"
@@ -1817,6 +1842,7 @@ async function pauseMonitor(userID, monitorID) {
if (monitorID in server.monitorList) {
server.monitorList[monitorID].stop();
server.monitorList[monitorID].active = 0;
}
}
@@ -1875,8 +1901,10 @@ gracefulShutdown(server.httpServer, {
});
// Catch unexpected errors here
process.addListener("unhandledRejection", (error, promise) => {
let unexpectedErrorHandler = (error, promise) => {
console.trace(error);
UptimeKumaServer.errorLog(error, false);
console.error("If you keep encountering errors, please report to https://github.com/louislam/uptime-kuma/issues");
});
};
process.addListener("unhandledRejection", unexpectedErrorHandler);
process.addListener("uncaughtException", unexpectedErrorHandler);

View File

@@ -42,24 +42,50 @@ module.exports.generalSocketHandler = (socket, server) => {
});
socket.on("getGameList", async (callback) => {
callback({
ok: true,
gameList: getGameList(),
});
});
socket.on("testChrome", (executable, callback) => {
// Just noticed that await call could block the whole socket.io server!!! Use pure promise instead.
testChrome(executable).then((version) => {
try {
checkLogin(socket);
callback({
ok: true,
msg: "Found Chromium/Chrome. Version: " + version,
gameList: getGameList(),
});
}).catch((e) => {
} catch (e) {
callback({
ok: false,
msg: e.message,
});
});
}
});
socket.on("testChrome", (executable, callback) => {
try {
checkLogin(socket);
// Just noticed that await call could block the whole socket.io server!!! Use pure promise instead.
testChrome(executable).then((version) => {
callback({
ok: true,
msg: "Found Chromium/Chrome. Version: " + version,
});
}).catch((e) => {
callback({
ok: false,
msg: e.message,
});
});
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
// Disconnect all other socket clients of the user
socket.on("disconnectOtherSocketClients", async () => {
try {
checkLogin(socket);
server.disconnectAllSocketClients(socket.userID, socket.id);
} catch (e) {
log.warn("disconnectAllSocketClients", e.message);
}
});
};

View File

@@ -147,7 +147,7 @@ module.exports.statusPageSocketHandler = (socket) => {
config.logo = `/upload/${filename}?t=` + Date.now();
} else {
config.icon = imgDataUrl;
config.logo = imgDataUrl;
}
statusPage.slug = config.slug;

View File

@@ -4,14 +4,15 @@ const fs = require("fs");
const http = require("http");
const { Server } = require("socket.io");
const { R } = require("redbean-node");
const { log } = require("../src/util");
const { log, isDev } = require("../src/util");
const Database = require("./database");
const util = require("util");
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
const { Settings } = require("./settings");
const dayjs = require("dayjs");
const childProcess = require("child_process");
const childProcessAsync = require("promisify-child-process");
const path = require("path");
const { isSSL, sslKey, sslCert, sslKeyPassphrase } = require("./config");
// DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()`, put at the bottom of this file instead.
/**
@@ -62,22 +63,17 @@ class UptimeKumaServer {
*/
jwtSecret = null;
static getInstance(args) {
static getInstance() {
if (UptimeKumaServer.instance == null) {
UptimeKumaServer.instance = new UptimeKumaServer(args);
UptimeKumaServer.instance = new UptimeKumaServer();
}
return UptimeKumaServer.instance;
}
constructor(args) {
// SSL
const sslKey = args["ssl-key"] || process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || undefined;
const sslCert = args["ssl-cert"] || process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || undefined;
const sslKeyPassphrase = args["ssl-key-passphrase"] || process.env.UPTIME_KUMA_SSL_KEY_PASSPHRASE || process.env.SSL_KEY_PASSPHRASE || undefined;
constructor() {
log.info("server", "Creating express and socket.io instance");
this.app = express();
if (sslKey && sslCert) {
if (isSSL) {
log.info("server", "Server Type: HTTPS");
this.httpServer = https.createServer({
key: fs.readFileSync(sslKey),
@@ -103,7 +99,66 @@ class UptimeKumaServer {
UptimeKumaServer.monitorTypeList["real-browser"] = new RealBrowserMonitorType();
UptimeKumaServer.monitorTypeList["tailscale-ping"] = new TailscalePing();
this.io = new Server(this.httpServer);
// Allow all CORS origins (polling) in development
let cors = undefined;
if (isDev) {
cors = {
origin: "*",
};
}
this.io = new Server(this.httpServer, {
cors,
allowRequest: async (req, callback) => {
let transport;
// It should be always true, but just in case, because this property is not documented
if (req._query) {
transport = req._query.transport;
} else {
log.error("socket", "Ops!!! Cannot get transport type, assume that it is polling");
transport = "polling";
}
const clientIP = await this.getClientIPwithProxy(req.connection.remoteAddress, req.headers);
log.info("socket", `New ${transport} connection, IP = ${clientIP}`);
// The following check is only for websocket connections, polling connections are already protected by CORS
if (transport === "polling") {
callback(null, true);
} else if (transport === "websocket") {
const bypass = process.env.UPTIME_KUMA_WS_ORIGIN_CHECK === "bypass";
if (bypass) {
log.info("auth", "WebSocket origin check is bypassed");
callback(null, true);
} else if (!req.headers.origin) {
log.info("auth", "WebSocket with no origin is allowed");
callback(null, true);
} else {
let host = req.headers.host;
let origin = req.headers.origin;
try {
let originURL = new URL(origin);
let xForwardedFor;
if (await Settings.get("trustProxy")) {
xForwardedFor = req.headers["x-forwarded-for"];
}
if (host !== originURL.host && xForwardedFor !== originURL.host) {
callback(null, false);
log.error("auth", `Origin (${origin}) does not match host (${host}), IP: ${clientIP}`);
} else {
callback(null, true);
}
} catch (e) {
// Invalid origin url, probably not from browser
callback(null, false);
log.error("auth", `Invalid origin url (${origin}), IP: ${clientIP}`);
}
}
}
}
});
}
/** Initialise app after the database has been set up */
@@ -238,20 +293,28 @@ class UptimeKumaServer {
/**
* Get the IP of the client connected to the socket
* @param {Socket} socket
* @returns {string}
* @returns {Promise<string>}
*/
async getClientIP(socket) {
let clientIP = socket.client.conn.remoteAddress;
getClientIP(socket) {
return this.getClientIPwithProxy(socket.client.conn.remoteAddress, socket.client.conn.request.headers);
}
/**
*
* @param {string} clientIP
* @param {IncomingHttpHeaders} headers
* @returns {Promise<string>}
*/
async getClientIPwithProxy(clientIP, headers) {
if (clientIP === undefined) {
clientIP = "";
}
if (await Settings.get("trustProxy")) {
const forwardedFor = socket.client.conn.request.headers["x-forwarded-for"];
const forwardedFor = headers["x-forwarded-for"];
return (typeof forwardedFor === "string" ? forwardedFor.split(",")[0].trim() : null)
|| socket.client.conn.request.headers["x-real-ip"]
|| headers["x-real-ip"]
|| clientIP.replace(/^::ffff:/, "");
} else {
return clientIP.replace(/^::ffff:/, "");
@@ -341,7 +404,11 @@ class UptimeKumaServer {
* @returns {Promise<void>}
*/
async start() {
this.startServices();
let enable = await Settings.get("nscd");
if (enable || enable === null) {
await this.startNSCDServices();
}
}
/**
@@ -349,18 +416,22 @@ class UptimeKumaServer {
* @returns {Promise<void>}
*/
async stop() {
this.stopServices();
let enable = await Settings.get("nscd");
if (enable || enable === null) {
await this.stopNSCDServices();
}
}
/**
* Start all system services (e.g. nscd)
* For now, only used in Docker
*/
startServices() {
async startNSCDServices() {
if (process.env.UPTIME_KUMA_IS_CONTAINER) {
try {
log.info("services", "Starting nscd");
childProcess.execSync("sudo service nscd start", { stdio: "pipe" });
await childProcessAsync.exec("sudo service nscd start");
} catch (e) {
log.info("services", "Failed to start nscd");
}
@@ -370,16 +441,35 @@ class UptimeKumaServer {
/**
* Stop all system services
*/
stopServices() {
async stopNSCDServices() {
if (process.env.UPTIME_KUMA_IS_CONTAINER) {
try {
log.info("services", "Stopping nscd");
childProcess.execSync("sudo service nscd stop");
await childProcessAsync.exec("sudo service nscd stop");
} catch (e) {
log.info("services", "Failed to stop nscd");
}
}
}
/**
* Force connected sockets of a user to refresh and disconnect.
* Used for resetting password.
* @param {string} userID
* @param {string?} currentSocketID
*/
disconnectAllSocketClients(userID, currentSocketID = undefined) {
for (const socket of this.io.sockets.sockets.values()) {
if (socket.userID === userID && socket.id !== currentSocketID) {
try {
socket.emit("refresh");
socket.disconnect();
} catch (e) {
}
}
}
}
}
module.exports = {

View File

@@ -1,7 +1,7 @@
const tcpp = require("tcp-ping");
const ping = require("@louislam/ping");
const { R } = require("redbean-node");
const { log, genSecret } = require("../src/util");
const { log, genSecret, badgeConstants } = require("../src/util");
const passwordHash = require("./password-hash");
const { Resolver } = require("dns");
const childProcess = require("child_process");
@@ -9,7 +9,6 @@ const iconv = require("iconv-lite");
const chardet = require("chardet");
const mqtt = require("mqtt");
const chroma = require("chroma-js");
const { badgeConstants } = require("./config");
const mssql = require("mssql");
const { Client } = require("pg");
const postgresConParse = require("pg-connection-string").parse;
@@ -22,6 +21,7 @@ const protojs = require("protobufjs");
const radiusClient = require("node-radius-client");
const redis = require("redis");
const oidc = require("openid-client");
const tls = require("tls");
const {
dictionaries: {
@@ -33,6 +33,7 @@ const dayjs = require("dayjs");
// SASLOptions used in JSDoc
// eslint-disable-next-line no-unused-vars
const { Kafka, SASLOptions } = require("kafkajs");
const crypto = require("crypto");
const isWindows = process.platform === /^win/.test(process.platform);
/**
@@ -286,22 +287,22 @@ exports.kafkaProducerAsync = function (brokers, topic, message, options = {}, sa
producer.connect().then(
() => {
try {
producer.send({
topic: topic,
messages: [{
value: message,
}],
});
connectedToKafka = true;
clearTimeout(timeoutID);
producer.send({
topic: topic,
messages: [{
value: message,
}],
}).then((_) => {
resolve("Message sent successfully");
} catch (e) {
}).catch((e) => {
connectedToKafka = true;
producer.disconnect();
clearTimeout(timeoutID);
reject(new Error("Error sending message: " + e.message));
}
}).finally(() => {
connectedToKafka = true;
clearTimeout(timeoutID);
});
}
).catch(
(e) => {
@@ -313,8 +314,10 @@ exports.kafkaProducerAsync = function (brokers, topic, message, options = {}, sa
);
producer.on("producer.network.request_timeout", (_) => {
clearTimeout(timeoutID);
reject(new Error("producer.network.request_timeout"));
if (!connectedToKafka) {
clearTimeout(timeoutID);
reject(new Error("producer.network.request_timeout"));
}
});
producer.on("producer.disconnect", (_) => {
@@ -392,6 +395,9 @@ exports.mssqlQuery = async function (connectionString, query) {
try {
pool = new mssql.ConnectionPool(connectionString);
await pool.connect();
if (!query) {
query = "SELECT 1";
}
await pool.request().query(query);
pool.close();
} catch (e) {
@@ -412,12 +418,22 @@ exports.postgresQuery = function (connectionString, query) {
return new Promise((resolve, reject) => {
const config = postgresConParse(connectionString);
if (config.password === "") {
// See https://github.com/brianc/node-postgres/issues/1927
return reject(new Error("Password is undefined."));
// Fix #3868, which true/false is not parsed to boolean
if (typeof config.ssl === "string") {
config.ssl = config.ssl === "true";
}
const client = new Client({ connectionString });
if (config.password === "") {
// See https://github.com/brianc/node-postgres/issues/1927
reject(new Error("Password is undefined."));
return;
}
const client = new Client(config);
client.on("error", (error) => {
log.debug("postgres", "Error caught in the error event handler.");
reject(error);
});
client.connect((err) => {
if (err) {
@@ -441,6 +457,7 @@ exports.postgresQuery = function (connectionString, query) {
});
} catch (e) {
reject(e);
client.end();
}
}
});
@@ -452,11 +469,15 @@ exports.postgresQuery = function (connectionString, query) {
* Run a query on MySQL/MariaDB
* @param {string} connectionString The database connection string
* @param {string} query The query to validate the database with
* @param {?string} password The password to use
* @returns {Promise<(string)>}
*/
exports.mysqlQuery = function (connectionString, query) {
exports.mysqlQuery = function (connectionString, query, password = undefined) {
return new Promise((resolve, reject) => {
const connection = mysql.createConnection(connectionString);
const connection = mysql.createConnection({
uri: connectionString,
password
});
connection.on("error", (err) => {
reject(err);
@@ -695,20 +716,27 @@ const parseCertificateInfo = function (info) {
/**
* Check if certificate is valid
* @param {Object} res Response object from axios
* @param {tls.TLSSocket} socket TLSSocket, which may or may not be connected
* @returns {Object} Object containing certificate information
*/
exports.checkCertificate = function (res) {
if (!res.request.res.socket) {
throw new Error("No socket found");
exports.checkCertificate = function (socket) {
let certInfoStartTime = dayjs().valueOf();
// Return null if there is no socket
if (socket === undefined || socket == null) {
return null;
}
const info = res.request.res.socket.getPeerCertificate(true);
const valid = res.request.res.socket.authorized || false;
const info = socket.getPeerCertificate(true);
const valid = socket.authorized || false;
log.debug("cert", "Parsing Certificate Info");
const parsedInfo = parseCertificateInfo(info);
if (process.env.TIMELOGGER === "1") {
log.debug("monitor", "Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms");
}
return {
valid: valid,
certInfo: parsedInfo
@@ -1053,6 +1081,47 @@ module.exports.grpcQuery = async (options) => {
});
};
/**
* Returns an array of SHA256 fingerprints for all known root certificates.
* @returns {Set} A set of SHA256 fingerprints.
*/
module.exports.rootCertificatesFingerprints = () => {
let fingerprints = tls.rootCertificates.map(cert => {
let certLines = cert.split("\n");
certLines.shift();
certLines.pop();
let certBody = certLines.join("");
let buf = Buffer.from(certBody, "base64");
const shasum = crypto.createHash("sha256");
shasum.update(buf);
return shasum.digest("hex").toUpperCase().replace(/(.{2})(?!$)/g, "$1:");
});
fingerprints.push("6D:99:FB:26:5E:B1:C5:B3:74:47:65:FC:BC:64:8F:3C:D8:E1:BF:FA:FD:C4:C2:F9:9B:9D:47:CF:7F:F1:C2:4F"); // ISRG X1 cross-signed with DST X3
fingerprints.push("8B:05:B6:8C:C6:59:E5:ED:0F:CB:38:F2:C9:42:FB:FD:20:0E:6F:2F:F9:F8:5D:63:C6:99:4E:F5:E0:B0:27:01"); // ISRG X2 cross-signed with ISRG X1
return new Set(fingerprints);
};
module.exports.SHAKE256_LENGTH = 16;
/**
*
* @param {string} data
* @param {number} len
* @return {string}
*/
module.exports.shake256 = (data, len) => {
if (!data) {
return "";
}
return crypto.createHash("shake256", { outputLength: len })
.update(data)
.digest("hex");
};
// For unit test, export functions
if (process.env.TEST_BACKEND) {
module.exports.__test = {
@@ -1062,3 +1131,29 @@ if (process.env.TEST_BACKEND) {
return module.exports.__test[functionName];
};
}
/**
* Generates an abort signal with the specified timeout.
* @param {number} timeoutMs - The timeout in milliseconds.
* @returns {AbortSignal | null} - The generated abort signal, or null if not supported.
*/
module.exports.axiosAbortSignal = (timeoutMs) => {
try {
// Just in case, as 0 timeout here will cause the request to be aborted immediately
if (!timeoutMs || timeoutMs <= 0) {
timeoutMs = 5000;
}
return AbortSignal.timeout(timeoutMs);
} catch (_) {
// v16-: AbortSignal.timeout is not supported
try {
const abortController = new AbortController();
setTimeout(() => abortController.abort(), timeoutMs);
return abortController.signal;
} catch (_) {
// v15-: AbortController is not supported
return null;
}
}
};

View File

@@ -8,9 +8,9 @@
:placeholder="placeholder"
:disabled="!enabled"
>
<a class="btn btn-outline-primary" @click="action()">
<button type="button" class="btn btn-outline-primary" :aria-label="actionAriaLabel" @click="action()">
<font-awesome-icon :icon="icon" />
</a>
</button>
</div>
</template>
@@ -66,6 +66,13 @@ export default {
action: {
type: Function,
default: () => {},
},
/**
* The aria-label of the action button
*/
actionAriaLabel: {
type: String,
required: true,
}
},
emits: [ "update:modelValue" ],

View File

@@ -1,11 +1,11 @@
<template>
<div class="input-group mb-3">
<select ref="select" v-model="model" class="form-select" :disabled="disabled">
<option v-for="option in options" :key="option" :value="option.value">{{ option.label }}</option>
<select :id="id" ref="select" v-model="model" class="form-select" :disabled="disabled" :required="required">
<option v-for="option in options" :key="option" :value="option.value" :disabled="option.disabled">{{ option.label }}</option>
</select>
<a class="btn btn-outline-primary" @click="action()">
<font-awesome-icon :icon="icon" />
</a>
<button type="button" class="btn btn-outline-primary" :class="{ disabled: actionDisabled }" :aria-label="actionAriaLabel" @click="action()">
<font-awesome-icon :icon="icon" aria-hidden="true" />
</button>
</div>
</template>
@@ -20,6 +20,13 @@ export default {
type: Array,
default: () => [],
},
/**
* The id of the form which will be targeted by a <label for=..
*/
id: {
type: String,
required: true,
},
/**
* The value of the select field.
*/
@@ -50,6 +57,29 @@ export default {
action: {
type: Function,
default: () => {},
},
/**
* The aria-label of the action button
*/
actionAriaLabel: {
type: String,
required: true,
},
/**
* Whether the action button is disabled.
* @example true
*/
actionDisabled: {
type: Boolean,
default: false
},
/**
* Whether the select field is required.
* @example true
*/
required: {
type: Boolean,
default: false,
}
},
emits: [ "update:modelValue" ],

View File

@@ -135,7 +135,7 @@
<script lang="ts">
import { Modal } from "bootstrap";
import CopyableInput from "./CopyableInput.vue";
import { default as serverConfig } from "../../server/config.js";
import { badgeConstants } from "../util.ts";
export default {
components: {
@@ -230,7 +230,7 @@ export default {
"labelColor",
],
},
badgeConstants: serverConfig.badgeConstants,
badgeConstants,
};
},

View File

@@ -70,7 +70,7 @@ export default {
Confirm,
},
props: {},
emits: [ "added" ],
emits: [ "added", "deleted" ],
data() {
return {
modal: null,
@@ -167,6 +167,7 @@ export default {
this.processing = false;
if (res.ok) {
this.$emit("deleted", this.id);
this.modal.hide();
}
});

View File

@@ -11,7 +11,7 @@
/>
</div>
<div
v-if="size !== 'small' && beatList.length > 4 && $root.styleElapsedTime !== 'none'"
v-if="!$root.isMobile && size !== 'small' && beatList.length > 4 && $root.styleElapsedTime !== 'none'"
class="d-flex justify-content-between align-items-center word" :style="timeStyle"
>
<div>{{ timeSinceFirstBeat }} ago</div>

View File

@@ -5,18 +5,18 @@
<h1 class="h3 mb-3 fw-normal" />
<div v-if="!tokenRequired" class="form-floating">
<input id="floatingInput" v-model="username" type="text" class="form-control" placeholder="Username">
<input id="floatingInput" v-model="username" type="text" class="form-control" placeholder="Username" autocomplete="username" required>
<label for="floatingInput">{{ $t("Username") }}</label>
</div>
<div v-if="!tokenRequired" class="form-floating mt-3">
<input id="floatingPassword" v-model="password" type="password" class="form-control" placeholder="Password">
<input id="floatingPassword" v-model="password" type="password" class="form-control" placeholder="Password" autocomplete="current-password" required>
<label for="floatingPassword">{{ $t("Password") }}</label>
</div>
<div v-if="tokenRequired">
<div class="form-floating mt-3">
<input id="otp" v-model="token" type="text" maxlength="6" class="form-control" placeholder="123456">
<input id="otp" v-model="token" type="text" maxlength="6" class="form-control" placeholder="123456" autocomplete="one-time-code" required>
<label for="otp">{{ $t("Token") }}</label>
</div>
</div>

View File

@@ -29,10 +29,10 @@ export default {
},
computed: {
startDateTime() {
return dayjs(this.maintenance.timeslotList[0].startDate).tz(this.maintenance.timezone).format(SQL_DATETIME_FORMAT_WITHOUT_SECOND);
return dayjs(this.maintenance.timeslotList[0].startDate).tz(this.maintenance.timezone, true).format(SQL_DATETIME_FORMAT_WITHOUT_SECOND);
},
endDateTime() {
return dayjs(this.maintenance.timeslotList[0].endDate).tz(this.maintenance.timezone).format(SQL_DATETIME_FORMAT_WITHOUT_SECOND);
return dayjs(this.maintenance.timeslotList[0].endDate).tz(this.maintenance.timezone, true).format(SQL_DATETIME_FORMAT_WITHOUT_SECOND);
}
},
};

View File

@@ -16,7 +16,10 @@
</a>
<form>
<input
v-model="searchText" class="form-control search-input" :placeholder="$t('Search...')"
v-model="searchText"
class="form-control search-input"
:placeholder="$t('Search...')"
:aria-label="$t('Search monitored sites')"
autocomplete="off"
/>
</form>

View File

@@ -258,6 +258,10 @@ export default {
<style lang="scss" scoped>
@import "../assets/vars.scss";
.dropdown-item {
cursor: pointer;
}
.clear-filters-btn {
font-size: 0.8em;
margin-right: 5px;

View File

@@ -1,5 +1,5 @@
<template>
<div class="dropdown" @focusin="open = true" @focusout="handleFocusOut">
<div tabindex="-1" class="dropdown" @focusin="open = true" @focusout="handleFocusOut">
<button type="button" class="filter-dropdown-status" :class="{ 'active': filterActive }" tabindex="0">
<div class="px-1 d-flex align-items-center">
<slot name="status"></slot>
@@ -107,6 +107,19 @@ export default {
display: flex;
align-items: center;
margin-left: 5px;
color: $link-color;
.dark & {
color: $dark-font-color;
}
&:focus {
background-color: $highlight-white;
.dark & {
background-color: $dark-font-color2;
}
}
&.active {
border: 1px solid $highlight;

View File

@@ -62,7 +62,7 @@
</span>
</div>
<div class="extra-info">
<div v-if="showCertificateExpiry && monitor.element.type === 'http'">
<div v-if="showCertificateExpiry && monitor.element.certExpiryDaysRemaining">
<Tag :item="{name: $t('Cert Exp.'), value: formattedCertExpiryMessage(monitor), color: certExpiryColor(monitor)}" :size="'sm'" />
</div>
<div v-if="showTags">

View File

@@ -28,6 +28,7 @@
v-model="currentPassword"
type="password"
class="form-control"
autocomplete="current-password"
required
/>
</div>
@@ -43,7 +44,7 @@
<div v-if="uri && twoFAStatus == false" class="mt-3">
<label for="basic-url" class="form-label">{{ $t("twoFAVerifyLabel") }}</label>
<div class="input-group">
<input v-model="token" type="text" maxlength="6" class="form-control">
<input v-model="token" type="text" maxlength="6" class="form-control" autocomplete="one-time-code" required>
<button class="btn btn-outline-primary" type="button" @click="verifyToken()">{{ $t("Verify Token") }}</button>
</div>
<p v-show="tokenValid" class="mt-2" style="color: green;">{{ $t("tokenValidSettingsMsg") }}</p>

View File

@@ -13,6 +13,15 @@
<div class="mb-3">
<label for="ntfy-priority" class="form-label">{{ $t("Priority") }}</label>
<input id="ntfy-priority" v-model="$parent.notification.ntfyPriority" type="number" class="form-control" required min="1" max="5" step="1">
<div class="form-text">
<p v-if="$parent.notification.ntfyPriority >= 5">
{{ $t("ntfyPriorityHelptextAllEvents") }}
</p>
<i18n-t v-else tag="p" keypath="ntfyPriorityHelptextAllExceptDown">
<code>DOWN</code>
<code>{{ $parent.notification.ntfyPriority + 1 }}</code>
</i18n-t>
</div>
</div>
<div class="mb-3">
<label for="authentication-method" class="form-label">{{ $t("ntfyAuthenticationMethod") }}</label>

View File

@@ -5,6 +5,14 @@
<input id="hostname" v-model="$parent.notification.smtpHost" type="text" class="form-control" required>
</div>
<i18n-t tag="div" keypath="Either enter the hostname of the server you want to connect to or localhost if you intend to use a locally configured mail transfer agent" class="form-text">
<template #localhost>
<code>localhost</code>
</template>
<template #local_mta>
<a href="https://wikipedia.org/wiki/Mail_Transfer_Agent" target="_blank">{{ $t("locally configured mail transfer agent") }}</a>
</template>
</i18n-t>
<div class="mb-3">
<label for="port" class="form-label">{{ $t("Port") }}</label>
<input id="port" v-model="$parent.notification.smtpPort" type="number" class="form-control" required min="0" max="65535" step="1">

View File

@@ -1,53 +1,63 @@
<template>
<div>
<div class="add-btn">
<button class="btn btn-primary me-2" type="button" @click="$refs.apiKeyDialog.show()">
<font-awesome-icon icon="plus" /> {{ $t("Add API Key") }}
</button>
<div
v-if="settings.disableAuth"
class="mt-5 d-flex align-items-center justify-content-center my-3"
>
{{ $t("apiKeysDisabledMsg") }}
</div>
<div v-else>
<div class="add-btn">
<button class="btn btn-primary me-2" type="button" @click="$refs.apiKeyDialog.show()">
<font-awesome-icon icon="plus" /> {{ $t("Add API Key") }}
</button>
</div>
<div>
<span v-if="Object.keys(keyList).length === 0" class="d-flex align-items-center justify-content-center my-3">
{{ $t("No API Keys") }}
</span>
<div>
<span
v-if="Object.keys(keyList).length === 0"
class="d-flex align-items-center justify-content-center my-3"
>
{{ $t("No API Keys") }}
</span>
<div
v-for="(item, index) in keyList"
:key="index"
class="item"
:class="item.status"
>
<div class="left-part">
<div
class="circle"
></div>
<div class="info">
<div class="title">{{ item.name }}</div>
<div class="status">
{{ $t("apiKey-" + item.status) }}
</div>
<div class="date">
{{ $t("Created") }}: {{ item.createdDate }}
</div>
<div class="date">
{{ $t("Expires") }}: {{ item.expires || $t("Never") }}
<div
v-for="(item, index) in keyList"
:key="index"
class="item"
:class="item.status"
>
<div class="left-part">
<div class="circle"></div>
<div class="info">
<div class="title">{{ item.name }}</div>
<div class="status">
{{ $t("apiKey-" + item.status) }}
</div>
<div class="date">
{{ $t("Created") }}: {{ item.createdDate }}
</div>
<div class="date">
{{ $t("Expires") }}:
{{ item.expires || $t("Never") }}
</div>
</div>
</div>
</div>
<div class="buttons">
<div class="btn-group" role="group">
<button v-if="item.active" class="btn btn-normal" @click="disableDialog(item.id)">
<font-awesome-icon icon="pause" /> {{ $t("Disable") }}
</button>
<div class="buttons">
<div class="btn-group" role="group">
<button v-if="item.active" class="btn btn-normal" @click="disableDialog(item.id)">
<font-awesome-icon icon="pause" /> {{ $t("Disable") }}
</button>
<button v-if="!item.active" class="btn btn-primary" @click="enableKey(item.id)">
<font-awesome-icon icon="play" /> {{ $t("Enable") }}
</button>
<button v-if="!item.active" class="btn btn-primary" @click="enableKey(item.id)">
<font-awesome-icon icon="play" /> {{ $t("Enable") }}
</button>
<button class="btn btn-danger" @click="deleteDialog(item.id)">
<font-awesome-icon icon="trash" /> {{ $t("Delete") }}
</button>
<button class="btn btn-danger" @click="deleteDialog(item.id)">
<font-awesome-icon icon="trash" /> {{ $t("Delete") }}
</button>
</div>
</div>
</div>
</div>
@@ -90,6 +100,9 @@ export default {
let result = Object.values(this.$root.apiKeyList);
return result;
},
settings() {
return this.$parent.$parent.$parent.settings;
},
},
methods: {
@@ -127,9 +140,11 @@ export default {
* Pause maintenance
*/
disableKey() {
this.$root.getSocket().emit("disableAPIKey", this.selectedKeyID, (res) => {
this.$root.toastRes(res);
});
this.$root
.getSocket()
.emit("disableAPIKey", this.selectedKeyID, (res) => {
this.$root.toastRes(res);
});
},
/**
@@ -145,113 +160,113 @@ export default {
</script>
<style lang="scss" scoped>
@import "../../assets/vars.scss";
.mobile {
.item {
flex-direction: column;
align-items: flex-start;
margin-bottom: 20px;
}
}
.add-btn {
padding-top: 20px;
padding-bottom: 20px;
}
@import "../../assets/vars.scss";
.mobile {
.item {
display: flex;
align-items: center;
gap: 10px;
text-decoration: none;
border-radius: 10px;
transition: all ease-in-out 0.15s;
justify-content: space-between;
padding: 10px;
min-height: 90px;
margin-bottom: 5px;
flex-direction: column;
align-items: flex-start;
margin-bottom: 20px;
}
}
&:hover {
background-color: $highlight-white;
.add-btn {
padding-top: 20px;
padding-bottom: 20px;
}
.item {
display: flex;
align-items: center;
gap: 10px;
text-decoration: none;
border-radius: 10px;
transition: all ease-in-out 0.15s;
justify-content: space-between;
padding: 10px;
min-height: 90px;
margin-bottom: 5px;
&:hover {
background-color: $highlight-white;
}
&.active {
.circle {
background-color: $primary;
}
}
&.active {
.circle {
background-color: $primary;
}
}
&.inactive {
.circle {
background-color: $danger;
}
}
&.expired {
.left-part {
opacity: 0.3;
}
.circle {
background-color: $dark-font-color;
}
&.inactive {
.circle {
background-color: $danger;
}
}
&.expired {
.left-part {
display: flex;
gap: 12px;
align-items: center;
.circle {
width: 25px;
height: 25px;
border-radius: 50rem;
}
.info {
.title {
font-weight: bold;
font-size: 20px;
}
.status {
font-size: 14px;
}
}
opacity: 0.3;
}
.buttons {
display: flex;
gap: 8px;
flex-direction: row-reverse;
.circle {
background-color: $dark-font-color;
}
}
.btn-group {
width: 310px;
.left-part {
display: flex;
gap: 12px;
align-items: center;
.circle {
width: 25px;
height: 25px;
border-radius: 50rem;
}
.info {
.title {
font-weight: bold;
font-size: 20px;
}
.status {
font-size: 14px;
}
}
}
.date {
margin-top: 5px;
display: block;
font-size: 14px;
background-color: rgba(255, 255, 255, 0.5);
border-radius: 20px;
padding: 0 10px;
width: fit-content;
.buttons {
display: flex;
gap: 8px;
flex-direction: row-reverse;
.dark & {
color: white;
background-color: rgba(255, 255, 255, 0.1);
.btn-group {
width: 310px;
}
}
}
.dark {
.item {
&:hover {
background-color: $dark-bg2;
}
.date {
margin-top: 5px;
display: block;
font-size: 14px;
background-color: rgba(255, 255, 255, 0.5);
border-radius: 20px;
padding: 0 10px;
width: fit-content;
.dark & {
color: white;
background-color: rgba(255, 255, 255, 0.1);
}
}
.dark {
.item {
&:hover {
background-color: $dark-bg2;
}
}
}
</style>

View File

@@ -150,6 +150,43 @@
</div>
</div>
<!-- DNS Cache (nscd) -->
<div v-if="$root.info.isContainer" class="mb-4">
<label class="form-label">
{{ $t("enableNSCD") }}
</label>
<div class="form-check">
<input
id="nscdEnable"
v-model="settings.nscd"
class="form-check-input"
type="radio"
name="nscd"
:value="true"
required
/>
<label class="form-check-label" for="nscdEnable">
{{ $t("Enable") }}
</label>
</div>
<div class="form-check">
<input
id="nscdDisable"
v-model="settings.nscd"
class="form-check-input"
type="radio"
name="nscd"
:value="false"
required
/>
<label class="form-check-label" for="nscdDisable">
{{ $t("Disable") }}
</label>
</div>
</div>
<!-- DNS Cache -->
<div class="mb-4">
<label class="form-label">

View File

@@ -27,13 +27,13 @@
<div class="mt-1 mb-3 ps-2 cert-exp-days col-12 col-xl-6">
<div v-for="day in settings.tlsExpiryNotifyDays" :key="day" class="d-flex align-items-center justify-content-between cert-exp-day-row py-2">
<span>{{ day }} {{ $tc("day", day) }}</span>
<button type="button" class="btn-rm-expiry btn btn-outline-danger ms-2 py-1" @click="removeExpiryNotifDay(day)">
<font-awesome-icon class="" icon="times" />
<button type="button" class="btn-rm-expiry btn btn-outline-danger ms-2 py-1" :aria-label="$t('Remove the expiry notification')" @click="removeExpiryNotifDay(day)">
<font-awesome-icon icon="times" />
</button>
</div>
</div>
<div class="col-12 col-xl-6">
<ActionInput v-model="expiryNotifInput" :type="'number'" :placeholder="$t('day')" :icon="'plus'" :action="() => addExpiryNotifDay(expiryNotifInput)" />
<ActionInput v-model="expiryNotifInput" :type="'number'" :placeholder="$t('day')" :icon="'plus'" :action="() => addExpiryNotifDay(expiryNotifInput)" :action-aria-label="$t('Add a new expiry notification day')" />
</div>
<div>
<button class="btn btn-primary" type="button" @click="saveSettings()">

View File

@@ -19,6 +19,7 @@
v-model="password.currentPassword"
type="password"
class="form-control"
autocomplete="current-password"
required
/>
</div>
@@ -32,6 +33,7 @@
v-model="password.newPassword"
type="password"
class="form-control"
autocomplete="new-password"
required
/>
</div>
@@ -46,6 +48,7 @@
type="password"
class="form-control"
:class="{ 'is-invalid': invalidPassword }"
autocomplete="new-password"
required
/>
<div class="invalid-feedback">

View File

@@ -57,10 +57,29 @@ for (let lang in languageList) {
const rtlLangs = [ "fa", "ar-SY", "ur" ];
export const currentLocale = () => localStorage.locale
|| languageList[navigator.language] && navigator.language
|| languageList[navigator.language.substring(0, 2)] && navigator.language.substring(0, 2)
|| "en";
/**
* Find the best matching locale to display
* If no locale can be matched, the default is "en"
* @returns {string} the locale that should be displayed
*/
export function currentLocale() {
for (const locale of [ localStorage.locale, navigator.language, ...navigator.languages ]) {
// localstorage might not have a value or there might not be a language in `navigator.language`
if (!locale) {
continue;
}
if (locale in messages) {
return locale;
}
// some locales are further specified such as "en-US".
// If we only have a generic locale for this, we can use it too
const genericLocale = locale.split("-")[0];
if (genericLocale in messages) {
return genericLocale;
}
}
return "en";
}
export const localeDirection = () => {
return rtlLangs.includes(currentLocale()) ? "rtl" : "ltr";

View File

@@ -683,5 +683,6 @@
"languageName": "العربية",
"Game": "الألعاب",
"List": "القائمة",
"statusMaintenance": "الصيانة"
"statusMaintenance": "الصيانة",
"Home": "الرئيسة"
}

View File

@@ -57,6 +57,8 @@
"Friendly Name": "Friendly Name",
"URL": "URL",
"Hostname": "Hostname",
"locally configured mail transfer agent": "locally configured mail transfer agent",
"Either enter the hostname of the server you want to connect to or localhost if you intend to use a locally configured mail transfer agent": "Either enter the hostname of the server you want to connect to or {localhost} if you intend to use a {local_mta}",
"Port": "Port",
"Heartbeat Interval": "Heartbeat Interval",
"Request Timeout": "Request Timeout",
@@ -183,6 +185,7 @@
"Pink": "Pink",
"Custom": "Custom",
"Search...": "Search…",
"Search monitored sites": "Search monitored sites",
"Avg. Ping": "Avg. Ping",
"Avg. Response": "Avg. Response",
"Entry Page": "Entry Page",
@@ -334,6 +337,8 @@
"Fingerprint:": "Fingerprint:",
"No status pages": "No status pages",
"Domain Name Expiry Notification": "Domain Name Expiry Notification",
"Add a new expiry notification day": "Add a new expiry notification day",
"Remove the expiry notification": "Remove the expiry notification day",
"Proxy": "Proxy",
"Date Created": "Date Created",
"Footer Text": "Footer Text",
@@ -369,6 +374,8 @@
"Setup Docker Host": "Setup Docker Host",
"Connection Type": "Connection Type",
"Docker Daemon": "Docker Daemon",
"noDockerHostMsg": "Not Available. Setup a Docker Host First.",
"DockerHostRequired": "Please set the Docker Host for this monitor.",
"deleteDockerHostMsg": "Are you sure want to delete this docker host for all monitors?",
"socket": "Socket",
"tcp": "TCP / HTTP",
@@ -449,9 +456,10 @@
"Server Timezone": "Server Timezone",
"statusPageMaintenanceEndDate": "End",
"IconUrl": "Icon URL",
"Enable DNS Cache": "Enable DNS Cache for HTTP(s) monitors",
"Enable DNS Cache": "(Deprecated) Enable DNS Cache for HTTP(s) monitors",
"Enable": "Enable",
"Disable": "Disable",
"enableNSCD": "Enable NSCD (Name Service Cache Daemon) for caching all DNS requests",
"chromeExecutable": "Chrome/Chromium Executable",
"chromeExecutableAutoDetect": "Auto Detect",
"chromeExecutableDescription": "For Docker users, if Chromium is not yet installed, it may take a few minutes to install and display the test result. It takes 1GB of disk space.",
@@ -653,6 +661,10 @@
"Notify Channel": "Notify Channel",
"aboutNotifyChannel": "Notify channel will trigger a desktop or mobile notification for all members of the channel, whether their availability is set to active or away.",
"Uptime Kuma URL": "Uptime Kuma URL",
"setup a new monitor group": "setup a new monitor group",
"openModalTo": "open modal to {0}",
"Add a domain": "Add a domain",
"Remove domain": "Remove domain '{0}'",
"Icon Emoji": "Icon Emoji",
"signalImportant": "IMPORTANT: You cannot mix groups and numbers in recipients!",
"aboutWebhooks": "More info about Webhooks on: {0}",
@@ -746,6 +758,8 @@
"lunaseaDeviceID": "Device ID",
"lunaseaUserID": "User ID",
"ntfyAuthenticationMethod": "Authentication Method",
"ntfyPriorityHelptextAllEvents": "All events are send with the maximum priority",
"ntfyPriorityHelptextAllExceptDown": "All events are send with this priority, except {0}-events, which have a priority of {1}",
"ntfyUsernameAndPassword": "Username and Password",
"twilioAccountSID": "Account SID",
"twilioApiKey": "Api Key (optional)",
@@ -806,5 +820,6 @@
"showCertificateExpiry": "Show Certificate Expiry",
"noOrBadCertificate": "No/Bad Certificate",
"gamedigGuessPort": "Gamedig: Guess Port",
"gamedigGuessPortDescription": "The port used by Valve Server Query Protocol may be different from the client port. Try this if the monitor cannot connect to your server."
"gamedigGuessPortDescription": "The port used by Valve Server Query Protocol may be different from the client port. Try this if the monitor cannot connect to your server.",
"apiKeysDisabledMsg": "API keys are disabled because authentication is disabled."
}

View File

@@ -569,5 +569,18 @@
"dayOfMonth": "Hilabeteko eguna",
"lastDay": "Azken eguna",
"lastDay1": "Hilabeteko azken eguna",
"Resend Notification if Down X times consecutively": "Bidali jakinarazpena X aldiz jarraian erortzen bada"
"Resend Notification if Down X times consecutively": "Bidali jakinarazpena X aldiz jarraian erortzen bada",
"Add New Tag": "Gehitu etiketa berria",
"Schedule maintenance": "Programatu mantenua",
"Start of maintenance": "Mantenuaren hasiera",
"All Status Pages": "Egoera orrialde guztiak",
"Custom": "Pertsonalizatua",
"showCertificateExpiry": "Erakutsi ziurtagiriaren iraungitzea",
"Close": "Itxi",
"Request Body": "Eskaera gorputza",
"Mechanism": "Mekanismoa",
"Home": "Hasiera",
"filterActive": "Aktibo",
"filterActivePaused": "Geldituta",
"Expected Value": "Esperotako balioa"
}

View File

@@ -739,5 +739,6 @@
"pagertreeHigh": "גבוהה",
"pagertreeCritical": "קריטי",
"pagertreeResolve": "הגדרה אוטומטית",
"ntfyUsernameAndPassword": "שם משתמש וסיסמא"
"ntfyUsernameAndPassword": "שם משתמש וסיסמא",
"Cannot connect to the socket server": "לא ניתן להתחבר לשקע השרת"
}

View File

@@ -177,7 +177,7 @@
"Entry Page": "Pagina Principale",
"statusPageNothing": "Non c'è nulla qui, aggiungi un gruppo oppure un monitor.",
"No Services": "Nessun servizio",
"All Systems Operational": "Tutti i sistemi sono funzionali",
"All Systems Operational": "Tutti i sistemi sono funzionanti",
"Partially Degraded Service": "Servizio parzialmente degradato",
"Degraded Service": "Servizio degradato",
"Add Group": "Aggiungi gruppo",
@@ -608,5 +608,7 @@
"Cannot connect to the socket server": "Impossibile connettersi al server socket",
"Reconnecting...": "Riconnessione...",
"Expected Value": "Valore atteso",
"Json Query": "Query Json"
"Json Query": "Query Json",
"deleteMaintenanceMsg": "Sei sicuro di voler cancellare questa attività di manutenzione?",
"dnsPortDescription": "Porta server DNS. Default 53. Puoi cambiare questa porta in ogni momento."
}

View File

@@ -215,5 +215,6 @@
"Cannot connect to the socket server": "Nemožno sa pripojiť k socket serveru",
"Invert Keyword": "Prevrátiť kľúčové slovo",
"Expected Value": "Očakávaná hodnota",
"Json Query": "Json Query"
"Json Query": "Json Query",
"Method": "Metoda"
}

191
src/lang/te.json Normal file
View File

@@ -0,0 +1,191 @@
{
"languageName": "తెలుగు",
"Settings": "సెట్టింగ్‌లు",
"Dashboard": "డాష్బోర్డ్",
"Help": "సహాయం",
"Language": "భాష",
"Appearance": "ప్రదర్శన",
"Theme": "నేపథ్యం",
"General": "సాధారణ",
"Game": "ఆట",
"Version": "సంస్కరణ",
"Check Update On GitHub": "GitHubలో నవీకరణను తనిఖీ చేయండి",
"List": "జాబితా",
"Home": "స్వస్థల o",
"Add": "జోడించు",
"Quick Stats": "త్వరిత గణాంకాలు",
"Up": "పనిచేస్తుంది",
"Down": "ఆగిపోయింది",
"Pending": "అనిశ్చిత",
"Maintenance": "నిర్వహణ",
"Unknown": "తెలియని స్థితి",
"Reconnecting...": "మళ్లీ కనెక్ట్ అవుతోంది...",
"General Monitor Type": "సాధారణ మానిటర్ రకం",
"Passive Monitor Type": "నిష్క్రియాత్మక మానిటర్ రకం",
"markdownSupported": "మార్క్‌డౌన్ సింటాక్స్‌కు మద్దతు ఉంది",
"Pause": "విరామం",
"Name": "పేరు",
"Status": "స్థితి",
"DateTime": "తేదీ సమయం",
"Message": "సందేశం",
"No important events": "ముఖ్యమైన సంఘటనలు లేవు",
"Resume": "పునఃప్రారంభం",
"Edit": "సవరించు",
"Current": "ప్రస్తుత",
"Uptime": "సమయ వ్యవధి",
"Monitor": "మానిటర్ | మానిటర్లు",
"day": "రోజు | రోజులు",
"-day": "-రోజు",
"hour": "గంట",
"-hour": "-గంట",
"Response": "ప్రతిస్పందన",
"Ping": "పింగ్",
"Keyword": "కీవర్డ్",
"Invert Keyword": "విలోమ కీవర్డ్",
"Expected Value": "అంచనా విలువ",
"Json Query": "Json ప్రశ్న",
"URL": "URL",
"Hostname": "హోస్ట్ పేరు",
"Port": "పోర్ట్",
"Heartbeat Interval": "హృదయ స్పందన విరామం",
"Request Timeout": "అభ్యర్థన ముగిసె గడువు",
"timeoutAfter": "{0} సెకన్ల తర్వాత గడువు ముగిసింది",
"Heartbeat Retry Interval": "హృదయ స్పందన పునఃప్రయత్న విరామం",
"Advanced": "ఆధునిక",
"checkEverySecond": "ప్రతి {0} సెకన్లకు తనిఖీ చేయండి",
"retryCheckEverySecond": "ప్రతి {0} సెకన్లకు మళ్లీ ప్రయత్నించండి",
"resendDisabled": "మల్లిపంపడము అచేతనము చేయబడ్డది",
"ignoreTLSError": "HTTPS వెబ్‌సైట్‌ల కోసం TLS/SSL లోపాన్ని విస్మరించండి",
"maxRedirectDescription": "అనుసరించాల్సిన దారి మళ్లింపుల గరిష్ట సంఖ్య. దారి మళ్లింపులను నిలిపివేయడానికి 0కి సెట్ చేయండి.",
"Upside Down Mode": "అప్‌సైడ్ డౌన్ మోడ్",
"Max. Redirects": "గరిష్టంగా దారి మళ్లింపులు",
"Push URL": "పుష్ URL",
"needPushEvery": "మీరు ప్రతి {0} సెకన్లకు ఈ URLకి కాల్ చేయాలి.",
"Save": "సేవ్ చేయండి",
"Notifications": "నోటిఫికేషన్లు",
"Setup Notification": "నోటిఫికేషన్ సెటప్ చేయండి",
"Light": "కాంతి",
"Dark": "వెలుతురు లేని",
"Auto": "ఆటో",
"Theme - Heartbeat Bar": "థీమ్ - హార్ట్‌బీట్ బార్",
"styleElapsedTimeShowNoLine": "చూపించు (పంక్తి లేదు)",
"styleElapsedTimeShowWithLine": "చూపించు (పంక్తితో)",
"Normal": "సాధారణ",
"Bottom": "దిగువన",
"None": "ఏదీ లేదు",
"Timezone": "సమయమండలం",
"Allow indexing": "ఇండెక్సింగ్‌ని అనుమతించండి",
"Change Password": "పాస్‌వర్డ్ మార్చండి",
"Current Password": "ప్రస్తుత పాస్వర్డ్",
"New Password": "కొత్త పాస్వర్డ్",
"Repeat New Password": "కొత్త పాస్‌వర్డ్‌ని మళ్లీ నమోదు చేయండి",
"Disable Auth": "ప్రామాణీకరణను నిలిపివేయండి",
"Enable Auth": "ప్రామాణీకరణను ప్రారంభించండి",
"Please use this option carefully!": "దయచేసి ఈ ఎంపికను జాగ్రత్తగా ఉపయోగించండి!",
"Logout": "లాగ్అవుట్",
"Leave": "వదిలేయండి",
"Confirm": "నిర్ధారించండి",
"Yes": "అవును",
"No": "లేదు",
"Username": "వినియోగదారు పేరు",
"Password": "పాస్వర్డ్",
"Remember me": "నన్ను గుర్తు పెట్టుకో",
"Login": "లాగిన్",
"add one": "ఒకటి జోడించండి",
"Notification Type": "నోటిఫికేషన్ రకం",
"Email": "ఇమెయిల్",
"Test": "పరీక్షించండి",
"Resolver Server": "రిసోల్వర్ సర్వర్",
"Resource Record Type": "రిసోర్స్ రికార్డ్ రకం",
"Last Result": "చివరి ఫలితం",
"Repeat Password": "పాస్‌వర్డ్‌ని మళ్లీ నమోదు చేయండి",
"Import Backup": "బ్యాకప్‌ని దిగుమతి చేయండి",
"Export": "ఎగుమతి",
"Import": "దిగుమతి",
"respTime": "ప్రతిస్పందన. సమయం (మిసె)",
"Default enabled": "డిఫాల్ట్ ప్రారంభించబడింది",
"Create": "సృష్టించు",
"Clear Data": "డేటాను క్లియర్ చేయండి",
"Events": "ఈవెంట్స్",
"Heartbeats": "హృదయ స్పందన",
"Auto Get": "స్వయంచాలక పొందండి",
"Affected Monitors": "ప్రభావిత మానిటర్లు",
"Pick Affected Monitors...": "ప్రభావిత మానిటర్‌లను ఎంచుకోండి…",
"All Status Pages": "అన్ని స్థితి పేజీలు",
"Select status pages...": "స్థితి పేజీలను ఎంచుకోండి…",
"alertWrongFileType": "దయచేసి JSON ఫైల్‌ని ఎంచుకోండి.",
"Clear all statistics": "అన్ని గణాంకాలను క్లియర్ చేయండి",
"Skip existing": "ఉనికిని దాటవేయి",
"Options": "ఎంపికలు",
"Keep both": "రెండు ఉంచండి",
"Verify Token": "టోకెన్‌ని ధృవీకరించండి",
"Setup 2FA": "సెటప్ 2FA",
"Disable 2FA": "2FAని నిలిపివేయండి",
"2FA Settings": "2FA సెట్టింగ్‌లు",
"filterActive": "చురుకుగా",
"filterActivePaused": "ఆగిపోయింది",
"Active": "చురుకుగా",
"Inactive": "నిష్క్రియ",
"Token": "టోకెన్",
"Tags": "టాగ్లు",
"Add New Tag": "కొత్త ట్యాగ్‌ని జోడించండి",
"Tag with this name already exist.": "ఈ పేరుతో ట్యాగ్ ఇప్పటికే ఉంది.",
"color": "రంగు",
"value (optional)": "విలువ (ఐచ్ఛికం)",
"Gray": "బూడిద రంగు",
"Red": "ఎరుపు",
"Orange": "నారింజ రంగు",
"Green": "ఆకుపచ్చ",
"Blue": "నీలం",
"Indigo": "నీలిమందు రంగు",
"Purple": "ఊదా రంగు",
"Pink": "పింక్ కలర్",
"Search...": "వెతకండి…",
"Avg. Ping": "సగటు పింగ్",
"Avg. Response": "సగటు ప్రతిస్పందన",
"statusPageRefreshIn": "సెకన్లలో రిఫ్రెష్ చేయండి: {0}",
"New Update": "కొత్త నవీకరణ",
"Primary Base URL": "ప్రాథమిక URL",
"Add New Monitor": "కొత్త మానిటర్‌ని జోడించండి",
"statusMaintenance": "స్థితి నిర్వహణ",
"Cannot connect to the socket server": "సాకెట్ సర్వర్‌కి కనెక్ట్ చేయడం సాధ్యపడదు",
"Specific Monitor Type": "నిర్దిష్ట మానిటర్ రకం",
"pauseDashboardHome": "డాష్‌బోర్డ్ హోమ్‌నకు విరామం",
"Delete": "తొలగించు",
"Cert Exp.": "సర్టిఫికేట్ గడువు.",
"Monitor Type": "మానిటర్ రకం",
"Friendly Name": "స్నేహపూర్వక పేరు",
"Retries": "పునఃప్రయత్నాలు",
"Resend Notification if Down X times consecutively": "వరుసగా X సార్లు డౌన్ అయితే నోటిఫికేషన్‌ని మళ్లీ పంపండి",
"resendEveryXTimes": "ప్రతి {0} సార్లు మళ్లీ పంపండి",
"retriesDescription": "సేవ డౌన్‌గా గుర్తించబడి నోటిఫికేషన్ పంపబడటానికి ముందు గరిష్ట సంఖ్యలో పునఃప్రయత్నాలు",
"upsideDownModeDescription": "స్థితిని తలక్రిందులుగా తిప్పండి. సేవ చేరుకోగలిగితే, అది పని చేయనట్లు పరిగణించబడుతుంది.",
"Accepted Status Codes": "ఆమోదించబడిన HTTP స్థితి కోడ్‌లు",
"pushOptionalParams": "ఐచ్ఛిక పారామితులు: {0}",
"Not available, please setup.": "అందుబాటులో లేదు, దయచేసి సెటప్ చేయండి.",
"styleElapsedTime": "హృదయ స్పందన పట్టీ కింద గడిచిన సమయం",
"Search Engine Visibility": "శోధన ఇంజిన్ దృశ్యమానత",
"Discourage search engines from indexing site": "ఇండెక్సింగ్ సైట్ నుండి శోధన ఇంజిన్‌లను నిరుత్సాహపరచండి",
"Update Password": "పాస్‌వర్డ్‌ని నవీకరించండి",
"disableauth.message1": "మీరు ఖచ్చితంగా <strong>ప్రామాణీకరణను నిలిపివేయాలనుకుంటున్నారా</strong>?",
"disableauth.message2": "ఇది Cloudflare యాక్సెస్, Authelia లేదా ఇతర ప్రమాణీకరణ మెకానిజమ్‌ల వంటి Uptime Kuma ముందు <strong>థర్డ్-పార్టీ ప్రామాణీకరణను అమలు చేయాలనుకుంటున్న</strong> దృశ్యాల కోసం రూపొందించబడింది.",
"I understand, please disable": "నాకు అర్థమైంది, దయచేసి నిలిపివేయండి",
"No Monitors, please": "దయచేసి మానిటర్లు వద్దు",
"Certificate Info": "సర్టిఫికేట్ సమాచారం",
"Create your admin account": "మీ నిర్వాహక ఖాతాను సృష్టించండి",
"Export Backup": "బ్యాకప్ ఎగుమతి",
"notAvailableShort": "లేదు/అందుబాటులో లేదు",
"Apply on all existing monitors": "ఇప్పటికే ఉన్న అన్ని మానిటర్‌లపై వర్తించండి",
"Schedule maintenance": "షెడ్యూల్ నిర్వహణ",
"Start of maintenance": "నిర్వహణ ప్రారంభం",
"alertNoFile": "దయచేసి దిగుమతి చేయడానికి ఫైల్‌ను ఎంచుకోండి.",
"Overwrite": "ఓవర్రైట్",
"Enable 2FA": "2FAని ప్రారంభించండి",
"Two Factor Authentication": "రెండు కారకాల ప్రమాణీకరణ",
"Show URI": "URIని చూపు",
"Add New below or Select...": "దిగువన కొత్తది జోడించండి లేదా ఎంచుకోండి…",
"Tag with this value already exist.": "ఈ విలువతో ట్యాగ్ ఇప్పటికే ఉంది.",
"Custom": "కస్టమ్",
"Entry Page": "ఎంట్రీ పేజీ",
"statusPageNothing": "ఇక్కడ ఏమీ లేదు, దయచేసి సమూహాన్ని లేదా మానిటర్‌ని జోడించండి."
}

View File

@@ -672,5 +672,8 @@
"recurringIntervalMessage": "ดำเนินการทุกวัน | ดำเนินการทุก {0} วัน",
"chromeExecutableAutoDetect": "ตรวจจับอัตโนมัติ",
"chromeExecutableDescription": "สำหรับผู้ใช้งาน Docker, ถ้ายังไม่ได้ติดตั่ง Chromium, อาจจะเสียเวลาในการติดตั่งและแสดงผลการทดสอบเพิ่มเติม, ใช้พื้นที่ประมาณ 1GB",
"notificationRegional": "ภูมิภาค"
"notificationRegional": "ภูมิภาค",
"timeoutAfter": "หมดเวลาหลังจาก {0} วินาที",
"Select": "เลือก",
"Expected Value": "ค่าที่คาดหวัง"
}

View File

@@ -64,7 +64,7 @@
"Accepted Status Codes": "Припустимі коди статусу",
"Save": "Зберегти",
"Notifications": "Сповіщення",
"Not available, please setup.": "Доступних сповіщень немає, необхідно створити.",
"Not available, please setup.": "Недоступно, будь ласка, налаштуйте.",
"Setup Notification": "Створити сповіщення",
"Light": "Світла",
"Dark": "Темна",
@@ -445,7 +445,7 @@
"Don't know how to get the token? Please read the guide:": "Не знаєте, як отримати токен? Прочитайте посібник:",
"The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.": "Поточне з’єднання може бути втрачено, якщо ви зараз під’єднуєтеся через Cloudflare Tunnel. Ви дійсно хочете зробити це? Для підтвердження введіть поточний пароль.",
"Other Software": "Інше програмне забезпечення",
"For example: nginx, Apache and Traefik.": "Наприклад: nginx, Apache and Traefik.",
"For example: nginx, Apache and Traefik.": "Наприклад: nginx, Apache і Traefik.",
"Please read": "Будь ласка, прочитайте",
"Subject:": "Тема:",
"Valid To:": "Дійсний до:",

View File

@@ -204,7 +204,7 @@
"webhookJsonDesc": "{0} کسی بھی جدید HTTP سرورز جیسے Express.js کے لیے اچھا ہے",
"webhookFormDataDesc": "{multipart} پی ایچ پی کے لیے اچھا ہے۔ JSON کو {decodeFunction} کے ساتھ پارس کرنے کی ضرورت ہوگی",
"webhookAdditionalHeadersTitle": "اضافی ہیڈرز",
"webhookAdditionalHeadersDesc": "ویب ہک کے ساتھ بھیجے گئے اضافی ہیڈر سیٹ کرتا ہے۔",
"webhookAdditionalHeadersDesc": "ویب ہک کے ساتھ بھیجے گئے اضافی ہیڈر سیٹ کرتا ہے۔ ہر ہیڈر کو JSON کلید/قدر کے طور پر بیان کیا جانا چاہیے۔",
"Webhook URL": "ابھوک دیہی",
"Application Token": "ایپلیکیشن ٹوکن",
"Server URL": "سرور URL",
@@ -429,7 +429,7 @@
"No Maintenance": "کوئی دیکھ بھال نہیں",
"weekdayShortTue": "منگل",
"Add New Tag": "نیا ٹیگ شامل کریں",
"Enable DNS Cache": "ڈی این ایس کیشے کو فعال کریں",
"Enable DNS Cache": "HTTP(s) مانیٹرز کے لیے DNS کیش کو فعال کریں",
"Effective Date Range": "مؤثر تاریخ کی حد (اختیاری)",
"Schedule Maintenance": "شیڈول کی بحالی",
"Date and Time": "تاریخ اور وقت",
@@ -461,5 +461,23 @@
"installing": "تنصیب",
"chromeExecutableAutoDetect": "آٹو کھوج",
"Edit Maintenance": "دیکھ بھال میں ترمیم کریں",
"Reconnecting...": "دوبارہ رابطہ قائم کرنا..."
"Reconnecting...": "دوبارہ رابطہ قائم کرنا...",
"Request Timeout": "ٹائم آؤٹ کی درخواست کریں",
"timeoutAfter": "{0} سیکنڈ کے بعد ٹائم آؤٹ",
"styleElapsedTime": "دل کی دھڑکن بار کے نیچے گزرا ہوا وقت",
"webhookCustomBodyDesc": "درخواست کے لیے حسب ضرورت HTTP باڈی کی وضاحت کریں۔ ٹیمپلیٹ متغیرات {msg}، {heartbeat}، {monitor} قبول کیے گئے ہیں۔",
"filterActive": "فعال",
"Check/Uncheck": "چیک/ان چیک کریں",
"styleElapsedTimeShowNoLine": "دکھائیں (کوئی لائن نہیں)",
"styleElapsedTimeShowWithLine": "دکھائیں (لائن کے ساتھ)",
"filterActivePaused": "روک دیا گیا",
"webhookBodyPresetOption": "پیش سیٹ - {0}",
"webhookBodyCustomOption": "حسب ضرورت باڈی",
"Select": "منتخب کریں",
"selectedMonitorCount": "منتخب کردہ: {0}",
"tailscalePingWarning": "Tailscale Ping مانیٹر استعمال کرنے کے لیے، آپ کو Docker کے بغیر Uptime Kuma انسٹال کرنا ہوگا اور اپنے سرور پر Tailscale کلائنٹ بھی انسٹال کرنا ہوگا۔",
"uninstall": "ان انسٹال کریں",
"Invert Keyword": "مطلوبہ الفاظ کو الٹ دیں",
"Expected Value": "متوقع قدر",
"Json Query": "Json استفسار"
}

View File

@@ -91,21 +91,20 @@ export default {
this.socket.initedSocketIO = true;
let protocol = (location.protocol === "https:") ? "wss://" : "ws://";
let protocol = location.protocol + "//";
let wsHost;
let url;
const env = process.env.NODE_ENV || "production";
if (env === "development" && isDevContainer()) {
wsHost = protocol + getDevContainerServerHostname();
url = protocol + getDevContainerServerHostname();
} else if (env === "development" || localStorage.dev === "dev") {
wsHost = protocol + location.hostname + ":3001";
url = protocol + location.hostname + ":3001";
} else {
wsHost = protocol + location.host;
// Connect to the current url
url = undefined;
}
socket = io(wsHost, {
transports: [ "websocket" ],
});
socket = io(url);
socket.on("info", (info) => {
this.info = info;
@@ -288,6 +287,10 @@ export default {
socket.on("initServerTimezone", () => {
socket.emit("initServerTimezone", dayjs.tz.guess());
});
socket.on("refresh", () => {
location.reload();
});
},
/**

View File

@@ -82,7 +82,7 @@
<option value="redis">
Redis
</option>
<option value="tailscale-ping">
<option v-if="!$root.info.isContainer" value="tailscale-ping">
Tailscale Ping
</option>
</optgroup>
@@ -99,18 +99,6 @@
<input id="name" v-model="monitor.name" type="text" class="form-control" required>
</div>
<!-- Parent Monitor -->
<div class="my-3">
<label for="parent" class="form-label">{{ $t("Monitor Group") }}</label>
<ActionSelect
v-model="monitor.parent"
:options="parentMonitorOptionsList"
:disabled="sortedGroupMonitorList.length === 0 && draftGroupName == null"
:icon="'plus'"
:action="() => $refs.createGroupDialog.show()"
/>
</div>
<!-- URL -->
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'real-browser' " class="my-3">
<label for="url" class="form-label">{{ $t("URL") }}</label>
@@ -297,22 +285,19 @@
<!-- Docker Host -->
<!-- For Docker Type -->
<div v-if="monitor.type === 'docker'" class="my-3">
<h2 class="mb-2">{{ $t("Docker Host") }}</h2>
<p v-if="$root.dockerHostList.length === 0">
{{ $t("Not available, please setup.") }}
</p>
<div v-else class="mb-3">
<div class="mb-3">
<label for="docker-host" class="form-label">{{ $t("Docker Host") }}</label>
<select id="docket-host" v-model="monitor.docker_host" class="form-select">
<option v-for="host in $root.dockerHostList" :key="host.id" :value="host.id">{{ host.name }}</option>
</select>
<a href="#" @click="$refs.dockerHostDialog.show(monitor.docker_host)">{{ $t("Edit") }}</a>
<ActionSelect
id="docker-host"
v-model="monitor.docker_host"
:action-aria-label="$t('openModalTo', $t('Setup Docker Host'))"
:options="dockerHostOptionsList"
:disabled="$root.dockerHostList == null || $root.dockerHostList.length === 0"
:icon="'plus'"
:action="() => $refs.dockerHostDialog.show()"
:required="true"
/>
</div>
<button class="btn btn-primary me-2" type="button" @click="$refs.dockerHostDialog.show()">
{{ $t("Setup Docker Host") }}
</button>
</div>
<!-- MQTT -->
@@ -382,11 +367,20 @@
<input id="connectionString" v-model="monitor.databaseConnectionString" type="text" class="form-control" required>
</div>
</template>
<template v-if="monitor.type === 'mysql'">
<div class="my-3">
<label for="mysql-password" class="form-label">{{ $t("Password") }}</label>
<!-- TODO: Rename monitor.radiusPassword to monitor.password for general use -->
<HiddenInput id="mysql-password" v-model="monitor.radiusPassword" autocomplete="false"></HiddenInput>
</div>
</template>
<!-- SQL Server / PostgreSQL / MySQL -->
<template v-if="monitor.type === 'sqlserver' || monitor.type === 'postgres' || monitor.type === 'mysql'">
<div class="my-3">
<label for="sqlQuery" class="form-label">{{ $t("Query") }}</label>
<textarea id="sqlQuery" v-model="monitor.databaseQuery" class="form-control" :placeholder="$t('Example:', [ 'select getdate()' ])" required></textarea>
<textarea id="sqlQuery" v-model="monitor.databaseQuery" class="form-control" :placeholder="$t('Example:', [ 'SELECT 1' ])"></textarea>
</div>
</template>
@@ -413,7 +407,7 @@
</div>
<!-- Timeout: HTTP / Keyword only -->
<div v-if="monitor.type === 'http' || monitor.type === 'keyword'" class="my-3">
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query'" class="my-3">
<label for="timeout" class="form-label">{{ $t("Request Timeout") }} ({{ $t("timeoutAfter", [ monitor.timeout || clampTimeout(monitor.interval) ]) }})</label>
<input id="timeout" v-model="monitor.timeout" type="number" class="form-control" required min="0" step="0.1">
</div>
@@ -472,7 +466,7 @@
</div>
<!-- HTTP / Keyword only -->
<template v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'grpc-keyword' ">
<template v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'grpc-keyword' ">
<div class="my-3">
<label for="maxRedirects" class="form-label">{{ $t("Max. Redirects") }}</label>
<input id="maxRedirects" v-model="monitor.maxredirects" type="number" class="form-control" required min="0" step="1">
@@ -504,6 +498,20 @@
</div>
</template>
<!-- Parent Monitor -->
<div class="my-3">
<label for="monitorGroupSelector" class="form-label">{{ $t("Monitor Group") }}</label>
<ActionSelect
id="monitorGroupSelector"
v-model="monitor.parent"
:action-aria-label="$t('openModalTo', 'setup a new monitor group')"
:options="parentMonitorOptionsList"
:disabled="sortedGroupMonitorList.length === 0 && draftGroupName == null"
:icon="'plus'"
:action="() => $refs.createGroupDialog.show()"
/>
</div>
<!-- Description -->
<div class="my-3">
<label for="description" class="form-label">{{ $t("Description") }}</label>
@@ -840,9 +848,9 @@ import NotificationDialog from "../components/NotificationDialog.vue";
import DockerHostDialog from "../components/DockerHostDialog.vue";
import ProxyDialog from "../components/ProxyDialog.vue";
import TagsManager from "../components/TagsManager.vue";
import { genSecret, isDev, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND } from "../util.ts";
import { genSecret, isDev, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND, sleep } from "../util.ts";
import { hostNameRegexPattern } from "../util-frontend";
import { sleep } from "../util";
import HiddenInput from "../components/HiddenInput.vue";
const toast = useToast();
@@ -855,7 +863,7 @@ const monitorDefaults = {
interval: 60,
retryInterval: 60,
resendInterval: 0,
maxretries: 1,
maxretries: 0,
timeout: 48,
notificationIDList: {},
ignoreTls: false,
@@ -880,11 +888,14 @@ const monitorDefaults = {
kafkaProducerSaslOptions: {
mechanism: "None",
},
kafkaProducerSsl: false,
kafkaProducerAllowAutoTopicCreation: false,
gamedigGivenPortOnly: true,
};
export default {
components: {
HiddenInput,
ActionSelect,
ProxyDialog,
CopyableInput,
@@ -1106,6 +1117,21 @@ message HealthCheckResponse {
return list;
},
dockerHostOptionsList() {
if (this.$root.dockerHostList && this.$root.dockerHostList.length > 0) {
return this.$root.dockerHostList.map((host) => {
return {
label: host.name,
value: host.id
};
});
} else {
return [{
label: this.$t("noDockerHostMsg"),
value: null,
}];
}
}
},
watch: {
"$root.proxyList"() {
@@ -1338,6 +1364,12 @@ message HealthCheckResponse {
return false;
}
}
if (this.monitor.type === "docker") {
if (this.monitor.docker_host == null) {
toast.error(this.$t("DockerHostRequired"));
return false;
}
}
return true;
},

View File

@@ -163,6 +163,10 @@ export default {
this.settings.entryPage = "dashboard";
}
if (this.settings.nscd === undefined) {
this.settings.nscd = true;
}
if (this.settings.dnsCache === undefined) {
this.settings.dnsCache = false;
}

View File

@@ -69,13 +69,17 @@
<div class="my-3">
<label class="form-label">
{{ $t("Domain Names") }}
<font-awesome-icon icon="plus-circle" class="btn-add-domain action text-primary" @click="addDomainField" />
<button class="p-0 bg-transparent border-0" :aria-label="$t('Add a domain')" @click="addDomainField">
<font-awesome-icon icon="plus-circle" class="action text-primary" />
</button>
</label>
<ul class="list-group domain-name-list">
<li v-for="(domain, index) in config.domainNameList" :key="index" class="list-group-item">
<input v-model="config.domainNameList[index]" type="text" class="no-bg domain-input" placeholder="example.com" />
<font-awesome-icon icon="times" class="action remove ms-2 me-3 text-danger" @click="removeDomain(index)" />
<button class="p-0 bg-transparent border-0" :aria-label="$t('Remove domain', [ domain ])" @click="removeDomain(index)">
<font-awesome-icon icon="times" class="action remove ms-2 me-3 text-danger" />
</button>
</li>
</ul>
</div>
@@ -102,7 +106,7 @@
<!-- Sidebar Footer -->
<div class="sidebar-footer">
<button class="btn btn-success me-2" @click="save">
<button class="btn btn-success me-2" :disabled="loading" @click="save">
<font-awesome-icon icon="save" />
{{ $t("Save") }}
</button>
@@ -438,6 +442,7 @@ export default {
lastUpdateTime: dayjs(),
updateCountdown: null,
updateCountdownText: null,
loading: true,
};
},
computed: {
@@ -697,6 +702,8 @@ export default {
this.incident = res.data.incident;
this.maintenanceList = res.data.maintenanceList;
this.$root.publicGroupList = res.data.publicGroupList;
this.loading = false;
}).catch( function (error) {
if (error.response.status === 404) {
location.href = "/page-not-found";
@@ -806,6 +813,7 @@ export default {
/** Save the status page */
save() {
this.loading = true;
let startTime = new Date();
this.config.slug = this.config.slug.trim().toLowerCase();
@@ -823,10 +831,12 @@ export default {
}
setTimeout(() => {
this.loading = false;
location.href = "/status/" + this.config.slug;
}, time);
} else {
this.loading = false;
toast.error(res.msg);
}
});

View File

@@ -6,9 +6,12 @@
//
// Backend uses the compiled file util.js
// Frontend uses util.ts
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.localToUTC = exports.utcToLocal = exports.utcToISODateTime = exports.isoToUTCDateTime = exports.parseTimeFromTimeObject = exports.parseTimeObject = exports.getMaintenanceRelativeURL = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.MIN_INTERVAL_SECOND = exports.MAX_INTERVAL_SECOND = exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = exports.SQL_DATETIME_FORMAT = exports.SQL_DATE_FORMAT = exports.STATUS_PAGE_MAINTENANCE = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.MAINTENANCE = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isDev = void 0;
const dayjs = require("dayjs");
exports.localToUTC = exports.utcToLocal = exports.utcToISODateTime = exports.isoToUTCDateTime = exports.parseTimeFromTimeObject = exports.parseTimeObject = exports.getMaintenanceRelativeURL = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.badgeConstants = exports.MIN_INTERVAL_SECOND = exports.MAX_INTERVAL_SECOND = exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = exports.SQL_DATETIME_FORMAT = exports.SQL_DATE_FORMAT = exports.STATUS_PAGE_MAINTENANCE = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.MAINTENANCE = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isDev = void 0;
const dayjs_1 = __importDefault(require("dayjs"));
exports.isDev = process.env.NODE_ENV === "development";
exports.appName = "Uptime Kuma";
exports.DOWN = 0;
@@ -24,6 +27,25 @@ exports.SQL_DATETIME_FORMAT = "YYYY-MM-DD HH:mm:ss";
exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = "YYYY-MM-DD HH:mm";
exports.MAX_INTERVAL_SECOND = 2073600; // 24 days
exports.MIN_INTERVAL_SECOND = 20; // 20 seconds
exports.badgeConstants = {
naColor: "#999",
defaultUpColor: "#66c20a",
defaultWarnColor: "#eed202",
defaultDownColor: "#c2290a",
defaultPendingColor: "#f8a306",
defaultMaintenanceColor: "#1747f5",
defaultPingColor: "blue",
defaultStyle: "flat",
defaultPingValueSuffix: "ms",
defaultPingLabelSuffix: "h",
defaultUptimeValueSuffix: "%",
defaultUptimeLabelSuffix: "h",
defaultCertExpValueSuffix: " days",
defaultCertExpLabelSuffix: "h",
// Values Come From Default Notification Times
defaultCertExpireWarnDays: "14",
defaultCertExpireDownDays: "7"
};
/** Flip the status of s */
function flipStatus(s) {
if (s === exports.UP) {
@@ -101,17 +123,20 @@ class Logger {
* @param level Log level. One of INFO, WARN, ERROR, DEBUG or can be customized.
*/
log(module, msg, level) {
if (level === "DEBUG" && !exports.isDev) {
return;
}
if (this.hideLog[level] && this.hideLog[level].includes(module.toLowerCase())) {
return;
}
module = module.toUpperCase();
level = level.toUpperCase();
let now;
if (dayjs.tz) {
now = dayjs.tz(new Date()).format();
if (dayjs_1.default.tz) {
now = dayjs_1.default.tz(new Date()).format();
}
else {
now = dayjs().format();
now = (0, dayjs_1.default)().format();
}
const formattedMessage = (typeof msg === "string") ? `${now} [${module}] ${level}: ${msg}` : msg;
if (level === "INFO") {
@@ -200,7 +225,7 @@ function polyfill() {
exports.polyfill = polyfill;
class TimeLogger {
constructor() {
this.startTime = dayjs().valueOf();
this.startTime = (0, dayjs_1.default)().valueOf();
}
/**
* Output time since start of monitor
@@ -208,7 +233,7 @@ class TimeLogger {
*/
print(name) {
if (exports.isDev && process.env.TIMELOGGER === "1") {
console.log(name + ": " + (dayjs().valueOf() - this.startTime) + "ms");
console.log(name + ": " + ((0, dayjs_1.default)().valueOf() - this.startTime) + "ms");
}
}
}
@@ -372,21 +397,21 @@ exports.parseTimeFromTimeObject = parseTimeFromTimeObject;
* @returns ISO Date time
*/
function isoToUTCDateTime(input) {
return dayjs(input).utc().format(exports.SQL_DATETIME_FORMAT);
return (0, dayjs_1.default)(input).utc().format(exports.SQL_DATETIME_FORMAT);
}
exports.isoToUTCDateTime = isoToUTCDateTime;
/**
* @param input
*/
function utcToISODateTime(input) {
return dayjs.utc(input).toISOString();
return dayjs_1.default.utc(input).toISOString();
}
exports.utcToISODateTime = utcToISODateTime;
/**
* For SQL_DATETIME_FORMAT
*/
function utcToLocal(input, format = exports.SQL_DATETIME_FORMAT) {
return dayjs.utc(input).local().format(format);
return dayjs_1.default.utc(input).local().format(format);
}
exports.utcToLocal = utcToLocal;
/**
@@ -396,6 +421,6 @@ exports.utcToLocal = utcToLocal;
* @returns Date in requested format
*/
function localToUTC(input, format = exports.SQL_DATETIME_FORMAT) {
return dayjs(input).utc().format(format);
return (0, dayjs_1.default)(input).utc().format(format);
}
exports.localToUTC = localToUTC;

View File

@@ -6,7 +6,7 @@
// Backend uses the compiled file util.js
// Frontend uses util.ts
import * as dayjs from "dayjs";
import dayjs from "dayjs";
import * as timezone from "dayjs/plugin/timezone";
import * as utc from "dayjs/plugin/utc";
@@ -29,6 +29,26 @@ export const SQL_DATETIME_FORMAT_WITHOUT_SECOND = "YYYY-MM-DD HH:mm";
export const MAX_INTERVAL_SECOND = 2073600; // 24 days
export const MIN_INTERVAL_SECOND = 20; // 20 seconds
export const badgeConstants = {
naColor: "#999",
defaultUpColor: "#66c20a",
defaultWarnColor: "#eed202",
defaultDownColor: "#c2290a",
defaultPendingColor: "#f8a306",
defaultMaintenanceColor: "#1747f5",
defaultPingColor: "blue", // as defined by badge-maker / shields.io
defaultStyle: "flat",
defaultPingValueSuffix: "ms",
defaultPingLabelSuffix: "h",
defaultUptimeValueSuffix: "%",
defaultUptimeLabelSuffix: "h",
defaultCertExpValueSuffix: " days",
defaultCertExpLabelSuffix: "h",
// Values Come From Default Notification Times
defaultCertExpireWarnDays: "14",
defaultCertExpireDownDays: "7"
};
/** Flip the status of s */
export function flipStatus(s: number) {
if (s === UP) {
@@ -115,6 +135,10 @@ class Logger {
* @param level Log level. One of INFO, WARN, ERROR, DEBUG or can be customized.
*/
log(module: string, msg: any, level: string) {
if (level === "DEBUG" && !isDev) {
return;
}
if (this.hideLog[level] && this.hideLog[level].includes(module.toLowerCase())) {
return;
}

View File

@@ -3,42 +3,62 @@ import { currentLocale } from "../../../src/i18n";
describe("Test i18n.js", () => {
it("currentLocale()", () => {
const setLanguage = (language) => {
Object.defineProperty(window.navigator, 'language', {
value: language,
writable: true
const setLanguages = (languages) => {
Object.defineProperty(navigator, 'language', {
value: languages[0],
writable: true
});
Object.defineProperty(navigator, 'languages', {
value: languages,
writable: true
});
}
setLanguage('en-EN');
setLanguages(['en-EN']);
expect(currentLocale()).equal("en");
setLanguage('zh-HK');
setLanguages(['zh-HK']);
expect(currentLocale()).equal("zh-HK");
// Note that in Safari on iOS prior to 10.2, the country code returned is lowercase: "en-us", "fr-fr" etc.
// https://developer.mozilla.org/en-US/docs/Web/API/Navigator/language
setLanguage('zh-hk');
setLanguages(['zh-hk']);
expect(currentLocale()).equal("en");
setLanguage('en-US');
setLanguages(['en-US']);
expect(currentLocale()).equal("en");
setLanguage('ja-ZZ');
setLanguages(['ja-ZZ']);
expect(currentLocale()).equal("ja");
setLanguage('zz-ZZ');
setLanguages(['zz-ZZ']);
expect(currentLocale()).equal("en");
setLanguage('zz-ZZ');
setLanguages(['zz-ZZ']);
expect(currentLocale()).equal("en");
setLanguage('en');
localStorage.locale = "en";
setLanguages(['en-US', 'en', 'pl', 'ja']);
expect(currentLocale()).equal("en");
localStorage.locale = "zh-HK";
expect(currentLocale()).equal("zh-HK");
setLanguages(['en-US', 'pl', 'ja']);
expect(currentLocale()).equal("en");
setLanguages(['abc', 'en-US', 'pl', 'ja']);
expect(currentLocale()).equal("en");
setLanguages(['fil-PH', 'pl']);
expect(currentLocale()).equal("pl");
setLanguages(['shi-Latn-MA', 'pl']);
expect(currentLocale()).equal("pl");
setLanguages(['pl']);
localStorage.locale = "ja-ZZ";
expect(currentLocale()).equal("ja");
setLanguages(['pl']);
localStorage.locale = "invalid-lang";
expect(currentLocale()).equal("pl");
});
});
});

View File

@@ -11,7 +11,8 @@
"removeComments": false,
"preserveConstEnums": true,
"sourceMap": false,
"strict": true
"strict": true,
"esModuleInterop": true
},
"files": [
"./src/util.ts"