mirror of
				https://github.com/louislam/uptime-kuma.git
				synced 2025-10-25 07:39:22 +08:00 
			
		
		
		
	Merge branch 'master' into logging
This commit is contained in:
		| @@ -1,6 +1,6 @@ | ||||
| # Project Info | ||||
|  | ||||
| First of all, thank you everyone who made pull requests for Uptime Kuma, I never thought GitHub Community can be that nice! And also because of this, I also never thought other people actually read my code and edit my code. It is not structed and commented so well, lol. Sorry about that. | ||||
| First of all, thank you everyone who made pull requests for Uptime Kuma, I never thought GitHub Community can be that nice! And also because of this, I also never thought other people actually read my code and edit my code. It is not structured and commented so well, lol. Sorry about that. | ||||
|  | ||||
| The project was created with vite.js (vue3). Then I created a sub-directory called "server" for server part. Both frontend and backend share the same package.json. | ||||
|  | ||||
| @@ -27,7 +27,7 @@ The frontend code build into "dist" directory. The server (express.js) exposes t | ||||
|  | ||||
| ## Can I create a pull request for Uptime Kuma? | ||||
|  | ||||
| Generally, if the pull request is working fine and it do not affect any existing logic, workflow and perfomance, I will merge into the master branch once it is tested. | ||||
| Generally, if the pull request is working fine and it do not affect any existing logic, workflow and performance, I will merge into the master branch once it is tested. | ||||
|  | ||||
| If you are not sure whether I will accept your pull request, feel free to create an empty pull request draft first. | ||||
|  | ||||
| @@ -66,13 +66,13 @@ I do not have such knowledge to test it. | ||||
|  | ||||
| #### ⚠ Low Priority - Harsh Mode | ||||
|  | ||||
| Some pull requests are required to modifiy the core. To be honest, I do not want anyone to try to do that, because it would spend a lot of your time. I will review your pull request harshly. Also you may need to write a lot of unit tests to ensure that there is no breaking change. | ||||
| Some pull requests are required to modify the core. To be honest, I do not want anyone to try to do that, because it would spend a lot of your time. I will review your pull request harshly. Also you may need to write a lot of unit tests to ensure that there is no breaking change. | ||||
|  | ||||
| - Touch large parts of code of any very important features | ||||
| - Touch monitoring logic | ||||
| - Drop a table or drop a column for any reason | ||||
| - Touch the entry point of Docker or Node.js | ||||
| - Modifiy auth | ||||
| - Modify auth | ||||
|  | ||||
|  | ||||
| #### *️⃣ Low Priority | ||||
| @@ -114,7 +114,7 @@ I personally do not like something need to learn so much and need to config so m | ||||
|  | ||||
| - Node.js >= 14 | ||||
| - Git | ||||
| - IDE that supports ESLint and EditorConfig (I am using Intellji Idea) | ||||
| - IDE that supports ESLint and EditorConfig (I am using IntelliJ IDEA) | ||||
| - A SQLite tool (SQLite Expert Personal is suggested) | ||||
|  | ||||
| ## Install dependencies | ||||
| @@ -141,7 +141,7 @@ express.js is just used for serving the frontend built files (index.html, .js an | ||||
|  | ||||
| - model/ (Object model, auto mapping to the database table name) | ||||
| - modules/ (Modified 3rd-party modules) | ||||
| - notification-providers/ (indivdual notification logic) | ||||
| - notification-providers/ (individual notification logic) | ||||
| - routers/ (Express Routers) | ||||
| - scoket-handler (Socket.io Handlers) | ||||
| - server.js (Server main logic) | ||||
|   | ||||
							
								
								
									
										33
									
								
								config/jest-debug-env.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								config/jest-debug-env.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| const PuppeteerEnvironment = require("jest-environment-puppeteer"); | ||||
| const util = require("util"); | ||||
|  | ||||
| class DebugEnv extends PuppeteerEnvironment { | ||||
|     async handleTestEvent(event, state) { | ||||
|         const ignoredEvents = [ | ||||
|             "setup", | ||||
|             "add_hook", | ||||
|             "start_describe_definition", | ||||
|             "add_test", | ||||
|             "finish_describe_definition", | ||||
|             "run_start", | ||||
|             "run_describe_start", | ||||
|             "test_start", | ||||
|             "hook_start", | ||||
|             "hook_success", | ||||
|             "test_fn_start", | ||||
|             "test_fn_success", | ||||
|             "test_done", | ||||
|             "run_describe_finish", | ||||
|             "run_finish", | ||||
|             "teardown", | ||||
|             "test_fn_failure", | ||||
|         ]; | ||||
|         if (!ignoredEvents.includes(event.name)) { | ||||
|             console.log( | ||||
|                 new Date().toString() + ` Unhandled event [${event.name}] ` + util.inspect(event) | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| module.exports = DebugEnv; | ||||
| @@ -1,6 +1,20 @@ | ||||
| module.exports = { | ||||
|     "launch": { | ||||
|         "dumpio": true, | ||||
|         "slowMo": 500, | ||||
|         "headless": process.env.HEADLESS_TEST || false, | ||||
|         "userDataDir": "./data/test-chrome-profile", | ||||
|         args: [ | ||||
|             "--disable-setuid-sandbox", | ||||
|             "--disable-gpu", | ||||
|             "--disable-dev-shm-usage", | ||||
|             "--no-default-browser-check", | ||||
|             "--no-experiments", | ||||
|             "--no-first-run", | ||||
|             "--no-pings", | ||||
|             "--no-sandbox", | ||||
|             "--no-zygote", | ||||
|             "--single-process", | ||||
|         ], | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -5,6 +5,7 @@ module.exports = { | ||||
|         "__DEV__": true | ||||
|     }, | ||||
|     "testRegex": "./test/e2e.spec.js", | ||||
|     "testEnvironment": "./config/jest-debug-env.js", | ||||
|     "rootDir": "..", | ||||
|     "testTimeout": 30000, | ||||
| }; | ||||
|   | ||||
							
								
								
									
										10
									
								
								db/patch-monitor-basic-auth.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								db/patch-monitor-basic-auth.sql
									
									
									
									
									
										Normal 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; | ||||
|  | ||||
| ALTER TABLE monitor | ||||
|     ADD basic_auth_user TEXT default null; | ||||
|  | ||||
| ALTER TABLE monitor | ||||
|     ADD basic_auth_pass TEXT default null; | ||||
|  | ||||
| COMMIT; | ||||
							
								
								
									
										4973
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4973
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										31
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										31
									
								
								package.json
									
									
									
									
									
								
							| @@ -22,7 +22,7 @@ | ||||
|         "build": "vite build --config ./config/vite.config.js", | ||||
|         "test": "node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/ --test", | ||||
|         "test-with-build": "npm run build && npm test", | ||||
|         "jest": "node test/prepare-jest.js && npm run jest-frontend && npm run jest-backend && jest --config=./config/jest.config.js", | ||||
|         "jest": "node test/prepare-jest.js && npm run jest-frontend && npm run jest-backend && jest --runInBand --config=./config/jest.config.js", | ||||
|         "jest-frontend": "cross-env TEST_FRONTEND=1 jest --config=./config/jest-frontend.config.js", | ||||
|         "jest-backend": "cross-env TEST_BACKEND=1 jest --config=./config/jest-backend.config.js", | ||||
|         "tsc": "tsc", | ||||
| @@ -50,14 +50,15 @@ | ||||
|         "test-nodejs16": "docker build --progress plain -f test/ubuntu-nodejs16.dockerfile .", | ||||
|         "simple-dns-server": "node extra/simple-dns-server.js", | ||||
|         "update-language-files-with-base-lang": "cd extra/update-language-files && node index.js %npm_config_base_lang% && eslint ../../src/languages/**.js --fix", | ||||
|         "update-language-files": "cd extra/update-language-files && node index.js && eslint ../../src/languages/**.js --fix" | ||||
|         "update-language-files": "cd extra/update-language-files && node index.js && eslint ../../src/languages/**.js --fix", | ||||
|         "ncu-patch": "ncu -u -t patch" | ||||
|     }, | ||||
|     "dependencies": { | ||||
|         "@fortawesome/fontawesome-svg-core": "~1.2.36", | ||||
|         "@fortawesome/free-regular-svg-icons": "~5.15.4", | ||||
|         "@fortawesome/free-solid-svg-icons": "~5.15.4", | ||||
|         "@fortawesome/vue-fontawesome": "~3.0.0-4", | ||||
|         "@louislam/sqlite3": "~6.0.0", | ||||
|         "@fortawesome/vue-fontawesome": "~3.0.0-5", | ||||
|         "@louislam/sqlite3": "~6.0.1", | ||||
|         "@popperjs/core": "~2.10.2", | ||||
|         "args-parser": "~1.3.0", | ||||
|         "axios": "~0.21.4", | ||||
| @@ -74,7 +75,7 @@ | ||||
|         "express": "~4.17.1", | ||||
|         "express-basic-auth": "~1.2.0", | ||||
|         "form-data": "~4.0.0", | ||||
|         "http-graceful-shutdown": "~3.1.4", | ||||
|         "http-graceful-shutdown": "~3.1.5", | ||||
|         "iconv-lite": "^0.6.3", | ||||
|         "jsonwebtoken": "~8.5.1", | ||||
|         "jwt-decode": "^3.1.2", | ||||
| @@ -83,7 +84,7 @@ | ||||
|         "notp": "~2.0.3", | ||||
|         "password-hash": "~1.2.2", | ||||
|         "postcss-rtlcss": "~3.4.1", | ||||
|         "postcss-scss": "~4.0.1", | ||||
|         "postcss-scss": "~4.0.2", | ||||
|         "prom-client": "~13.2.0", | ||||
|         "prometheus-api-metrics": "~3.2.0", | ||||
|         "qrcode": "~1.4.4", | ||||
| @@ -103,30 +104,30 @@ | ||||
|         "vue-image-crop-upload": "~3.0.3", | ||||
|         "vue-multiselect": "~3.0.0-alpha.2", | ||||
|         "vue-qrcode": "~1.0.0", | ||||
|         "vue-router": "~4.0.11", | ||||
|         "vue-toastification": "~2.0.0-rc.1", | ||||
|         "vue-router": "~4.0.12", | ||||
|         "vue-toastification": "~2.0.0-rc.5", | ||||
|         "vuedraggable": "~4.1.0" | ||||
|     }, | ||||
|     "devDependencies": { | ||||
|         "@babel/eslint-parser": "~7.15.7", | ||||
|         "@babel/eslint-parser": "~7.15.8", | ||||
|         "@babel/preset-env": "^7.15.8", | ||||
|         "@types/bootstrap": "~5.1.6", | ||||
|         "@vitejs/plugin-legacy": "~1.6.2", | ||||
|         "@vitejs/plugin-legacy": "~1.6.3", | ||||
|         "@vitejs/plugin-vue": "~1.9.4", | ||||
|         "@vue/compiler-sfc": "~3.2.20", | ||||
|         "@vue/compiler-sfc": "~3.2.22", | ||||
|         "babel-plugin-rewire": "~1.2.0", | ||||
|         "core-js": "~3.18.1", | ||||
|         "core-js": "~3.18.3", | ||||
|         "cross-env": "~7.0.3", | ||||
|         "dns2": "~2.0.1", | ||||
|         "eslint": "~7.32.0", | ||||
|         "eslint-plugin-vue": "~7.18.0", | ||||
|         "jest": "~27.2.4", | ||||
|         "jest": "~27.2.5", | ||||
|         "jest-puppeteer": "~6.0.0", | ||||
|         "puppeteer": "~10.4.0", | ||||
|         "sass": "~1.42.1", | ||||
|         "stylelint": "~13.13.1", | ||||
|         "stylelint-config-standard": "~22.0.0", | ||||
|         "typescript": "~4.4.3", | ||||
|         "vite": "~2.6.13" | ||||
|         "typescript": "~4.4.4", | ||||
|         "vite": "~2.6.14" | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -52,6 +52,7 @@ class Database { | ||||
|         "patch-http-monitor-method-body-and-headers.sql": true, | ||||
|         "patch-2fa-invalidate-used-token.sql": true, | ||||
|         "patch-notification_sent_history.sql": true, | ||||
|         "patch-monitor-basic-auth.sql": true, | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -79,7 +80,7 @@ class Database { | ||||
|         log_info("db", `Data Dir: ${Database.dataDir}`); | ||||
|     } | ||||
|  | ||||
|     static async connect() { | ||||
|     static async connect(testMode = false) { | ||||
|         const acquireConnectionTimeout = 120 * 1000; | ||||
|  | ||||
|         const Dialect = require("knex/lib/dialects/sqlite3/index.js"); | ||||
| @@ -112,8 +113,13 @@ class Database { | ||||
|         await R.autoloadModels("./server/model"); | ||||
|  | ||||
|         await R.exec("PRAGMA foreign_keys = ON"); | ||||
|         if (testMode) { | ||||
|             // Change to MEMORY | ||||
|             await R.exec("PRAGMA journal_mode = MEMORY"); | ||||
|         } else { | ||||
|             // Change to WAL | ||||
|             await R.exec("PRAGMA journal_mode = WAL"); | ||||
|         } | ||||
|         await R.exec("PRAGMA cache_size = -12000"); | ||||
|         await R.exec("PRAGMA auto_vacuum = FULL"); | ||||
|  | ||||
|   | ||||
| @@ -58,6 +58,8 @@ class Monitor extends BeanModel { | ||||
|             method: this.method, | ||||
|             body: this.body, | ||||
|             headers: this.headers, | ||||
|             basic_auth_user: this.basic_auth_user, | ||||
|             basic_auth_pass: this.basic_auth_pass, | ||||
|             hostname: this.hostname, | ||||
|             port: this.port, | ||||
|             maxretries: this.maxretries, | ||||
| @@ -80,6 +82,15 @@ class Monitor extends BeanModel { | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Encode user and password to Base64 encoding | ||||
|      * for HTTP "basic" auth, as per RFC-7617 | ||||
|      * @returns {string} | ||||
|      */ | ||||
|     encodeBase64(user, pass) { | ||||
|         return Buffer.from(user + ":" + pass).toString("base64"); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Parse to boolean | ||||
|      * @returns {boolean} | ||||
| @@ -141,7 +152,16 @@ class Monitor extends BeanModel { | ||||
|                     // Do not do any queries/high loading things before the "bean.ping" | ||||
|                     let startTime = dayjs().valueOf(); | ||||
|  | ||||
|                     // HTTP basic auth | ||||
|                     let basicAuthHeader = {}; | ||||
|                     if (this.basic_auth_user) { | ||||
|                         basicAuthHeader = { | ||||
|                             "Authorization": "Basic " + this.encodeBase64(this.basic_auth_user, this.basic_auth_pass), | ||||
|                         }; | ||||
|                     } | ||||
|  | ||||
|                     log_debug("monitor", `[${this.name}] Prepare Options for axios`); | ||||
|  | ||||
|                     const options = { | ||||
|                         url: this.url, | ||||
|                         method: (this.method || "get").toLowerCase(), | ||||
| @@ -151,6 +171,7 @@ class Monitor extends BeanModel { | ||||
|                             "Accept": "*/*", | ||||
|                             "User-Agent": "Uptime-Kuma/" + version, | ||||
|                             ...(this.headers ? JSON.parse(this.headers) : {}), | ||||
|                             ...(basicAuthHeader), | ||||
|                         }, | ||||
|                         httpsAgent: new https.Agent({ | ||||
|                             maxCachedSessions: 0,      // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940) | ||||
|   | ||||
| @@ -14,8 +14,8 @@ class DingDing extends NotificationProvider { | ||||
|                 let params = { | ||||
|                     msgtype: "markdown", | ||||
|                     markdown: { | ||||
|                         title: monitorJSON["name"], | ||||
|                         text: `## [${this.statusToString(heartbeatJSON["status"])}] \n > ${heartbeatJSON["msg"]}  \n > Time(UTC):${heartbeatJSON["time"]}`, | ||||
|                         title: `[${this.statusToString(heartbeatJSON["status"])}] ${monitorJSON["name"]}`, | ||||
|                         text: `## [${this.statusToString(heartbeatJSON["status"])}] ${monitorJSON["name"]} \n > ${heartbeatJSON["msg"]}  \n > Time(UTC):${heartbeatJSON["time"]}`, | ||||
|                     } | ||||
|                 }; | ||||
|                 if (this.sendToDingDing(notification, params)) { | ||||
|   | ||||
| @@ -7,12 +7,12 @@ class Pushover extends NotificationProvider { | ||||
|  | ||||
|     async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { | ||||
|         let okMsg = "Sent Successfully."; | ||||
|         let pushoverlink = "https://api.pushover.net/1/messages.json" | ||||
|         let pushoverlink = "https://api.pushover.net/1/messages.json"; | ||||
|  | ||||
|         try { | ||||
|             if (heartbeatJSON == null) { | ||||
|                 let data = { | ||||
|                     "message": "<b>Uptime Kuma Pushover testing successful.</b>", | ||||
|                     "message": msg, | ||||
|                     "user": notification.pushoveruserkey, | ||||
|                     "token": notification.pushoverapptoken, | ||||
|                     "sound": notification.pushoversounds, | ||||
| @@ -21,8 +21,8 @@ class Pushover extends NotificationProvider { | ||||
|                     "retry": "30", | ||||
|                     "expire": "3600", | ||||
|                     "html": 1, | ||||
|                 } | ||||
|                 await axios.post(pushoverlink, data) | ||||
|                 }; | ||||
|                 await axios.post(pushoverlink, data); | ||||
|                 return okMsg; | ||||
|             } | ||||
|  | ||||
| @@ -36,11 +36,11 @@ class Pushover extends NotificationProvider { | ||||
|                 "retry": "30", | ||||
|                 "expire": "3600", | ||||
|                 "html": 1, | ||||
|             } | ||||
|             await axios.post(pushoverlink, data) | ||||
|             }; | ||||
|             await axios.post(pushoverlink, data); | ||||
|             return okMsg; | ||||
|         } catch (error) { | ||||
|             this.throwGeneralAxiosError(error) | ||||
|             this.throwGeneralAxiosError(error); | ||||
|         } | ||||
|  | ||||
|     } | ||||
|   | ||||
							
								
								
									
										44
									
								
								server/notification-providers/serwersms.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								server/notification-providers/serwersms.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| const NotificationProvider = require("./notification-provider"); | ||||
| const axios = require("axios"); | ||||
|  | ||||
| class SerwerSMS extends NotificationProvider { | ||||
|  | ||||
|     name = "serwersms"; | ||||
|  | ||||
|     async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { | ||||
|         let okMsg = "Sent Successfully."; | ||||
|  | ||||
|         try { | ||||
|             let config = { | ||||
|                 headers: { | ||||
|                     "Content-Type": "application/json", | ||||
|                 } | ||||
|             }; | ||||
|             let data = { | ||||
|                 "username": notification.serwersmsUsername, | ||||
|                 "password": notification.serwersmsPassword, | ||||
|                 "phone": notification.serwersmsPhoneNumber, | ||||
|                 "text": msg.replace(/[^\x00-\x7F]/g, ""), | ||||
|                 "sender": notification.serwersmsSenderName, | ||||
|             }; | ||||
|  | ||||
|             let resp = await axios.post("https://api2.serwersms.pl/messages/send_sms", data, config); | ||||
|  | ||||
|             if (!resp.data.success) { | ||||
|                 if (resp.data.error) { | ||||
|                     let error = `SerwerSMS.pl API returned error code ${resp.data.error.code} (${resp.data.error.type}) with error message: ${resp.data.error.message}`; | ||||
|                     this.throwGeneralAxiosError(error); | ||||
|                 } else { | ||||
|                     let error = "SerwerSMS.pl API returned an unexpected response"; | ||||
|                     this.throwGeneralAxiosError(error); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             return okMsg; | ||||
|         } catch (error) { | ||||
|             this.throwGeneralAxiosError(error); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| module.exports = SerwerSMS; | ||||
| @@ -24,6 +24,7 @@ const AliyunSms = require("./notification-providers/aliyun-sms"); | ||||
| const DingDing = require("./notification-providers/dingding"); | ||||
| const Bark = require("./notification-providers/bark"); | ||||
| const { log_info } = require("../src/util"); | ||||
| const SerwerSMS = require("./notification-providers/serwersms"); | ||||
|  | ||||
| class Notification { | ||||
|  | ||||
| @@ -59,6 +60,7 @@ class Notification { | ||||
|             new Telegram(), | ||||
|             new Webhook(), | ||||
|             new Bark(), | ||||
|             new SerwerSMS(), | ||||
|         ]; | ||||
|  | ||||
|         for (let item of list) { | ||||
|   | ||||
| @@ -178,7 +178,7 @@ exports.entryPage = "dashboard"; | ||||
|  | ||||
| (async () => { | ||||
|     Database.init(args); | ||||
|     await initDatabase(); | ||||
|     await initDatabase(testMode); | ||||
|  | ||||
|     exports.entryPage = await setting("entryPage"); | ||||
|  | ||||
| @@ -572,8 +572,8 @@ exports.entryPage = "dashboard"; | ||||
|  | ||||
|                 await updateMonitorNotification(bean.id, notificationIDList); | ||||
|  | ||||
|                 await startMonitor(socket.userID, bean.id); | ||||
|                 await sendMonitorList(socket); | ||||
|                 await startMonitor(socket.userID, bean.id); | ||||
|  | ||||
|                 log_info("monitor", `Added Monitor: ${monitorID} User ID: ${socket.userID}`); | ||||
|  | ||||
| @@ -611,6 +611,8 @@ exports.entryPage = "dashboard"; | ||||
|                 bean.method = monitor.method; | ||||
|                 bean.body = monitor.body; | ||||
|                 bean.headers = monitor.headers; | ||||
|                 bean.basic_auth_user = monitor.basic_auth_user; | ||||
|                 bean.basic_auth_pass = monitor.basic_auth_pass; | ||||
|                 bean.interval = monitor.interval; | ||||
|                 bean.retryInterval = monitor.retryInterval; | ||||
|                 bean.hostname = monitor.hostname; | ||||
| @@ -1175,6 +1177,8 @@ exports.entryPage = "dashboard"; | ||||
|                                 method: monitorListData[i].method || "GET", | ||||
|                                 body: monitorListData[i].body, | ||||
|                                 headers: monitorListData[i].headers, | ||||
|                                 basic_auth_user: monitorListData[i].basic_auth_user, | ||||
|                                 basic_auth_pass: monitorListData[i].basic_auth_pass, | ||||
|                                 interval: monitorListData[i].interval, | ||||
|                                 retryInterval: retryInterval, | ||||
|                                 hostname: monitorListData[i].hostname, | ||||
| @@ -1453,14 +1457,14 @@ async function getMonitorJSONList(userID) { | ||||
|     return result; | ||||
| } | ||||
|  | ||||
| async function initDatabase() { | ||||
| async function initDatabase(testMode = false) { | ||||
|     if (! fs.existsSync(Database.path)) { | ||||
|         log_info("server", "Copying Database"); | ||||
|         fs.copyFileSync(Database.templatePath, Database.path); | ||||
|     } | ||||
|  | ||||
|     log_info("server", "Connecting to the Database"); | ||||
|     await Database.connect(); | ||||
|     await Database.connect(testMode); | ||||
|     log_info("server", "Connected"); | ||||
|  | ||||
|     // Patch the database | ||||
|   | ||||
| @@ -12,6 +12,7 @@ $dark-font-color2: #020b05; | ||||
| $dark-bg: #0d1117; | ||||
| $dark-bg2: #070a10; | ||||
| $dark-border-color: #1d2634; | ||||
| $dark-header-bg: #161b22; | ||||
|  | ||||
| $easing-in: cubic-bezier(0.54, 0.78, 0.55, 0.97); | ||||
| $easing-out: cubic-bezier(0.25, 0.46, 0.45, 0.94); | ||||
|   | ||||
| @@ -137,7 +137,7 @@ export default { | ||||
|     justify-content: space-between; | ||||
|  | ||||
|     .dark & { | ||||
|         background-color: #161b22; | ||||
|         background-color: $dark-header-bg; | ||||
|         border-bottom: 0; | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										28
									
								
								src/components/notifications/SerwerSMS.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/components/notifications/SerwerSMS.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| <template> | ||||
|     <div class="mb-3"> | ||||
|         <label for="serwersms-username" class="form-label">{{ $t('serwersmsAPIUser') }}</label> | ||||
|         <input id="serwersms-username" v-model="$parent.notification.serwersmsUsername" type="text" class="form-control" required> | ||||
|     </div> | ||||
|     <div class="mb-3"> | ||||
|         <label for="serwersms-key" class="form-label">{{ $t('serwersmsAPIPassword') }}</label> | ||||
|         <HiddenInput id="serwersms-key" v-model="$parent.notification.serwersmsPassword" :required="true" autocomplete="one-time-code"></HiddenInput> | ||||
|     </div> | ||||
|     <div class="mb-3"> | ||||
|         <label for="serwersms-phone-number" class="form-label">{{ $t("serwersmsPhoneNumber") }}</label> | ||||
|         <input id="serwersms-phone-number" v-model="$parent.notification.serwersmsPhoneNumber" type="text" class="form-control" required> | ||||
|     </div> | ||||
|     <div class="mb-3"> | ||||
|         <label for="serwersms-sender-name" class="form-label">{{ $t("serwersmsSenderName") }}</label> | ||||
|         <input id="serwersms-sender-name" v-model="$parent.notification.serwersmsSenderName" type="text" minlength="3" maxlength="11" class="form-control"> | ||||
|     </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import HiddenInput from "../HiddenInput.vue"; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|         HiddenInput, | ||||
|     }, | ||||
| }; | ||||
| </script> | ||||
| @@ -22,6 +22,7 @@ import Matrix from "./Matrix.vue"; | ||||
| import AliyunSMS from "./AliyunSms.vue"; | ||||
| import DingDing from "./DingDing.vue"; | ||||
| import Bark from "./Bark.vue"; | ||||
| import SerwerSMS from "./SerwerSMS.vue"; | ||||
|  | ||||
| /** | ||||
|  * Manage all notification form. | ||||
| @@ -52,7 +53,8 @@ const NotificationFormList = { | ||||
|     "mattermost": Mattermost, | ||||
|     "matrix": Matrix, | ||||
|     "DingDing": DingDing, | ||||
|     "Bark": Bark | ||||
|     "Bark": Bark, | ||||
|     "serwersms": SerwerSMS, | ||||
| } | ||||
|  | ||||
| export default NotificationFormList | ||||
|   | ||||
							
								
								
									
										25
									
								
								src/components/settings/About.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/components/settings/About.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| <template> | ||||
|     <div class="d-flex justify-content-center align-items-center"> | ||||
|         <div class="logo d-flex flex-column justify-content-center align-items-center"> | ||||
|             <object class="my-4" width="200" height="200" data="/icon.svg" /> | ||||
|             <div class="fs-4 fw-bold">Uptime Kuma</div> | ||||
|             <div>{{ $t("Version") }}: {{ $root.info.version }}</div> | ||||
|             <div class="my-1 update-link"><a href="https://github.com/louislam/uptime-kuma/releases" target="_blank" rel="noopener">{{ $t("Check Update On GitHub") }}</a></div> | ||||
|         </div> | ||||
|     </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| export default { | ||||
|  | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| .logo { | ||||
|     margin: 4em 1em; | ||||
| } | ||||
| .update-link { | ||||
|     font-size: 0.9em; | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										143
									
								
								src/components/settings/Appearance.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								src/components/settings/Appearance.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,143 @@ | ||||
| <template> | ||||
|     <div> | ||||
|         <div class="my-4"> | ||||
|             <label for="language" class="form-label"> | ||||
|                 {{ $t("Language") }} | ||||
|             </label> | ||||
|             <select id="language" v-model="$root.language" class="form-select"> | ||||
|                 <option | ||||
|                     v-for="(lang, i) in $i18n.availableLocales" | ||||
|                     :key="`Lang${i}`" | ||||
|                     :value="lang" | ||||
|                 > | ||||
|                     {{ $i18n.messages[lang].languageName }} | ||||
|                 </option> | ||||
|             </select> | ||||
|         </div> | ||||
|         <div class="my-4"> | ||||
|             <label for="timezone" class="form-label">{{ $t("Theme") }}</label> | ||||
|             <div> | ||||
|                 <div | ||||
|                     class="btn-group" | ||||
|                     role="group" | ||||
|                     aria-label="Basic checkbox toggle button group" | ||||
|                 > | ||||
|                     <input | ||||
|                         id="btncheck1" | ||||
|                         v-model="$root.userTheme" | ||||
|                         type="radio" | ||||
|                         class="btn-check" | ||||
|                         name="theme" | ||||
|                         autocomplete="off" | ||||
|                         value="light" | ||||
|                     /> | ||||
|                     <label class="btn btn-outline-primary" for="btncheck1"> | ||||
|                         {{ $t("Light") }} | ||||
|                     </label> | ||||
|  | ||||
|                     <input | ||||
|                         id="btncheck2" | ||||
|                         v-model="$root.userTheme" | ||||
|                         type="radio" | ||||
|                         class="btn-check" | ||||
|                         name="theme" | ||||
|                         autocomplete="off" | ||||
|                         value="dark" | ||||
|                     /> | ||||
|                     <label class="btn btn-outline-primary" for="btncheck2"> | ||||
|                         {{ $t("Dark") }} | ||||
|                     </label> | ||||
|  | ||||
|                     <input | ||||
|                         id="btncheck3" | ||||
|                         v-model="$root.userTheme" | ||||
|                         type="radio" | ||||
|                         class="btn-check" | ||||
|                         name="theme" | ||||
|                         autocomplete="off" | ||||
|                         value="auto" | ||||
|                     /> | ||||
|                     <label class="btn btn-outline-primary" for="btncheck3"> | ||||
|                         {{ $t("Auto") }} | ||||
|                     </label> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|         <div class="my-4"> | ||||
|             <label class="form-label">{{ $t("Theme - Heartbeat Bar") }}</label> | ||||
|             <div> | ||||
|                 <div | ||||
|                     class="btn-group" | ||||
|                     role="group" | ||||
|                     aria-label="Basic checkbox toggle button group" | ||||
|                 > | ||||
|                     <input | ||||
|                         id="btncheck4" | ||||
|                         v-model="$root.userHeartbeatBar" | ||||
|                         type="radio" | ||||
|                         class="btn-check" | ||||
|                         name="heartbeatBarTheme" | ||||
|                         autocomplete="off" | ||||
|                         value="normal" | ||||
|                     /> | ||||
|                     <label class="btn btn-outline-primary" for="btncheck4"> | ||||
|                         {{ $t("Normal") }} | ||||
|                     </label> | ||||
|  | ||||
|                     <input | ||||
|                         id="btncheck5" | ||||
|                         v-model="$root.userHeartbeatBar" | ||||
|                         type="radio" | ||||
|                         class="btn-check" | ||||
|                         name="heartbeatBarTheme" | ||||
|                         autocomplete="off" | ||||
|                         value="bottom" | ||||
|                     /> | ||||
|                     <label class="btn btn-outline-primary" for="btncheck5"> | ||||
|                         {{ $t("Bottom") }} | ||||
|                     </label> | ||||
|  | ||||
|                     <input | ||||
|                         id="btncheck6" | ||||
|                         v-model="$root.userHeartbeatBar" | ||||
|                         type="radio" | ||||
|                         class="btn-check" | ||||
|                         name="heartbeatBarTheme" | ||||
|                         autocomplete="off" | ||||
|                         value="none" | ||||
|                     /> | ||||
|                     <label class="btn btn-outline-primary" for="btncheck6"> | ||||
|                         {{ $t("None") }} | ||||
|                     </label> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| export default { | ||||
|  | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| @import "../../assets/vars.scss"; | ||||
|  | ||||
| .btn-check:active + .btn-outline-primary, | ||||
| .btn-check:checked + .btn-outline-primary, | ||||
| .btn-check:hover + .btn-outline-primary { | ||||
|     color: #fff; | ||||
|  | ||||
|     .dark & { | ||||
|         color: #000; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .dark { | ||||
|     .list-group-item { | ||||
|         background-color: $dark-bg2; | ||||
|         color: $dark-font-color; | ||||
|     } | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										213
									
								
								src/components/settings/Backup.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										213
									
								
								src/components/settings/Backup.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,213 @@ | ||||
| <template> | ||||
|     <div> | ||||
|         <div class="my-4"> | ||||
|             <h4 class="mt-4 mb-2">{{ $t("Export Backup") }}</h4> | ||||
|  | ||||
|             <p> | ||||
|                 {{ $t("backupDescription") }} <br /> | ||||
|                 ({{ $t("backupDescription2") }}) <br /> | ||||
|             </p> | ||||
|  | ||||
|             <div class="mb-2"> | ||||
|                 <button class="btn btn-primary" @click="downloadBackup"> | ||||
|                     {{ $t("Export") }} | ||||
|                 </button> | ||||
|             </div> | ||||
|  | ||||
|             <p> | ||||
|                 <strong>{{ $t("backupDescription3") }}</strong> | ||||
|             </p> | ||||
|         </div> | ||||
|  | ||||
|         <div class="my-4"> | ||||
|             <h4 class="mt-4 mb-2">{{ $t("Import Backup") }}</h4> | ||||
|  | ||||
|             <label class="form-label">{{ $t("Options") }}:</label> | ||||
|             <br /> | ||||
|             <div class="form-check form-check-inline"> | ||||
|                 <input | ||||
|                     id="radioKeep" | ||||
|                     v-model="importHandle" | ||||
|                     class="form-check-input" | ||||
|                     type="radio" | ||||
|                     name="radioImportHandle" | ||||
|                     value="keep" | ||||
|                 /> | ||||
|                 <label class="form-check-label" for="radioKeep"> | ||||
|                     {{ $t("Keep both") }} | ||||
|                 </label> | ||||
|             </div> | ||||
|             <div class="form-check form-check-inline"> | ||||
|                 <input | ||||
|                     id="radioSkip" | ||||
|                     v-model="importHandle" | ||||
|                     class="form-check-input" | ||||
|                     type="radio" | ||||
|                     name="radioImportHandle" | ||||
|                     value="skip" | ||||
|                 /> | ||||
|                 <label class="form-check-label" for="radioSkip"> | ||||
|                     {{ $t("Skip existing") }} | ||||
|                 </label> | ||||
|             </div> | ||||
|             <div class="form-check form-check-inline"> | ||||
|                 <input | ||||
|                     id="radioOverwrite" | ||||
|                     v-model="importHandle" | ||||
|                     class="form-check-input" | ||||
|                     type="radio" | ||||
|                     name="radioImportHandle" | ||||
|                     value="overwrite" | ||||
|                 /> | ||||
|                 <label class="form-check-label" for="radioOverwrite"> | ||||
|                     {{ $t("Overwrite") }} | ||||
|                 </label> | ||||
|             </div> | ||||
|             <div class="form-text mb-2"> | ||||
|                 {{ $t("importHandleDescription") }} | ||||
|             </div> | ||||
|  | ||||
|             <div class="mb-2"> | ||||
|                 <input | ||||
|                     id="importBackup" | ||||
|                     type="file" | ||||
|                     class="form-control" | ||||
|                     accept="application/json" | ||||
|                 /> | ||||
|             </div> | ||||
|  | ||||
|             <div class="input-group mb-2 justify-content-end"> | ||||
|                 <button | ||||
|                     type="button" | ||||
|                     class="btn btn-outline-primary" | ||||
|                     :disabled="processing" | ||||
|                     @click="confirmImport" | ||||
|                 > | ||||
|                     <div | ||||
|                         v-if="processing" | ||||
|                         class="spinner-border spinner-border-sm me-1" | ||||
|                     ></div> | ||||
|                     {{ $t("Import") }} | ||||
|                 </button> | ||||
|             </div> | ||||
|  | ||||
|             <div | ||||
|                 v-if="importAlert" | ||||
|                 class="alert alert-danger mt-3" | ||||
|                 style="padding: 6px 16px" | ||||
|             > | ||||
|                 {{ importAlert }} | ||||
|             </div> | ||||
|         </div> | ||||
|  | ||||
|         <Confirm | ||||
|             ref="confirmImport" | ||||
|             btn-style="btn-danger" | ||||
|             :yes-text="$t('Yes')" | ||||
|             :no-text="$t('No')" | ||||
|             @yes="importBackup" | ||||
|         > | ||||
|             {{ $t("confirmImportMsg") }} | ||||
|         </Confirm> | ||||
|     </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import Confirm from "../../components/Confirm.vue"; | ||||
| import dayjs from "dayjs"; | ||||
| import { useToast } from "vue-toastification"; | ||||
|  | ||||
| const toast = useToast(); | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|         Confirm, | ||||
|     }, | ||||
|  | ||||
|     data() { | ||||
|         return { | ||||
|             processing: false, | ||||
|             importHandle: "skip", | ||||
|             importAlert: null, | ||||
|         }; | ||||
|     }, | ||||
|  | ||||
|     methods: { | ||||
|         confirmImport() { | ||||
|             this.$refs.confirmImport.show(); | ||||
|         }, | ||||
|  | ||||
|         downloadBackup() { | ||||
|             let time = dayjs().format("YYYY_MM_DD-hh_mm_ss"); | ||||
|             let fileName = `Uptime_Kuma_Backup_${time}.json`; | ||||
|             let monitorList = Object.values(this.$root.monitorList); | ||||
|             let exportData = { | ||||
|                 version: this.$root.info.version, | ||||
|                 notificationList: this.$root.notificationList, | ||||
|                 monitorList: monitorList, | ||||
|             }; | ||||
|             exportData = JSON.stringify(exportData, null, 4); | ||||
|             let downloadItem = document.createElement("a"); | ||||
|             downloadItem.setAttribute( | ||||
|                 "href", | ||||
|                 "data:application/json;charset=utf-8," + | ||||
|                     encodeURIComponent(exportData) | ||||
|             ); | ||||
|             downloadItem.setAttribute("download", fileName); | ||||
|             downloadItem.click(); | ||||
|         }, | ||||
|  | ||||
|         importBackup() { | ||||
|             this.processing = true; | ||||
|             let uploadItem = document.getElementById("importBackup").files; | ||||
|  | ||||
|             if (uploadItem.length <= 0) { | ||||
|                 this.processing = false; | ||||
|                 return (this.importAlert = this.$t("alertNoFile")); | ||||
|             } | ||||
|  | ||||
|             if (uploadItem.item(0).type !== "application/json") { | ||||
|                 this.processing = false; | ||||
|                 return (this.importAlert = this.$t("alertWrongFileType")); | ||||
|             } | ||||
|  | ||||
|             let fileReader = new FileReader(); | ||||
|             fileReader.readAsText(uploadItem.item(0)); | ||||
|  | ||||
|             fileReader.onload = (item) => { | ||||
|                 this.$root.uploadBackup( | ||||
|                     item.target.result, | ||||
|                     this.importHandle, | ||||
|                     (res) => { | ||||
|                         this.processing = false; | ||||
|  | ||||
|                         if (res.ok) { | ||||
|                             toast.success(res.msg); | ||||
|                         } else { | ||||
|                             toast.error(res.msg); | ||||
|                         } | ||||
|                     } | ||||
|                 ); | ||||
|             }; | ||||
|         }, | ||||
|     }, | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| @import "../../assets/vars.scss"; | ||||
|  | ||||
| .dark { | ||||
|     #importBackup { | ||||
|         &::file-selector-button { | ||||
|             color: $primary; | ||||
|             background-color: $dark-bg; | ||||
|         } | ||||
|  | ||||
|         &:hover:not(:disabled):not([readonly])::file-selector-button { | ||||
|             color: $dark-font-color2; | ||||
|             background-color: $primary; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										192
									
								
								src/components/settings/General.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										192
									
								
								src/components/settings/General.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,192 @@ | ||||
| <template> | ||||
|     <div> | ||||
|         <form class="my-4" @submit.prevent="saveGeneral"> | ||||
|             <!-- Timezone --> | ||||
|             <div class="mb-4"> | ||||
|                 <label for="timezone" class="form-label"> | ||||
|                     {{ $t("Timezone") }} | ||||
|                 </label> | ||||
|                 <select id="timezone" v-model="$root.userTimezone" class="form-select"> | ||||
|                     <option value="auto"> | ||||
|                         {{ $t("Auto") }}: {{ guessTimezone }} | ||||
|                     </option> | ||||
|                     <option | ||||
|                         v-for="(timezone, index) in timezoneList" | ||||
|                         :key="index" | ||||
|                         :value="timezone.value" | ||||
|                     > | ||||
|                         {{ timezone.name }} | ||||
|                     </option> | ||||
|                 </select> | ||||
|             </div> | ||||
|  | ||||
|             <!-- Search Engine --> | ||||
|             <div class="mb-4"> | ||||
|                 <label class="form-label"> | ||||
|                     {{ $t("Search Engine Visibility") }} | ||||
|                 </label> | ||||
|  | ||||
|                 <div class="form-check"> | ||||
|                     <input | ||||
|                         id="searchEngineIndexYes" | ||||
|                         v-model="settings.searchEngineIndex" | ||||
|                         class="form-check-input" | ||||
|                         type="radio" | ||||
|                         name="flexRadioDefault" | ||||
|                         :value="true" | ||||
|                         required | ||||
|                     /> | ||||
|                     <label class="form-check-label" for="searchEngineIndexYes"> | ||||
|                         {{ $t("Allow indexing") }} | ||||
|                     </label> | ||||
|                 </div> | ||||
|                 <div class="form-check"> | ||||
|                     <input | ||||
|                         id="searchEngineIndexNo" | ||||
|                         v-model="settings.searchEngineIndex" | ||||
|                         class="form-check-input" | ||||
|                         type="radio" | ||||
|                         name="flexRadioDefault" | ||||
|                         :value="false" | ||||
|                         required | ||||
|                     /> | ||||
|                     <label class="form-check-label" for="searchEngineIndexNo"> | ||||
|                         {{ $t("Discourage search engines from indexing site") }} | ||||
|                     </label> | ||||
|                 </div> | ||||
|             </div> | ||||
|  | ||||
|             <!-- Entry Page --> | ||||
|             <div class="mb-4"> | ||||
|                 <label class="form-label">{{ $t("Entry Page") }}</label> | ||||
|  | ||||
|                 <div class="form-check"> | ||||
|                     <input | ||||
|                         id="entryPageYes" | ||||
|                         v-model="settings.entryPage" | ||||
|                         class="form-check-input" | ||||
|                         type="radio" | ||||
|                         name="statusPage" | ||||
|                         value="dashboard" | ||||
|                         required | ||||
|                     /> | ||||
|                     <label class="form-check-label" for="entryPageYes"> | ||||
|                         {{ $t("Dashboard") }} | ||||
|                     </label> | ||||
|                 </div> | ||||
|  | ||||
|                 <div class="form-check"> | ||||
|                     <input | ||||
|                         id="entryPageNo" | ||||
|                         v-model="settings.entryPage" | ||||
|                         class="form-check-input" | ||||
|                         type="radio" | ||||
|                         name="statusPage" | ||||
|                         value="statusPage" | ||||
|                         required | ||||
|                     /> | ||||
|                     <label class="form-check-label" for="entryPageNo"> | ||||
|                         {{ $t("Status Page") }} | ||||
|                     </label> | ||||
|                 </div> | ||||
|             </div> | ||||
|  | ||||
|             <!-- Primary Base URL --> | ||||
|             <div class="mb-4"> | ||||
|                 <label class="form-label" for="primaryBaseURL"> | ||||
|                     {{ $t("Primary Base URL") }} | ||||
|                 </label> | ||||
|  | ||||
|                 <div class="input-group mb-3"> | ||||
|                     <input | ||||
|                         id="primaryBaseURL" | ||||
|                         v-model="settings.primaryBaseURL" | ||||
|                         class="form-control" | ||||
|                         name="primaryBaseURL" | ||||
|                         placeholder="https://" | ||||
|                         pattern="https?://.+" | ||||
|                     /> | ||||
|                     <button class="btn btn-outline-primary" type="button" @click="autoGetPrimaryBaseURL"> | ||||
|                         {{ $t("Auto Get") }} | ||||
|                     </button> | ||||
|                 </div> | ||||
|  | ||||
|                 <div class="form-text"></div> | ||||
|             </div> | ||||
|  | ||||
|             <!-- Steam API Key --> | ||||
|             <div class="mb-4"> | ||||
|                 <label class="form-label" for="steamAPIKey"> | ||||
|                     {{ $t("Steam API Key") }} | ||||
|                 </label> | ||||
|                 <HiddenInput | ||||
|                     id="steamAPIKey" | ||||
|                     v-model="settings.steamAPIKey" | ||||
|                     autocomplete="one-time-code" | ||||
|                 /> | ||||
|                 <div class="form-text"> | ||||
|                     {{ $t("steamApiKeyDescription") }} | ||||
|                     <a href="https://steamcommunity.com/dev" target="_blank"> | ||||
|                         https://steamcommunity.com/dev | ||||
|                     </a> | ||||
|                 </div> | ||||
|             </div> | ||||
|  | ||||
|             <!-- Save Button --> | ||||
|             <div> | ||||
|                 <button class="btn btn-primary" type="submit"> | ||||
|                     {{ $t("Save") }} | ||||
|                 </button> | ||||
|             </div> | ||||
|         </form> | ||||
|     </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import HiddenInput from "../../components/HiddenInput.vue"; | ||||
| import dayjs from "dayjs"; | ||||
| import utc from "dayjs/plugin/utc"; | ||||
| import timezone from "dayjs/plugin/timezone"; | ||||
| import { timezoneList } from "../../util-frontend"; | ||||
| dayjs.extend(utc); | ||||
| dayjs.extend(timezone); | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|         HiddenInput, | ||||
|     }, | ||||
|  | ||||
|     data() { | ||||
|         return { | ||||
|             timezoneList: timezoneList(), | ||||
|         }; | ||||
|     }, | ||||
|  | ||||
|     computed: { | ||||
|         settings() { | ||||
|             return this.$parent.$parent.$parent.settings; | ||||
|         }, | ||||
|         saveSettings() { | ||||
|             return this.$parent.$parent.$parent.saveSettings; | ||||
|         }, | ||||
|         settingsLoaded() { | ||||
|             return this.$parent.$parent.$parent.settingsLoaded; | ||||
|         }, | ||||
|         guessTimezone() { | ||||
|             return dayjs.tz.guess(); | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     methods: { | ||||
|         saveGeneral() { | ||||
|             localStorage.timezone = this.$root.userTimezone; | ||||
|             this.saveSettings(); | ||||
|         }, | ||||
|         autoGetPrimaryBaseURL() { | ||||
|             this.settings.primaryBaseURL = location.protocol + "//" + location.host; | ||||
|         }, | ||||
|     }, | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style></style> | ||||
							
								
								
									
										133
									
								
								src/components/settings/MonitorHistory.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								src/components/settings/MonitorHistory.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,133 @@ | ||||
| <template> | ||||
|     <div> | ||||
|         <div class="my-4"> | ||||
|             <label for="keepDataPeriodDays" class="form-label"> | ||||
|                 {{ | ||||
|                     $t("clearDataOlderThan", [ | ||||
|                         settings.keepDataPeriodDays, | ||||
|                     ]) | ||||
|                 }} | ||||
|             </label> | ||||
|             <input | ||||
|                 id="keepDataPeriodDays" | ||||
|                 v-model="settings.keepDataPeriodDays" | ||||
|                 type="number" | ||||
|                 class="form-control" | ||||
|                 required | ||||
|                 min="1" | ||||
|                 step="1" | ||||
|             /> | ||||
|         </div> | ||||
|         <div class="my-4"> | ||||
|             <button class="btn btn-primary" type="button" @click="saveSettings()"> | ||||
|                 {{ $t("Save") }} | ||||
|             </button> | ||||
|         </div> | ||||
|         <div class="my-4"> | ||||
|             <div class="my-3"> | ||||
|                 <button class="btn btn-outline-info me-2" @click="shrinkDatabase"> | ||||
|                     {{ $t("Shrink Database") }} ({{ databaseSizeDisplay }}) | ||||
|                 </button> | ||||
|                 <div class="form-text mt-2 mb-4 ms-2">{{ $t("shrinkDatabaseDescription") }}</div> | ||||
|             </div> | ||||
|             <button | ||||
|                 id="clearAllStats-btn" | ||||
|                 class="btn btn-outline-danger me-2 mb-2" | ||||
|                 @click="confirmClearStatistics" | ||||
|             > | ||||
|                 {{ $t("Clear all statistics") }} | ||||
|             </button> | ||||
|         </div> | ||||
|         <Confirm | ||||
|             ref="confirmClearStatistics" | ||||
|             btn-style="btn-danger" | ||||
|             :yes-text="$t('Yes')" | ||||
|             :no-text="$t('No')" | ||||
|             @yes="clearStatistics" | ||||
|         > | ||||
|             {{ $t("confirmClearStatisticsMsg") }} | ||||
|         </Confirm> | ||||
|     </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import Confirm from "../../components/Confirm.vue"; | ||||
| import { debug } from "../../util.ts"; | ||||
| import { useToast } from "vue-toastification"; | ||||
|  | ||||
| const toast = useToast(); | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|         Confirm, | ||||
|     }, | ||||
|  | ||||
|     data() { | ||||
|         return { | ||||
|             databaseSize: 0, | ||||
|         }; | ||||
|     }, | ||||
|  | ||||
|     computed: { | ||||
|         settings() { | ||||
|             return this.$parent.$parent.$parent.settings; | ||||
|         }, | ||||
|         saveSettings() { | ||||
|             return this.$parent.$parent.$parent.saveSettings; | ||||
|         }, | ||||
|         settingsLoaded() { | ||||
|             return this.$parent.$parent.$parent.settingsLoaded; | ||||
|         }, | ||||
|         databaseSizeDisplay() { | ||||
|             return ( | ||||
|                 Math.round((this.databaseSize / 1024 / 1024) * 10) / 10 + " MB" | ||||
|             ); | ||||
|         }, | ||||
|     }, | ||||
|  | ||||
|     mounted() { | ||||
|         this.loadDatabaseSize(); | ||||
|     }, | ||||
|  | ||||
|     methods: { | ||||
|         loadDatabaseSize() { | ||||
|             debug("load database size"); | ||||
|             this.$root.getSocket().emit("getDatabaseSize", (res) => { | ||||
|                 if (res.ok) { | ||||
|                     this.databaseSize = res.size; | ||||
|                     debug("database size: " + res.size); | ||||
|                 } else { | ||||
|                     debug(res); | ||||
|                 } | ||||
|             }); | ||||
|         }, | ||||
|  | ||||
|         shrinkDatabase() { | ||||
|             this.$root.getSocket().emit("shrinkDatabase", (res) => { | ||||
|                 if (res.ok) { | ||||
|                     this.loadDatabaseSize(); | ||||
|                     toast.success("Done"); | ||||
|                 } else { | ||||
|                     debug(res); | ||||
|                 } | ||||
|             }); | ||||
|         }, | ||||
|  | ||||
|         confirmClearStatistics() { | ||||
|             this.$refs.confirmClearStatistics.show(); | ||||
|         }, | ||||
|  | ||||
|         clearStatistics() { | ||||
|             this.$root.clearStatistics((res) => { | ||||
|                 if (res.ok) { | ||||
|                     this.$router.go(); | ||||
|                 } else { | ||||
|                     toast.error(res.msg); | ||||
|                 } | ||||
|             }); | ||||
|         }, | ||||
|     }, | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style></style> | ||||
							
								
								
									
										46
									
								
								src/components/settings/Notifications.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/components/settings/Notifications.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| <template> | ||||
|     <div> | ||||
|         <div class="notification-list my-4"> | ||||
|             <p v-if="$root.notificationList.length === 0"> | ||||
|                 {{ $t("Not available, please setup.") }} | ||||
|             </p> | ||||
|             <p v-else> | ||||
|                 {{ $t("notificationDescription") }} | ||||
|             </p> | ||||
|  | ||||
|             <ul class="list-group mb-3" style="border-radius: 1rem;"> | ||||
|                 <li v-for="(notification, index) in $root.notificationList" :key="index" class="list-group-item"> | ||||
|                     {{ notification.name }}<br> | ||||
|                     <a href="#" @click="$refs.notificationDialog.show(notification.id)">{{ $t("Edit") }}</a> | ||||
|                 </li> | ||||
|             </ul> | ||||
|  | ||||
|             <button class="btn btn-primary me-2" type="button" @click="$refs.notificationDialog.show()"> | ||||
|                 {{ $t("Setup Notification") }} | ||||
|             </button> | ||||
|         </div> | ||||
|  | ||||
|         <NotificationDialog ref="notificationDialog" /> | ||||
|     </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import NotificationDialog from "../../components/NotificationDialog.vue"; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|         NotificationDialog | ||||
|     }, | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| @import "../../assets/vars.scss"; | ||||
|  | ||||
| .dark { | ||||
|     .list-group-item { | ||||
|         background-color: $dark-bg2; | ||||
|         color: $dark-font-color; | ||||
|     } | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										323
									
								
								src/components/settings/Security.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										323
									
								
								src/components/settings/Security.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,323 @@ | ||||
| <template> | ||||
|     <div> | ||||
|         <div v-if="settingsLoaded" class="my-4"> | ||||
|             <!-- Change Password --> | ||||
|             <template v-if="!settings.disableAuth"> | ||||
|                 <p> | ||||
|                     {{ $t("Current User") }}: <strong>{{ username }}</strong> | ||||
|                     <button v-if="! settings.disableAuth" id="logout-btn" class="btn btn-danger ms-4 me-2 mb-2" @click="$root.logout">{{ $t("Logout") }}</button> | ||||
|                 </p> | ||||
|  | ||||
|                 <h5 class="my-4">{{ $t("Change Password") }}</h5> | ||||
|                 <form class="mb-3" @submit.prevent="savePassword"> | ||||
|                     <div class="mb-3"> | ||||
|                         <label for="current-password" class="form-label"> | ||||
|                             {{ $t("Current Password") }} | ||||
|                         </label> | ||||
|                         <input | ||||
|                             id="current-password" | ||||
|                             v-model="password.currentPassword" | ||||
|                             type="password" | ||||
|                             class="form-control" | ||||
|                             required | ||||
|                         /> | ||||
|                     </div> | ||||
|  | ||||
|                     <div class="mb-3"> | ||||
|                         <label for="new-password" class="form-label"> | ||||
|                             {{ $t("New Password") }} | ||||
|                         </label> | ||||
|                         <input | ||||
|                             id="new-password" | ||||
|                             v-model="password.newPassword" | ||||
|                             type="password" | ||||
|                             class="form-control" | ||||
|                             required | ||||
|                         /> | ||||
|                     </div> | ||||
|  | ||||
|                     <div class="mb-3"> | ||||
|                         <label for="repeat-new-password" class="form-label"> | ||||
|                             {{ $t("Repeat New Password") }} | ||||
|                         </label> | ||||
|                         <input | ||||
|                             id="repeat-new-password" | ||||
|                             v-model="password.repeatNewPassword" | ||||
|                             type="password" | ||||
|                             class="form-control" | ||||
|                             :class="{ 'is-invalid': invalidPassword }" | ||||
|                             required | ||||
|                         /> | ||||
|                         <div class="invalid-feedback"> | ||||
|                             {{ $t("passwordNotMatchMsg") }} | ||||
|                         </div> | ||||
|                     </div> | ||||
|  | ||||
|                     <div> | ||||
|                         <button class="btn btn-primary" type="submit"> | ||||
|                             {{ $t("Update Password") }} | ||||
|                         </button> | ||||
|                     </div> | ||||
|                 </form> | ||||
|             </template> | ||||
|  | ||||
|             <div v-if="! settings.disableAuth" class="mt-5 mb-3"> | ||||
|                 <h5 class="my-4"> | ||||
|                     {{ $t("Two Factor Authentication") }} | ||||
|                 </h5> | ||||
|                 <div class="mb-4"> | ||||
|                     <button | ||||
|                         class="btn btn-primary me-2" | ||||
|                         type="button" | ||||
|                         @click="$refs.TwoFADialog.show()" | ||||
|                     > | ||||
|                         {{ $t("2FA Settings") }} | ||||
|                     </button> | ||||
|                 </div> | ||||
|             </div> | ||||
|  | ||||
|             <div class="my-4"> | ||||
|                 <!-- Advanced --> | ||||
|                 <h5 class="my-4">{{ $t("Advanced") }}</h5> | ||||
|  | ||||
|                 <div class="mb-4"> | ||||
|                     <button v-if="settings.disableAuth" id="enableAuth-btn" class="btn btn-outline-primary me-2 mb-2" @click="enableAuth">{{ $t("Enable Auth") }}</button> | ||||
|                     <button v-if="! settings.disableAuth" id="disableAuth-btn" class="btn btn-primary me-2 mb-2" @click="confirmDisableAuth">{{ $t("Disable Auth") }}</button> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|  | ||||
|         <TwoFADialog ref="TwoFADialog" /> | ||||
|  | ||||
|         <Confirm ref="confirmDisableAuth" btn-style="btn-danger" :yes-text="$t('I understand, please disable')" :no-text="$t('Leave')" @yes="disableAuth"> | ||||
|             <template v-if="$i18n.locale === 'es-ES' "> | ||||
|                 <p>Seguro que deseas <strong>deshabilitar la autenticación</strong>?</p> | ||||
|                 <p>Es para <strong>quien implementa autenticación de terceros</strong> ante Uptime Kuma como por ejemplo Cloudflare Access.</p> | ||||
|                 <p>Por favor usar con cuidado.</p> | ||||
|             </template> | ||||
|  | ||||
|             <template v-else-if="$i18n.locale === 'pt-BR' "> | ||||
|                 <p>Você tem certeza que deseja <strong>desativar a autenticação</strong>?</p> | ||||
|                 <p>Isso é para <strong>alguém que tem autenticação de terceiros</strong> na frente do 'UpTime Kuma' como o Cloudflare Access.</p> | ||||
|                 <p>Por favor, utilize isso com cautela.</p> | ||||
|             </template> | ||||
|  | ||||
|             <template v-else-if="$i18n.locale === 'zh-HK' "> | ||||
|                 <p>你是否確認<strong>取消登入認証</strong>?</p> | ||||
|                 <p>這個功能是設計給已有<strong>第三方認証</strong>的用家,例如 Cloudflare Access。</p> | ||||
|                 <p>請小心使用。</p> | ||||
|             </template> | ||||
|  | ||||
|             <template v-else-if="$i18n.locale === 'zh-CN' "> | ||||
|                 <p>是否确定 <strong>取消登录验证</strong>?</p> | ||||
|                 <p>这是为 <strong>有第三方认证</strong> 的用户提供的功能,如 Cloudflare Access</p> | ||||
|                 <p>请谨慎使用!</p> | ||||
|             </template> | ||||
|  | ||||
|             <template v-else-if="$i18n.locale === 'zh-TW' "> | ||||
|                 <p>你是否要<strong>取消登入驗證</strong>?</p> | ||||
|                 <p>此功能是設計給已有<strong>第三方認證</strong>的使用者,例如 Cloudflare Access。</p> | ||||
|                 <p>請謹慎使用。</p> | ||||
|             </template> | ||||
|  | ||||
|             <template v-else-if="$i18n.locale === 'de-DE' "> | ||||
|                 <p>Bist du sicher das du die <strong>Authentifizierung deaktivieren</strong> möchtest?</p> | ||||
|                 <p>Es ist für <strong>jemanden der eine externe Authentifizierung</strong> vor Uptime Kuma geschaltet hat, wie z.B. Cloudflare Access.</p> | ||||
|                 <p>Bitte mit Vorsicht nutzen.</p> | ||||
|             </template> | ||||
|  | ||||
|             <template v-else-if="$i18n.locale === 'sr' "> | ||||
|                 <p>Да ли сте сигурни да желите да <strong>искључите аутентификацију</strong>?</p> | ||||
|                 <p>То је за <strong>оне који имају додату аутентификацију</strong> испред Uptime Kuma као на пример Cloudflare Access.</p> | ||||
|                 <p>Молим Вас користите ово са пажњом.</p> | ||||
|             </template> | ||||
|  | ||||
|             <template v-else-if="$i18n.locale === 'sr-latn' "> | ||||
|                 <p>Da li ste sigurni da želite da <strong>isključite autentifikaciju</strong>?</p> | ||||
|                 <p>To je za <strong>one koji imaju dodatu autentifikaciju</strong> ispred Uptime Kuma kao na primer Cloudflare Access.</p> | ||||
|                 <p>Molim Vas koristite ovo sa pažnjom.</p> | ||||
|             </template> | ||||
|  | ||||
|             <template v-if="$i18n.locale === 'hr-HR' "> | ||||
|                 <p>Jeste li sigurni da želite <strong>isključiti autentikaciju</strong>?</p> | ||||
|                 <p>To je za <strong>korisnike koji imaju vanjsku autentikaciju stranice</strong> ispred Uptime Kume, poput usluge Cloudflare Access.</p> | ||||
|                 <p>Pažljivo koristite ovu opciju.</p> | ||||
|             </template> | ||||
|  | ||||
|             <template v-else-if="$i18n.locale === 'tr-TR' "> | ||||
|                 <p><strong>Şifreli girişi devre dışı bırakmak istediğinizden</strong>emin misiniz?</p> | ||||
|                 <p>Bu, Uptime Kuma'nın önünde Cloudflare Access gibi <strong>üçüncü taraf yetkilendirmesi olan</strong> kişiler içindir.</p> | ||||
|                 <p>Lütfen dikkatli kullanın.</p> | ||||
|             </template> | ||||
|  | ||||
|             <template v-else-if="$i18n.locale === 'ko-KR' "> | ||||
|                 <p>정말로 <strong>인증 기능을 끌까요</strong>?</p> | ||||
|                 <p>이 기능은 <strong>Cloudflare Access와 같은 서드파티 인증</strong>을 Uptime Kuma 앞에 둔 사용자를 위한 기능이에요.</p> | ||||
|                 <p>신중하게 사용하세요.</p> | ||||
|             </template> | ||||
|  | ||||
|             <template v-else-if="$i18n.locale === 'pl' "> | ||||
|                 <p>Czy na pewno chcesz <strong>wyłączyć autoryzację</strong>?</p> | ||||
|                 <p>Jest przeznaczony dla <strong>kogoś, kto ma autoryzację zewnętrzną</strong> przed Uptime Kuma, taką jak Cloudflare Access.</p> | ||||
|                 <p>Proszę używać ostrożnie.</p> | ||||
|             </template> | ||||
|  | ||||
|             <template v-else-if="$i18n.locale === 'et-EE' "> | ||||
|                 <p>Kas soovid <strong>lülitada autentimise välja</strong>?</p> | ||||
|                 <p>Kastuamiseks <strong>välise autentimispakkujaga</strong>, näiteks Cloudflare Access.</p> | ||||
|                 <p>Palun kasuta vastutustundlikult.</p> | ||||
|             </template> | ||||
|  | ||||
|             <template v-else-if="$i18n.locale === 'it-IT' "> | ||||
|                 <p>Si è certi di voler <strong>disabilitare l'autenticazione</strong>?</p> | ||||
|                 <p>È per <strong>chi ha l'autenticazione gestita da terze parti</strong> messa davanti ad Uptime Kuma, ad esempio Cloudflare Access.</p> | ||||
|                 <p>Utilizzare con attenzione.</p> | ||||
|             </template> | ||||
|  | ||||
|             <template v-else-if="$i18n.locale === 'id-ID' "> | ||||
|                 <p>Apakah Anda yakin ingin <strong>menonaktifkan autentikasi</strong>?</p> | ||||
|                 <p>Ini untuk <strong>mereka yang memiliki autentikasi pihak ketiga</strong> diletakkan di depan Uptime Kuma, misalnya akses Cloudflare.</p> | ||||
|                 <p>Gunakan dengan hati-hati.</p> | ||||
|             </template> | ||||
|  | ||||
|             <template v-else-if="$i18n.locale === 'ru-RU' "> | ||||
|                 <p>Вы уверены, что хотите <strong>отключить авторизацию</strong>?</p> | ||||
|                 <p>Это подходит для <strong>тех, у кого стоит другая авторизация</strong> перед открытием Uptime Kuma, например Cloudflare Access.</p> | ||||
|                 <p>Пожалуйста, используйте с осторожностью.</p> | ||||
|             </template> | ||||
|  | ||||
|             <template v-else-if="$i18n.locale === 'fa' "> | ||||
|                 <p>آیا مطمئن هستید که میخواهید <strong>احراز هویت را غیر فعال کنید</strong>?</p> | ||||
|                 <p>این ویژگی برای کسانی است که <strong> لایه امنیتی شخص ثالث دیگر بر روی این آدرس فعال کردهاند</strong>، مانند Cloudflare Access.</p> | ||||
|                 <p>لطفا از این امکان با دقت استفاده کنید.</p> | ||||
|             </template> | ||||
|  | ||||
|             <template v-else-if="$i18n.locale === 'bg-BG' "> | ||||
|                 <p>Сигурни ли сте, че желаете да <strong>изключите удостоверяването</strong>?</p> | ||||
|                 <p>Използва се в случаите, когато <strong>има настроен алтернативен метод за удостоверяване</strong> преди Uptime Kuma, например Cloudflare Access.</p> | ||||
|                 <p>Моля, използвайте с повишено внимание.</p> | ||||
|             </template> | ||||
|  | ||||
|             <template v-else-if="$i18n.locale === 'hu' "> | ||||
|                 <p>Biztos benne, hogy <strong>kikapcsolja a hitelesítést</strong>?</p> | ||||
|                 <p>Akkor érdemes, ha <strong>van 3rd-party hitelesítés</strong> az Uptime Kuma-t megelőzően mint a Cloudflare Access.</p> | ||||
|                 <p>Használja megfontoltan!</p> | ||||
|             </template> | ||||
|  | ||||
|             <template v-else-if="$i18n.locale === 'nb-NO' "> | ||||
|                 <p>Er du sikker på at du vil <strong>deaktiver autentisering</strong>?</p> | ||||
|                 <p>Dette er for <strong>de som har tredjepartsautorisering</strong> foran Uptime Kuma, for eksempel Cloudflare Access.</p> | ||||
|                 <p>Vennligst vær forsiktig.</p> | ||||
|             </template> | ||||
|  | ||||
|             <!-- English (en) --> | ||||
|             <template v-else> | ||||
|                 <p>Are you sure want to <strong>disable auth</strong>?</p> | ||||
|                 <p>It is for <strong>someone who have 3rd-party auth</strong> in front of Uptime Kuma such as Cloudflare Access.</p> | ||||
|                 <p>Please use it carefully.</p> | ||||
|             </template> | ||||
|         </Confirm> | ||||
|     </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import Confirm from "../../components/Confirm.vue"; | ||||
| import TwoFADialog from "../../components/TwoFADialog.vue"; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|         Confirm, | ||||
|         TwoFADialog | ||||
|     }, | ||||
|  | ||||
|     data() { | ||||
|         return { | ||||
|             username: "", | ||||
|             invalidPassword: false, | ||||
|             password: { | ||||
|                 currentPassword: "", | ||||
|                 newPassword: "", | ||||
|                 repeatNewPassword: "", | ||||
|             } | ||||
|         }; | ||||
|     }, | ||||
|  | ||||
|     computed: { | ||||
|         settings() { | ||||
|             return this.$parent.$parent.$parent.settings; | ||||
|         }, | ||||
|         saveSettings() { | ||||
|             return this.$parent.$parent.$parent.saveSettings; | ||||
|         }, | ||||
|         settingsLoaded() { | ||||
|             return this.$parent.$parent.$parent.settingsLoaded; | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     watch: { | ||||
|         "password.repeatNewPassword"() { | ||||
|             this.invalidPassword = false; | ||||
|         }, | ||||
|     }, | ||||
|  | ||||
|     mounted() { | ||||
|         this.loadUsername(); | ||||
|     }, | ||||
|  | ||||
|     methods: { | ||||
|         savePassword() { | ||||
|             if (this.password.newPassword !== this.password.repeatNewPassword) { | ||||
|                 this.invalidPassword = true; | ||||
|             } else { | ||||
|                 this.$root | ||||
|                     .getSocket() | ||||
|                     .emit("changePassword", this.password, (res) => { | ||||
|                         this.$root.toastRes(res); | ||||
|                         if (res.ok) { | ||||
|                             this.password.currentPassword = ""; | ||||
|                             this.password.newPassword = ""; | ||||
|                             this.password.repeatNewPassword = ""; | ||||
|                         } | ||||
|                     }); | ||||
|             } | ||||
|         }, | ||||
|  | ||||
|         loadUsername() { | ||||
|             const jwtPayload = this.$root.getJWTPayload(); | ||||
|  | ||||
|             if (jwtPayload) { | ||||
|                 this.username = jwtPayload.username; | ||||
|             } | ||||
|         }, | ||||
|  | ||||
|         disableAuth() { | ||||
|             this.settings.disableAuth = true; | ||||
|             this.saveSettings(); | ||||
|         }, | ||||
|  | ||||
|         enableAuth() { | ||||
|             this.settings.disableAuth = false; | ||||
|             this.saveSettings(); | ||||
|             this.$root.storage().removeItem("token"); | ||||
|             location.reload(); | ||||
|         }, | ||||
|  | ||||
|         confirmDisableAuth() { | ||||
|             this.$refs.confirmDisableAuth.show(); | ||||
|         }, | ||||
|  | ||||
|     }, | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| @import "../../assets/vars.scss"; | ||||
|  | ||||
| h5:after { | ||||
|     content: ""; | ||||
|     display: block; | ||||
|     width: 50%; | ||||
|     padding-top: 8px; | ||||
|     border-bottom: 1px solid $dark-border-color; | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										91
									
								
								src/i18n.js
									
									
									
									
									
								
							
							
						
						
									
										91
									
								
								src/i18n.js
									
									
									
									
									
								
							| @@ -1,62 +1,45 @@ | ||||
| import { createI18n } from "vue-i18n/index"; | ||||
| import daDK from "./languages/da-DK"; | ||||
| import deDE from "./languages/de-DE"; | ||||
| import en from "./languages/en"; | ||||
| import esEs from "./languages/es-ES"; | ||||
| import etEE from "./languages/et-EE"; | ||||
| import fa from "./languages/fa"; | ||||
| import frFR from "./languages/fr-FR"; | ||||
| import hu from "./languages/hu"; | ||||
| import hrHR from "./languages/hr-HR"; | ||||
| import itIT from "./languages/it-IT"; | ||||
| import idID from "./languages/id-ID"; | ||||
| import ja from "./languages/ja"; | ||||
| import koKR from "./languages/ko-KR"; | ||||
| import nlNL from "./languages/nl-NL"; | ||||
| import nbNO from "./languages/nb-NO"; | ||||
| import pl from "./languages/pl"; | ||||
| import ptBR from "./languages/pt-BR"; | ||||
| import bgBG from "./languages/bg-BG"; | ||||
| import ruRU from "./languages/ru-RU"; | ||||
| import sr from "./languages/sr"; | ||||
| import srLatn from "./languages/sr-latn"; | ||||
| import svSE from "./languages/sv-SE"; | ||||
| import trTR from "./languages/tr-TR"; | ||||
| import vi from "./languages/vi"; | ||||
| import zhCN from "./languages/zh-CN"; | ||||
| import zhHK from "./languages/zh-HK"; | ||||
| import zhTW from "./languages/zh-TW"; | ||||
|  | ||||
| const languageList = { | ||||
|     en, | ||||
|     "zh-HK": zhHK, | ||||
|     "bg-BG": bgBG, | ||||
|     "de-DE": deDE, | ||||
|     "nl-NL": nlNL, | ||||
|     "nb-NO": nbNO, | ||||
|     "es-ES": esEs, | ||||
|     "fa": fa, | ||||
|     "pt-BR": ptBR, | ||||
|     "fr-FR": frFR, | ||||
|     "hu": hu, | ||||
|     "hr-HR": hrHR, | ||||
|     "it-IT": itIT, | ||||
|     "id-ID" : idID, | ||||
|     "ja": ja, | ||||
|     "da-DK": daDK, | ||||
|     "sr": sr, | ||||
|     "sr-latn": srLatn, | ||||
|     "sv-SE": svSE, | ||||
|     "tr-TR": trTR, | ||||
|     "ko-KR": koKR, | ||||
|     "ru-RU": ruRU, | ||||
|     "zh-CN": zhCN, | ||||
|     "pl": pl, | ||||
|     "et-EE": etEE, | ||||
|     "vi": vi, | ||||
|     "zh-TW": zhTW | ||||
|     "zh-HK": "繁體中文 (香港)", | ||||
|     "bg-BG": "Български", | ||||
|     "de-DE": "Deutsch (Deutschland)", | ||||
|     "nl-NL": "Nederlands", | ||||
|     "nb-NO": "Norsk", | ||||
|     "es-ES": "Español", | ||||
|     "fa": "Farsi", | ||||
|     "pt-BR": "Português (Brasileiro)", | ||||
|     "fr-FR": "Français (France)", | ||||
|     "hu": "Magyar", | ||||
|     "hr-HR": "Hrvatski", | ||||
|     "it-IT": "Italiano (Italian)", | ||||
|     "id-ID": "Bahasa Indonesia (Indonesian)", | ||||
|     "ja": "日本語", | ||||
|     "da-DK": "Danish (Danmark)", | ||||
|     "sr": "Српски", | ||||
|     "sr-latn": "Srpski", | ||||
|     "sv-SE": "Svenska", | ||||
|     "tr-TR": "Türkçe", | ||||
|     "ko-KR": "한국어", | ||||
|     "ru-RU": "Русский", | ||||
|     "zh-CN": "简体中文", | ||||
|     "pl": "Polski", | ||||
|     "et-EE": "eesti", | ||||
|     "vi": "Vietnamese", | ||||
|     "zh-TW": "繁體中文 (台灣)" | ||||
| }; | ||||
|  | ||||
| let messages = { | ||||
|     en, | ||||
| }; | ||||
|  | ||||
| for (let lang in languageList) { | ||||
|     messages[lang] = { | ||||
|         languageName: languageList[lang] | ||||
|     }; | ||||
| } | ||||
|  | ||||
| const rtlLangs = ["fa"]; | ||||
|  | ||||
| export const currentLocale = () => localStorage.locale | ||||
| @@ -73,5 +56,5 @@ export const i18n = createI18n({ | ||||
|     fallbackLocale: "en", | ||||
|     silentFallbackWarn: true, | ||||
|     silentTranslationWarn: true, | ||||
|     messages: languageList, | ||||
|     messages: messages, | ||||
| }); | ||||
|   | ||||
| @@ -4,11 +4,8 @@ | ||||
| 2. Create a language file (e.g. `zh-TW.js`). The filename must be ISO language code: http://www.lingoes.net/en/translator/langcode.htm | ||||
| 3. Run `npm run update-language-files`. You can also use this command to check if there are new strings to translate for your language. | ||||
| 4. Your language file should be filled in. You can translate now. | ||||
| 5. Translate `src/pages/Settings.vue` (search for a `Confirm` component with `rel="confirmDisableAuth"`). | ||||
| 6. Import your language file in `src/i18n.js` and add it to `languageList` constant. | ||||
| 5. Translate `src/components/settings/Security.vue` (search for a `Confirm` component with `rel="confirmDisableAuth"`). | ||||
| 6. Add it into `languageList` constant. | ||||
| 7. Make a [pull request](https://github.com/louislam/uptime-kuma/pulls) when you have done. | ||||
|  | ||||
| One of good examples: | ||||
| https://github.com/louislam/uptime-kuma/pull/316/files | ||||
|  | ||||
| If you do not have programming skills, let me know in [Issues section](https://github.com/louislam/uptime-kuma/issues). I will assist you. 😏 | ||||
|   | ||||
| @@ -89,7 +89,7 @@ export default { | ||||
|     Timezone: "Часова зона", | ||||
|     "Search Engine Visibility": "Видимост за търсачки", | ||||
|     "Allow indexing": "Разреши индексиране", | ||||
|     "Discourage search engines from indexing site": "Обезкуражи индексирането на сайта от търсачките", | ||||
|     "Discourage search engines from indexing site": "Не позволявай на търсачките да индексират този сайт", | ||||
|     "Change Password": "Промени парола", | ||||
|     "Current Password": "Текуща парола", | ||||
|     "New Password": "Нова парола", | ||||
| @@ -307,4 +307,5 @@ export default { | ||||
|     PasswordsDoNotMatch: "Паролите не съвпадат.", | ||||
|     "Current User": "Текущ потребител", | ||||
|     recent: "Скорошни", | ||||
|     shrinkDatabaseDescription: "Инициира \"VACUUM\" за \"SQLite\" база данни. Ако Вашата база данни е създадена след версия 1.10.0, \"AUTO_VACUUM\" функцията е активна и това действие не нужно.", | ||||
| }; | ||||
|   | ||||
| @@ -307,4 +307,10 @@ export default { | ||||
|     steamApiKeyDescription: "For monitoring a Steam Game Server you need a Steam Web-API key. You can register your API key here: ", | ||||
|     "Current User": "Current User", | ||||
|     recent: "Recent", | ||||
|     shrinkDatabaseDescription: "Trigger database VACUUM for SQLite. If your database is created after 1.10.0, AUTO_VACUUM is already enabled and this action is not needed.", | ||||
|     serwersms: "SerwerSMS.pl", | ||||
|     serwersmsAPIUser: "API Username (incl. webapi_ prefix)", | ||||
|     serwersmsAPIPassword: "API Password", | ||||
|     serwersmsPhoneNumber: "Phone number", | ||||
|     serwersmsSenderName: "SMS Sender Name (registered via customer portal)", | ||||
| }; | ||||
|   | ||||
| @@ -136,7 +136,7 @@ export default { | ||||
|     Heartbeats: "Controlli", | ||||
|     "Auto Get": "Auto Get", | ||||
|     backupDescription: "È possibile fare il backup di tutti i monitoraggi e di tutte le notifiche in un file JSON.", | ||||
|     backupDescription2: "P.S.: lo storico e i dati relativi agli eventi non saranno inclusi.", | ||||
|     backupDescription2: "P.S.: lo storico e i dati relativi agli eventi non saranno inclusi", | ||||
|     backupDescription3: "Dati sensibili come i token di autenticazione saranno inclusi nel backup, tenere quindi in un luogo sicuro.", | ||||
|     alertNoFile: "Selezionare il file da importare.", | ||||
|     alertWrongFileType: "Selezionare un file JSON.", | ||||
| @@ -172,7 +172,7 @@ export default { | ||||
|     "Search...": "Cerca...", | ||||
|     "Avg. Ping": "Tempo di risposta al ping medio", | ||||
|     "Avg. Response": "Tempo di risposta medio", | ||||
|     "Entry Page": "Entry Page", | ||||
|     "Entry Page": "Pagina Principale", | ||||
|     statusPageNothing: "Non c'è nulla qui, aggiungere un gruppo oppure un monitoraggio.", | ||||
|     "No Services": "Nessun Servizio", | ||||
|     "All Systems Operational": "Tutti i sistemi sono funzionali", | ||||
| @@ -307,4 +307,5 @@ export default { | ||||
|     steamApiKeyDescription: "Per monitorare un server di gioco Steam si necessita della chiave Web-API di Steam. È possibile registrare la propria chiave API qui: ", | ||||
|     "Current User": "Utente corrente", | ||||
|     recent: "Recenti", | ||||
|     shrinkDatabaseDescription: "Lancia il comando VACUUM sul database SQLite. Se il database è stato creato dopo la versione 1.10.0, AUTO_VACUUM è già abilitato e questa azione non è necessaria.", | ||||
| }; | ||||
|   | ||||
| @@ -307,4 +307,9 @@ export default { | ||||
|     recent: "Ostatnie", | ||||
|     clicksendsms: "ClickSend SMS", | ||||
|     apiCredentials: "Poświadczenia API", | ||||
|     serwersms: "SerwerSMS.pl", | ||||
|     serwersmsAPIUser: "Nazwa użytkownika API (z prefiksem webapi_)", | ||||
|     serwersmsAPIPassword: "Hasło API", | ||||
|     serwersmsPhoneNumber: "Numer telefonu", | ||||
|     serwersmsSenderName: "Nazwa nadawcy (zatwierdzona w panelu klienta)", | ||||
| }; | ||||
|   | ||||
| @@ -29,7 +29,7 @@ | ||||
|                     </router-link> | ||||
|                 </li> | ||||
|                 <li v-if="$root.loggedIn" class="nav-item"> | ||||
|                     <router-link to="/settings" class="nav-link"> | ||||
|                     <router-link to="/settings" class="nav-link" :class="{ active: $route.path.includes('settings') }"> | ||||
|                         <font-awesome-icon icon="cog" /> {{ $t("Settings") }} | ||||
|                     </router-link> | ||||
|                 </li> | ||||
| @@ -188,8 +188,8 @@ main { | ||||
|  | ||||
| .dark { | ||||
|     header { | ||||
|         background-color: #161b22; | ||||
|         border-bottom-color: #161b22 !important; | ||||
|         background-color: $dark-header-bg; | ||||
|         border-bottom-color: $dark-header-bg !important; | ||||
|  | ||||
|         span { | ||||
|             color: #f0f6fc; | ||||
|   | ||||
| @@ -12,6 +12,7 @@ import mobile from "./mixins/mobile"; | ||||
| import publicMixin from "./mixins/public"; | ||||
| import socket from "./mixins/socket"; | ||||
| import theme from "./mixins/theme"; | ||||
| import lang from "./mixins/lang"; | ||||
| import { router } from "./router"; | ||||
| import { appName } from "./util.ts"; | ||||
|  | ||||
| @@ -22,6 +23,7 @@ const app = createApp({ | ||||
|         mobile, | ||||
|         datetime, | ||||
|         publicMixin, | ||||
|         lang, | ||||
|     ], | ||||
|     data() { | ||||
|         return { | ||||
|   | ||||
							
								
								
									
										33
									
								
								src/mixins/lang.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/mixins/lang.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| import { currentLocale } from "../i18n"; | ||||
| import { setPageLocale } from "../util-frontend"; | ||||
| const langModules = import.meta.glob("../languages/*.js"); | ||||
|  | ||||
| export default { | ||||
|     data() { | ||||
|         return { | ||||
|             language: currentLocale(), | ||||
|         }; | ||||
|     }, | ||||
|  | ||||
|     async created() { | ||||
|         if (this.language !== "en") { | ||||
|             await this.changeLang(this.language); | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     watch: { | ||||
|         async language(lang) { | ||||
|             await this.changeLang(lang); | ||||
|         }, | ||||
|     }, | ||||
|  | ||||
|     methods: { | ||||
|         async changeLang(lang) { | ||||
|             let message = (await langModules["../languages/" + lang + ".js"]()).default; | ||||
|             this.$i18n.setLocaleMessage(lang, message); | ||||
|             this.$i18n.locale = lang; | ||||
|             localStorage.locale = lang; | ||||
|             setPageLocale(); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| @@ -194,7 +194,7 @@ | ||||
|                             </div> | ||||
|  | ||||
|                             <div class="mt-5 mb-1"> | ||||
|                                 <button class="btn btn-primary" type="submit" :disabled="processing">{{ $t("Save") }}</button> | ||||
|                                 <button id="monitor-submit-btn" class="btn btn-primary" type="submit" :disabled="processing">{{ $t("Save") }}</button> | ||||
|                             </div> | ||||
|                         </div> | ||||
|  | ||||
| @@ -265,6 +265,19 @@ | ||||
|                                     <label for="headers" class="form-label">{{ $t("Headers") }}</label> | ||||
|                                     <textarea id="headers" v-model="monitor.headers" class="form-control" :placeholder="headersPlaceholder"></textarea> | ||||
|                                 </div> | ||||
|  | ||||
|                                 <!-- HTTP Basic Auth --> | ||||
|                                 <h4 class="mt-5 mb-2">{{ $t("HTTP Basic Auth") }}</h4> | ||||
|  | ||||
|                                 <div class="my-3"> | ||||
|                                     <label for="basicauth" class="form-label">{{ $t("Username") }}</label> | ||||
|                                     <input id="basicauth-user" v-model="monitor.basic_auth_user" type="text" class="form-control" :placeholder="$t('Username')"> | ||||
|                                 </div> | ||||
|  | ||||
|                                 <div class="my-3"> | ||||
|                                     <label for="basicauth" class="form-label">{{ $t("Password") }}</label> | ||||
|                                     <input id="basicauth-pass" v-model="monitor.basic_auth_pass" type="password" class="form-control" :placeholder="$t('Password')"> | ||||
|                                 </div> | ||||
|                             </template> | ||||
|                         </div> | ||||
|                     </div> | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| <template> | ||||
|     <transition name="slide-fade" appear> | ||||
|     <div> | ||||
|         <h1 v-show="show" class="mb-3"> | ||||
|             {{ $t("Settings") }} | ||||
| @@ -7,521 +6,88 @@ | ||||
|  | ||||
|         <div class="shadow-box"> | ||||
|             <div class="row"> | ||||
|                     <div class="col-md-6"> | ||||
|                         <h2 class="mb-2">{{ $t("Appearance") }}</h2> | ||||
|  | ||||
|                         <div class="mb-3"> | ||||
|                             <label for="language" class="form-label">{{ $t("Language") }}</label> | ||||
|                             <select id="language" v-model="$i18n.locale" class="form-select"> | ||||
|                                 <option v-for="(lang, i) in $i18n.availableLocales" :key="`Lang${i}`" :value="lang"> | ||||
|                                     {{ $i18n.messages[lang].languageName }} | ||||
|                                 </option> | ||||
|                             </select> | ||||
|                 <div class="settings-menu"> | ||||
|                     <router-link | ||||
|                         v-for="(item, key) in subMenus" | ||||
|                         :key="key" | ||||
|                         :to="`/settings/${key}`" | ||||
|                     > | ||||
|                         <div class="menu-item"> | ||||
|                             {{ item.title }} | ||||
|                         </div> | ||||
|  | ||||
|                         <div class="mb-3"> | ||||
|                             <label for="timezone" class="form-label">{{ $t("Theme") }}</label> | ||||
|  | ||||
|                             <div> | ||||
|                                 <div class="btn-group" role="group" aria-label="Basic checkbox toggle button group"> | ||||
|                                     <input id="btncheck1" v-model="$root.userTheme" type="radio" class="btn-check" name="theme" autocomplete="off" value="light"> | ||||
|                                     <label class="btn btn-outline-primary" for="btncheck1">{{ $t("Light") }}</label> | ||||
|  | ||||
|                                     <input id="btncheck2" v-model="$root.userTheme" type="radio" class="btn-check" name="theme" autocomplete="off" value="dark"> | ||||
|                                     <label class="btn btn-outline-primary" for="btncheck2">{{ $t("Dark") }}</label> | ||||
|  | ||||
|                                     <input id="btncheck3" v-model="$root.userTheme" type="radio" class="btn-check" name="theme" autocomplete="off" value="auto"> | ||||
|                                     <label class="btn btn-outline-primary" for="btncheck3">{{ $t("Auto") }}</label> | ||||
|                     </router-link> | ||||
|                 </div> | ||||
|                 <div class="settings-content"> | ||||
|                     <div class="settings-content-header"> | ||||
|                         {{ subMenus[currentPage].title }} | ||||
|                     </div> | ||||
|                         </div> | ||||
|  | ||||
|                         <div class="mb-3"> | ||||
|                             <label class="form-label">{{ $t("Theme - Heartbeat Bar") }}</label> | ||||
|                             <div> | ||||
|                                 <div class="btn-group" role="group" aria-label="Basic checkbox toggle button group"> | ||||
|                                     <input id="btncheck4" v-model="$root.userHeartbeatBar" type="radio" class="btn-check" name="heartbeatBarTheme" autocomplete="off" value="normal"> | ||||
|                                     <label class="btn btn-outline-primary" for="btncheck4">{{ $t("Normal") }}</label> | ||||
|  | ||||
|                                     <input id="btncheck5" v-model="$root.userHeartbeatBar" type="radio" class="btn-check" name="heartbeatBarTheme" autocomplete="off" value="bottom"> | ||||
|                                     <label class="btn btn-outline-primary" for="btncheck5">{{ $t("Bottom") }}</label> | ||||
|  | ||||
|                                     <input id="btncheck6" v-model="$root.userHeartbeatBar" type="radio" class="btn-check" name="heartbeatBarTheme" autocomplete="off" value="none"> | ||||
|                                     <label class="btn btn-outline-primary" for="btncheck6">{{ $t("None") }}</label> | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                         </div> | ||||
|  | ||||
|                         <!-- General Settings --> | ||||
|                         <h2 class="mt-5 mb-2">{{ $t("General") }}</h2> | ||||
|  | ||||
|                         <form class="mb-3" @submit.prevent="saveGeneral"> | ||||
|                             <!-- Timezone --> | ||||
|                             <div class="mb-4"> | ||||
|                                 <label for="timezone" class="form-label">{{ $t("Timezone") }}</label> | ||||
|                                 <select id="timezone" v-model="$root.userTimezone" class="form-select"> | ||||
|                                     <option value="auto"> | ||||
|                                         {{ $t("Auto") }}: {{ guessTimezone }} | ||||
|                                     </option> | ||||
|                                     <option v-for="(timezone, index) in timezoneList" :key="index" :value="timezone.value"> | ||||
|                                         {{ timezone.name }} | ||||
|                                     </option> | ||||
|                                 </select> | ||||
|                             </div> | ||||
|  | ||||
|                             <!-- Search Engine --> | ||||
|                             <div class="mb-4"> | ||||
|                                 <label class="form-label">{{ $t("Search Engine Visibility") }}</label> | ||||
|  | ||||
|                                 <div class="form-check"> | ||||
|                                     <input id="searchEngineIndexYes" v-model="settings.searchEngineIndex" class="form-check-input" type="radio" name="flexRadioDefault" :value="true" required> | ||||
|                                     <label class="form-check-label" for="searchEngineIndexYes"> | ||||
|                                         {{ $t("Allow indexing") }} | ||||
|                                     </label> | ||||
|                                 </div> | ||||
|                                 <div class="form-check"> | ||||
|                                     <input id="searchEngineIndexNo" v-model="settings.searchEngineIndex" class="form-check-input" type="radio" name="flexRadioDefault" :value="false" required> | ||||
|                                     <label class="form-check-label" for="searchEngineIndexNo"> | ||||
|                                         {{ $t("Discourage search engines from indexing site") }} | ||||
|                                     </label> | ||||
|                                 </div> | ||||
|                             </div> | ||||
|  | ||||
|                             <!-- Entry Page --> | ||||
|                             <div class="mb-4"> | ||||
|                                 <label class="form-label">{{ $t("Entry Page") }}</label> | ||||
|  | ||||
|                                 <div class="form-check"> | ||||
|                                     <input id="entryPageYes" v-model="settings.entryPage" class="form-check-input" type="radio" name="statusPage" value="dashboard" required> | ||||
|                                     <label class="form-check-label" for="entryPageYes"> | ||||
|                                         {{ $t("Dashboard") }} | ||||
|                                     </label> | ||||
|                                 </div> | ||||
|  | ||||
|                                 <div class="form-check"> | ||||
|                                     <input id="entryPageNo" v-model="settings.entryPage" class="form-check-input" type="radio" name="statusPage" value="statusPage" required> | ||||
|                                     <label class="form-check-label" for="entryPageNo"> | ||||
|                                         {{ $t("Status Page") }} | ||||
|                                     </label> | ||||
|                                 </div> | ||||
|                             </div> | ||||
|  | ||||
|                             <!-- Primary Base URL --> | ||||
|                             <div class="mb-4"> | ||||
|                                 <label class="form-label" for="primaryBaseURL">{{ $t("Primary Base URL") }}</label> | ||||
|  | ||||
|                                 <div class="input-group mb-3"> | ||||
|                                     <input id="primaryBaseURL" v-model="settings.primaryBaseURL" class="form-control" name="primaryBaseURL" placeholder="https://" pattern="https?://.+"> | ||||
|                                     <button class="btn btn-outline-primary" type="button" @click="autoGetPrimaryBaseURL">{{ $t("Auto Get") }}</button> | ||||
|                                 </div> | ||||
|  | ||||
|                                 <div class="form-text"> | ||||
|                                 </div> | ||||
|                             </div> | ||||
|  | ||||
|                             <!-- Steam API Key --> | ||||
|                             <div class="mb-4"> | ||||
|                                 <label class="form-label" for="steamAPIKey">{{ $t("Steam API Key") }}</label> | ||||
|                                 <HiddenInput id="steamAPIKey" v-model="settings.steamAPIKey" /> | ||||
|                                 <div class="form-text"> | ||||
|                                     {{ $t("steamApiKeyDescription") }}<a href="https://steamcommunity.com/dev" target="_blank">https://steamcommunity.com/dev</a> | ||||
|                                 </div> | ||||
|                             </div> | ||||
|  | ||||
|                             <!-- Monitor History --> | ||||
|                             <div class="mb-4"> | ||||
|                                 <h4 class="mt-4">{{ $t("Monitor History") }}</h4> | ||||
|                                 <div class="mt-2"> | ||||
|                                     <label for="keepDataPeriodDays" class="form-label">{{ $t("clearDataOlderThan", [ settings.keepDataPeriodDays ]) }}</label> | ||||
|                                     <input id="keepDataPeriodDays" v-model="settings.keepDataPeriodDays" type="number" class="form-control" required min="1" step="1"> | ||||
|                                 </div> | ||||
|                             </div> | ||||
|  | ||||
|                             <!-- Save Button --> | ||||
|                             <div> | ||||
|                                 <button class="btn btn-primary" type="submit"> | ||||
|                                     {{ $t("Save") }} | ||||
|                                 </button> | ||||
|                             </div> | ||||
|                         </form> | ||||
|  | ||||
|                         <template v-if="loaded"> | ||||
|                             <!-- Change Password --> | ||||
|                             <template v-if="! settings.disableAuth"> | ||||
|                                 <h2 class="mt-5 mb-2">{{ $t("Change Password") }}</h2> | ||||
|                                 <p>{{ $t("Current User") }}: <strong>{{ username }}</strong></p> | ||||
|                                 <form class="mb-3" @submit.prevent="savePassword"> | ||||
|                                     <div class="mb-3"> | ||||
|                                         <label for="current-password" class="form-label">{{ $t("Current Password") }}</label> | ||||
|                                         <input id="current-password" v-model="password.currentPassword" type="password" class="form-control" required> | ||||
|                                     </div> | ||||
|  | ||||
|                                     <div class="mb-3"> | ||||
|                                         <label for="new-password" class="form-label">{{ $t("New Password") }}</label> | ||||
|                                         <input id="new-password" v-model="password.newPassword" type="password" class="form-control" required> | ||||
|                                     </div> | ||||
|  | ||||
|                                     <div class="mb-3"> | ||||
|                                         <label for="repeat-new-password" class="form-label">{{ $t("Repeat New Password") }}</label> | ||||
|                                         <input id="repeat-new-password" v-model="password.repeatNewPassword" type="password" class="form-control" :class="{ 'is-invalid' : invalidPassword }" required> | ||||
|                                         <div class="invalid-feedback"> | ||||
|                                             {{ $t("passwordNotMatchMsg") }} | ||||
|                                         </div> | ||||
|                                     </div> | ||||
|  | ||||
|                                     <div> | ||||
|                                         <button class="btn btn-primary" type="submit"> | ||||
|                                             {{ $t("Update Password") }} | ||||
|                                         </button> | ||||
|                                     </div> | ||||
|                                 </form> | ||||
|                             </template> | ||||
|  | ||||
|                             <div v-if="! settings.disableAuth" class="mt-5 mb-3"> | ||||
|                                 <h2 class="mb-2"> | ||||
|                                     {{ $t("Two Factor Authentication") }} | ||||
|                                 </h2> | ||||
|                                 <button class="btn btn-primary me-2" type="button" @click="$refs.TwoFADialog.show()">{{ $t("2FA Settings") }}</button> | ||||
|                             </div> | ||||
|  | ||||
|                             <h2 class="mt-5 mb-2">{{ $t("Export Backup") }}</h2> | ||||
|  | ||||
|                             <p> | ||||
|                                 {{ $t("backupDescription") }} <br /> | ||||
|                                 ({{ $t("backupDescription2") }}) <br /> | ||||
|                             </p> | ||||
|  | ||||
|                             <div class="mb-2"> | ||||
|                                 <button class="btn btn-primary" @click="downloadBackup">{{ $t("Export") }}</button> | ||||
|                             </div> | ||||
|  | ||||
|                             <p><strong>{{ $t("backupDescription3") }}</strong></p> | ||||
|  | ||||
|                             <h2 class="mt-5 mb-2">{{ $t("Import Backup") }}</h2> | ||||
|  | ||||
|                             <label class="form-label">{{ $t("Options") }}:</label> | ||||
|                             <br> | ||||
|                             <div class="form-check form-check-inline"> | ||||
|                                 <input id="radioKeep" v-model="importHandle" class="form-check-input" type="radio" name="radioImportHandle" value="keep"> | ||||
|                                 <label class="form-check-label" for="radioKeep">{{ $t("Keep both") }}</label> | ||||
|                             </div> | ||||
|                             <div class="form-check form-check-inline"> | ||||
|                                 <input id="radioSkip" v-model="importHandle" class="form-check-input" type="radio" name="radioImportHandle" value="skip"> | ||||
|                                 <label class="form-check-label" for="radioSkip">{{ $t("Skip existing") }}</label> | ||||
|                             </div> | ||||
|                             <div class="form-check form-check-inline"> | ||||
|                                 <input id="radioOverwrite" v-model="importHandle" class="form-check-input" type="radio" name="radioImportHandle" value="overwrite"> | ||||
|                                 <label class="form-check-label" for="radioOverwrite">{{ $t("Overwrite") }}</label> | ||||
|                             </div> | ||||
|                             <div class="form-text mb-2"> | ||||
|                                 {{ $t("importHandleDescription") }} | ||||
|                             </div> | ||||
|  | ||||
|                             <div class="mb-2"> | ||||
|                                 <input id="importBackup" type="file" class="form-control" accept="application/json"> | ||||
|                             </div> | ||||
|  | ||||
|                             <div class="input-group mb-2 justify-content-end"> | ||||
|                                 <button type="button" class="btn btn-outline-primary" :disabled="processing" @click="confirmImport"> | ||||
|                                     <div v-if="processing" class="spinner-border spinner-border-sm me-1"></div> | ||||
|                                     {{ $t("Import") }} | ||||
|                                 </button> | ||||
|                             </div> | ||||
|  | ||||
|                             <div v-if="importAlert" class="alert alert-danger mt-3" style="padding: 6px 16px;"> | ||||
|                                 {{ importAlert }} | ||||
|                             </div> | ||||
|  | ||||
|                             <!-- Advanced --> | ||||
|                             <h2 class="mt-5 mb-2">{{ $t("Advanced") }}</h2> | ||||
|  | ||||
|                             <div class="mb-3"> | ||||
|                                 <button v-if="settings.disableAuth" class="btn btn-outline-primary me-2 mb-2" @click="enableAuth">{{ $t("Enable Auth") }}</button> | ||||
|                                 <button v-if="! settings.disableAuth" class="btn btn-primary me-2 mb-2" @click="confirmDisableAuth">{{ $t("Disable Auth") }}</button> | ||||
|                                 <button v-if="! settings.disableAuth" class="btn btn-danger me-2 mb-2" @click="$root.logout">{{ $t("Logout") }}</button> | ||||
|                                 <button class="btn btn-outline-danger me-2 mb-2" @click="confirmClearStatistics">{{ $t("Clear all statistics") }}</button> | ||||
|                                 <button class="btn btn-info me-2 mb-2" @click="shrinkDatabase">{{ $t("Shrink Database") }} ({{ databaseSizeDisplay }})</button> | ||||
|                             </div> | ||||
|                         </template> | ||||
|                     </div> | ||||
|  | ||||
|                     <div class="col-md-6"> | ||||
|                         <div v-if="$root.isMobile" class="mt-3" /> | ||||
|  | ||||
|                         <!-- Notifications --> | ||||
|                         <div class="notification-list "> | ||||
|                             <h2>{{ $t("Notifications") }}</h2> | ||||
|                             <p v-if="$root.notificationList.length === 0"> | ||||
|                                 {{ $t("Not available, please setup.") }} | ||||
|                             </p> | ||||
|                             <p v-else> | ||||
|                                 {{ $t("notificationDescription") }} | ||||
|                             </p> | ||||
|  | ||||
|                             <ul class="list-group mb-3" style="border-radius: 1rem;"> | ||||
|                                 <li v-for="(notification, index) in $root.notificationList" :key="index" class="list-group-item"> | ||||
|                                     {{ notification.name }}<br> | ||||
|                                     <a href="#" @click="$refs.notificationDialog.show(notification.id)">{{ $t("Edit") }}</a> | ||||
|                                 </li> | ||||
|                             </ul> | ||||
|  | ||||
|                             <button class="btn btn-primary me-2" type="button" @click="$refs.notificationDialog.show()"> | ||||
|                                 {{ $t("Setup Notification") }} | ||||
|                             </button> | ||||
|                         </div> | ||||
|  | ||||
|                         <!-- Info --> | ||||
|                         <h2 class="mt-5">{{ $t("Info") }}</h2> | ||||
|  | ||||
|                         {{ $t("Version") }}: {{ $root.info.version }} <br /> | ||||
|                         <a href="https://github.com/louislam/uptime-kuma/releases" target="_blank" rel="noopener">{{ $t("Check Update On GitHub") }}</a> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|  | ||||
|             <NotificationDialog ref="notificationDialog" /> | ||||
|             <TwoFADialog ref="TwoFADialog" /> | ||||
|  | ||||
|             <Confirm ref="confirmDisableAuth" btn-style="btn-danger" :yes-text="$t('I understand, please disable')" :no-text="$t('Leave')" @yes="disableAuth"> | ||||
|                 <template v-if="$i18n.locale === 'es-ES' "> | ||||
|                     <p>Seguro que deseas <strong>deshabilitar la autenticación</strong>?</p> | ||||
|                     <p>Es para <strong>quien implementa autenticación de terceros</strong> ante Uptime Kuma como por ejemplo Cloudflare Access.</p> | ||||
|                     <p>Por favor usar con cuidado.</p> | ||||
|                 </template> | ||||
|  | ||||
|                 <template v-else-if="$i18n.locale === 'pt-BR' "> | ||||
|                     <p>Você tem certeza que deseja <strong>desativar a autenticação</strong>?</p> | ||||
|                     <p>Isso é para <strong>alguém que tem autenticação de terceiros</strong> na frente do 'UpTime Kuma' como o Cloudflare Access.</p> | ||||
|                     <p>Por favor, utilize isso com cautela.</p> | ||||
|                 </template> | ||||
|  | ||||
|                 <template v-else-if="$i18n.locale === 'zh-HK' "> | ||||
|                     <p>你是否確認<strong>取消登入認証</strong>?</p> | ||||
|                     <p>這個功能是設計給已有<strong>第三方認証</strong>的用家,例如 Cloudflare Access。</p> | ||||
|                     <p>請小心使用。</p> | ||||
|                 </template> | ||||
|  | ||||
|                 <template v-else-if="$i18n.locale === 'zh-CN' "> | ||||
|                     <p>是否确定 <strong>取消登录验证</strong>?</p> | ||||
|                     <p>这是为 <strong>有第三方认证</strong> 的用户提供的功能,如 Cloudflare Access</p> | ||||
|                     <p>请谨慎使用!</p> | ||||
|                 </template> | ||||
|  | ||||
|                 <template v-else-if="$i18n.locale === 'zh-TW' "> | ||||
|                     <p>你是否要<strong>取消登入驗證</strong>?</p> | ||||
|                     <p>此功能是設計給已有<strong>第三方認證</strong>的使用者,例如 Cloudflare Access。</p> | ||||
|                     <p>請謹慎使用。</p> | ||||
|                 </template> | ||||
|  | ||||
|                 <template v-else-if="$i18n.locale === 'de-DE' "> | ||||
|                     <p>Bist du sicher das du die <strong>Authentifizierung deaktivieren</strong> möchtest?</p> | ||||
|                     <p>Es ist für <strong>jemanden der eine externe Authentifizierung</strong> vor Uptime Kuma geschaltet hat, wie z.B. Cloudflare Access.</p> | ||||
|                     <p>Bitte mit Vorsicht nutzen.</p> | ||||
|                 </template> | ||||
|  | ||||
|                 <template v-else-if="$i18n.locale === 'sr' "> | ||||
|                     <p>Да ли сте сигурни да желите да <strong>искључите аутентификацију</strong>?</p> | ||||
|                     <p>То је за <strong>оне који имају додату аутентификацију</strong> испред Uptime Kuma као на пример Cloudflare Access.</p> | ||||
|                     <p>Молим Вас користите ово са пажњом.</p> | ||||
|                 </template> | ||||
|  | ||||
|                 <template v-else-if="$i18n.locale === 'sr-latn' "> | ||||
|                     <p>Da li ste sigurni da želite da <strong>isključite autentifikaciju</strong>?</p> | ||||
|                     <p>To je za <strong>one koji imaju dodatu autentifikaciju</strong> ispred Uptime Kuma kao na primer Cloudflare Access.</p> | ||||
|                     <p>Molim Vas koristite ovo sa pažnjom.</p> | ||||
|                 </template> | ||||
|  | ||||
|                 <template v-if="$i18n.locale === 'hr-HR' "> | ||||
|                     <p>Jeste li sigurni da želite <strong>isključiti autentikaciju</strong>?</p> | ||||
|                     <p>To je za <strong>korisnike koji imaju vanjsku autentikaciju stranice</strong> ispred Uptime Kume, poput usluge Cloudflare Access.</p> | ||||
|                     <p>Pažljivo koristite ovu opciju.</p> | ||||
|                 </template> | ||||
|  | ||||
|                 <template v-else-if="$i18n.locale === 'tr-TR' "> | ||||
|                     <p><strong>Şifreli girişi devre dışı bırakmak istediğinizden</strong>emin misiniz?</p> | ||||
|                     <p>Bu, Uptime Kuma'nın önünde Cloudflare Access gibi <strong>üçüncü taraf yetkilendirmesi olan</strong> kişiler içindir.</p> | ||||
|                     <p>Lütfen dikkatli kullanın.</p> | ||||
|                 </template> | ||||
|  | ||||
|                 <template v-else-if="$i18n.locale === 'ko-KR' "> | ||||
|                     <p>정말로 <strong>인증 기능을 끌까요</strong>?</p> | ||||
|                     <p>이 기능은 <strong>Cloudflare Access와 같은 서드파티 인증</strong>을 Uptime Kuma 앞에 둔 사용자를 위한 기능이에요.</p> | ||||
|                     <p>신중하게 사용하세요.</p> | ||||
|                 </template> | ||||
|  | ||||
|                 <template v-else-if="$i18n.locale === 'pl' "> | ||||
|                     <p>Czy na pewno chcesz <strong>wyłączyć autoryzację</strong>?</p> | ||||
|                     <p>Jest przeznaczony dla <strong>kogoś, kto ma autoryzację zewnętrzną</strong> przed Uptime Kuma, taką jak Cloudflare Access.</p> | ||||
|                     <p>Proszę używać ostrożnie.</p> | ||||
|                 </template> | ||||
|  | ||||
|                 <template v-else-if="$i18n.locale === 'et-EE' "> | ||||
|                     <p>Kas soovid <strong>lülitada autentimise välja</strong>?</p> | ||||
|                     <p>Kastuamiseks <strong>välise autentimispakkujaga</strong>, näiteks Cloudflare Access.</p> | ||||
|                     <p>Palun kasuta vastutustundlikult.</p> | ||||
|                 </template> | ||||
|  | ||||
|                 <template v-else-if="$i18n.locale === 'it-IT' "> | ||||
|                     <p>Si è certi di voler <strong>disabilitare l'autenticazione</strong>?</p> | ||||
|                     <p>È per <strong>chi ha l'autenticazione gestita da terze parti</strong> messa davanti ad Uptime Kuma, ad esempio Cloudflare Access.</p> | ||||
|                     <p>Utilizzare con attenzione.</p> | ||||
|                 </template> | ||||
|  | ||||
|                 <template v-else-if="$i18n.locale === 'id-ID' "> | ||||
|                     <p>Apakah Anda yakin ingin <strong>menonaktifkan autentikasi</strong>?</p> | ||||
|                     <p>Ini untuk <strong>mereka yang memiliki autentikasi pihak ketiga</strong> diletakkan di depan Uptime Kuma, misalnya akses Cloudflare.</p> | ||||
|                     <p>Gunakan dengan hati-hati.</p> | ||||
|                 </template> | ||||
|  | ||||
|                 <template v-else-if="$i18n.locale === 'ru-RU' "> | ||||
|                     <p>Вы уверены, что хотите <strong>отключить авторизацию</strong>?</p> | ||||
|                     <p>Это подходит для <strong>тех, у кого стоит другая авторизация</strong> перед открытием Uptime Kuma, например Cloudflare Access.</p> | ||||
|                     <p>Пожалуйста, используйте с осторожностью.</p> | ||||
|                 </template> | ||||
|  | ||||
|                 <template v-else-if="$i18n.locale === 'fa' "> | ||||
|                     <p>آیا مطمئن هستید که میخواهید <strong>احراز هویت را غیر فعال کنید</strong>?</p> | ||||
|                     <p>این ویژگی برای کسانی است که <strong> لایه امنیتی شخص ثالث دیگر بر روی این آدرس فعال کردهاند</strong>، مانند Cloudflare Access.</p> | ||||
|                     <p>لطفا از این امکان با دقت استفاده کنید.</p> | ||||
|                 </template> | ||||
|  | ||||
|                 <template v-else-if="$i18n.locale === 'bg-BG' "> | ||||
|                     <p>Сигурни ли сте, че желаете да <strong>изключите удостоверяването</strong>?</p> | ||||
|                     <p>Използва се в случаите, когато <strong>има настроен алтернативен метод за удостоверяване</strong> преди Uptime Kuma, например Cloudflare Access.</p> | ||||
|                     <p>Моля, използвайте с повишено внимание.</p> | ||||
|                 </template> | ||||
|  | ||||
|                 <template v-else-if="$i18n.locale === 'hu' "> | ||||
|                     <p>Biztos benne, hogy <strong>kikapcsolja a hitelesítést</strong>?</p> | ||||
|                     <p>Akkor érdemes, ha <strong>van 3rd-party hitelesítés</strong> az Uptime Kuma-t megelőzően mint a Cloudflare Access.</p> | ||||
|                     <p>Használja megfontoltan!</p> | ||||
|                 </template> | ||||
|  | ||||
|                 <template v-else-if="$i18n.locale === 'nb-NO' "> | ||||
|                     <p>Er du sikker på at du vil <strong>deaktiver autentisering</strong>?</p> | ||||
|                     <p>Dette er for <strong>de som har tredjepartsautorisering</strong> foran Uptime Kuma, for eksempel Cloudflare Access.</p> | ||||
|                     <p>Vennligst vær forsiktig.</p> | ||||
|                 </template> | ||||
|  | ||||
|                 <!-- English (en) --> | ||||
|                 <template v-else> | ||||
|                     <p>Are you sure want to <strong>disable auth</strong>?</p> | ||||
|                     <p>It is for <strong>someone who have 3rd-party auth</strong> in front of Uptime Kuma such as Cloudflare Access.</p> | ||||
|                     <p>Please use it carefully.</p> | ||||
|                 </template> | ||||
|             </Confirm> | ||||
|  | ||||
|             <Confirm ref="confirmClearStatistics" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="clearStatistics"> | ||||
|                 {{ $t("confirmClearStatisticsMsg") }} | ||||
|             </Confirm> | ||||
|             <Confirm ref="confirmImport" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="importBackup"> | ||||
|                 {{ $t("confirmImportMsg") }} | ||||
|             </Confirm> | ||||
|         </div> | ||||
|                     <div class="mx-3"> | ||||
|                         <router-view v-slot="{ Component }"> | ||||
|                             <transition name="slide-fade" appear> | ||||
|                                 <component :is="Component" /> | ||||
|                             </transition> | ||||
|                         </router-view> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import HiddenInput from "../components/HiddenInput.vue"; | ||||
| import Confirm from "../components/Confirm.vue"; | ||||
| import dayjs from "dayjs"; | ||||
| import utc from "dayjs/plugin/utc"; | ||||
| import timezone from "dayjs/plugin/timezone"; | ||||
| import NotificationDialog from "../components/NotificationDialog.vue"; | ||||
| import TwoFADialog from "../components/TwoFADialog.vue"; | ||||
| import jwt_decode from "jwt-decode"; | ||||
| dayjs.extend(utc); | ||||
| dayjs.extend(timezone); | ||||
|  | ||||
| import { timezoneList, setPageLocale } from "../util-frontend"; | ||||
| import { useToast } from "vue-toastification"; | ||||
| import { log_debug } from "../util.ts"; | ||||
|  | ||||
| const toast = useToast(); | ||||
| import { useRoute } from "vue-router"; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|         NotificationDialog, | ||||
|         TwoFADialog, | ||||
|         Confirm, | ||||
|         HiddenInput, | ||||
|     }, | ||||
|  | ||||
|     data() { | ||||
|         return { | ||||
|             timezoneList: timezoneList(), | ||||
|             guessTimezone: dayjs.tz.guess(), | ||||
|             show: true, | ||||
|             invalidPassword: false, | ||||
|             password: { | ||||
|                 currentPassword: "", | ||||
|                 newPassword: "", | ||||
|                 repeatNewPassword: "", | ||||
|             }, | ||||
|             settings: { | ||||
|  | ||||
|             }, | ||||
|             loaded: false, | ||||
|             importAlert: null, | ||||
|             importHandle: "skip", | ||||
|             processing: false, | ||||
|             databaseSize: 0, | ||||
|             settings: {}, | ||||
|             settingsLoaded: false, | ||||
|         }; | ||||
|     }, | ||||
|  | ||||
|     computed: { | ||||
|         databaseSizeDisplay() { | ||||
|             return Math.round(this.databaseSize / 1024 / 1024 * 10) / 10 + " MB"; | ||||
|         currentPage() { | ||||
|             let pathEnd = useRoute().path.split("/").at(-1); | ||||
|             if (pathEnd == "settings" || pathEnd == null) { | ||||
|                 return "general"; | ||||
|             } | ||||
|             return pathEnd; | ||||
|         }, | ||||
|  | ||||
|     watch: { | ||||
|         "password.repeatNewPassword"() { | ||||
|             this.invalidPassword = false; | ||||
|         subMenus() { | ||||
|             return { | ||||
|                 general: { | ||||
|                     title: this.$t("General"), | ||||
|                 }, | ||||
|  | ||||
|         "$i18n.locale"() { | ||||
|             localStorage.locale = this.$i18n.locale; | ||||
|             setPageLocale(); | ||||
|                 appearance: { | ||||
|                     title: this.$t("Appearance"), | ||||
|                 }, | ||||
|                 notifications: { | ||||
|                     title: this.$t("Notifications"), | ||||
|                 }, | ||||
|                 "monitor-history": { | ||||
|                     title: this.$t("Monitor History"), | ||||
|                 }, | ||||
|                 security: { | ||||
|                     title: this.$t("Security"), | ||||
|                 }, | ||||
|                 backup: { | ||||
|                     title: this.$t("Backup"), | ||||
|                 }, | ||||
|                 about: { | ||||
|                     title: this.$t("About"), | ||||
|                 }, | ||||
|             }; | ||||
|         }, | ||||
|     }, | ||||
|  | ||||
|     mounted() { | ||||
|         this.loadUsername(); | ||||
|         this.loadSettings(); | ||||
|         this.loadDatabaseSize(); | ||||
|     }, | ||||
|  | ||||
|     methods: { | ||||
|  | ||||
|         saveGeneral() { | ||||
|             localStorage.timezone = this.$root.userTimezone; | ||||
|             this.saveSettings(); | ||||
|         }, | ||||
|  | ||||
|         savePassword() { | ||||
|             if (this.password.newPassword !== this.password.repeatNewPassword) { | ||||
|                 this.invalidPassword = true; | ||||
|             } else { | ||||
|                 this.$root.getSocket().emit("changePassword", this.password, (res) => { | ||||
|                     this.$root.toastRes(res); | ||||
|                     if (res.ok) { | ||||
|                         this.password.currentPassword = ""; | ||||
|                         this.password.newPassword = ""; | ||||
|                         this.password.repeatNewPassword = ""; | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|         }, | ||||
|  | ||||
|         loadUsername() { | ||||
|             const jwtPayload = this.$root.getJWTPayload(); | ||||
|  | ||||
|             if (jwtPayload) { | ||||
|                 this.username = jwtPayload.username; | ||||
|             } | ||||
|         }, | ||||
|  | ||||
|         loadSettings() { | ||||
|             this.$root.getSocket().emit("getSettings", (res) => { | ||||
|                 this.settings = res.data; | ||||
| @@ -538,7 +104,7 @@ export default { | ||||
|                     this.settings.keepDataPeriodDays = 180; | ||||
|                 } | ||||
|  | ||||
|                 this.loaded = true; | ||||
|                 this.settingsLoaded = true; | ||||
|             }); | ||||
|         }, | ||||
|  | ||||
| @@ -548,116 +114,6 @@ export default { | ||||
|                 this.loadSettings(); | ||||
|             }); | ||||
|         }, | ||||
|  | ||||
|         confirmDisableAuth() { | ||||
|             this.$refs.confirmDisableAuth.show(); | ||||
|         }, | ||||
|  | ||||
|         confirmClearStatistics() { | ||||
|             this.$refs.confirmClearStatistics.show(); | ||||
|         }, | ||||
|  | ||||
|         confirmImport() { | ||||
|             this.$refs.confirmImport.show(); | ||||
|         }, | ||||
|  | ||||
|         disableAuth() { | ||||
|             this.settings.disableAuth = true; | ||||
|             this.saveSettings(); | ||||
|         }, | ||||
|  | ||||
|         enableAuth() { | ||||
|             this.settings.disableAuth = false; | ||||
|             this.saveSettings(); | ||||
|             this.$root.storage().removeItem("token"); | ||||
|             location.reload(); | ||||
|         }, | ||||
|  | ||||
|         downloadBackup() { | ||||
|             let time = dayjs().format("YYYY_MM_DD-hh_mm_ss"); | ||||
|             let fileName = `Uptime_Kuma_Backup_${time}.json`; | ||||
|             let monitorList = Object.values(this.$root.monitorList); | ||||
|             let exportData = { | ||||
|                 version: this.$root.info.version, | ||||
|                 notificationList: this.$root.notificationList, | ||||
|                 monitorList: monitorList, | ||||
|             }; | ||||
|             exportData = JSON.stringify(exportData, null, 4); | ||||
|             let downloadItem = document.createElement("a"); | ||||
|             downloadItem.setAttribute("href", "data:application/json;charset=utf-8," + encodeURIComponent(exportData)); | ||||
|             downloadItem.setAttribute("download", fileName); | ||||
|             downloadItem.click(); | ||||
|         }, | ||||
|  | ||||
|         importBackup() { | ||||
|             this.processing = true; | ||||
|             let uploadItem = document.getElementById("importBackup").files; | ||||
|  | ||||
|             if (uploadItem.length <= 0) { | ||||
|                 this.processing = false; | ||||
|                 return this.importAlert = this.$t("alertNoFile"); | ||||
|             } | ||||
|  | ||||
|             if (uploadItem.item(0).type !== "application/json") { | ||||
|                 this.processing = false; | ||||
|                 return this.importAlert = this.$t("alertWrongFileType"); | ||||
|             } | ||||
|  | ||||
|             let fileReader = new FileReader(); | ||||
|             fileReader.readAsText(uploadItem.item(0)); | ||||
|  | ||||
|             fileReader.onload = item => { | ||||
|                 this.$root.uploadBackup(item.target.result, this.importHandle, (res) => { | ||||
|                     this.processing = false; | ||||
|  | ||||
|                     if (res.ok) { | ||||
|                         toast.success(res.msg); | ||||
|                     } else { | ||||
|                         toast.error(res.msg); | ||||
|                     } | ||||
|                 }); | ||||
|             }; | ||||
|         }, | ||||
|  | ||||
|         clearStatistics() { | ||||
|             this.$root.clearStatistics((res) => { | ||||
|                 if (res.ok) { | ||||
|                     this.$router.go(); | ||||
|                 } else { | ||||
|                     toast.error(res.msg); | ||||
|                 } | ||||
|             }); | ||||
|         }, | ||||
|  | ||||
|         autoGetPrimaryBaseURL() { | ||||
|             this.settings.primaryBaseURL = location.protocol + "//" + location.host; | ||||
|         }, | ||||
|  | ||||
|         shrinkDatabase() { | ||||
|             this.$root.getSocket().emit("shrinkDatabase", (res) => { | ||||
|                 if (res.ok) { | ||||
|                     this.loadDatabaseSize(); | ||||
|                     toast.success("Done"); | ||||
|                 } else { | ||||
|                     log_debug("settings", res); | ||||
|                 } | ||||
|             }); | ||||
|         }, | ||||
|  | ||||
|         loadDatabaseSize() { | ||||
|             log_debug("settings", "load database size"); | ||||
|             this.$root.getSocket().emit("getDatabaseSize", (res) => { | ||||
|  | ||||
|                 if (res.ok) { | ||||
|                     this.databaseSize = res.size; | ||||
|                     log_debug("settings", "database size: " + res.size); | ||||
|                 } else { | ||||
|                     log_debug("settings", res); | ||||
|                 } | ||||
|  | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|     }, | ||||
| }; | ||||
| </script> | ||||
| @@ -667,37 +123,7 @@ export default { | ||||
|  | ||||
| .shadow-box { | ||||
|     padding: 20px; | ||||
| } | ||||
|  | ||||
| .btn-check:active + .btn-outline-primary, | ||||
| .btn-check:checked + .btn-outline-primary, | ||||
| .btn-check:hover + .btn-outline-primary { | ||||
|     color: #fff; | ||||
| } | ||||
|  | ||||
| .dark { | ||||
|     .list-group-item { | ||||
|         background-color: $dark-bg2; | ||||
|         color: $dark-font-color; | ||||
|     } | ||||
|  | ||||
|     .btn-check:active + .btn-outline-primary, | ||||
|     .btn-check:checked + .btn-outline-primary, | ||||
|     .btn-check:hover + .btn-outline-primary { | ||||
|         color: #000; | ||||
|     } | ||||
|  | ||||
|     #importBackup { | ||||
|         &::file-selector-button { | ||||
|             color: $primary; | ||||
|             background-color: $dark-bg; | ||||
|         } | ||||
|  | ||||
|         &:hover:not(:disabled):not([readonly])::file-selector-button { | ||||
|             color: $dark-font-color2; | ||||
|             background-color: $primary; | ||||
|         } | ||||
|     } | ||||
|     min-height: calc(100vh - 155px); | ||||
| } | ||||
|  | ||||
| footer { | ||||
| @@ -707,4 +133,59 @@ footer { | ||||
|     padding-bottom: 30px; | ||||
|     text-align: center; | ||||
| } | ||||
|  | ||||
| .settings-menu { | ||||
|     flex: 0 0 auto; | ||||
|     width: 300px; | ||||
|  | ||||
|     a { | ||||
|         text-decoration: none !important; | ||||
|     } | ||||
|  | ||||
|     .menu-item { | ||||
|         border-radius: 10px; | ||||
|         margin: 0.5em; | ||||
|         padding: 0.7em 1em; | ||||
|         cursor: pointer; | ||||
|     } | ||||
|  | ||||
|     .menu-item:hover { | ||||
|         background: $highlight-white; | ||||
|  | ||||
|         .dark & { | ||||
|             background: $dark-header-bg; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     .active .menu-item { | ||||
|         background: $highlight-white; | ||||
|         border-left: 4px solid $primary; | ||||
|         border-top-left-radius: 0; | ||||
|         border-bottom-left-radius: 0; | ||||
|  | ||||
|         .dark & { | ||||
|             background: $dark-header-bg; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| .settings-content { | ||||
|     flex: 0 0 auto; | ||||
|     width: calc(100% - 300px); | ||||
|  | ||||
|     .settings-content-header { | ||||
|         width: calc(100% + 20px); | ||||
|         border-bottom: 1px solid #dee2e6; | ||||
|         border-radius: 0 10px 0 0; | ||||
|         margin-top: -20px; | ||||
|         margin-right: -20px; | ||||
|         padding: 12.5px 1em; | ||||
|         font-size: 26px; | ||||
|  | ||||
|         .dark & { | ||||
|             background: $dark-header-bg; | ||||
|             border-bottom: 0; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -474,7 +474,7 @@ export default { | ||||
|                 groupName = "Services"; | ||||
|             } | ||||
|  | ||||
|             this.$root.publicGroupList.push({ | ||||
|             this.$root.publicGroupList.unshift({ | ||||
|                 name: groupName, | ||||
|                 monitorList: [], | ||||
|             }); | ||||
|   | ||||
| @@ -11,6 +11,14 @@ import Setup from "./pages/Setup.vue"; | ||||
| const StatusPage = () => import("./pages/StatusPage.vue"); | ||||
| import Entry from "./pages/Entry.vue"; | ||||
|  | ||||
| import Appearance from "./components/settings/Appearance.vue"; | ||||
| import General from "./components/settings/General.vue"; | ||||
| import Notifications from "./components/settings/Notifications.vue"; | ||||
| import MonitorHistory from "./components/settings/MonitorHistory.vue"; | ||||
| import Security from "./components/settings/Security.vue"; | ||||
| import Backup from "./components/settings/Backup.vue"; | ||||
| import About from "./components/settings/About.vue"; | ||||
|  | ||||
| const routes = [ | ||||
|     { | ||||
|         path: "/", | ||||
| @@ -59,6 +67,37 @@ const routes = [ | ||||
|                     { | ||||
|                         path: "/settings", | ||||
|                         component: Settings, | ||||
|                         children: [ | ||||
|                             { | ||||
|                                 path: "general", | ||||
|                                 alias: "", | ||||
|                                 component: General, | ||||
|                             }, | ||||
|                             { | ||||
|                                 path: "appearance", | ||||
|                                 component: Appearance, | ||||
|                             }, | ||||
|                             { | ||||
|                                 path: "notifications", | ||||
|                                 component: Notifications, | ||||
|                             }, | ||||
|                             { | ||||
|                                 path: "monitor-history", | ||||
|                                 component: MonitorHistory, | ||||
|                             }, | ||||
|                             { | ||||
|                                 path: "security", | ||||
|                                 component: Security, | ||||
|                             }, | ||||
|                             { | ||||
|                                 path: "backup", | ||||
|                                 component: Backup, | ||||
|                             }, | ||||
|                             { | ||||
|                                 path: "about", | ||||
|                                 component: About, | ||||
|                             }, | ||||
|                         ] | ||||
|                     }, | ||||
|                 ], | ||||
|             }, | ||||
|   | ||||
							
								
								
									
										110
									
								
								test/e2e.spec.js
									
									
									
									
									
								
							
							
						
						
									
										110
									
								
								test/e2e.spec.js
									
									
									
									
									
								
							| @@ -59,18 +59,38 @@ describe("Init", () => { | ||||
|  | ||||
|         // Go to / | ||||
|         await page.goto(baseURL); | ||||
|         await sleep(3000); | ||||
|         await page.waitForSelector("h1.mb-3"); | ||||
|         pathname = await page.evaluate(() => location.pathname); | ||||
|         expect(pathname).toEqual("/dashboard"); | ||||
|     }); | ||||
|  | ||||
|     it("should create monitor", async () => { | ||||
|         // Create monitor | ||||
|         await page.goto(baseURL + "/add"); | ||||
|         await page.waitForSelector("#name"); | ||||
|  | ||||
|         await page.type("#name", "Myself"); | ||||
|         await page.waitForSelector("#url"); | ||||
|         await page.click("#url", { clickCount: 3 }); | ||||
|         await page.keyboard.type(baseURL); | ||||
|         await page.keyboard.press("Enter"); | ||||
|  | ||||
|         await page.waitForFunction(() => { | ||||
|             const badge = document.querySelector("span.badge"); | ||||
|             return badge && badge.innerText == "100%"; | ||||
|         }, { timeout: 5000 }); | ||||
|  | ||||
|     }); | ||||
|  | ||||
|     // Settings Page | ||||
|     /* | ||||
|     describe("Settings", () => { | ||||
|         beforeAll(async () => { | ||||
|         beforeEach(async () => { | ||||
|             await page.goto(baseURL + "/settings"); | ||||
|         }); | ||||
|  | ||||
|         it("Change Language", async () => { | ||||
|             await page.goto(baseURL + "/settings/appearance"); | ||||
|             await page.waitForSelector("#language"); | ||||
|  | ||||
|             await page.select("#language", "zh-HK"); | ||||
| @@ -83,20 +103,33 @@ describe("Init", () => { | ||||
|         }); | ||||
|  | ||||
|         it("Change Theme", async () => { | ||||
|             await sleep(1000); | ||||
|             await page.goto(baseURL + "/settings/appearance"); | ||||
|  | ||||
|             // Dark | ||||
|             await click(page, ".btn[for=btncheck2]"); | ||||
|             await page.waitForSelector("div.dark"); | ||||
|  | ||||
|             await sleep(1000); | ||||
|             await page.waitForSelector(".btn[for=btncheck1]"); | ||||
|  | ||||
|             // Light | ||||
|             await click(page, ".btn[for=btncheck1]"); | ||||
|             await page.waitForSelector("div.light"); | ||||
|         }); | ||||
|  | ||||
|         // TODO: Heartbeat Bar Style | ||||
|         it("Change Heartbeat Bar Style", async () => { | ||||
|             await page.goto(baseURL + "/settings/appearance"); | ||||
|  | ||||
|             // Bottom | ||||
|             await click(page, ".btn[for=btncheck5]"); | ||||
|             await page.waitForSelector("div.hp-bar-big"); | ||||
|  | ||||
|             // None | ||||
|             await click(page, ".btn[for=btncheck6]"); | ||||
|             await page.waitForSelector("div.hp-bar-big", { | ||||
|                 hidden: true, | ||||
|                 timeout: 1000 | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         // TODO: Timezone | ||||
|  | ||||
| @@ -108,14 +141,14 @@ describe("Init", () => { | ||||
|             // Yes | ||||
|             await click(page, "#searchEngineIndexYes"); | ||||
|             await click(page, "form > div > .btn[type=submit]"); | ||||
|             await sleep(2000); | ||||
|             await sleep(1000); | ||||
|             res = await axios.get(baseURL + "/robots.txt"); | ||||
|             expect(res.data).not.toContain("Disallow: /"); | ||||
|  | ||||
|             // No | ||||
|             await click(page, "#searchEngineIndexNo"); | ||||
|             await click(page, "form > div > .btn[type=submit]"); | ||||
|             await sleep(2000); | ||||
|             await sleep(1000); | ||||
|             res = await axios.get(baseURL + "/robots.txt"); | ||||
|             expect(res.data).toContain("Disallow: /"); | ||||
|         }); | ||||
| @@ -125,25 +158,25 @@ describe("Init", () => { | ||||
|  | ||||
|             // Default | ||||
|             await newPage.goto(baseURL); | ||||
|             await sleep(3000); | ||||
|             await newPage.waitForSelector("h1.mb-3", { timeout: 3000 }); | ||||
|             let pathname = await newPage.evaluate(() => location.pathname); | ||||
|             expect(pathname).toEqual("/dashboard"); | ||||
|  | ||||
|             // Status Page | ||||
|             await click(page, "#entryPageNo"); | ||||
|             await click(page, "form > div > .btn[type=submit]"); | ||||
|             await sleep(4000); | ||||
|             await sleep(1000); | ||||
|             await newPage.goto(baseURL); | ||||
|             await sleep(4000); | ||||
|             await newPage.waitForSelector("img.logo", { timeout: 3000 }); | ||||
|             pathname = await newPage.evaluate(() => location.pathname); | ||||
|             expect(pathname).toEqual("/status"); | ||||
|  | ||||
|             // Back to Dashboard | ||||
|             await click(page, "#entryPageYes"); | ||||
|             await click(page, "form > div > .btn[type=submit]"); | ||||
|             await sleep(4000); | ||||
|             await sleep(1000); | ||||
|             await newPage.goto(baseURL); | ||||
|             await sleep(4000); | ||||
|             await newPage.waitForSelector("h1.mb-3", { timeout: 3000 }); | ||||
|             pathname = await newPage.evaluate(() => location.pathname); | ||||
|             expect(pathname).toEqual("/dashboard"); | ||||
|  | ||||
| @@ -151,7 +184,7 @@ describe("Init", () => { | ||||
|         }); | ||||
|  | ||||
|         it("Change Password (wrong current password)", async () => { | ||||
|             await page.goto(baseURL + "/settings"); | ||||
|             await page.goto(baseURL + "/settings/security"); | ||||
|             await page.waitForSelector("#current-password"); | ||||
|  | ||||
|             await page.type("#current-password", "wrong_passw$$d"); | ||||
| @@ -159,10 +192,10 @@ describe("Init", () => { | ||||
|             await page.type("#repeat-new-password", "new_password123"); | ||||
|  | ||||
|             // Save | ||||
|             await click(page, "form > div > .btn[type=submit]", 1); | ||||
|             await sleep(4000); | ||||
|             await click(page, "form > div > .btn[type=submit]", 0); | ||||
|             await sleep(1000); | ||||
|  | ||||
|             await click(page, ".btn-danger.btn.me-2"); | ||||
|             await click(page, "#logout-btn"); | ||||
|             await login("admin", "new_password123"); | ||||
|             let elementCount = await page.evaluate(() => document.querySelectorAll("#floatingPassword").length); | ||||
|             expect(elementCount).toEqual(1); | ||||
| @@ -171,24 +204,26 @@ describe("Init", () => { | ||||
|         }); | ||||
|  | ||||
|         it("Change Password (wrong repeat)", async () => { | ||||
|             await page.goto(baseURL + "/settings"); | ||||
|             await page.goto(baseURL + "/settings/security"); | ||||
|             await page.waitForSelector("#current-password"); | ||||
|  | ||||
|             await page.type("#current-password", "admin123"); | ||||
|             await page.type("#new-password", "new_password123"); | ||||
|             await page.type("#repeat-new-password", "new_password1234567898797898"); | ||||
|  | ||||
|             await click(page, "form > div > .btn[type=submit]", 1); | ||||
|             await sleep(4000); | ||||
|             await click(page, "form > div > .btn[type=submit]", 0); | ||||
|             await sleep(1000); | ||||
|  | ||||
|             await click(page, ".btn-danger.btn.me-2"); | ||||
|             await click(page, "#logout-btn"); | ||||
|             await login("admin", "new_password123"); | ||||
|  | ||||
|             let elementCount = await page.evaluate(() => document.querySelectorAll("#floatingPassword").length); | ||||
|             expect(elementCount).toEqual(1); | ||||
|  | ||||
|             await login("admin", "admin123"); | ||||
|             await sleep(3000); | ||||
|             await page.waitForSelector("#current-password"); | ||||
|             let pathname = await page.evaluate(() => location.pathname); | ||||
|             expect(pathname).toEqual("/settings/security"); | ||||
|         }); | ||||
|  | ||||
|         // TODO: 2FA | ||||
| @@ -197,11 +232,38 @@ describe("Init", () => { | ||||
|  | ||||
|         // TODO: Import Backup | ||||
|  | ||||
|         // TODO: Disable Auth | ||||
|  | ||||
|         // TODO: Clear Stats | ||||
|         it("Should disable & enable auth", async () => { | ||||
|             await page.goto(baseURL + "/settings/security"); | ||||
|             await click(page, "#disableAuth-btn"); | ||||
|             await click(page, ".btn.btn-danger[data-bs-dismiss='modal']", 2); // Not a good way to do it | ||||
|             await page.waitForSelector("#enableAuth-btn", { timeout: 3000 }); | ||||
|             await page.waitForSelector("#logout-btn", { | ||||
|                 hidden: true, | ||||
|                 timeout: 3000 | ||||
|             }); | ||||
|  | ||||
|             const newPage = await browser.newPage(); | ||||
|             await newPage.goto(baseURL); | ||||
|             await newPage.waitForSelector("span.badge", { timeout: 3000 }); | ||||
|             newPage.close(); | ||||
|  | ||||
|             await click(page, "#enableAuth-btn"); | ||||
|             await login("admin", "admin123"); | ||||
|             await page.waitForSelector("#disableAuth-btn", { timeout: 3000 }); | ||||
|         }); | ||||
|  | ||||
|         // it("Should clear all statistics", async () => { | ||||
|         //     await page.goto(baseURL + "/settings/monitor-history"); | ||||
|         //     await click(page, "#clearAllStats-btn"); | ||||
|         //     await click(page, ".btn.btn-danger"); | ||||
|         //     await page.waitForFunction(() => { | ||||
|         //         const badge = document.querySelector("span.badge"); | ||||
|         //         return badge && badge.innerText == "0%"; | ||||
|         //     }, { timeout: 3000 }); | ||||
|         // }); | ||||
|     }); | ||||
|      */ | ||||
|  | ||||
|     /* | ||||
|      * TODO | ||||
|      * Create Monitor - All type | ||||
|   | ||||
		Reference in New Issue
	
	Block a user