Merge branch 'master' into feature/#1221-clickable-hostaname-on-status-page

pull/1741/head
Matthew Nickson 2 years ago committed by GitHub
commit f1d24782f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -86,8 +86,8 @@ I personally do not like something need to learn so much and need to config so m
## Name convention
- Javascript/Typescript: camelCaseType
- SQLite: underscore_type
- CSS/SCSS: dash-type
- SQLite: snake_case (Underscore)
- CSS/SCSS: kebab-case (Dash)
## Tools

@ -8,9 +8,6 @@ Do not use the issue tracker or discuss it in the public as it will cause more d
## Supported Versions
Use this section to tell people about which versions of your project are
currently being supported with security updates.
### Uptime Kuma Versions
You should use or upgrade to the latest version of Uptime Kuma. All `1.X.X` versions are upgradable to the lastest version.

@ -14,8 +14,7 @@ export default defineConfig({
plugins: [
vue(),
legacy({
targets: [ "ie > 11" ],
additionalLegacyPolyfills: [ "regenerator-runtime/runtime" ]
targets: [ "since 2015" ],
}),
visualizer({
filename: "tmp/dist-stats.html"

@ -0,0 +1,18 @@
BEGIN TRANSACTION;
ALTER TABLE monitor
ADD auth_method VARCHAR(250);
ALTER TABLE monitor
ADD auth_domain TEXT;
ALTER TABLE monitor
ADD auth_workstation TEXT;
COMMIT;
BEGIN TRANSACTION;
UPDATE monitor
SET auth_method = 'basic'
WHERE basic_auth_user is not null;
COMMIT;

@ -0,0 +1,10 @@
BEGIN TRANSACTION;
ALTER TABLE monitor
ADD database_connection_string VARCHAR(2000);
ALTER TABLE monitor
ADD database_query TEXT;
COMMIT

@ -12,7 +12,8 @@ RUN apt update && \
apt --yes --no-install-recommends install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \
sqlite3 iputils-ping util-linux dumb-init && \
pip3 --no-cache-dir install apprise==0.9.8.3 && \
rm -rf /var/lib/apt/lists/*
rm -rf /var/lib/apt/lists/* && \
apt --yes autoremove
# Install cloudflared
# dpkg --add-architecture arm: cloudflared do not provide armhf, this is workaround. Read more: https://github.com/cloudflare/cloudflared/issues/583
@ -22,5 +23,6 @@ RUN node ./extra/download-cloudflared.js $TARGETPLATFORM && \
apt update && \
apt --yes --no-install-recommends install ./cloudflared.deb && \
rm -rf /var/lib/apt/lists/* && \
rm -f cloudflared.deb
rm -f cloudflared.deb && \
apt --yes autoremove

2742
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
{
"name": "uptime-kuma",
"version": "1.16.1",
"version": "1.17.0-beta.1",
"license": "MIT",
"repository": {
"type": "git",
@ -69,6 +69,8 @@
"@popperjs/core": "~2.10.2",
"args-parser": "~1.3.0",
"axios": "~0.26.1",
"axios-cached-dns-resolve": "^3.0.6",
"axios-ntlm": "^1.3.0",
"badge-maker": "^3.3.1",
"bcryptjs": "~2.4.3",
"bootstrap": "5.1.3",
@ -83,6 +85,7 @@
"compare-versions": "~3.6.0",
"compression": "^1.7.4",
"dayjs": "^1.11.0",
"esm-wallaby": "^3.2.26",
"express": "~4.17.3",
"express-basic-auth": "~1.2.1",
"express-static-gzip": "^2.1.7",
@ -96,6 +99,7 @@
"jwt-decode": "^3.1.2",
"limiter": "^2.1.0",
"mqtt": "^4.2.8",
"mssql": "^8.1.0",
"node-cloudflared-tunnel": "~1.0.9",
"nodemailer": "~6.6.5",
"notp": "~2.0.3",
@ -141,11 +145,13 @@
"concurrently": "^7.1.0",
"core-js": "~3.18.3",
"cross-env": "~7.0.3",
"delay": "^5.0.0",
"dns2": "~2.0.1",
"eslint": "~8.14.0",
"eslint-plugin-vue": "~8.7.1",
"jest": "~27.2.5",
"jest-puppeteer": "~6.0.3",
"lru-cache": "^7.7.1",
"npm-check-updates": "^12.5.9",
"postcss-html": "^1.3.1",
"puppeteer": "~13.1.3",

@ -22,7 +22,10 @@ async function sendNotificationList(socket) {
]);
for (let bean of list) {
result.push(bean.export());
let notificationObject = bean.export();
notificationObject.isDefault = (notificationObject.isDefault === 1);
notificationObject.active = (notificationObject.active === 1);
result.push(notificationObject);
}
io.to(socket.userID).emit("notificationList", result);

@ -59,6 +59,8 @@ class Database {
"patch-status-page-footer-css.sql": true,
"patch-added-mqtt-monitor.sql": true,
"patch-add-clickable-status-page-link.sql": true,
"patch-add-sqlserver-monitor.sql": true,
"patch-add-other-auth.sql": { parents: [ "patch-monitor-basic-auth.sql" ] },
};
/**

@ -7,7 +7,7 @@ dayjs.extend(timezone);
const axios = require("axios");
const { Prometheus } = require("../prometheus");
const { log, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mqttAsync } = require("../util-server");
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, mqttAsync, setSetting, httpNtlm } = require("../util-server");
const { R } = require("redbean-node");
const { BeanModel } = require("redbean-node/dist/bean-model");
const { Notification } = require("../notification");
@ -17,6 +17,12 @@ const version = require("../../package.json").version;
const apicache = require("../modules/apicache");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const axiosCachedDnsResolve = require("esm-wallaby")(module)("axios-cached-dns-resolve");
// create an axios client instance with the cached DNS resolve interceptor
const axiosClient = axios.create();
axiosCachedDnsResolve.registerInterceptor(axiosClient);
/**
* status:
* 0 = DOWN
@ -93,7 +99,12 @@ class Monitor extends BeanModel {
mqttUsername: this.mqttUsername,
mqttPassword: this.mqttPassword,
mqttTopic: this.mqttTopic,
mqttSuccessMessage: this.mqttSuccessMessage
mqttSuccessMessage: this.mqttSuccessMessage,
databaseConnectionString: this.databaseConnectionString,
databaseQuery: this.databaseQuery,
authMethod: this.authMethod,
authWorkstation: this.authWorkstation,
authDomain: this.authDomain,
};
if (includeSensitiveData) {
@ -219,7 +230,7 @@ class Monitor extends BeanModel {
// HTTP basic auth
let basicAuthHeader = {};
if (this.basic_auth_user) {
if (this.auth_method === "basic") {
basicAuthHeader = {
"Authorization": "Basic " + this.encodeBase64(this.basic_auth_user, this.basic_auth_pass),
};
@ -270,7 +281,21 @@ class Monitor extends BeanModel {
log.debug("monitor", `[${this.name}] Axios Options: ${JSON.stringify(options)}`);
log.debug("monitor", `[${this.name}] Axios Request`);
let res = await axios.request(options);
let res;
if (this.auth_method === "ntlm") {
options.httpsAgent.keepAlive = true;
res = await httpNtlm(options, {
username: this.basic_auth_user,
password: this.basic_auth_pass,
domain: this.authDomain,
workstation: this.authWorkstation ? this.authWorkstation : undefined
});
} else {
res = await axiosClient.request(options);
}
bean.msg = `${res.status} - ${res.statusText}`;
bean.ping = dayjs().valueOf() - startTime;
@ -318,7 +343,11 @@ class Monitor extends BeanModel {
bean.msg += ", keyword is found";
bean.status = UP;
} else {
throw new Error(bean.msg + ", but keyword is not found");
data = data.replace(/<[^>]*>?|[\n\r]|\s+/gm, " ");
if (data.length > 50) {
data = data.substring(0, 47) + "...";
}
throw new Error(bean.msg + ", but keyword is not in [" + data + "]");
}
}
@ -383,7 +412,7 @@ class Monitor extends BeanModel {
// If the previous beat was down or pending we use the regular
// beatInterval/retryInterval in the setTimeout further below
if (previousBeat.status !== UP || msSinceLastBeat > beatInterval * 1000 + bufferTime) {
if (previousBeat.status !== (this.isUpsideDown() ? DOWN : UP) || msSinceLastBeat > beatInterval * 1000 + bufferTime) {
throw new Error("No heartbeat in the time window");
} else {
let timeout = beatInterval * 1000 - msSinceLastBeat;
@ -411,7 +440,7 @@ class Monitor extends BeanModel {
throw new Error("Steam API Key not found");
}
let res = await axios.get(steamApiUrl, {
let res = await axiosClient.get(steamApiUrl, {
timeout: this.interval * 1000 * 0.8,
headers: {
"Accept": "*/*",
@ -449,6 +478,14 @@ class Monitor extends BeanModel {
interval: this.interval,
});
bean.status = UP;
} else if (this.type === "sqlserver") {
let startTime = dayjs().valueOf();
await mssqlQuery(this.databaseConnectionString, this.databaseQuery);
bean.msg = "";
bean.status = UP;
bean.ping = dayjs().valueOf() - startTime;
} else {
bean.msg = "Unknown Monitor Type";
bean.status = PENDING;
@ -499,7 +536,7 @@ class Monitor extends BeanModel {
}
if (bean.status === UP) {
log.info("monitor", `Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${beatInterval} seconds | Type: ${this.type}`);
log.debug("monitor", `Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${beatInterval} seconds | Type: ${this.type}`);
} else if (bean.status === PENDING) {
if (this.retryInterval > 0) {
beatInterval = this.retryInterval;
@ -843,10 +880,19 @@ class Monitor extends BeanModel {
if (tlsInfoObject && tlsInfoObject.certInfo && tlsInfoObject.certInfo.daysRemaining) {
const notificationList = await Monitor.getNotificationList(this);
log.debug("monitor", "call sendCertNotificationByTargetDays");
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, 21, notificationList);
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, 14, notificationList);
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, 7, notificationList);
let notifyDays = await setting("tlsExpiryNotifyDays");
if (notifyDays == null || !Array.isArray(notifyDays)) {
// Reset Default
setSetting("tlsExpiryNotifyDays", [ 7, 14, 21 ], "general");
notifyDays = [ 7, 14, 21 ];
}
if (notifyDays != null && Array.isArray(notifyDays)) {
for (const day of notifyDays) {
log.debug("monitor", "call sendCertNotificationByTargetDays", day);
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, day, notificationList);
}
}
}
}

@ -669,6 +669,11 @@ let needSetup = false;
bean.mqttPassword = monitor.mqttPassword;
bean.mqttTopic = monitor.mqttTopic;
bean.mqttSuccessMessage = monitor.mqttSuccessMessage;
bean.databaseConnectionString = monitor.databaseConnectionString;
bean.databaseQuery = monitor.databaseQuery;
bean.authMethod = monitor.authMethod;
bean.authWorkstation = monitor.authWorkstation;
bean.authDomain = monitor.authDomain;
await R.store(bean);
@ -1242,8 +1247,11 @@ let needSetup = false;
method: monitorListData[i].method || "GET",
body: monitorListData[i].body,
headers: monitorListData[i].headers,
authMethod: monitorListData[i].authMethod,
basic_auth_user: monitorListData[i].basic_auth_user,
basic_auth_pass: monitorListData[i].basic_auth_pass,
authWorkstation: monitorListData[i].authWorkstation,
authDomain: monitorListData[i].authDomain,
interval: monitorListData[i].interval,
retryInterval: retryInterval,
hostname: monitorListData[i].hostname,

@ -63,7 +63,10 @@ module.exports.cloudflaredSocketHandler = (socket) => {
socket.on(prefix + "stop", async (currentPassword, callback) => {
try {
checkLogin(socket);
await doubleCheckPassword(socket, currentPassword);
const disabledAuth = await setting("disableAuth");
if (!disabledAuth) {
await doubleCheckPassword(socket, currentPassword);
}
cloudflared.stop();
} catch (error) {
callback({

@ -10,6 +10,8 @@ const chardet = require("chardet");
const mqtt = require("mqtt");
const chroma = require("chroma-js");
const { badgeConstants } = require("./config");
const mssql = require("mssql");
const { NtlmClient } = require("axios-ntlm");
// From ping-lite
exports.WIN = /^win/.test(process.platform);
@ -172,6 +174,26 @@ exports.mqttAsync = function (hostname, topic, okMessage, options = {}) {
});
};
/**
* Use NTLM Auth for a http request.
* @param {Object} options The http request options
* @param {Object} ntlmOptions The auth options
* @returns {Promise<(string[]|Object[]|Object)>}
*/
exports.httpNtlm = function (options, ntlmOptions) {
return new Promise((resolve, reject) => {
let client = NtlmClient(ntlmOptions);
client(options)
.then((resp) => {
resolve(resp);
})
.catch((err) => {
reject(err);
});
});
};
/**
* Resolves a given record using the specified DNS server
* @param {string} hostname The hostname of the record to lookup
@ -207,6 +229,31 @@ exports.dnsResolve = function (hostname, resolverServer, resolverPort, rrtype) {
});
};
/**
* Run a query on SQL Server
* @param {string} connectionString The database connection string
* @param {string} query The query to validate the database with
* @returns {Promise<(string[]|Object[]|Object)>}
*/
exports.mssqlQuery = function (connectionString, query) {
return new Promise((resolve, reject) => {
mssql.on("error", err => {
reject(err);
});
mssql.connect(connectionString).then(pool => {
return pool.request()
.query(query);
}).then(result => {
resolve(result);
}).catch(err => {
reject(err);
}).finally(() => {
mssql.close();
});
});
};
/**
* Retrieve value of setting based on key
* @param {string} key Key of setting to retrieve

@ -34,6 +34,25 @@ textarea.form-control {
}
}
// optgroup
optgroup {
color: #b1b1b1;
option {
color: #212529;
}
}
.dark {
optgroup {
color: #535864;
option {
color: $dark-font-color;
}
}
}
// Scrollbar
::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 20px;
@ -363,6 +382,12 @@ textarea.form-control {
overflow-y: auto;
height: calc(100% - 65px);
}
@media (max-width: 770px) {
&.scrollbar {
height: calc(100% - 40px);
}
}
.item {
display: block;
@ -473,6 +498,14 @@ textarea.form-control {
outline: none !important;
}
h5.settings-subheading::after {
content: "";
display: block;
width: 50%;
padding-top: 8px;
border-bottom: 1px solid $dark-border-color;
}
// Localization
@import "localization.scss";

@ -0,0 +1,86 @@
<template>
<div class="input-group mb-3">
<input
ref="input"
v-model="model"
class="form-control"
:type="type"
:placeholder="placeholder"
:disabled="!enabled"
>
<a class="btn btn-outline-primary" @click="action()">
<font-awesome-icon :icon="icon" />
</a>
</div>
</template>
<script>
/**
* Generic input field with a customizable action on the right.
* Action is passed in as a function.
*/
export default {
props: {
/**
* The value of the input field.
*/
modelValue: {
type: String,
default: ""
},
/**
* Whether the input field is enabled / disabled.
*/
enabled: {
type: Boolean,
default: true
},
/**
* Placeholder text for the input field.
*/
placeholder: {
type: String,
default: ""
},
/**
* The icon displayed in the right button of the input field.
* Accepts a Font Awesome icon string identifier.
* @example "plus"
*/
icon: {
type: String,
required: true,
},
/**
* The input type of the input field.
* @example "email"
*/
type: {
type: String,
default: "text",
},
/**
* The action to be performed when the button is clicked.
* Action is passed in as a function.
*/
action: {
type: Function,
default: () => {},
}
},
emits: [ "update:modelValue" ],
computed: {
/**
* Send value update to parent on change.
*/
model: {
get() {
return this.modelValue;
},
set(value) {
this.$emit("update:modelValue", value);
}
}
},
};
</script>

@ -25,10 +25,12 @@ export default {
CertificateInfoRow,
},
props: {
/** Object representing certificate */
certInfo: {
type: Object,
required: true,
},
/** Is the TLS certificate valid? */
valid: {
type: Boolean,
required: true,

@ -56,12 +56,19 @@ export default {
Datetime,
},
props: {
/** Object representing certificate */
cert: {
type: Object,
required: true,
},
},
methods: {
/**
* Format the subject of the certificate
* @param {Object} subject Object representing the certificates
* subject
* @returns {string}
*/
formatSubject(subject) {
if (subject.O && subject.CN && subject.C) {
return `${subject.CN} - ${subject.O} (${subject.C})`;

@ -29,14 +29,17 @@ import { Modal } from "bootstrap";
export default {
props: {
/** Style of button */
btnStyle: {
type: String,
default: "btn-primary",
},
/** Text to use as yes */
yesText: {
type: String,
default: "Yes", // TODO: No idea what to translate this
},
/** Text to use as no */
noText: {
type: String,
default: "No",
@ -50,9 +53,13 @@ export default {
this.modal = new Modal(this.$refs.modal);
},
methods: {
/** Show the confirm dialog */
show() {
this.modal.show();
},
/**
* @emits string "yes" Notify the parent when Yes is pressed
*/
yes() {
this.$emit("yes");
},

@ -25,33 +25,41 @@ let timeout;
export default {
props: {
/** ID of this input */
id: {
type: String,
default: ""
},
/** Type of input */
type: {
type: String,
default: "text"
},
/** The value of the input */
modelValue: {
type: String,
default: ""
},
/** A placeholder to use */
placeholder: {
type: String,
default: ""
},
/** Should the field auto complete */
autocomplete: {
type: String,
default: undefined,
},
/** Is the input required? */
required: {
type: Boolean
},
/** Should the input be read only? */
readonly: {
type: String,
default: undefined,
},
/** Is the input disabled? */
disabled: {
type: String,
default: undefined,
@ -79,14 +87,21 @@ export default {
},
methods: {
/** Show the input */
showInput() {
this.visibility = "text";
},
/** Hide the input */
hideInput() {
this.visibility = "password";
},
/**
* Copy the provided text to the users clipboard
* @param {string} textToCopy
* @returns {Promise<void>}
*/
copyToClipboard(textToCopy) {
this.icon = "check";

@ -10,6 +10,7 @@ import { sleep } from "../util.ts";
export default {
props: {
/** Value to count */
value: {
type: [ String, Number ],
default: 0,
@ -18,6 +19,7 @@ export default {
type: Number,
default: 0.3,
},
/** Unit of the value */
unit: {
type: String,
default: "ms",
@ -43,9 +45,7 @@ export default {
let frames = 12;
let step = Math.floor(diff / frames);
if (isNaN(step) || ! this.isNum || (diff > 0 && step < 1) || (diff < 0 && step > 1) || diff === 0) {
// Lazy to NOT this condition, hahaha.
} else {
if (! (isNaN(step) || ! this.isNum || (diff > 0 && step < 1) || (diff < 0 && step > 1) || diff === 0)) {
for (let i = 1; i < frames; i++) {
this.output += step;
await sleep(15);

@ -13,10 +13,12 @@ dayjs.extend(relativeTime);
export default {
props: {
/** Value of date time */
value: {
type: String,
default: null,
},
/** Should only the date be displayed? */
dateOnly: {
type: Boolean,
default: false,

@ -17,14 +17,17 @@
export default {
props: {
/** Size of the heartbeat bar */
size: {
type: String,
default: "big",
},
/** ID of the monitor */
monitorId: {
type: Number,
required: true,
},
/** Array of the monitors heartbeats */
heartbeatList: {
type: Array,
default: null,
@ -160,12 +163,19 @@ export default {
this.resize();
},
methods: {
/** Resize the heartbeat bar */
resize() {
if (this.$refs.wrap) {
this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatMargin * 2));
}
},
/**
* Get the title of the beat.
* Used as the hover tooltip on the heartbeat bar.
* @param {Object} beat Beat to get title from
* @returns {string}
*/
getBeatTitle(beat) {
return `${this.$root.datetime(beat.time)}` + ((beat.msg) ? ` - ${beat.msg}` : "");
},

@ -24,25 +24,31 @@
<script>
export default {
props: {
/** The value of the input */
modelValue: {
type: String,
default: ""
},
/** A placeholder to use */
placeholder: {
type: String,
default: ""
},
/** Maximum length of the input */
maxlength: {
type: Number,
default: 255
},
/** Should the field auto complete */
autocomplete: {
type: String,
default: undefined,
},
/** Is the input required? */
required: {
type: Boolean
},
/** Should the input be read only? */
readonly: {
type: String,
default: undefined,
@ -68,9 +74,11 @@ export default {
},
methods: {
/** Show users input in plain text */
showInput() {
this.visibility = "text";
},
/** Censor users input */
hideInput() {
this.visibility = "password";
},

@ -55,6 +55,7 @@ export default {
};
},
methods: {
/** Submit the user details and attempt to log in */
submit() {
this.processing = true;

@ -58,6 +58,7 @@ export default {
Tag,
},
props: {
/** Should the scrollbar be shown */
scrollbar: {
type: Boolean,
},
@ -69,10 +70,22 @@ export default {
};
},
computed: {
/**
* Improve the sticky appearance of the list by increasing its
* height as user scrolls down.
* Not used on mobile.
*/
boxStyle() {
return {
height: `calc(100vh - 160px + ${this.windowTop}px)`,
};
if (window.innerWidth > 550) {
return {
height: `calc(100vh - 160px + ${this.windowTop}px)`,
};
} else {
return {
height: "calc(100vh - 160px)",
};
}
},
sortedMonitorList() {
@ -124,6 +137,7 @@ export default {
window.removeEventListener("scroll", this.onScroll);
},
methods: {
/** Handle user scroll */
onScroll() {
if (window.top.scrollY <= 133) {
this.windowTop = window.top.scrollY;
@ -131,9 +145,15 @@ export default {
this.windowTop = 133;
}
},
/**
* Get URL of monitor
* @param {number} id ID of monitor
* @returns {string} Relative URL of monitor
*/
monitorURL(id) {
return getMonitorRelativeURL(id);
},
/** Clear the search bar */
clearSearchText() {
this.searchText = "";
}

@ -125,11 +125,16 @@ export default {
},
methods: {
/** Show dialog to confirm deletion */
deleteConfirm() {
this.modal.hide();
this.$refs.confirmDelete.show();
},
/**
* Show settings for specified notification
* @param {number} notificationID ID of notification to show
*/
show(notificationID) {
if (notificationID) {
this.id = notificationID;
@ -152,6 +157,7 @@ export default {
this.modal.show();
},
/** Submit the form to the server */
submit() {
this.processing = true;
this.$root.getSocket().emit("addNotification", this.notification, this.id, (res) => {
@ -170,6 +176,7 @@ export default {
});
},
/** Test the notification endpoint */
test() {
this.processing = true;
this.$root.getSocket().emit("testNotification", this.notification, (res) => {
@ -178,6 +185,7 @@ export default {
});
},
/** Delete the notification endpoint */
deleteNotification() {
this.processing = true;
this.$root.getSocket().emit("deleteNotification", this.id, (res) => {
@ -190,6 +198,7 @@ export default {
});
},
/**
* Get a unique default name for the notification
* @param {keyof NotificationFormList} notificationKey
* @return {string}
*/

@ -35,6 +35,7 @@ Chart.register(LineController, BarController, LineElement, PointElement, TimeSca
export default {
components: { LineChart },
props: {
/** ID of monitor */
monitorId: {
type: Number,
required: true,

@ -130,11 +130,16 @@ export default {
},
methods: {
/** Show dialog to confirm deletion */
deleteConfirm() {
this.modal.hide();
this.$refs.confirmDelete.show();
},
/**
* Show settings for specified proxy
* @param {number} proxyID ID of proxy to show
*/
show(proxyID) {
if (proxyID) {
this.id = proxyID;
@ -163,6 +168,7 @@ export default {
this.modal.show();
},
/** Submit form data for saving */
submit() {
this.processing = true;
this.$root.getSocket().emit("addProxy", this.proxy, this.id, (res) => {
@ -180,6 +186,7 @@ export default {
});
},
/** Delete this proxy */
deleteProxy() {
this.processing = true;
this.$root.getSocket().emit("deleteProxy", this.id, (res) => {

@ -86,10 +86,12 @@ export default {
Tag,
},
props: {
/** Are we in edit mode? */
editMode: {
type: Boolean,
required: true,
},
/** Should tags be shown? */
showTags: {
type: Boolean,
}
@ -108,10 +110,20 @@ export default {
},
methods: {
/**
* Remove the specified group
* @param {number} index Index of group to remove
*/
removeGroup(index) {
this.$root.publicGroupList.splice(index, 1);
},
/**
* Remove a monitor from a group
* @param {number} groupIndex Index of group to remove monitor
* from
* @param {number} index Index of monitor to remove
*/
removeMonitor(groupIndex, index) {
this.$root.publicGroupList[groupIndex].monitorList.splice(index, 1);
},

@ -5,6 +5,7 @@
<script>
export default {
props: {
/** Current status of monitor */
status: {
type: Number,
default: 0,

@ -20,14 +20,20 @@
<script>
export default {
props: {
/** Object representing tag */
item: {
type: Object,
required: true,
},
/** Function to remove tag */
remove: {
type: Function,
default: null,
},
/**
* Size of tag
* @values normal, small
*/
size: {
type: String,
default: "normal",

@ -139,6 +139,7 @@ export default {
VueMultiselect,
},
props: {
/** Array of tags to be pre-selected */
preSelectedTags: {
type: Array,
default: () => [],
@ -241,9 +242,11 @@ export default {
this.getExistingTags();
},
methods: {
/** Show the add tag dialog */
showAddDialog() {
this.modal.show();
},
/** Get all existing tags */
getExistingTags() {
this.$root.getSocket().emit("getTags", (res) => {
if (res.ok) {
@ -253,6 +256,10 @@ export default {
}
});
},
/**
* Delete the specified tag
* @param {Object} tag Object representing tag to delete
*/
deleteTag(item) {
if (item.new) {
// Undo Adding a new Tag
@ -262,6 +269,13 @@ export default {
this.deleteTags.push(item);
}
},
/**
* Get colour of text inside the tag
* @param {Object} option The tag that needs to be displayed.
* Defaults to "white" unless the tag has no color, which will
* then return the body color (based on application theme)
* @returns string
*/
textColor(option) {
if (option.color) {
return "white";
@ -269,6 +283,7 @@ export default {
return this.$root.theme === "light" ? "var(--bs-body-color)" : "inherit";
}
},
/** Add a draft tag */
addDraftTag() {
console.log("Adding Draft Tag: ", this.newDraftTag);
if (this.newDraftTag.select != null) {
@ -296,6 +311,7 @@ export default {
}
this.clearDraftTag();
},
/** Remove a draft tag */
clearDraftTag() {
this.newDraftTag = {
name: null,
@ -307,26 +323,51 @@ export default {
};
this.modal.hide();
},
/**
* Add a tag asynchronously
* @param {Object} newTag Object representing new tag to add
* @returns {Promise<void>}
*/
addTagAsync(newTag) {
return new Promise((resolve) => {
this.$root.getSocket().emit("addTag", newTag, resolve);
});
},
/**
* Add a tag to a monitor asynchronously
* @param {number} tagId ID of tag to add
* @param {number} monitorId ID of monitor to add tag to
* @param {string} value Value of tag
* @returns {Promise<void>}
*/
addMonitorTagAsync(tagId, monitorId, value) {
return new Promise((resolve) => {
this.$root.getSocket().emit("addMonitorTag", tagId, monitorId, value, resolve);
});
},
/**
* Delete a tag from a monitor asynchronously
* @param {number} tagId ID of tag to remove
* @param {number} monitorId ID of monitor to remove tag from
* @param {string} value Value of tag
* @returns {Promise<void>}
*/
deleteMonitorTagAsync(tagId, monitorId, value) {
return new Promise((resolve) => {
this.$root.getSocket().emit("deleteMonitorTag", tagId, monitorId, value, resolve);
});
},
/** Handle pressing Enter key when inside the modal */
onEnter() {
if (!this.validateDraftTag.invalid) {
this.addDraftTag();
}
},
/**
* Submit the form data
* @param {number} monitorId ID of monitor this change affects
* @returns {void}
*/
async submit(monitorId) {
console.log(`Submitting tag changes for monitor ${monitorId}...`);
this.processing = true;

@ -29,10 +29,12 @@
<script>
export default {
props: {
/** Heading of the section */
heading: {
type: String,
default: "",
},
/** Should the section be open by default? */
defaultOpen: {
type: Boolean,
default: false,

@ -100,18 +100,22 @@ export default {
this.getStatus();
},
methods: {
/** Show the dialog */
show() {
this.modal.show();
},
/** Show dialog to confirm enabling 2FA */
confirmEnableTwoFA() {
this.$refs.confirmEnableTwoFA.show();
},
/** Show dialog to confirm disabling 2FA */
confirmDisableTwoFA() {
this.$refs.confirmDisableTwoFA.show();
},
/** Prepare 2FA configuration */
prepare2FA() {
this.processing = true;
@ -126,6 +130,7 @@ export default {
});
},
/** Save the current 2FA configuration */
save2FA() {
this.processing = true;
@ -143,6 +148,7 @@ export default {
});
},
/** Disable 2FA for this user */
disable2FA() {
this.processing = true;
@ -160,6 +166,7 @@ export default {
});
},
/** Verify the token generated by the user */
verifyToken() {
this.$root.getSocket().emit("verifyToken", this.token, this.currentPassword, (res) => {
if (res.ok) {
@ -170,6 +177,7 @@ export default {
});
},
/** Get current status of 2FA */
getStatus() {
this.$root.getSocket().emit("twoFAStatus", (res) => {
if (res.ok) {

@ -5,14 +5,17 @@
<script>
export default {
props: {
/** Monitor this represents */
monitor: {
type: Object,
default: null,
},
/** Type of monitor */
type: {
type: String,
default: null,
},
/** Is this a pill? */
pill: {
type: Boolean,
default: false,

@ -133,10 +133,15 @@ export default {
},
methods: {
/**
* Show the confimation dialog confirming the configuration
* be imported
*/
confirmImport() {
this.$refs.confirmImport.show();
},
/** Download a backup of the configuration */
downloadBackup() {
let time = dayjs().format("YYYY_MM_DD-hh_mm_ss");
let fileName = `Uptime_Kuma_Backup_${time}.json`;
@ -157,6 +162,10 @@ export default {
downloadItem.click();
},
/**
* Import the specified backup file
* @returns {?string}
*/
importBackup() {
this.processing = true;
let uploadItem = document.getElementById("import-backend").files;

@ -178,10 +178,12 @@ export default {
},
methods: {
/** Save the settings */
saveGeneral() {
localStorage.timezone = this.$root.userTimezone;
this.saveSettings();
},
/** Get the base URL of the application */
autoGetPrimaryBaseURL() {
this.settings.primaryBaseURL = location.protocol + "//" + location.host;
},

@ -90,6 +90,7 @@ export default {
},
methods: {
/** Get the current size of the database */
loadDatabaseSize() {
log.debug("monitorhistory", "load database size");
this.$root.getSocket().emit("getDatabaseSize", (res) => {
@ -102,6 +103,7 @@ export default {
});
},
/** Request that the database is shrunk */
shrinkDatabase() {
this.$root.getSocket().emit("shrinkDatabase", (res) => {
if (res.ok) {
@ -113,10 +115,12 @@ export default {
});
},
/** Show the dialog to confirm clearing stats */
confirmClearStatistics() {
this.$refs.confirmClearStatistics.show();
},
/** Send the request to clear stats */
clearStatistics() {
this.$root.clearStatistics((res) => {
if (res.ok) {

@ -20,16 +20,91 @@
</button>
</div>
<div class="my-4 pt-4">
<h5 class="my-4 settings-subheading">{{ $t("settingsCertificateExpiry") }}</h5>
<p>{{ $t("certificationExpiryDescription") }}</p>
<div class="mt-1 mb-3 ps-2 cert-exp-days col-12 col-xl-6">
<div v-for="day in settings.tlsExpiryNotifyDays" :key="day" class="d-flex align-items-center justify-content-between cert-exp-day-row py-2">
<span>{{ day }} {{ $tc("day", day) }}</span>
<button type="button" class="btn-rm-expiry btn btn-outline-danger ms-2 py-1" @click="removeExpiryNotifDay(day)">
<font-awesome-icon class="" icon="times" />
</button>
</div>
</div>
<div class="col-12 col-xl-6">
<ActionInput v-model="expiryNotifInput" :type="'number'" :placeholder="$t('day')" :icon="'plus'" :action="() => addExpiryNotifDay(expiryNotifInput)" />
</div>
<div>
<button class="btn btn-primary" type="button" @click="saveSettings()">
{{ $t("Save") }}
</button>
</div>
</div>
<NotificationDialog ref="notificationDialog" />
</div>
</template>
<script>
import NotificationDialog from "../../components/NotificationDialog.vue";
import ActionInput from "../ActionInput.vue";
export default {
components: {
NotificationDialog
NotificationDialog,
ActionInput,
},
data() {
return {
/**
* Variable to store the input for new certificate expiry day.
*/
expiryNotifInput: null,
};
},
computed: {
settings() {
return this.$parent.$parent.$parent.settings;
},
saveSettings() {
return this.$parent.$parent.$parent.saveSettings;
},
settingsLoaded() {
return this.$parent.$parent.$parent.settingsLoaded;
},
},
methods: {
/**
* Remove a day from expiry notification days.
* @param {number} day The day to remove.
*/
removeExpiryNotifDay(day) {
this.settings.tlsExpiryNotifyDays = this.settings.tlsExpiryNotifyDays.filter(d => d !== day);
},
/**
* Add a new expiry notification day.
* Will verify:
* - day is not null or empty string.
* - day is a number.
* - day is > 0.
* - The day is not already in the list.
* @param {number} day The day number to add.
*/
addExpiryNotifDay(day) {
if (day != null && day !== "") {
const parsedDay = parseInt(day);
if (parsedDay != null && !isNaN(parsedDay) && parsedDay > 0) {
if (!this.settings.tlsExpiryNotifyDays.includes(parsedDay)) {
this.settings.tlsExpiryNotifyDays.push(parseInt(day));
this.settings.tlsExpiryNotifyDays.sort((a, b) => a - b);
this.expiryNotifInput = null;
}
}
}
},
},
};
</script>
@ -37,10 +112,27 @@ export default {
<style lang="scss" scoped>
@import "../../assets/vars.scss";
.btn-rm-expiry {
padding-left: 11px;
padding-right: 11px;
}
.dark {
.list-group-item {
background-color: $dark-bg2;
color: $dark-font-color;
}
}
.cert-exp-days .cert-exp-day-row {
border-bottom: 1px solid rgba(0, 0, 0, 0.125);
.dark & {
border-bottom: 1px solid $dark-border-color;
}
}
.cert-exp-days .cert-exp-day-row:last-child {
border: none;
}
</style>

@ -68,7 +68,9 @@
<Confirm ref="confirmStop" btn-style="btn-danger" :yes-text="$t('Stop') + ' cloudflared'" :no-text="$t('Cancel')" @yes="stop">
{{ $t("The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.") }}
<div class="mt-3">
<p class="mt-2">{{ $t("disableCloudflaredNoAuthMsg") }}</p>
<div v-if="!settings.disableAuth" class="mt-3">
<label for="current-password2" class="form-label">
{{ $t("Current Password") }}
</label>
@ -108,7 +110,9 @@ export default {
return this.$root.cloudflared;
},
computed: {
settings() {
return this.$parent.$parent.$parent.settings;
},
},
watch: {
@ -120,14 +124,17 @@ export default {
this.$root.getSocket().emit(prefix + "leave");
},
methods: {
/** Start the Cloudflare tunnel */
start() {
this.$root.getSocket().emit(prefix + "start", this.cloudflareTunnelToken);
},
/** Stop the Cloudflare tunnel */
stop() {
this.$root.getSocket().emit(prefix + "stop", this.currentPassword, (res) => {
this.$root.toastRes(res);
});
},
/** Remove the token for the Cloudflare tunnel */
removeToken() {
this.$root.getSocket().emit(prefix + "removeToken");
this.cloudflareTunnelToken = "";

@ -8,7 +8,7 @@
<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>
<h5 class="my-4 settings-subheading">{{ $t("Change Password") }}</h5>
<form class="mb-3" @submit.prevent="savePassword">
<div class="mb-3">
<label for="current-password" class="form-label">
@ -62,7 +62,7 @@
</template>
<div v-if="! settings.disableAuth" class="mt-5 mb-3">
<h5 class="my-4">
<h5 class="my-4 settings-subheading">
{{ $t("Two Factor Authentication") }}
</h5>
<div class="mb-4">
@ -78,7 +78,7 @@
<div class="my-4">
<!-- Advanced -->
<h5 class="my-4">{{ $t("Advanced") }}</h5>
<h5 class="my-4 settings-subheading">{{ $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>
@ -90,162 +90,11 @@
<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 === 'sl-SI' ">
<p>Ali ste prepričani, da želite onemogočiti <strong>avtentikacijo</strong>?</p>
<p>Namenjen je <strong>nekomu, ki ima pred programom Uptime Kuma vklopljeno zunanje preverjanje pristnosti</strong>, na primer Cloudflare Access.</p>
<p>Uporabljajte previdno.</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><strong>Disabilitare l'autenticazione?</strong></p>
<p><strong>Questa opzione è per chi un sistema di autenticazione gestito da terze parti</strong> messo 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 === 'uk-UA' ">
<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, Authelia или друг механизъм за удостоверяване.</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 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>
<template v-else-if="$i18n.locale === 'cs-CZ' ">
<p>Opravdu chcete <strong>deaktivovat autentifikaci</strong>?</p>
<p>Tato možnost je určena pro případy, kdy <strong>máte autentifikaci zajištěnou třetí stranou</strong> ještě před přístupem do Uptime Kuma, například prostřednictvím Cloudflare Access.</p>
<p>Používejte ji prosím s rozmyslem.</p>
</template>
<template v-else-if="$i18n.locale === 'vi-VN' ">
<p>Bạn muốn <strong>TẮT XÁC THỰC</strong> không?</p>
<p>Điều này rất nguy hiểm<strong>BẤT KỲ AI</strong> cũng thể truy cập cướp quyền điều khiển.</p>
<p>Vui lòng <strong>cẩn thận</strong>.</p>
</template>
<template v-else-if="$i18n.locale === 'th-TH' ">
<p>ณตองการทจะ <strong>ดใชงานระบบรบรองความถกตองใชหรอไม</strong>?</p>
<p>ระบบนกออกแบบมาเพอการใชงานกบระบบรบรองความถกตองของบคคลทสามเช Cloudflare Access, Authelia หรอวการอ </p>
<p>โปรดใชความระมดระวงในการเลอกใชงานระบบน !</p>
</template>
<!-- English (en) -->
<template v-else>
<p>Are you sure want to <strong>disable authentication</strong>?</p>
<p>It is designed for scenarios <strong>where you intend to implement third-party authentication</strong> in front of Uptime Kuma such as Cloudflare Access, Authelia or other authentication mechanisms.</p>
<p>Please use this option carefully!</p>
</template>
<!-- eslint-disable-next-line vue/no-v-html -->
<p v-html="$t('disableauth.message1')"></p>
<!-- eslint-disable-next-line vue/no-v-html -->
<p v-html="$t('disableauth.message2')"></p>
<p>{{ $t("Please use this option carefully!") }}</p>
<div class="mb-3">
<label for="current-password2" class="form-label">
@ -303,6 +152,7 @@ export default {
},
methods: {
/** Check new passwords match before saving them */
savePassword() {
if (this.password.newPassword !== this.password.repeatNewPassword) {
this.invalidPassword = true;
@ -320,6 +170,7 @@ export default {
}
},
/** Disable authentication for web app access */
disableAuth() {
this.settings.disableAuth = true;
@ -332,6 +183,7 @@ export default {
}, this.password.currentPassword);
},
/** Enable authentication for web app access */
enableAuth() {
this.settings.disableAuth = false;
this.saveSettings();
@ -346,15 +198,3 @@ export default {
},
};
</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>

@ -55,8 +55,7 @@ export default {
Current: "Текущ",
Uptime: "Достъпност",
"Cert Exp.": "Вал. сертификат",
days: "дни",
day: "ден",
day: "ден | дни",
"-day": "-дни",
hour: "час",
"-hour": "-часa",
@ -90,13 +89,16 @@ export default {
"Search Engine Visibility": "Видимост за търсачки",
"Allow indexing": "Разреши индексиране",
"Discourage search engines from indexing site": "Не позволявай на търсачките да индексират този сайт",
"Change Password": "Промени парола",
"Change Password": "Промяна на парола",
"Current Password": "Текуща парола",
"New Password": "Нова парола",
"Repeat New Password": "Повторете новата парола",
"Update Password": "Актуализирай парола",
"Update Password": "Актуализирай паролата",
"Disable Auth": "Изключи удостоверяване",
"Enable Auth": "Включи удостоверяване",
"disableauth.message1": "Сигурни ли сте, че желаете да <strong>изключите удостоверяването</strong>?",
"disableauth.message2": "Използва се в случаите, когато <strong>има настроен алтернативен метод за удостоверяване</strong> преди Uptime Kuma, например Cloudflare Access, Authelia или друг механизъм за удостоверяване.",
"Please use this option carefully!": "Моля, използвайте с повишено внимание.",
Logout: "Изход от профила",
Leave: "Отказ",
"I understand, please disable": "Разбирам. Моля, изключи",
@ -145,7 +147,7 @@ export default {
"Setup 2FA": "Настройка 2FA",
"Enable 2FA": "Включи 2FA",
"Disable 2FA": "Изключи 2FA",
"2FA Settings": "Настройки 2FA",
"2FA Settings": "Настройка за 2FA",
"Two Factor Authentication": "Двуфакторно удостоверяване",
Active: "Активно",
Inactive: "Неактивно",
@ -299,7 +301,7 @@ export default {
HeadersInvalidFormat: "Заявените хедъри не са валидни JSON: ",
BodyInvalidFormat: "Заявеното съобщение не е валиден JSON: ",
"Monitor History": "История на мониторите",
clearDataOlderThan: "Ще се съхранява {0} дни.",
clearDataOlderThan: "Ще се съхранява за {0} дни.",
records: "записа",
"One record": "Един запис",
steamApiKeyDescription: "За да мониторирате Steam Gameserver се нуждаете от Steam Web-API ключ. Може да регистрирате Вашия API ключ тук: ",
@ -308,12 +310,12 @@ export default {
PasswordsDoNotMatch: "Паролите не съвпадат.",
"Current User": "Текущ потребител",
recent: "Скорошни",
shrinkDatabaseDescription: "Инициира \"VACUUM\" за \"SQLite\" база данни. Ако Вашата база данни е създадена след версия 1.10.0, \"AUTO_VACUUM\" функцията е активна и това действие не нужно.",
shrinkDatabaseDescription: "Инициира \"VACUUM\" за \"SQLite\" база данни. Ако Вашата база данни е създадена след версия 1.10.0, \"AUTO_VACUUM\" функцията е активна и това действие не е нужно.",
Done: "Готово",
Info: "Информация",
Security: "Сигурност",
"Steam API Key": "Steam API ключ",
"Shrink Database": "Редуциране база данни",
"Shrink Database": "Редуцирай базата данни",
"Pick a RR-Type...": "Изберете вида на ресурсния запис за мониторитане...",
"Pick Accepted Status Codes...": "Изберете статус кодове, които да се считат за успешен отговор...",
Default: "По подразбиране",
@ -422,6 +424,7 @@ export default {
Next: "Следващ",
"The slug is already taken. Please choose another slug.": "Този слъг вече се използва. Моля изберете друг.",
"No Proxy": "Без прокси",
Authentication: "Удостоверяване",
"HTTP Basic Auth": "HTTP основно удостоверяване",
"New Status Page": "Нова статус страница",
"Page Not Found": "Страницата не е открита",
@ -515,4 +518,18 @@ export default {
"Go back to the previous page.": "Да се върнете към предишната страница.",
"Coming Soon": "Очаквайте скоро",
wayToGetClickSendSMSToken: "Може да получите API потребителско име и API ключ от {0} .",
dnsPortDescription: "DNS порт на сървъра. По подразбиране е 53, но може да бъде променен по всяко време.",
error: "грешка",
critical: "критично",
wayToGetPagerDutyKey: "Може да го получите като посетите Service -> Service Directory -> (Select a service) -> Integrations -> Add integration. Тук трябва да потърсите \"Events API V2\". Повече информация {0}",
"Integration Key": "Ключ за интегриране",
"Integration URL": "URL адрес за интеграция",
"Auto resolve or acknowledged": "Автоматично разрешаване или потвърждаване",
"do nothing": "не прави нищо",
"auto acknowledged": "автоматично потвърждаване",
"auto resolve": "автоматично разрешаване",
"Connection String": "Стринг за връзка",
Query: "Заявка",
settingsCertificateExpiry: "Изтичане валидността на TLS сертификата",
certificationExpiryDescription: "HTTPS мониторите ще задействат известие, ако е наличен изтичащ TLS сертификат, през следващите:",
};

@ -56,8 +56,7 @@ export default {
Current: "Aktuální",
Uptime: "Doba provozu",
"Cert Exp.": "Platnost certifikátu",
days: "dny/í",
day: "den",
day: "den | dny/í",
"-day": "-dní",
hour: "hodina",
"-hour": "-hodin",
@ -101,6 +100,9 @@ export default {
"Update Password": "Aktualizovat heslo",
"Disable Auth": "Deaktivovat ověřování",
"Enable Auth": "Povolit ověřování",
"disableauth.message1": "Opravdu chcete <strong>deaktivovat autentifikaci</strong>?",
"disableauth.message2": "Tato možnost je určena pro případy, kdy <strong>máte autentifikaci zajištěnou třetí stranou</strong> ještě před přístupem do Uptime Kuma, například prostřednictvím Cloudflare Access.",
"Please use this option carefully!": "Používejte ji prosím s rozmyslem.",
Logout: "Odhlášení",
Leave: "Odejít",
"I understand, please disable": "Rozumím, chci ji deaktivovat",

@ -30,8 +30,7 @@ export default {
Current: "Aktuelt",
Uptime: "Oppetid",
"Cert Exp.": "Certifikatets udløb",
days: "Dage",
day: "Dag",
day: "Dag | Dage",
"-day": "-Dage",
hour: "Timer",
"-hour": "-Timer",

@ -30,8 +30,7 @@ export default {
Current: "Aktuell",
Uptime: "Verfügbarkeit",
"Cert Exp.": "Zertifikatsablauf",
days: "Tage",
day: "Tag",
day: "Tag | Tage",
"-day": "-Tage",
hour: "Stunde",
"-hour": "-Stunden",
@ -78,6 +77,9 @@ export default {
"Update Password": "Passwort aktualisieren",
"Disable Auth": "Authentifizierung deaktivieren",
"Enable Auth": "Authentifizierung aktivieren",
"disableauth.message1": "Bist du sicher das du die <strong>Authentifizierung deaktivieren</strong> möchtest?",
"disableauth.message2": "Es ist für <strong>jemanden der eine externe Authentifizierung</strong> vor Uptime Kuma geschaltet hat, wie z.B. Cloudflare Access.",
"Please use this option carefully!": "Bitte mit Vorsicht nutzen.",
Logout: "Ausloggen",
notificationDescription: "Benachrichtigungen müssen einem Monitor zugewiesen werden, damit diese funktionieren.",
Leave: "Verlassen",
@ -422,6 +424,7 @@ export default {
Next: "Weiter",
"The slug is already taken. Please choose another slug.": "Der Slug ist bereits in Verwendung. Bitte wähle einen anderen.",
"No Proxy": "Kein Proxy",
Authentication: "Authentifizierung",
"HTTP Basic Auth": "HTTP Basisauthentifizierung",
"New Status Page": "Neue Status-Seite",
"Page Not Found": "Seite nicht gefunden",

@ -57,8 +57,7 @@ export default {
Current: "Current",
Uptime: "Uptime",
"Cert Exp.": "Cert Exp.",
days: "days",
day: "day",
day: "day | days",
"-day": "-day",
hour: "hour",
"-hour": "-hour",
@ -102,6 +101,9 @@ export default {
"Update Password": "Update Password",
"Disable Auth": "Disable Auth",
"Enable Auth": "Enable Auth",
"disableauth.message1": "Are you sure want to <strong>disable authentication</strong>?",
"disableauth.message2": "It is designed for scenarios <strong>where you intend to implement third-party authentication</strong> in front of Uptime Kuma such as Cloudflare Access, Authelia or other authentication mechanisms.",
"Please use this option carefully!": "Please use this option carefully!",
Logout: "Logout",
Leave: "Leave",
"I understand, please disable": "I understand, please disable",
@ -439,6 +441,7 @@ export default {
Next: "Next",
"The slug is already taken. Please choose another slug.": "The slug is already taken. Please choose another slug.",
"No Proxy": "No Proxy",
Authentication: "Authentication",
"HTTP Basic Auth": "HTTP Basic Auth",
"New Status Page": "New Status Page",
"Page Not Found": "Page Not Found",
@ -525,4 +528,12 @@ export default {
"Go back to the previous page.": "Go back to the previous page.",
"Coming Soon": "Coming Soon",
wayToGetClickSendSMSToken: "You can get API Username and API Key from {0} .",
"Connection String": "Connection String",
"Query": "Query",
settingsCertificateExpiry: "TLS Certificate Expiry",
certificationExpiryDescription: "HTTPS Monitors trigger notification when TLS certificate expires in:",
"ntfy Topic": "ntfy Topic",
"Domain": "Domain",
"Workstation": "Workstation",
disableCloudflaredNoAuthMsg: "You are in No Auth mode, password is not require.",
};

@ -44,8 +44,7 @@ export default {
Current: "Actual",
Uptime: "Tiempo activo",
"Cert Exp.": "Caducidad cert.",
days: "días",
day: "día",
day: "día | días",
"-day": "-día",
hour: "hora",
"-hour": "-hora",
@ -85,6 +84,9 @@ export default {
"Update Password": "Actualizar contraseña",
"Disable Auth": "Deshabilitar Autenticación",
"Enable Auth": "Habilitar Autenticación",
"disableauth.message1": "Seguro que deseas <strong>deshabilitar la autenticación</strong>?",
"disableauth.message2": "Es para <strong>quien implementa autenticación de terceros</strong> ante Uptime Kuma como por ejemplo Cloudflare Access.",
"Please use this option carefully!": "Por favor usar con cuidado.",
Logout: "Cerrar sesión",
Leave: "Salir",
"I understand, please disable": "Entiendo, por favor deshabilitar",

@ -47,8 +47,7 @@ export default {
Current: "Hetkeseisund",
Uptime: "Eluiga",
"Cert Exp.": "Sert. aegumine",
days: "päeva",
day: "päev",
day: "päev | päeva",
"-day": "-päev",
hour: "tund",
"-hour": "-tund",
@ -88,6 +87,9 @@ export default {
"Update Password": "Uuenda salasõna",
"Disable Auth": "Lülita autentimine välja",
"Enable Auth": "Lülita autentimine sisse",
"disableauth.message1": "Kas soovid <strong>lülitada autentimise välja</strong>?",
"disableauth.message2": "Kastuamiseks <strong>välise autentimispakkujaga</strong>, näiteks Cloudflare Access.",
"Please use this option carefully!": "Palun kasuta vastutustundlikult.",
Logout: "Logi välja",
Leave: "Lahku",
"I understand, please disable": "Olen tutvunud riskidega, lülita välja",

@ -55,7 +55,6 @@ export default {
Current: "فعلی",
Uptime: "آپتایم",
"Cert Exp.": "تاریخ انقضای SSL",
days: "روز",
day: "روز",
"-day": "-روز",
hour: "ساعت",
@ -97,6 +96,9 @@ export default {
"Update Password": "بروز رسانی رمز عبور",
"Disable Auth": "غیر فعال سازی تایید هویت",
"Enable Auth": "فعال سازی تایید هویت",
"disableauth.message1": "آیا مطمئن هستید که میخواهید <strong>احراز هویت را غیر فعال کنید</strong>?",
"disableauth.message2": "این ویژگی برای کسانی است که <strong> لایه امنیتی شخص ثالث دیگر بر روی این آدرس فعال کرده‌اند</strong>، مانند Cloudflare Access.",
"Please use this option carefully!": "لطفا از این امکان با دقت استفاده کنید.",
Logout: "خروج",
Leave: "منصرف شدم",
"I understand, please disable": "متوجه هستم، لطفا غیرفعال کنید!",

@ -55,8 +55,7 @@ export default {
Current: "Actuellement",
Uptime: "Uptime",
"Cert Exp.": "Expiration SSL",
days: "jours",
day: "jour",
day: "jour | jours",
"-day": "-jours",
hour: "-heure",
"-hour": "-heures",

@ -56,8 +56,7 @@ export default {
Current: "Trenutno",
Uptime: "Dostupnost",
"Cert Exp.": "Istek cert.",
days: "dana",
day: "dan",
day: "dan | dana",
"-day": "-dnevno",
hour: "sat",
"-hour": "-satno",
@ -101,6 +100,9 @@ export default {
"Update Password": "Spremi novu lozinku",
"Disable Auth": "Onemogući autentikaciju",
"Enable Auth": "Omogući autentikaciju",
"disableauth.message1": "Jeste li sigurni da želite <strong>isključiti autentikaciju</strong>?",
"disableauth.message2": "To je za <strong>korisnike koji imaju vanjsku autentikaciju stranice</strong> ispred Uptime Kume, poput usluge Cloudflare Access.",
"Please use this option carefully!": "Pažljivo koristite ovu opciju.",
Logout: "Odjava",
Leave: "Poništi",
"I understand, please disable": "Razumijem, svejedno onemogući",

@ -55,7 +55,6 @@ export default {
Current: "Aktuális",
Uptime: "Uptime",
"Cert Exp.": "SSL lejárat",
days: "nap",
day: "nap",
"-day": " nap",
hour: "óra",
@ -97,6 +96,9 @@ export default {
"Update Password": "Jelszó módosítása",
"Disable Auth": "Hitelesítés tiltása",
"Enable Auth": "Hitelesítés engedélyezése",
"disableauth.message1": "Biztos benne, hogy <strong>kikapcsolja a hitelesítést</strong>?",
"disableauth.message2": "Akkor érdemes, ha <strong>van 3rd-party hitelesítés</strong> az Uptime Kuma-t megelőzően mint a Cloudflare Access.",
"Please use this option carefully!": "Használja megfontoltan!",
Logout: "Kijelentkezés",
Leave: "Elhagy",
"I understand, please disable": "Megértettem, kérem tiltsa le",

@ -55,8 +55,7 @@ export default {
Current: "Saat ini",
Uptime: "Waktu aktif",
"Cert Exp.": "Cert Exp.",
days: "hari-hari",
day: "hari",
day: "hari | hari-hari",
"-day": "-hari",
hour: "Jam",
"-hour": "-Jam",
@ -97,6 +96,9 @@ export default {
"Update Password": "Perbarui Kata Sandi",
"Disable Auth": "Nonaktifkan Autentikasi",
"Enable Auth": "Aktifkan Autentikasi",
"disableauth.message1": "Apakah Anda yakin ingin <strong>menonaktifkan autentikasi</strong>?",
"disableauth.message2": "Ini untuk <strong>mereka yang memiliki autentikasi pihak ketiga</strong> diletakkan di depan Uptime Kuma, misalnya akses Cloudflare.",
"Please use this option carefully!": "Gunakan dengan hati-hati.",
Logout: "Keluar",
Leave: "Pergi",
"I understand, please disable": "Saya mengerti, silakan dinonaktifkan",

@ -56,8 +56,7 @@ export default {
Current: "Corrente",
Uptime: "Tempo di attività",
"Cert Exp.": "Scadenza certificato",
days: "giorni",
day: "giorno",
day: "giorno | giorni",
"-day": "-giorni",
hour: "ora",
"-hour": "-ore",
@ -101,6 +100,9 @@ export default {
"Update Password": "Modifica password",
"Disable Auth": "Disabilita autenticazione",
"Enable Auth": "Abilita autenticazione",
"disableauth.message1": "<strong>Disabilitare l'autenticazione?</strong>",
"disableauth.message2": "<strong>Questa opzione è per chi un sistema di autenticazione gestito da terze parti</strong> messo davanti ad Uptime Kuma, ad esempio Cloudflare Access.",
"Please use this option carefully!": "Utilizzare con attenzione!",
Logout: "Esci",
Leave: "Annulla",
"I understand, please disable": "Lo capisco, disabilitare l'autenticazione.",

@ -44,8 +44,7 @@ export default {
Current: "現在",
Uptime: "起動時間",
"Cert Exp.": "証明書有効期限",
days: "日間",
day: "日",
day: "日 | 日間",
"-day": "-日",
hour: "時間",
"-hour": "-時間",

@ -55,7 +55,6 @@ export default {
Current: "현재",
Uptime: "업타임",
"Cert Exp.": "인증서 만료",
days: "일",
day: "일",
"-day": "-일",
hour: "시간",
@ -97,6 +96,9 @@ export default {
"Update Password": "비밀번호 변경",
"Disable Auth": "인증 비활성화",
"Enable Auth": "인증 활성화",
"disableauth.message1": "정말로 <strong>인증 기능을 끌까요</strong>?",
"disableauth.message2": "이 기능은 <strong>Cloudflare Access와 같은 서드파티 인증</strong>을 Uptime Kuma 앞에 둔 사용자를 위한 기능이에요.",
"Please use this option carefully!": "신중하게 사용하세요.",
Logout: "로그아웃",
Leave: "나가기",
"I understand, please disable": "기능에 대해 이해했으니 꺼주세요.",
@ -439,6 +441,7 @@ export default {
Next: "다음",
"The slug is already taken. Please choose another slug.": "이미 존재하는 주소에요. 다른 주소를 사용해 주세요.",
"No Proxy": "프록시 없음",
Authentication: "인증",
"HTTP Basic Auth": "HTTP 인증",
"New Status Page": "새로운 상태 페이지",
"Page Not Found": "페이지를 찾을 수 없어요",

@ -55,8 +55,7 @@ export default {
Current: "Nåværende",
Uptime: "Oppetid",
"Cert Exp.": "Sertifikat utløper",
days: "dager",
day: "dag",
day: "dag | dager",
"-day": "-dag",
hour: "time",
"-hour": "-time",
@ -97,6 +96,9 @@ export default {
"Update Password": "Oppdater passord",
"Disable Auth": "Deaktiver autentisering",
"Enable Auth": "Aktiver autentisering",
"disableauth.message1": "Er du sikker på at du vil <strong>deaktiver autentisering</strong>?",
"disableauth.message2": "Dette er for <strong>de som har tredjepartsautorisering</strong> foran Uptime Kuma, for eksempel Cloudflare Access.",
"Please use this option carefully!": "Vennligst vær forsiktig.",
Logout: "Logg ut",
Leave: "Forlat",
"I understand, please disable": "Jeg forstår, vennligst deaktiver",

@ -52,8 +52,7 @@ export default {
Current: "Huidig",
Uptime: "Uptime",
"Cert Exp.": "Cert. verl.",
days: "dagen",
day: "dag",
day: "dag | dagen",
"-day": "-dag",
hour: "uur",
"-hour": "-uur",

@ -55,8 +55,7 @@ export default {
Current: "Aktualny",
Uptime: "Czas pracy",
"Cert Exp.": "Certyfikat wygasa",
days: "dni",
day: "dzień",
day: "dzień | dni",
"-day": " dni",
hour: "godzina",
"-hour": " godzin",
@ -97,6 +96,9 @@ export default {
"Update Password": "Zaktualizuj hasło",
"Disable Auth": "Wyłącz autoryzację",
"Enable Auth": "Włącz autoryzację",
"disableauth.message1": "Czy na pewno chcesz <strong>wyłączyć autoryzację</strong>?",
"disableauth.message2": "Jest przeznaczony dla <strong>kogoś, kto ma autoryzację zewnętrzną</strong> przed Uptime Kuma, taką jak Cloudflare Access.",
"Please use this option carefully!": "Proszę używać ostrożnie.",
Logout: "Wyloguj",
Leave: "Zostaw",
"I understand, please disable": "Rozumiem, proszę wyłączyć",
@ -429,6 +431,7 @@ export default {
Next: "Dalej",
"The slug is already taken. Please choose another slug.": "Ten symbol jest już zajęty. Proszę, wybierz inny.",
"No Proxy": "Bez proxy",
Authentication: "Uwierzytelnianie",
"HTTP Basic Auth": "Podstawowa autoryzacja HTTP",
"New Status Page": "Nowa strona statusu",
"Page Not Found": "Strona nie została znaleziona",

@ -55,8 +55,7 @@ export default {
Current: "Atual",
Uptime: "Tempo de atividade",
"Cert Exp.": "Cert Exp.",
days: "dias",
day: "dia",
day: "dia | dias",
"-day": "-dia",
hour: "hora",
"-hour": "-hora",
@ -97,6 +96,9 @@ export default {
"Update Password": "Atualizar Senha",
"Disable Auth": "Desativar Autenticação",
"Enable Auth": "Ativar Autenticação",
"disableauth.message1": "Você tem certeza que deseja <strong>desativar a autenticação</strong>?",
"disableauth.message2": "Isso é para <strong>alguém que tem autenticação de terceiros</strong> na frente do 'UpTime Kuma' como o Cloudflare Access.",
"Please use this option carefully!": "Por favor, utilize isso com cautela.",
Logout: "Deslogar",
Leave: "Sair",
"I understand, please disable": "Eu entendo, por favor desative.",

@ -44,8 +44,7 @@ export default {
Current: "Текущий",
Uptime: "Аптайм",
"Cert Exp.": "Сертификат истекает",
days: "дней",
day: "день",
day: "день | дней",
"-day": " дней",
hour: "час",
"-hour": " часа",
@ -85,6 +84,9 @@ export default {
"Update Password": "Обновить пароль",
"Disable Auth": "Отключить авторизацию",
"Enable Auth": "Включить авторизацию",
"disableauth.message1": "Вы уверены, что хотите <strong>отключить авторизацию</strong>?",
"disableauth.message2": "Это подходит для <strong>тех, у кого стоит другая авторизация</strong> перед открытием Uptime Kuma, например Cloudflare Access.",
"Please use this option carefully!": "Пожалуйста, используйте с осторожностью.",
Logout: "Выйти",
Leave: "Отмена",
"I understand, please disable": "Я понимаю, всё равно отключить",
@ -352,7 +354,8 @@ export default {
"Start or end with a-z 0-9 only": "Начало и окончание имени только на символы: a-z 0-9",
"No consecutive dashes --": "Запрещено использовать тире --",
"HTTP Options": "HTTP Опции",
"Basic Auth": "HTTP Авторизация",
Authentication: "Аутентификация",
"HTTP Basic Auth": "HTTP Авторизация",
PushByTechulus: "Push by Techulus",
clicksendsms: "ClickSend SMS",
GoogleChat: "Google Chat (только Google Workspace)",

@ -56,8 +56,7 @@ export default {
Current: "Trenutno",
Uptime: "Uptime",
"Cert Exp.": "Potek certifikata",
days: "dni",
day: "dan",
day: "dan | dni",
"-day": "-dni",
hour: "ura",
"-hour": "-ur",
@ -101,6 +100,9 @@ export default {
"Update Password": "Posodobi geslo",
"Disable Auth": "Onemogoči auth",
"Enable Auth": "Omogoči auth",
"disableauth.message1": "Ali ste prepričani, da želite onemogočiti <strong>avtentikacijo</strong>?",
"disableauth.message2": "Namenjen je <strong>nekomu, ki ima pred programom Uptime Kuma vklopljeno zunanje preverjanje pristnosti</strong>, na primer Cloudflare Access.",
"Please use this option carefully!": "Uporabljajte previdno.",
Logout: "Odjava",
Leave: "Zapusti",
"I understand, please disable": "Razumem, prosim onemogočite",

@ -44,8 +44,7 @@ export default {
Current: "Trenutno",
Uptime: "Vreme rada",
"Cert Exp.": "Istek sert.",
days: "dana",
day: "dan",
day: "dan | dana",
"-day": "-dana",
hour: "sat",
"-hour": "-sata",
@ -85,6 +84,9 @@ export default {
"Update Password": "Izmeni lozinku",
"Disable Auth": "Isključi autentifikaciju",
"Enable Auth": "Uključi autentifikaciju",
"disableauth.message1": "Da li ste sigurni da želite da <strong>isključite autentifikaciju</strong>?",
"disableauth.message2": "To je za <strong>one koji imaju dodatu autentifikaciju</strong> ispred Uptime Kuma kao na primer Cloudflare Access.",
"Please use this option carefully!": "Molim Vas koristite ovo sa pažnjom.",
Logout: "Odloguj se",
Leave: "Izađi",
"I understand, please disable": "Razumem, molim te isključi",

@ -44,8 +44,7 @@ export default {
Current: "Тренутно",
Uptime: "Време рада",
"Cert Exp.": "Истек серт.",
days: "дана",
day: "дан",
day: "дан | дана",
"-day": "-дана",
hour: "сат",
"-hour": "-сата",
@ -85,6 +84,9 @@ export default {
"Update Password": "Измени лозинку",
"Disable Auth": "Искључи аутентификацију",
"Enable Auth": "Укључи аутентификацију",
"disableauth.message1": "Да ли сте сигурни да желите да <strong>искључите аутентификацију</strong>?",
"disableauth.message2": "То је за <strong>оне који имају додату аутентификацију</strong> испред Uptime Kuma као на пример Cloudflare Access.",
"Please use this option carefully!": "Молим Вас користите ово са пажњом.",
Logout: "Одлогуј се",
Leave: "Изађи",
"I understand, please disable": "Разумем, молим те искључи",

@ -44,8 +44,7 @@ export default {
Current: "Nuvarande",
Uptime: "Drifttid",
"Cert Exp.": "Certifikat utgår",
days: "dagar",
day: "dag",
day: "dag | dagar",
"-day": " dagar",
hour: "timme",
"-hour": " timmar",

@ -101,6 +101,9 @@ export default {
"Update Password": "อัพเดทรหัสผ่าน",
"Disable Auth": "ปิดใช้งานการตรวจสอบสิทธิ์",
"Enable Auth": "เปิดใช้งานการตรวจสอบสิทธิ์",
"disableauth.message1": "คุณต้องการที่จะ <strong>ปิดใช้งานระบบรับรองความถูกต้องใช่หรือไม่</strong>?",
"disableauth.message2": "ระบบนี้ถูกออกแบบมาเพื่อการใช้งานกับระบบรับรองความถูกต้องของบุคคลที่สามเช่น Cloudflare Access, Authelia หรือวิธีการอื่น ๆ",
"Please use this option carefully!": "โปรดใช้ความระมัดระวังในการเลือกใช้งานระบบนี้ !",
Logout: "ออกจากระบบ",
Leave: "ออก",
"I understand, please disable": "ฉันเข้าใจแล้ว, กรุณาปิดการใช้งาน",

@ -57,8 +57,7 @@ export default {
Current: "Şu anda",
Uptime: "Çalışma zamanı",
"Cert Exp.": "Sertifika Süresi",
days: "günler",
day: "gün",
day: "gün | günler",
"-day": "-gün",
hour: "saat",
"-hour": "-saat",
@ -102,6 +101,9 @@ export default {
"Update Password": "Şifreyi Değiştir",
"Disable Auth": "Şifreli girişi iptal et.",
"Enable Auth": "Şifreli girişi aktif et.",
"disableauth.message1": "<strong>Şifreli girişi devre dışı bırakmak istediğinizden</strong>emin misiniz?",
"disableauth.message2": "Bu, Uptime Kuma'nın önünde Cloudflare Access gibi <strong>üçüncü taraf yetkilendirmesi olan</strong> kişiler içindir.",
"Please use this option carefully!": "Lütfen dikkatli kullanın.",
Logout: ıkış yap",
Leave: "Ayrıl",
"I understand, please disable": "Evet farkındayım, iptal et",

@ -44,8 +44,7 @@ export default {
Current: "Поточний",
Uptime: "Аптайм",
"Cert Exp.": "Сертифікат спливає",
days: "днів",
day: "день",
day: "день | днів",
"-day": " днів",
hour: "година",
"-hour": " години",
@ -85,6 +84,9 @@ export default {
"Update Password": "Оновити пароль",
"Disable Auth": "Вимкнути авторизацію",
"Enable Auth": "Увімкнути авторизацію",
"disableauth.message1": "Ви впевнені, що бажаєте <strong>вимкнути авторизацію</strong>?",
"disableauth.message2": "Це підходить для <strong>тих, у кого встановлена інша авторизація</strong> пееред відкриттям Uptime Kuma, наприклад Cloudflare Access.",
"Please use this option carefully!": "Будь ласка, використовуйте з обережністю.",
Logout: "Вийти",
Leave: "Відміна",
"I understand, please disable": "Я розумію, все одно відключити",
@ -351,7 +353,8 @@ export default {
"Start or end with a-z 0-9 only": "Початок та закінчення імені лише на символи: a-z 0-9",
"No consecutive dashes --": "Заборонено використовувати тире --",
"HTTP Options": "HTTP Опції",
"Basic Auth": "HTTP Авторизація",
Authentication: "Аутентифікація",
"HTTP Basic Auth": "HTTP Авторизація",
PushByTechulus: "Push by Techulus",
clicksendsms: "ClickSend SMS",
GoogleChat: "Google Chat (тільки Google Workspace)",

@ -56,7 +56,6 @@ export default {
Current: "Hiện tại",
Uptime: "Uptime",
"Cert Exp.": "Cert hết hạn",
days: "ngày",
day: "ngày",
"-day": "-ngày",
hour: "giờ",
@ -101,6 +100,9 @@ export default {
"Update Password": "Cập nhật mật khẩu",
"Disable Auth": "Tắt xác minh",
"Enable Auth": "Bật xác minh",
"disableauth.message1": "Bạn có muốn <strong>TẮT XÁC THỰC</strong> không?",
"disableauth.message2": "Điều này rất nguy hiểm<strong>BẤT KỲ AI</strong> cũng có thể truy cập và cướp quyền điều khiển.",
"Please use this option carefully!": "Vui lòng <strong>cẩn thận</strong>.",
Logout: "Đăng xuất",
Leave: "Rời",
"I understand, please disable": "Tôi hiểu, làm ơn hãy tắt!",

@ -57,7 +57,6 @@ export default {
Current: "当前",
Uptime: "在线时间",
"Cert Exp.": "证书有效期",
days: "天",
day: "天",
"-day": " 天",
hour: "小时",
@ -102,6 +101,9 @@ export default {
"Update Password": "更新密码",
"Disable Auth": "禁用身份验证",
"Enable Auth": "启用身份验证",
"disableauth.message1": "是否确定 <strong>取消登录验证</strong>",
"disableauth.message2": "这是为 <strong>有第三方认证</strong> 的用户提供的功能,如 Cloudflare Access",
"Please use this option carefully!": "请谨慎使用!",
Logout: "退出",
Leave: "离开",
"I understand, please disable": "我已了解,继续禁用",
@ -437,6 +439,7 @@ export default {
Next: "下一步",
"The slug is already taken. Please choose another slug.": "该路径已被使用。请选择其他路径。",
"No Proxy": "无代理",
Authentication: "验证",
"HTTP Basic Auth": "HTTP 基础身份验证",
"New Status Page": "新的状态页",
"Page Not Found": "未找到该页面",
@ -523,11 +526,18 @@ export default {
dnsPortDescription: "DNS 服务器端口,默认为 53你可以在任何时候更改此端口.",
error: "错误",
critical: "关键",
wayToGetPagerDutyKey: "你可以在 Service -> Service Directory -> (Select a service) -> Integrations -> Add integration 页面中搜索 \"Events API V2\" 以获取此 Integration Key更多信息请参见 {0}",
wayToGetPagerDutyKey: "你可以在 Service -> Service Directory -> (选择一个 Service) -> Integrations -> Add integration 页面中搜索 \"Events API V2\" 以获取此 Integration Key更多信息请参见 {0}",
"Integration Key": "Integration Key",
"Integration URL": "Integration URL",
"Auto resolve or acknowledged": "自动标记为已解决或已读",
"do nothing": "不做任何操作",
"auto acknowledged": "自动标记为已读",
"auto resolve": "自动标记为已解决",
"Connection String": "连接字符串",
Query: "查询语句",
settingsCertificateExpiry: "TLS 证书过期通知",
certificationExpiryDescription: "HTTPS 监控项发现被监控目标的 TLS 证书剩余有效期少于以下天数时将发出通知:",
"ntfy Topic": "ntfy 主题",
"Domain": "域名",
"Workstation": "工作站",
};

@ -30,7 +30,6 @@ export default {
Current: "目前",
Uptime: "上線率",
"Cert Exp.": "証書期限",
days: "日",
day: "日",
"-day": "日",
hour: "小時",
@ -78,6 +77,9 @@ export default {
"Update Password": "更新密碼",
"Disable Auth": "取消登入認証",
"Enable Auth": "開啟登入認証",
"disableauth.message1": "你是否確認<strong>取消登入認証</strong>",
"disableauth.message2": "這個功能是設計給已有<strong>第三方認証</strong>的用家,例如 Cloudflare Access。",
"Please use this option carefully!": "請小心使用。",
Logout: "登出",
notificationDescription: "新增後,你需要在監測器裡啟用。",
Leave: "離開",

@ -56,7 +56,6 @@ export default {
Current: "目前",
Uptime: "運作率",
"Cert Exp.": "憑證期限",
days: "天",
day: "天",
"-day": "天",
hour: "小時",
@ -101,6 +100,9 @@ export default {
"Update Password": "更新密碼",
"Disable Auth": "停用驗證",
"Enable Auth": "啟用驗證",
"disableauth.message1": ">你是否要<strong>取消登入驗證</strong>",
"disableauth.message2": "此功能是設計給已有<strong>第三方認證</strong>的使用者,例如 Cloudflare Access。",
"Please use this option carefully!": "請謹慎使用。",
Logout: "登出",
Leave: "離開",
"I understand, please disable": "我了解了,請停用",
@ -429,6 +431,7 @@ export default {
Next: "下一步",
"The slug is already taken. Please choose another slug.": "此 slug 已被使用。請選擇其他 slug。",
"No Proxy": "無 Proxy",
Authentication: "驗證",
"HTTP Basic Auth": "HTTP 基本驗證",
"New Status Page": "新狀態頁",
"Page Not Found": "找不到頁面",

@ -18,14 +18,31 @@ export default {
},
methods: {
/**
* Return a given value in the format YYYY-MM-DD HH:mm:ss
* @param {any} value Value to format as date time
* @returns {string}
*/
datetime(value) {
return this.datetimeFormat(value, "YYYY-MM-DD HH:mm:ss");
},
/**
* Return a given value in the format YYYY-MM-DD
* @param {any} value Value to format as date
* @returns {string}
*/
date(value) {
return this.datetimeFormat(value, "YYYY-MM-DD");
},
/**
* Return a given value in the format HH:mm or if second is set
* to true, HH:mm:ss
* @param {any} value Value to format
* @param {boolean} second Should seconds be included?
* @returns {string}
*/
time(value, second = true) {
let secondString;
if (second) {
@ -36,6 +53,12 @@ export default {
return this.datetimeFormat(value, "HH:mm" + secondString);
},
/**
* Return a value in a custom format
* @param {any} value Value to format
* @param {any} format Format to return value in
* @returns {string}
*/
datetimeFormat(value, format) {
if (value !== undefined && value !== "") {
return dayjs.utc(value).tz(this.timezone).format(format);

@ -22,6 +22,7 @@ export default {
},
methods: {
/** Change the application language */
async changeLang(lang) {
let message = (await langModules["../languages/" + lang + ".js"]()).default;
this.$i18n.setLocaleMessage(lang, message);

@ -12,11 +12,13 @@ export default {
},
methods: {
/** Handle screen resize */
onResize() {
this.windowWidth = window.innerWidth;
this.updateBody();
},
/** Add css-class "mobile" to body if needed */
updateBody() {
if (this.isMobile) {
document.body.classList.add("mobile");

@ -62,6 +62,12 @@ export default {
methods: {
/**
* Initialize connection to socket server
* @param {boolean} [bypass = false] Should the check for if we
* are on a status page be bypassed?
* @returns {(void|null)}
*/
initSocketIO(bypass = false) {
// No need to re-init
if (this.socket.initedSocketIO) {
@ -258,10 +264,18 @@ export default {
socket.on("cloudflared_token", (res) => this.cloudflared.cloudflareTunnelToken = res);
},
/**
* The storage currently in use
* @returns {Storage}
*/
storage() {
return (this.remember) ? localStorage : sessionStorage;
},
/**
* Get payload of JWT cookie
* @returns {(Object|undefined)}
*/
getJWTPayload() {
const jwtToken = this.$root.storage().token;
@ -271,10 +285,18 @@ export default {
return undefined;
},
/**
* Get current socket
* @returns {Socket}
*/
getSocket() {
return socket;
},
/**
* Show success or error toast dependant on response status code
* @param {Object} res Response object
*/
toastRes(res) {
if (res.ok) {
toast.success(res.msg);
@ -283,14 +305,35 @@ export default {
}
},
/**
* Show a success toast
* @param {string} msg Message to show
*/
toastSuccess(msg) {
toast.success(msg);
},
/**
* Show an error toast
* @param {string} msg Message to show
*/
toastError(msg) {
toast.error(msg);
},
/**
* Callback for login
* @callback loginCB
* @param {Object} res Response object
*/
/**
* Send request to log user in
* @param {string} username Username to log in with
* @param {string} password Password to log in with
* @param {string} token User token
* @param {loginCB} callback Callback to call with result
*/
login(username, password, token, callback) {
socket.emit("login", {
username,
@ -315,6 +358,10 @@ export default {
});
},
/**
* Log in using a token
* @param {string} token Token to log in with
*/
loginByToken(token) {
socket.emit("loginByToken", token, (res) => {
this.allowLoginDialog = true;
@ -328,6 +375,7 @@ export default {
});
},
/** Log out of the web application */
logout() {
socket.emit("logout", () => { });
this.storage().removeItem("token");
@ -337,26 +385,54 @@ export default {
this.clearData();
},
/**
* Callback for general socket requests
* @callback socketCB
* @param {Object} res Result of operation
*/
/** Prepare 2FA configuration */
prepare2FA(callback) {
socket.emit("prepare2FA", callback);
},
/**
* Save the current 2FA configuration
* @param {any} secret Unused
* @param {socketCB} callback
*/
save2FA(secret, callback) {
socket.emit("save2FA", callback);
},
/**
* Disable 2FA for this user
* @param {socketCB} callback
*/
disable2FA(callback) {
socket.emit("disable2FA", callback);
},
/**
* Verify the provided 2FA token
* @param {string} token Token to verify
* @param {socketCB} callback
*/
verifyToken(token, callback) {
socket.emit("verifyToken", token, callback);
},
/**
* Get current 2FA status
* @param {socketCB} callback
*/
twoFAStatus(callback) {
socket.emit("twoFAStatus", callback);
},
/**
* Get list of monitors
* @param {socketCB} callback
*/
getMonitorList(callback) {
if (! callback) {
callback = () => { };
@ -364,36 +440,74 @@ export default {
socket.emit("getMonitorList", callback);
},
/**
* Add a monitor
* @param {Object} monitor Object representing monitor to add
* @param {socketCB} callback
*/
add(monitor, callback) {
socket.emit("add", monitor, callback);
},
/**
* Delete monitor by ID
* @param {number} monitorID ID of monitor to delete
* @param {socketCB} callback
*/
deleteMonitor(monitorID, callback) {
socket.emit("deleteMonitor", monitorID, callback);
},
/** Clear the hearbeat list */
clearData() {
console.log("reset heartbeat list");
this.heartbeatList = {};
this.importantHeartbeatList = {};
},
/**
* Upload the provided backup
* @param {string} uploadedJSON JSON to upload
* @param {string} importHandle Type of import. If set to
* most data in database will be replaced
* @param {socketCB} callback
*/
uploadBackup(uploadedJSON, importHandle, callback) {
socket.emit("uploadBackup", uploadedJSON, importHandle, callback);
},
/**
* Clear events for a specified monitor
* @param {number} monitorID ID of monitor to clear
* @param {socketCB} callback
*/
clearEvents(monitorID, callback) {
socket.emit("clearEvents", monitorID, callback);
},
/**
* Clear the heartbeats of a specified monitor
* @param {number} monitorID Id of monitor to clear
* @param {socketCB} callback
*/
clearHeartbeats(monitorID, callback) {
socket.emit("clearHeartbeats", monitorID, callback);
},
/**
* Clear all statistics
* @param {socketCB} callback
*/
clearStatistics(callback) {
socket.emit("clearStatistics", callback);
},
/**
* Get monitor beats for a specific monitor in a time range
* @param {number} monitorID ID of monitor to fetch
* @param {number} period Time in hours from now
* @param {socketCB} callback
*/
getMonitorBeats(monitorID, period, callback) {
socket.emit("getMonitorBeats", monitorID, period, callback);
}

@ -75,6 +75,7 @@ export default {
},
methods: {
/** Update the theme color meta tag */
updateThemeColorMeta() {
if (this.theme === "dark") {
document.querySelector("#theme-color").setAttribute("content", "#161B22");

@ -51,6 +51,7 @@ export default {
};
},
methods: {
/** Submit form data to add new status page */
async submit() {
this.processing = true;

@ -77,7 +77,7 @@
<h4>{{ $t("Cert Exp.") }}</h4>
<p>(<Datetime :value="tlsInfo.certInfo.validTo" date-only />)</p>
<span class="num">
<a href="#" @click.prevent="toggleCertInfoBox = !toggleCertInfoBox">{{ tlsInfo.certInfo.daysRemaining }} {{ $t("days") }}</a>
<a href="#" @click.prevent="toggleCertInfoBox = !toggleCertInfoBox">{{ tlsInfo.certInfo.daysRemaining }} {{ $tc("day", tlsInfo.certInfo.daysRemaining) }}</a>
</span>
</div>
</div>
@ -289,39 +289,47 @@ export default {
},
methods: {
/** Request a test notification be sent for this monitor */
testNotification() {
this.$root.getSocket().emit("testNotification", this.monitor.id);
toast.success("Test notification is requested.");
},
/** Show dialog to confirm pause */
pauseDialog() {
this.$refs.confirmPause.show();
},
/** Resume this monitor */
resumeMonitor() {
this.$root.getSocket().emit("resumeMonitor", this.monitor.id, (res) => {
this.$root.toastRes(res);
});
},
/** Request that this monitor is paused */
pauseMonitor() {
this.$root.getSocket().emit("pauseMonitor", this.monitor.id, (res) => {
this.$root.toastRes(res);
});
},
/** Show dialog to confirm deletion */
deleteDialog() {
this.$refs.confirmDelete.show();
},
/** Show dialog to confirm clearing events */
clearEventsDialog() {
this.$refs.confirmClearEvents.show();
},
/** Show dialog to confirm clearing heartbeats */
clearHeartbeatsDialog() {
this.$refs.confirmClearHeartbeats.show();
},
/** Request that this monitor is deleted */
deleteMonitor() {
this.$root.deleteMonitor(this.monitor.id, (res) => {
if (res.ok) {
@ -333,6 +341,7 @@ export default {
});
},
/** Request that this monitors events are cleared */
clearEvents() {
this.$root.clearEvents(this.monitor.id, (res) => {
if (! res.ok) {
@ -341,6 +350,7 @@ export default {
});
},
/** Request that this monitors heartbeats are cleared */
clearHeartbeats() {
this.$root.clearHeartbeats(this.monitor.id, (res) => {
if (! res.ok) {
@ -349,6 +359,11 @@ export default {
});
},
/**
* Return the correct title for the ping stat
* @param {boolean} [average=false] Is the statistic an average?
* @returns {string} Title formated dependant on monitor type
*/
pingTitle(average = false) {
let translationPrefix = "";
if (average) {

@ -11,30 +11,41 @@
<div class="my-3">
<label for="type" class="form-label">{{ $t("Monitor Type") }}</label>
<select id="type" v-model="monitor.type" class="form-select">
<option value="http">
HTTP(s)
</option>
<option value="port">
TCP Port
</option>
<option value="ping">
Ping
</option>
<option value="keyword">
HTTP(s) - {{ $t("Keyword") }}
</option>
<option value="dns">
DNS
</option>
<option value="push">
Push
</option>
<option value="steam">
{{ $t("Steam Game Server") }}
</option>
<option value="mqtt">
MQTT
</option>
<optgroup label="General Monitor Type">
<option value="http">
HTTP(s)
</option>
<option value="port">
TCP Port
</option>
<option value="ping">
Ping
</option>
<option value="keyword">
HTTP(s) - {{ $t("Keyword") }}
</option>
<option value="dns">
DNS
</option>
</optgroup>
<optgroup label="Passive Monitor Type">
<option value="push">
Push
</option>
</optgroup>
<optgroup label="Specific Monitor Type">
<option value="steam">
{{ $t("Steam Game Server") }}
</option>
<option value="mqtt">
MQTT
</option>
<option value="sqlserver">
SQL Server
</option>
</optgroup>
</select>
</div>
@ -157,6 +168,18 @@
</div>
</template>
<!-- SQL Server -->
<template v-if="monitor.type === 'sqlserver'">
<div class="my-3">
<label for="sqlserverConnectionString" class="form-label">SQL Server {{ $t("Connection String") }}</label>
<input id="sqlserverConnectionString" v-model="monitor.databaseConnectionString" type="text" class="form-control">
</div>
<div class="my-3">
<label for="sqlserverQuery" class="form-label">SQL Server {{ $t("Query") }}</label>
<textarea id="sqlserverQuery" v-model="monitor.databaseQuery" class="form-control" placeholder="Example: select getdate()"></textarea>
</div>
</template>
<!-- Interval -->
<div class="my-3">
<label for="interval" class="form-label">{{ $t("Heartbeat Interval") }} ({{ $t("checkEverySecond", [ monitor.interval ]) }})</label>
@ -345,18 +368,46 @@
<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>
<!-- HTTP Auth -->
<h4 class="mt-5 mb-2">{{ $t("Authentication") }}</h4>
<!-- Method -->
<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')">
<label for="method" class="form-label">{{ $t("Method") }}</label>
<select id="method" v-model="monitor.authMethod" class="form-select">
<option :value="null">
{{ $t("None") }}
</option>
<option value="basic">
{{ $t("HTTP Basic Auth") }}
</option>
<option value="ntlm">
NTLM
</option>
</select>
</div>
<template v-if="monitor.authMethod && monitor.authMethod !== null ">
<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" autocomplete="new-password" class="form-control" :placeholder="$t('Password')">
</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" autocomplete="new-password" class="form-control" :placeholder="$t('Password')">
</div>
<template v-if="monitor.authMethod === 'ntlm' ">
<div class="my-3">
<label for="basicauth" class="form-label">{{ $t("Domain") }}</label>
<input id="basicauth-domain" v-model="monitor.authDomain" type="text" class="form-control" :placeholder="$t('Domain')">
</div>
<div class="my-3">
<label for="basicauth" class="form-label">{{ $t("Workstation") }}</label>
<input id="basicauth-workstation" v-model="monitor.authWorkstation" type="text" class="form-control" :placeholder="$t('Workstation')">
</div>
</template>
</template>
</template>
</div>
</div>
@ -522,6 +573,7 @@ export default {
this.dnsresolvetypeOptions = dnsresolvetypeOptions;
},
methods: {
/** Initialize the edit monitor form */
init() {
if (this.isAdd) {
@ -532,6 +584,7 @@ export default {
method: "GET",
interval: 60,
retryInterval: this.interval,
databaseConnectionString: "Server=<hostname>,<port>;Database=<your database>;User Id=<your user id>;Password=<your password>;Encrypt=<true/false>;TrustServerCertificate=<Yes/No>;Connection Timeout=<int>",
maxretries: 0,
notificationIDList: {},
ignoreTls: false,
@ -546,6 +599,7 @@ export default {
mqttPassword: "",
mqttTopic: "",
mqttSuccessMessage: "",
authMethod: null,
};
if (this.$root.proxyList && !this.monitor.proxyId) {
@ -578,6 +632,10 @@ export default {
},
/**
* Validate form input
* @returns {boolean} Is the form input valid?
*/
isInputValid() {
if (this.monitor.body) {
try {
@ -598,6 +656,10 @@ export default {
return true;
},
/**
* Submit the form data for processing
* @returns {void}
*/
async submit() {
this.processing = true;
@ -642,14 +704,20 @@ export default {
}
},
// Added a Notification Event
// Enable it if the notification is added in EditMonitor.vue
/**
* Added a Notification Event
* Enable it if the notification is added in EditMonitor.vue
* @param {number} id ID of notification to add
*/
addedNotification(id) {
this.monitor.notificationIDList[id] = true;
},
// Added a Proxy Event
// Enable it if the proxy is added in EditMonitor.vue
/**
* Added a Proxy Event
* Enable it if the proxy is added in EditMonitor.vue
* @param {number} id ID of proxy to add
*/
addedProxy(id) {
this.monitor.proxyId = id;
},

@ -1,6 +1,6 @@
<template>
<transition name="slide-fade" appear>
<MonitorList />
<MonitorList :scrollbar="true" />
</transition>
</template>
@ -14,3 +14,11 @@ export default {
};
</script>
<style lang="scss" scoped>
@import "../assets/vars";
.shadow-box {
padding: 20px;
}
</style>

@ -51,6 +51,11 @@ export default {
},
methods: {
/**
* Get the correct URL for the icon
* @param {string} icon Path for icon
* @returns {string} Correctly formatted path including port numbers
*/
icon(icon) {
if (icon === "/icon.svg") {
return icon;

@ -45,6 +45,7 @@ export default {
},
methods: {
/** Go back 1 in browser history */
goBack() {
history.back();
}

@ -118,13 +118,17 @@ export default {
methods: {
// For desktop only, mobile do nothing
/**
* Load the general settings page
* For desktop only, on mobile do nothing
*/
loadGeneralPage() {
if (!this.currentPage && !this.$root.isMobile) {
this.$router.push("/settings/general");
}
},
/** Load settings from server */
loadSettings() {
this.$root.getSocket().emit("getSettings", (res) => {
this.settings = res.data;
@ -145,13 +149,24 @@ export default {
this.settings.keepDataPeriodDays = 180;
}
if (this.settings.tlsExpiryNotifyDays === undefined) {
this.settings.tlsExpiryNotifyDays = [ 7, 14, 21 ];
}
this.settingsLoaded = true;
});
},
/**
* Callback for saving settings
* @callback saveSettingsCB
* @param {Object} res Result of operation
*/
/**
* Save Settings
* @param currentPassword (Optional) Only need for disableAuth to true
* @param {saveSettingsCB} [callback]
* @param {string} [currentPassword] Only need for disableAuth to true
*/
saveSettings(callback, currentPassword) {
this.$root.getSocket().emit("setSettings", this.settings, currentPassword, (res) => {

@ -71,6 +71,10 @@ export default {
});
},
methods: {
/**
* Submit form data for processing
* @returns {void}
*/
submit() {
this.processing = true;

@ -332,6 +332,7 @@ export default {
},
props: {
/** Override for the status page slug */
overrideSlug: {
type: String,
required: false,
@ -587,10 +588,16 @@ export default {
}
},
/**
* Provide syntax highlighting for CSS
* @param {string} code Text to highlight
* @returns {string}
*/
highlighter(code) {
return highlight(code, languages.css);
},
/** Update the heartbeat list and update favicon if neccessary */
updateHeartbeatList() {
// If editMode, it will use the data from websocket.
if (! this.editMode) {
@ -619,14 +626,19 @@ export default {
}
},
/** Enable editing mode */
edit() {
if (this.hasToken) {
this.$root.initSocketIO(true);
this.enableEditMode = true;
this.clickedEditButton = true;
// Try to fix #1658
this.loadedData = true;
}
},
/** Save the status page */
save() {
let startTime = new Date();
this.config.slug = this.config.slug.trim().toLowerCase();
@ -654,10 +666,12 @@ export default {
});
},
/** Show dialog confirming deletion */
deleteDialog() {
this.$refs.confirmDelete.show();
},
/** Request deletion of this status page */
deleteStatusPage() {
this.$root.getSocket().emit("deleteStatusPage", this.slug, (res) => {
if (res.ok) {
@ -669,10 +683,16 @@ export default {
});
},
/**
* Returns label for a specifed monitor
* @param {Object} monitor Object representing monitor
* @returns {string}
*/
monitorSelectorLabel(monitor) {
return `${monitor.name}`;
},
/** Add a group to the status page */
addGroup() {
let groupName = this.$t("Untitled Group");
@ -686,27 +706,32 @@ export default {
});
},
/** Add a domain to the status page */
addDomainField() {
this.config.domainNameList.push("");
},
/** Discard changes to status page */
discard() {
location.href = "/status/" + this.slug;
},
/**
* Crop Success
* Set URL of new image after successful crop operation
* @param {string} imgDataUrl URL of image in data:// format
*/
cropSuccess(imgDataUrl) {
this.imgDataUrl = imgDataUrl;
},
/** Show image crop dialog if in edit mode */
showImageCropUploadMethod() {
if (this.editMode) {
this.showImageCropUpload = true;
}
},
/** Create an incident for this status page */
createIncident() {
this.enableEditIncidentMode = true;
@ -721,6 +746,7 @@ export default {
};
},
/** Post the incident to the status page */
postIncident() {
if (this.incident.title === "" || this.incident.content === "") {
toast.error(this.$t("Please input title and content"));
@ -740,14 +766,13 @@ export default {
},
/**
* Click Edit Button
*/
/** Click Edit Button */
editIncident() {
this.enableEditIncidentMode = true;
this.previousIncident = Object.assign({}, this.incident);
},
/** Cancel creation or editing of incident */
cancelIncident() {
this.enableEditIncidentMode = false;
@ -757,16 +782,25 @@ export default {
}
},
/** Unpin the incident */
unpinIncident() {
this.$root.getSocket().emit("unpinIncident", this.slug, () => {
this.incident = null;
});
},
/**
* Get the relative time difference of a date from now
* @returns {string}
*/
dateFromNow(date) {
return dayjs.utc(date).fromNow();
},
/**
* Remove a domain from the status page
* @param {number} index Index of domain to remove
*/
removeDomain(index) {
this.config.domainNameList.splice(index, 1);
},

@ -65,12 +65,12 @@ const routes = [
path: "/add",
component: EditMonitor,
},
{
path: "/list",
component: List,
},
],
},
{
path: "/list",
component: List,
},
{
path: "/settings",
component: Settings,

@ -26,8 +26,8 @@ function getTimezoneOffset(timeZone) {
/**
* Returns a list of timezones sorted by their offset from UTC.
* @param {Array} timezones - An array of timezone objects.
* @returns {Array} A list of the given timezones sorted by their offset from UTC.
* @param {Object[]} timezones An array of timezone objects.
* @returns {Object[]} A list of the given timezones sorted by their offset from UTC.
*
* Generated by Trelent
*/
@ -63,6 +63,7 @@ export function timezoneList() {
return result;
}
/** Set the locale of the HTML page */
export function setPageLocale() {
const html = document.documentElement;
html.setAttribute("lang", currentLocale() );
@ -70,7 +71,9 @@ export function setPageLocale() {
}
/**
* Get the base URL
* Mainly used for dev, because the backend and the frontend are in different ports.
* @returns {string}
*/
export function getResBaseURL() {
const env = process.env.NODE_ENV;

@ -18,6 +18,7 @@ exports.PENDING = 2;
exports.STATUS_PAGE_ALL_DOWN = 0;
exports.STATUS_PAGE_ALL_UP = 1;
exports.STATUS_PAGE_PARTIAL_DOWN = 2;
/** Flip the status of s */
function flipStatus(s) {
if (s === exports.UP) {
return exports.DOWN;
@ -28,6 +29,10 @@ function flipStatus(s) {
return s;
}
exports.flipStatus = flipStatus;
/**
* Delays for specified number of seconds
* @param ms Number of milliseconds to sleep for
*/
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
@ -83,6 +88,12 @@ class Logger {
this.debug("server", this.hideLog);
}
}
/**
* Write a message to the log
* @param module The module the log comes from
* @param msg Message to write
* @param level Log level. One of INFO, WARN, ERROR, DEBUG or can be customized.
*/
log(module, msg, level) {
if (this.hideLog[level] && this.hideLog[level].includes(module)) {
return;
@ -109,18 +120,44 @@ class Logger {
console.log(formattedMessage);
}
}
/**
* Log an INFO message
* @param module Module log comes from
* @param msg Message to write
*/
info(module, msg) {
this.log(module, msg, "info");
}
/**
* Log a WARN message
* @param module Module log comes from
* @param msg Message to write
*/
warn(module, msg) {
this.log(module, msg, "warn");
}
/**
* Log an ERROR message
* @param module Module log comes from
* @param msg Message to write
*/
error(module, msg) {
this.log(module, msg, "error");
}
/**
* Log a DEBUG message
* @param module Module log comes from
* @param msg Message to write
*/
debug(module, msg) {
this.log(module, msg, "debug");
}
/**
* Log an exeption as an ERROR
* @param module Module log comes from
* @param exception The exeption to include
* @param msg The message to write
*/
exception(module, exception, msg) {
let finalMessage = exception;
if (msg) {
@ -130,13 +167,13 @@ class Logger {
}
}
exports.log = new Logger();
/**
* String.prototype.replaceAll() polyfill
* https://gomakethings.com/how-to-replace-a-section-of-a-string-with-another-one-with-vanilla-js/
* @author Chris Ferdinandi
* @license MIT
*/
function polyfill() {
/**
* String.prototype.replaceAll() polyfill
* https://gomakethings.com/how-to-replace-a-section-of-a-string-with-another-one-with-vanilla-js/
* @author Chris Ferdinandi
* @license MIT
*/
if (!String.prototype.replaceAll) {
String.prototype.replaceAll = function (str, newStr) {
// If a regex pattern
@ -153,6 +190,10 @@ class TimeLogger {
constructor() {
this.startTime = dayjs().valueOf();
}
/**
* Output time since start of monitor
* @param name Name of monitor
*/
print(name) {
if (exports.isDev && process.env.TIMELOGGER === "1") {
console.log(name + ": " + (dayjs().valueOf() - this.startTime) + "ms");
@ -201,6 +242,13 @@ let getRandomBytes = ((typeof window !== 'undefined' && window.crypto)
: function () {
return require("crypto").randomBytes;
})();
/**
* Get a random integer suitable for use in cryptography between upper
* and lower bounds.
* @param min Minimum value of integer
* @param max Maximum value of integer
* @returns Cryptographically suitable random integer
*/
function getCryptoRandomInt(min, max) {
// synchronous version of: https://github.com/joepie91/node-random-number-csprng
const range = max - min;
@ -231,6 +279,11 @@ function getCryptoRandomInt(min, max) {
}
}
exports.getCryptoRandomInt = getCryptoRandomInt;
/**
* Generate a secret
* @param length Lenght of secret to generate
* @returns
*/
function genSecret(length = 64) {
let secret = "";
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
@ -241,6 +294,11 @@ function genSecret(length = 64) {
return secret;
}
exports.genSecret = genSecret;
/**
* Get the path of a monitor
* @param id ID of monitor
* @returns Formatted relative path
*/
function getMonitorRelativeURL(id) {
return "/dashboard/" + id;
}

@ -19,7 +19,7 @@ export const STATUS_PAGE_ALL_DOWN = 0;
export const STATUS_PAGE_ALL_UP = 1;
export const STATUS_PAGE_PARTIAL_DOWN = 2;
/** Flip the status of s */
export function flipStatus(s: number) {
if (s === UP) {
return DOWN;
@ -32,6 +32,10 @@ export function flipStatus(s: number) {
return s;
}
/**
* Delays for specified number of seconds
* @param ms Number of milliseconds to sleep for
*/
export function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
@ -94,6 +98,12 @@ class Logger {
}
}
/**
* Write a message to the log
* @param module The module the log comes from
* @param msg Message to write
* @param level Log level. One of INFO, WARN, ERROR, DEBUG or can be customized.
*/
log(module: string, msg: any, level: string) {
if (this.hideLog[level] && this.hideLog[level].includes(module)) {
return;
@ -120,22 +130,48 @@ class Logger {
}
}
/**
* Log an INFO message
* @param module Module log comes from
* @param msg Message to write
*/
info(module: string, msg: any) {
this.log(module, msg, "info");
}
/**
* Log a WARN message
* @param module Module log comes from
* @param msg Message to write
*/
warn(module: string, msg: any) {
this.log(module, msg, "warn");
}
error(module: string, msg: any) {
/**
* Log an ERROR message
* @param module Module log comes from
* @param msg Message to write
*/
error(module: string, msg: any) {
this.log(module, msg, "error");
}
debug(module: string, msg: any) {
/**
* Log a DEBUG message
* @param module Module log comes from
* @param msg Message to write
*/
debug(module: string, msg: any) {
this.log(module, msg, "debug");
}
/**
* Log an exeption as an ERROR
* @param module Module log comes from
* @param exception The exeption to include
* @param msg The message to write
*/
exception(module: string, exception: any, msg: any) {
let finalMessage = exception
@ -151,13 +187,13 @@ export const log = new Logger();
declare global { interface String { replaceAll(str: string, newStr: string): string; } }
/**
* String.prototype.replaceAll() polyfill
* https://gomakethings.com/how-to-replace-a-section-of-a-string-with-another-one-with-vanilla-js/
* @author Chris Ferdinandi
* @license MIT
*/
export function polyfill() {
/**
* String.prototype.replaceAll() polyfill
* https://gomakethings.com/how-to-replace-a-section-of-a-string-with-another-one-with-vanilla-js/
* @author Chris Ferdinandi
* @license MIT
*/
if (!String.prototype.replaceAll) {
String.prototype.replaceAll = function (str: string, newStr: string) {
// If a regex pattern
@ -177,7 +213,10 @@ export class TimeLogger {
constructor() {
this.startTime = dayjs().valueOf();
}
/**
* Output time since start of monitor
* @param name Name of monitor
*/
print(name: string) {
if (isDev && process.env.TIMELOGGER === "1") {
console.log(name + ": " + (dayjs().valueOf() - this.startTime) + "ms")
@ -231,6 +270,13 @@ let getRandomBytes = (
}
)();
/**
* Get a random integer suitable for use in cryptography between upper
* and lower bounds.
* @param min Minimum value of integer
* @param max Maximum value of integer
* @returns Cryptographically suitable random integer
*/
export function getCryptoRandomInt(min: number, max: number):number {
// synchronous version of: https://github.com/joepie91/node-random-number-csprng
@ -267,6 +313,11 @@ export function getCryptoRandomInt(min: number, max: number):number {
}
}
/**
* Generate a random alphanumeric string of fixed length
* @param length Length of string to generate
* @returns string
*/
export function genSecret(length = 64) {
let secret = "";
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
@ -277,6 +328,11 @@ export function genSecret(length = 64) {
return secret;
}
/**
* Get the path of a monitor
* @param id ID of monitor
* @returns Formatted relative path
*/
export function getMonitorRelativeURL(id: string) {
return "/dashboard/" + id;
}

@ -159,7 +159,6 @@ describe("Test genSecret", () => {
expect(secret).toContain("A");
expect(secret).toContain("9");
});
});
describe("Test reset-password", () => {
@ -169,6 +168,9 @@ describe("Test reset-password", () => {
});
describe("Test Discord Notification Provider", () => {
const hostname = "discord.com";
const port = 1337;
const sendNotification = async (hostname, port, type) => {
const discordProvider = new Discord();
@ -191,63 +193,35 @@ describe("Test Discord Notification Provider", () => {
);
};
it("should send hostname for dns monitors", async () => {
const hostname = "discord.com";
await sendNotification(hostname, null, "dns");
expect(axios.post.mock.lastCall[1].embeds[0].fields[1].value).toBe(
hostname
);
});
it("should send hostname for ping monitors", async () => {
const hostname = "discord.com";
await sendNotification(hostname, null, "ping");
expect(axios.post.mock.lastCall[1].embeds[0].fields[1].value).toBe(
hostname
);
expect(axios.post.mock.lastCall[1].embeds[0].fields[1].value).toBe(hostname);
});
it("should send hostname for port monitors", async () => {
const hostname = "discord.com";
const port = 1337;
await sendNotification(hostname, port, "port");
expect(axios.post.mock.lastCall[1].embeds[0].fields[1].value).toBe(
`${hostname}:${port}`
);
});
it("should send hostname for steam monitors", async () => {
const hostname = "discord.com";
const port = 1337;
await sendNotification(hostname, port, "steam");
expect(axios.post.mock.lastCall[1].embeds[0].fields[1].value).toBe(
`${hostname}:${port}`
);
it.each([ "dns", "port", "steam" ])("should send hostname for %p monitors", async (type) => {
await sendNotification(hostname, port, type);
expect(axios.post.mock.lastCall[1].embeds[0].fields[1].value).toBe(`${hostname}:${port}`);
});
});
describe("The function filterAndJoin", () => {
it("should join and array of strings to one string", () => {
const result = utilServerRewire.filterAndJoin(["one", "two", "three"]);
const result = utilServerRewire.filterAndJoin([ "one", "two", "three" ]);
expect(result).toBe("onetwothree");
});
it("should join strings using a given connector", () => {
const result = utilServerRewire.filterAndJoin(["one", "two", "three"], "-");
const result = utilServerRewire.filterAndJoin([ "one", "two", "three" ], "-");
expect(result).toBe("one-two-three");
});
it("should filter null, undefined and empty strings before joining", () => {
const result = utilServerRewire.filterAndJoin([undefined, "", "three"], "--");
const result = utilServerRewire.filterAndJoin([ undefined, "", "three" ], "--");
expect(result).toBe("three");
});
it("should return an empty string if all parts are filtered out", () => {
const result = utilServerRewire.filterAndJoin([undefined, "", ""], "--");
const result = utilServerRewire.filterAndJoin([ undefined, "", "" ], "--");
expect(result).toBe("");
});
});

Loading…
Cancel
Save