Compare commits

..

20 Commits

Author SHA1 Message Date
Louis Lam
7c13b1b6cb Update to 1.15.0-beta.1 2022-04-19 20:07:18 +08:00
Louis Lam
10f6a3c4f5 Merge pull request #1229 from Computroniks/#1209-Logout-button-in-navbar
Add #1209: logout button in navbar
2022-04-19 19:59:52 +08:00
Louis Lam
cd7c2beca6 Update CSS for dropdown menu 2022-04-19 19:46:21 +08:00
Louis Lam
8ee99760ec Minor lint 2022-04-19 19:41:52 +08:00
Louis Lam
cb55e23718 Add $root.username 2022-04-19 19:40:28 +08:00
Louis Lam
200fdfb808 Merge code manually since some code moved to another file 2022-04-19 16:46:45 +08:00
Louis Lam
29d2d95c71 Merge branch '1.14.X'
# Conflicts:
#	package.json
#	server/server.js
2022-04-19 16:43:13 +08:00
Louis Lam
b782b25e17 Update to 1.14.1 2022-04-19 16:01:08 +08:00
Louis Lam
919393cac9 Partially change the server core into a class, remove all require("./server") #1520 2022-04-19 15:38:59 +08:00
Louis Lam
17d4003e5c Add dropdown menu 2022-04-19 00:39:49 +08:00
Louis Lam
cefb5bb60a Merge branch 'master' into #1209-Logout-button-in-navbar 2022-04-18 20:39:24 +08:00
Louis Lam
9bf3b3a0f4 Update README.md 2022-04-18 19:15:50 +08:00
Louis Lam
e2c45f93bf Merge pull request #1509 from chakflying/feat/mqtt-optional-message
Feat: Allow MQTT successMessage to be optional
2022-04-18 19:06:39 +08:00
Louis Lam
addf75daa7 Fix MQTT password do not save 2022-04-18 19:05:14 +08:00
Louis Lam
359a490ae3 Fix #1510 2022-04-18 15:21:58 +08:00
Nelson Chan
cd38dd3f68 Feat: Allow MQTT successMessage to be optional 2022-04-18 13:04:55 +08:00
Louis Lam
f712fe85e5 Update node-sqlite, sqlite from 3.36 to 3.38 2022-04-18 12:44:32 +08:00
Matthew Nickson
88604845e6 Merge branch 'master' into #1209-Logout-button-in-navbar 2022-03-27 13:48:50 +01:00
Computroniks
97a5b400db Added log out button to nav bar
Implements Logout button in navbar #1209

Signed-off-by: Computroniks <mnickson@sidingsmedia.com>
2022-01-27 19:45:31 +00:00
Computroniks
ca89f84b9a Added sign-out-alt icon
Signed-off-by: Computroniks <mnickson@sidingsmedia.com>
2022-01-27 18:57:14 +00:00
20 changed files with 1235 additions and 496 deletions

View File

@@ -154,10 +154,17 @@ https://www.reddit.com/r/UptimeKuma/
## Contribute
### Test Beta Version
Check out the latest beta release here: https://github.com/louislam/uptime-kuma/releases
### Bug Reports / Feature Requests
If you want to report a bug or request a new feature. Free feel to open a [new issue](https://github.com/louislam/uptime-kuma/issues).
## Translations
If you want to translate Uptime Kuma into your language, please read: https://github.com/louislam/uptime-kuma/tree/master/src/languages
If you want to modify Uptime Kuma, this guideline may be useful for you: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md
Feel free to correct my grammar in this README, source code, or wiki, as my mother language is not English and my grammar is not that great.
Unfortunately, English proofreading is needed too because my grammar is not that great. Feel free to correct my grammar in this README, source code, or wiki.
## Pull Requests
If you want to modify Uptime Kuma, this guideline may be useful for you: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md

View File

@@ -4,6 +4,7 @@ const Database = require("../server/database");
const { R } = require("redbean-node");
const readline = require("readline");
const { initJWTSecret } = require("../server/util-server");
const User = require("../server/model/user");
const args = require("args-parser")(process.argv);
const rl = readline.createInterface({
input: process.stdin,
@@ -30,7 +31,7 @@ const main = async () => {
let confirmPassword = await question("Confirm New Password: ");
if (password === confirmPassword) {
await user.resetPassword(password);
await User.resetPassword(user.id, password);
// Reset all sessions by reset jwt secret
await initJWTSecret();

1337
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "uptime-kuma",
"version": "1.15.0-beta.0",
"version": "1.15.0-beta.1",
"license": "MIT",
"repository": {
"type": "git",
@@ -37,7 +37,7 @@
"build-docker-nightly-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push",
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
"setup": "git checkout 1.14.0 && npm ci --production && npm run download-dist",
"setup": "git checkout 1.14.1 && npm ci --production && npm run download-dist",
"download-dist": "node extra/download-dist.js",
"mark-as-nightly": "node extra/mark-as-nightly.js",
"reset-password": "node extra/reset-password.js",
@@ -62,7 +62,7 @@
"@fortawesome/free-regular-svg-icons": "~5.15.4",
"@fortawesome/free-solid-svg-icons": "~5.15.4",
"@fortawesome/vue-fontawesome": "~3.0.0-5",
"@louislam/sqlite3": "~6.0.1",
"@louislam/sqlite3": "~15.0.3",
"@popperjs/core": "~2.10.2",
"args-parser": "~1.3.0",
"axios": "~0.26.1",

View File

@@ -3,7 +3,8 @@
*/
const { TimeLogger } = require("../src/util");
const { R } = require("redbean-node");
const { io } = require("./server");
const { UptimeKumaServer } = require("./uptime-kuma-server");
const io = UptimeKumaServer.getInstance().io;
const { setting } = require("./util-server");
const checkVersion = require("./check-version");

View File

@@ -5,17 +5,29 @@ const { R } = require("redbean-node");
class User extends BeanModel {
/**
* Direct execute, no need R.store()
*
* Fix #1510, as in the context reset-password.js, there is no auto model mapping. Call this static function instead.
* @param userID
* @param newPassword
* @returns {Promise<void>}
*/
static async resetPassword(userID, newPassword) {
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
passwordHash.generate(newPassword),
userID
]);
}
/**
*
* @param newPassword
* @returns {Promise<void>}
*/
async resetPassword(newPassword) {
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
passwordHash.generate(newPassword),
this.id
]);
await User.resetPassword(this.id, newPassword);
this.password = newPassword;
}
}
module.exports = User;

View File

@@ -3,7 +3,7 @@ const HttpProxyAgent = require("http-proxy-agent");
const HttpsProxyAgent = require("https-proxy-agent");
const SocksProxyAgent = require("socks-proxy-agent");
const { debug } = require("../src/util");
const server = require("./server");
const { UptimeKumaServer } = require("./uptime-kuma-server");
class Proxy {
@@ -151,6 +151,8 @@ class Proxy {
* @returns {Promise<void>}
*/
static async reloadProxy() {
const server = UptimeKumaServer.getInstance();
let updatedList = await R.getAssoc("SELECT id, proxy_id FROM monitor");
for (let monitorID in server.monitorList) {

View File

@@ -1,15 +1,16 @@
let express = require("express");
const { allowDevAllOrigin } = require("../util-server");
const { R } = require("redbean-node");
const server = require("../server");
const apicache = require("../modules/apicache");
const Monitor = require("../model/monitor");
const dayjs = require("dayjs");
const { UP, flipStatus, log } = require("../../src/util");
const StatusPage = require("../model/status_page");
const { UptimeKumaServer } = require("../uptime-kuma-server");
let router = express.Router();
let cache = apicache.middleware;
const server = UptimeKumaServer.getInstance();
let io = server.io;
router.get("/api/entry-page", async (request, response) => {

View File

@@ -1,3 +1,8 @@
/*
* Uptime Kuma Server
* node "server/server.js"
* DO NOT require("./server") in other modules, it likely creates circular dependency!
*/
console.log("Welcome to Uptime Kuma");
// Check Node.js Version
@@ -26,14 +31,10 @@ log.info("server", "Node Env: " + process.env.NODE_ENV);
log.info("server", "Importing Node libraries");
const fs = require("fs");
const http = require("http");
const https = require("https");
log.info("server", "Importing 3rd-party libraries");
log.debug("server", "Importing express");
const express = require("express");
log.debug("server", "Importing socket.io");
const { Server } = require("socket.io");
log.debug("server", "Importing redbean-node");
const { R } = require("redbean-node");
log.debug("server", "Importing jsonwebtoken");
@@ -50,26 +51,10 @@ log.debug("server", "Importing 2FA Modules");
const notp = require("notp");
const base32 = require("thirty-two");
/**
* `module.exports` (alias: `server`) should be inside this class, in order to avoid circular dependency issue.
* @type {UptimeKumaServer}
*/
class UptimeKumaServer {
/**
* Main monitor list
* @type {{}}
*/
monitorList = {};
entryPage = "dashboard";
async sendMonitorList(socket) {
let list = await getMonitorJSONList(socket.userID);
io.to(socket.userID).emit("monitorList", list);
return list;
}
}
const server = module.exports = new UptimeKumaServer();
const { UptimeKumaServer } = require("./uptime-kuma-server");
const server = UptimeKumaServer.getInstance(args);
const io = module.exports.io = server.io;
const app = server.app;
log.info("server", "Importing this project modules");
log.debug("server", "Importing Monitor");
@@ -112,10 +97,7 @@ const port = [ args.port, process.env.UPTIME_KUMA_PORT, process.env.PORT, 3001 ]
.map(portValue => parseInt(portValue))
.find(portValue => !isNaN(portValue));
// SSL
const sslKey = args["ssl-key"] || process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || undefined;
const sslCert = args["ssl-cert"] || process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || undefined;
const disableFrameSameOrigin = args["disable-frame-sameorigin"] || !!process.env.UPTIME_KUMA_DISABLE_FRAME_SAMEORIGIN || false;
const disableFrameSameOrigin = !!process.env.UPTIME_KUMA_DISABLE_FRAME_SAMEORIGIN || args["disable-frame-sameorigin"] || false;
const cloudflaredToken = args["cloudflared-token"] || process.env.UPTIME_KUMA_CLOUDFLARED_TOKEN || undefined;
// 2FA / notp verification defaults
@@ -134,25 +116,6 @@ if (config.demoMode) {
log.info("server", "==== Demo Mode ====");
}
log.info("server", "Creating express and socket.io instance");
const app = express();
let httpServer;
if (sslKey && sslCert) {
log.info("server", "Server Type: HTTPS");
httpServer = https.createServer({
key: fs.readFileSync(sslKey),
cert: fs.readFileSync(sslCert)
}, app);
} else {
log.info("server", "Server Type: HTTP");
httpServer = http.createServer(app);
}
const io = new Server(httpServer);
module.exports.io = io;
// Must be after io instantiation
const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sendInfo, sendProxyList } = require("./client");
const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
@@ -721,6 +684,7 @@ try {
bean.pushToken = monitor.pushToken;
bean.proxyId = Number.isInteger(monitor.proxyId) ? monitor.proxyId : null;
bean.mqttUsername = monitor.mqttUsername;
bean.mqttPassword = monitor.mqttPassword;
bean.mqttTopic = monitor.mqttTopic;
bean.mqttSuccessMessage = monitor.mqttSuccessMessage;
@@ -1482,12 +1446,12 @@ try {
log.info("server", "Init the server");
httpServer.once("error", async (err) => {
server.httpServer.once("error", async (err) => {
console.error("Cannot listen: " + err.message);
await shutdownFunction();
});
httpServer.listen(port, hostname, () => {
server.httpServer.listen(port, hostname, () => {
if (hostname) {
log.info("server", `Listening on ${hostname}:${port}`);
} else {
@@ -1577,27 +1541,6 @@ async function afterLogin(socket, user) {
}
}
/**
* Get a list of monitors for the given user.
* @param {string} userID - The ID of the user to get monitors for.
* @returns {Promise<Object>} A promise that resolves to an object with monitor IDs as keys and monitor objects as values.
*
* Generated by Trelent
*/
async function getMonitorJSONList(userID) {
let result = {};
let monitorList = await R.find("monitor", " user_id = ? ORDER BY weight DESC, name", [
userID,
]);
for (let monitor of monitorList) {
result[monitor.id] = await monitor.toJSON();
}
return result;
}
/**
* Connect to the database and patch it if necessary.
*
@@ -1739,7 +1682,7 @@ function finalFunction() {
log.info("server", "Graceful shutdown successful!");
}
gracefulShutdown(httpServer, {
gracefulShutdown(server.httpServer, {
signals: "SIGINT SIGTERM",
timeout: 30000, // timeout: 30 secs
development: false, // not in dev mode

View File

@@ -1,6 +1,7 @@
const { checkLogin, setSetting, setting, doubleCheckPassword } = require("../util-server");
const { CloudflaredTunnel } = require("node-cloudflared-tunnel");
const { io } = require("../server");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const io = UptimeKumaServer.getInstance().io;
const prefix = "cloudflared_";
const cloudflared = new CloudflaredTunnel();
@@ -86,5 +87,7 @@ module.exports.autoStart = async (token) => {
module.exports.stop = async () => {
console.log("Stop cloudflared");
cloudflared.stop();
if (cloudflared) {
cloudflared.stop();
}
};

View File

@@ -1,7 +1,8 @@
const { checkLogin } = require("../util-server");
const { Proxy } = require("../proxy");
const { sendProxyList } = require("../client");
const server = require("../server");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const server = UptimeKumaServer.getInstance();
module.exports.proxySocketHandler = (socket) => {
socket.on("addProxy", async (proxy, proxyID, callback) => {

View File

@@ -6,7 +6,7 @@ const ImageDataURI = require("../image-data-uri");
const Database = require("../database");
const apicache = require("../modules/apicache");
const StatusPage = require("../model/status_page");
const server = require("../server");
const { UptimeKumaServer } = require("../uptime-kuma-server");
module.exports.statusPageSocketHandler = (socket) => {
@@ -215,6 +215,8 @@ module.exports.statusPageSocketHandler = (socket) => {
];
await R.exec(`DELETE FROM \`group\` WHERE id NOT IN (${slots}) AND status_page_id = ?`, data);
const server = UptimeKumaServer.getInstance();
// Also change entry page to new slug if it is the default one, and slug is changed.
if (server.entryPage === "statusPage-" + slug && statusPage.slug !== slug) {
server.entryPage = "statusPage-" + statusPage.slug;
@@ -284,6 +286,8 @@ module.exports.statusPageSocketHandler = (socket) => {
// Delete a status page
socket.on("deleteStatusPage", async (slug, callback) => {
const server = UptimeKumaServer.getInstance();
try {
checkLogin(socket);

View File

@@ -0,0 +1,90 @@
const express = require("express");
const https = require("https");
const fs = require("fs");
const http = require("http");
const { Server } = require("socket.io");
const { R } = require("redbean-node");
const { log } = require("../src/util");
/**
* `module.exports` (alias: `server`) should be inside this class, in order to avoid circular dependency issue.
* @type {UptimeKumaServer}
*/
class UptimeKumaServer {
/**
*
* @type {UptimeKumaServer}
*/
static instance = null;
/**
* Main monitor list
* @type {{}}
*/
monitorList = {};
entryPage = "dashboard";
app = undefined;
httpServer = undefined;
io = undefined;
static getInstance(args) {
if (UptimeKumaServer.instance == null) {
UptimeKumaServer.instance = new UptimeKumaServer(args);
}
return UptimeKumaServer.instance;
}
constructor(args) {
// SSL
const sslKey = args["ssl-key"] || process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || undefined;
const sslCert = args["ssl-cert"] || process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || undefined;
log.info("server", "Creating express and socket.io instance");
this.app = express();
if (sslKey && sslCert) {
log.info("server", "Server Type: HTTPS");
this.httpServer = https.createServer({
key: fs.readFileSync(sslKey),
cert: fs.readFileSync(sslCert)
}, this.app);
} else {
log.info("server", "Server Type: HTTP");
this.httpServer = http.createServer(this.app);
}
this.io = new Server(this.httpServer);
}
async sendMonitorList(socket) {
let list = await this.getMonitorJSONList(socket.userID);
this.io.to(socket.userID).emit("monitorList", list);
return list;
}
/**
* Get a list of monitors for the given user.
* @param {string} userID - The ID of the user to get monitors for.
* @returns {Promise<Object>} A promise that resolves to an object with monitor IDs as keys and monitor objects as values.
*
* Generated by Trelent
*/
async getMonitorJSONList(userID) {
let result = {};
let monitorList = await R.find("monitor", " user_id = ? ORDER BY weight DESC, name", [
userID,
]);
for (let monitor of monitorList) {
result[monitor.id] = await monitor.toJSON();
}
return result;
}
}
module.exports = {
UptimeKumaServer
};

View File

@@ -135,10 +135,10 @@ exports.mqttAsync = function (hostname, topic, okMessage, options = {}) {
if (messageTopic == topic) {
client.end();
clearTimeout(timeoutID);
if (message.toString() === okMessage) {
resolve(`Topic: ${messageTopic}; Message: ${message.toString()}`);
if (okMessage != null && okMessage !== "" && message.toString() !== okMessage) {
reject(new Error(`Message Mismatch - Topic: ${messageTopic}; Message: ${message.toString()}`));
} else {
reject(new Error(`Error; Topic: ${messageTopic}; Message: ${message.toString()}`));
resolve(`Topic: ${messageTopic}; Message: ${message.toString()}`);
}
}
});

View File

@@ -4,7 +4,7 @@
<!-- Change Password -->
<template v-if="!settings.disableAuth">
<p>
{{ $t("Current User") }}: <strong>{{ username }}</strong>
{{ $t("Current User") }}: <strong>{{ $root.username }}</strong>
<button v-if="! settings.disableAuth" id="logout-btn" class="btn btn-danger ms-4 me-2 mb-2" @click="$root.logout">{{ $t("Logout") }}</button>
</p>
@@ -269,7 +269,6 @@ export default {
data() {
return {
username: "",
invalidPassword: false,
password: {
currentPassword: "",
@@ -297,10 +296,6 @@ export default {
},
},
mounted() {
this.loadUsername();
},
methods: {
savePassword() {
if (this.password.newPassword !== this.password.repeatNewPassword) {
@@ -319,14 +314,6 @@ export default {
}
},
loadUsername() {
const jwtPayload = this.$root.getJWTPayload();
if (jwtPayload) {
this.username = jwtPayload.username;
}
},
disableAuth() {
this.settings.disableAuth = true;

View File

@@ -34,11 +34,13 @@ import {
faAward,
faLink,
faChevronDown,
faSignOutAlt,
faPen,
faExternalLinkSquareAlt,
faSpinner,
faUndo,
faPlusCircle,
faAngleDown,
} from "@fortawesome/free-solid-svg-icons";
library.add(
@@ -72,11 +74,13 @@ library.add(
faAward,
faLink,
faChevronDown,
faSignOutAlt,
faPen,
faExternalLinkSquareAlt,
faSpinner,
faUndo,
faPlusCircle,
faAngleDown,
);
export { FontAwesomeIcon };

View File

@@ -32,9 +32,27 @@
</router-link>
</li>
<li v-if="$root.loggedIn" class="nav-item">
<router-link to="/settings" class="nav-link" :class="{ active: $route.path.includes('settings') }">
<font-awesome-icon icon="cog" /> {{ $t("Settings") }}
</router-link>
<div class="dropdown dropdown-profile-pic">
<div type="button" class="nav-link" data-bs-toggle="dropdown">
<div class="profile-pic">{{ $root.usernameFirstChar }}</div>
<font-awesome-icon icon="angle-down" />
</div>
<ul class="dropdown-menu">
<li><span class="dropdown-item-text">Signed in as <strong>{{ $root.username }}</strong></span></li>
<li><hr class="dropdown-divider"></li>
<li>
<router-link to="/settings" class="dropdown-item" :class="{ active: $route.path.includes('settings') }">
<font-awesome-icon icon="cog" /> {{ $t("Settings") }}
</router-link>
</li>
<li v-if="$root.loggedIn && $root.storage().token !== 'autoLogin'">
<button class="dropdown-item" @click="$root.logout">
<font-awesome-icon icon="sign-out-alt" />
{{ $t("Logout") }}
</button>
</li>
</ul>
</div>
</li>
</ul>
</header>
@@ -192,6 +210,79 @@ main {
z-index: 99999;
}
// Profile Pic Button with Dropdown
.dropdown-profile-pic {
user-select: none;
.nav-link {
cursor: pointer;
display: flex;
gap: 6px;
align-items: center;
background-color: rgba(200, 200, 200, 0.2);
padding: 0.5rem 0.8rem;
&:hover {
background-color: rgba(255, 255, 255, 0.2);
}
}
.dropdown-menu {
transition: all 0.2s;
padding-left: 0;
margin-top: 8px !important;
border-radius: 20px;
.dropdown-divider {
margin: 0;
border-top: 1px solid rgba(0, 0, 0, 0.4);
background-color: transparent;
}
.dropdown-item-text {
font-size: 14px;
padding-bottom: 0.7rem;
}
.dropdown-item {
padding: 0.7rem 1rem;
}
.dark & {
background-color: $dark-bg;
color: $dark-font-color;
border-color: $dark-border-color;
.dropdown-item {
color: $dark-font-color;
&.active {
color: $dark-font-color2;
background-color: $highlight !important;
}
&:hover {
background-color: $dark-bg2;
}
}
}
}
.profile-pic {
display: flex;
align-items: center;
justify-content: center;
color: white;
background-color: $primary;
width: 24px;
height: 24px;
margin-right: 5px;
border-radius: 50rem;
font-weight: bold;
font-size: 10px;
}
}
.dark {
header {
background-color: $dark-header-bg;

View File

@@ -28,6 +28,7 @@ export default {
connectCount: 0,
initedSocketIO: false,
},
username: null,
remember: (localStorage.remember !== "0"),
allowLoginDialog: false, // Allowed to show login dialog, but "loggedIn" have to be true too. This exists because prevent the login dialog show 0.1s in first before the socket server auth-ed.
loggedIn: false,
@@ -102,6 +103,7 @@ export default {
socket.on("autoLogin", (monitorID, data) => {
this.loggedIn = true;
this.username = "No Auth";
this.storage().token = "autoLogin";
this.allowLoginDialog = false;
});
@@ -233,7 +235,6 @@ export default {
if (token !== "autoLogin") {
this.loginByToken(token);
} else {
// Timeout if it is not actually auto login
setTimeout(() => {
if (! this.loggedIn) {
@@ -241,7 +242,6 @@ export default {
this.$root.storage().removeItem("token");
}
}, 5000);
}
} else {
this.allowLoginDialog = true;
@@ -305,6 +305,7 @@ export default {
this.storage().token = res.token;
this.socket.token = res.token;
this.loggedIn = true;
this.username = this.getJWTPayload()?.username;
// Trigger Chrome Save Password
history.pushState({}, "");
@@ -322,6 +323,7 @@ export default {
this.logout();
} else {
this.loggedIn = true;
this.username = this.getJWTPayload()?.username;
}
});
},
@@ -331,6 +333,7 @@ export default {
this.storage().removeItem("token");
this.socket.token = null;
this.loggedIn = false;
this.username = null;
this.clearData();
},
@@ -398,6 +401,14 @@ export default {
computed: {
usernameFirstChar() {
if (typeof this.username == "string" && this.username.length >= 1) {
return this.username.charAt(0).toUpperCase();
} else {
return "🐻";
}
},
lastHeartbeatList() {
let result = {};

View File

@@ -141,7 +141,7 @@
<div class="my-3">
<label for="mqttSuccessMessage" class="form-label">MQTT {{ $t("successMessage") }}</label>
<input id="mqttSuccessMessage" v-model="monitor.mqttSuccessMessage" type="text" class="form-control" required>
<input id="mqttSuccessMessage" v-model="monitor.mqttSuccessMessage" type="text" class="form-control">
<div class="form-text">
{{ $t("successMessageExplanation") }}
</div>

View File

@@ -16,6 +16,14 @@
{{ item.title }}
</div>
</router-link>
<!-- Logout Button -->
<a v-if="$root.isMobile && $root.loggedIn && $root.storage().token !== 'autoLogin'" class="logout" @click.prevent="$root.logout">
<div class="menu-item">
<font-awesome-icon icon="sign-out-alt" />
{{ $t("Logout") }}
</div>
</a>
</div>
<div class="settings-content col-lg-9 col-md-7">
<div v-if="currentPage" class="settings-content-header">
@@ -233,4 +241,8 @@ footer {
}
}
}
.logout {
color: $danger !important;
}
</style>