LouisLam
4 years ago
17 changed files with 1533 additions and 0 deletions
@ -0,0 +1,33 @@ |
|||
const dayjs = require("dayjs"); |
|||
const {BeanModel} = require("redbean-node/dist/bean-model"); |
|||
|
|||
class Monitor extends BeanModel { |
|||
|
|||
toJSON() { |
|||
return { |
|||
id: this.id, |
|||
name: this.name, |
|||
url: this.url, |
|||
upRate: this.upRate, |
|||
active: this.active, |
|||
type: this.type, |
|||
interval: this.interval, |
|||
}; |
|||
} |
|||
|
|||
start(io) { |
|||
const beat = () => { |
|||
console.log(`Monitor ${this.id}: Heartbeat`) |
|||
io.to(this.user_id).emit("heartbeat", dayjs().unix()); |
|||
} |
|||
|
|||
beat(); |
|||
this.heartbeatInterval = setInterval(beat, this.interval * 1000); |
|||
} |
|||
|
|||
stop() { |
|||
clearInterval(this.heartbeatInterval) |
|||
} |
|||
} |
|||
|
|||
module.exports = Monitor; |
@ -0,0 +1,379 @@ |
|||
const express = require('express'); |
|||
const app = express(); |
|||
const http = require('http'); |
|||
const server = http.createServer(app); |
|||
const { Server } = require("socket.io"); |
|||
const io = new Server(server); |
|||
const axios = require('axios'); |
|||
const dayjs = require("dayjs"); |
|||
const {R} = require("redbean-node"); |
|||
const passwordHash = require('password-hash'); |
|||
const jwt = require('jsonwebtoken'); |
|||
const Monitor = require("./model/monitor"); |
|||
const {sleep} = require("./util"); |
|||
|
|||
|
|||
let stop = false; |
|||
let interval = 6000; |
|||
let totalClient = 0; |
|||
let jwtSecret = null; |
|||
let loadFromDatabase = true; |
|||
let monitorList = {}; |
|||
|
|||
(async () => { |
|||
|
|||
R.setup('sqlite', { |
|||
filename: '../data/kuma.db' |
|||
}); |
|||
R.freeze(true) |
|||
await R.autoloadModels("./model"); |
|||
|
|||
await initDatabase(); |
|||
|
|||
app.use('/', express.static("public")); |
|||
|
|||
io.on('connection', async (socket) => { |
|||
console.log('a user connected'); |
|||
totalClient++; |
|||
|
|||
socket.on('disconnect', () => { |
|||
console.log('user disconnected'); |
|||
totalClient--; |
|||
}); |
|||
|
|||
// Public API
|
|||
|
|||
socket.on("loginByToken", async (token, callback) => { |
|||
|
|||
try { |
|||
let decoded = jwt.verify(token, jwtSecret); |
|||
|
|||
console.log("Username from JWT: " + decoded.username) |
|||
|
|||
let user = await R.findOne("user", " username = ? AND active = 1 ", [ |
|||
decoded.username |
|||
]) |
|||
|
|||
if (user) { |
|||
await afterLogin(socket, user) |
|||
|
|||
callback({ |
|||
ok: true, |
|||
}) |
|||
} else { |
|||
callback({ |
|||
ok: false, |
|||
msg: "The user is inactive or deleted." |
|||
}) |
|||
} |
|||
} catch (error) { |
|||
callback({ |
|||
ok: false, |
|||
msg: "Invalid token." |
|||
}) |
|||
} |
|||
|
|||
}); |
|||
|
|||
socket.on("login", async (data, callback) => { |
|||
console.log("Login") |
|||
|
|||
let user = await R.findOne("user", " username = ? AND active = 1 ", [ |
|||
data.username |
|||
]) |
|||
|
|||
if (user && passwordHash.verify(data.password, user.password)) { |
|||
|
|||
await afterLogin(socket, user) |
|||
|
|||
callback({ |
|||
ok: true, |
|||
token: jwt.sign({ |
|||
username: data.username |
|||
}, jwtSecret) |
|||
}) |
|||
} else { |
|||
callback({ |
|||
ok: false, |
|||
msg: "Incorrect username or password." |
|||
}) |
|||
} |
|||
|
|||
}); |
|||
|
|||
socket.on("logout", async (callback) => { |
|||
socket.leave(socket.userID) |
|||
socket.userID = null; |
|||
callback(); |
|||
}); |
|||
|
|||
// Auth Only API
|
|||
|
|||
socket.on("add", async (monitor, callback) => { |
|||
try { |
|||
checkLogin(socket) |
|||
|
|||
let bean = R.dispense("monitor") |
|||
bean.import(monitor) |
|||
bean.user_id = socket.userID |
|||
await R.store(bean) |
|||
|
|||
callback({ |
|||
ok: true, |
|||
msg: "Added Successfully.", |
|||
monitorID: bean.id |
|||
}); |
|||
|
|||
await sendMonitorList(socket); |
|||
|
|||
} catch (e) { |
|||
callback({ |
|||
ok: false, |
|||
msg: e.message |
|||
}); |
|||
} |
|||
}); |
|||
|
|||
socket.on("getMonitor", async (monitorID, callback) => { |
|||
try { |
|||
checkLogin(socket) |
|||
|
|||
console.log(`Get Monitor: ${monitorID} User ID: ${socket.userID}`) |
|||
|
|||
let bean = await R.findOne("monitor", " id = ? AND user_id = ? ", [ |
|||
monitorID, |
|||
socket.userID, |
|||
]) |
|||
|
|||
callback({ |
|||
ok: true, |
|||
monitor: bean.toJSON(), |
|||
}); |
|||
|
|||
} catch (e) { |
|||
callback({ |
|||
ok: false, |
|||
msg: e.message |
|||
}); |
|||
} |
|||
}); |
|||
|
|||
// Start or Resume the monitor
|
|||
socket.on("resumeMonitor", async (monitorID, callback) => { |
|||
try { |
|||
checkLogin(socket) |
|||
await startMonitor(socket.userID, monitorID); |
|||
await sendMonitorList(socket); |
|||
|
|||
callback({ |
|||
ok: true, |
|||
msg: "Paused Successfully." |
|||
}); |
|||
|
|||
} catch (e) { |
|||
callback({ |
|||
ok: false, |
|||
msg: e.message |
|||
}); |
|||
} |
|||
}); |
|||
|
|||
socket.on("pauseMonitor", async (monitorID, callback) => { |
|||
try { |
|||
checkLogin(socket) |
|||
await pauseMonitor(socket.userID, monitorID) |
|||
await sendMonitorList(socket); |
|||
|
|||
callback({ |
|||
ok: true, |
|||
msg: "Paused Successfully." |
|||
}); |
|||
|
|||
|
|||
} catch (e) { |
|||
callback({ |
|||
ok: false, |
|||
msg: e.message |
|||
}); |
|||
} |
|||
}); |
|||
|
|||
socket.on("deleteMonitor", async (monitorID, callback) => { |
|||
try { |
|||
checkLogin(socket) |
|||
|
|||
console.log(`Delete Monitor: ${monitorID} User ID: ${socket.userID}`) |
|||
|
|||
if (monitorID in monitorList) { |
|||
monitorList[monitorID].stop(); |
|||
delete monitorList[monitorID] |
|||
} |
|||
|
|||
await R.exec("DELETE FROM monitor WHERE id = ? AND user_id = ? ", [ |
|||
monitorID, |
|||
socket.userID |
|||
]); |
|||
|
|||
callback({ |
|||
ok: true, |
|||
msg: "Deleted Successfully." |
|||
}); |
|||
|
|||
await sendMonitorList(socket); |
|||
|
|||
} catch (e) { |
|||
callback({ |
|||
ok: false, |
|||
msg: e.message |
|||
}); |
|||
} |
|||
}); |
|||
|
|||
socket.on("changePassword", async (password, callback) => { |
|||
try { |
|||
checkLogin(socket) |
|||
|
|||
if (! password.currentPassword) { |
|||
throw new Error("Invalid new password") |
|||
} |
|||
|
|||
let user = await R.findOne("user", " id = ? AND active = 1 ", [ |
|||
socket.userID |
|||
]) |
|||
|
|||
if (user && passwordHash.verify(password.currentPassword, user.password)) { |
|||
|
|||
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [ |
|||
passwordHash.generate(password.newPassword), |
|||
socket.userID |
|||
]); |
|||
|
|||
callback({ |
|||
ok: true, |
|||
msg: "Password has been updated successfully." |
|||
}) |
|||
} else { |
|||
throw new Error("Incorrect current password") |
|||
} |
|||
|
|||
} catch (e) { |
|||
callback({ |
|||
ok: false, |
|||
msg: e.message |
|||
}); |
|||
} |
|||
}); |
|||
}); |
|||
|
|||
server.listen(3001, () => { |
|||
console.log('Listening on 3001'); |
|||
startMonitors(); |
|||
}); |
|||
|
|||
})(); |
|||
|
|||
async function checkOwner(userID, monitorID) { |
|||
let row = await R.getRow("SELECT id FROM monitor WHERE id = ? AND user_id = ? ", [ |
|||
monitorID, |
|||
userID, |
|||
]) |
|||
|
|||
if (! row) { |
|||
throw new Error("You do not own this monitor."); |
|||
} |
|||
} |
|||
|
|||
async function sendMonitorList(socket) { |
|||
io.to(socket.userID).emit("monitorList", await getMonitorJSONList(socket.userID)) |
|||
} |
|||
|
|||
async function afterLogin(socket, user) { |
|||
socket.userID = user.id; |
|||
socket.join(user.id) |
|||
socket.emit("monitorList", await getMonitorJSONList(user.id)) |
|||
} |
|||
|
|||
async function getMonitorJSONList(userID) { |
|||
let result = []; |
|||
|
|||
let monitorList = await R.find("monitor", " user_id = ? ORDER BY weight DESC ", [ |
|||
userID |
|||
]) |
|||
|
|||
for (let monitor of monitorList) { |
|||
result.push(monitor.toJSON()) |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
function checkLogin(socket) { |
|||
if (! socket.userID) { |
|||
throw new Error("You are not logged in."); |
|||
} |
|||
} |
|||
|
|||
async function initDatabase() { |
|||
let jwtSecretBean = await R.findOne("setting", " `key` = ? ", [ |
|||
"jwtSecret" |
|||
]); |
|||
|
|||
if (! jwtSecretBean) { |
|||
console.log("JWT secret is not found, generate one.") |
|||
jwtSecretBean = R.dispense("setting") |
|||
jwtSecretBean.key = "jwtSecret" |
|||
|
|||
jwtSecretBean.value = passwordHash.generate(dayjs() + "") |
|||
await R.store(jwtSecretBean) |
|||
} else { |
|||
console.log("Load JWT secret from database.") |
|||
} |
|||
|
|||
jwtSecret = jwtSecretBean.value; |
|||
} |
|||
|
|||
async function startMonitor(userID, monitorID) { |
|||
await checkOwner(userID, monitorID) |
|||
|
|||
console.log(`Resume Monitor: ${monitorID} User ID: ${userID}`) |
|||
|
|||
await R.exec("UPDATE monitor SET active = 1 WHERE id = ? AND user_id = ? ", [ |
|||
monitorID, |
|||
userID |
|||
]); |
|||
|
|||
let monitor = await R.findOne("monitor", " id = ? ", [ |
|||
monitorID |
|||
]) |
|||
|
|||
monitorList[monitor.id] = monitor; |
|||
monitor.start(io) |
|||
} |
|||
|
|||
async function pauseMonitor(userID, monitorID) { |
|||
await checkOwner(userID, monitorID) |
|||
|
|||
console.log(`Pause Monitor: ${monitorID} User ID: ${userID}`) |
|||
|
|||
await R.exec("UPDATE monitor SET active = 0 WHERE id = ? AND user_id = ? ", [ |
|||
monitorID, |
|||
userID |
|||
]); |
|||
|
|||
if (monitorID in monitorList) { |
|||
monitorList[monitorID].stop(); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Resume active monitors |
|||
*/ |
|||
async function startMonitors() { |
|||
let list = await R.find("monitor", " active = 1 ") |
|||
|
|||
for (let monitor of list) { |
|||
monitor.start(io) |
|||
monitorList[monitor.id] = monitor; |
|||
} |
|||
} |
|||
|
@ -0,0 +1,3 @@ |
|||
exports.sleep = (ms) => { |
|||
return new Promise(resolve => setTimeout(resolve, ms)); |
|||
} |
@ -0,0 +1,13 @@ |
|||
<template> |
|||
<router-view /> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
|
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss"> |
|||
|
|||
</style> |
@ -0,0 +1,57 @@ |
|||
@import "vars.scss"; |
|||
@import "node_modules/bootstrap/scss/bootstrap"; |
|||
|
|||
#app { |
|||
font-family: ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,segoe ui,Roboto,helvetica neue,Arial,noto sans,sans-serif,apple color emoji,segoe ui emoji,segoe ui symbol,noto color emoji; |
|||
} |
|||
|
|||
.shadow-box { |
|||
overflow: hidden; |
|||
box-shadow: 0 15px 70px rgba(0, 0, 0, .1); |
|||
padding: 10px; |
|||
border-radius: 10px; |
|||
|
|||
&.big-padding { |
|||
padding: 20px; |
|||
} |
|||
} |
|||
|
|||
.btn { |
|||
padding-left: 20px; |
|||
padding-right: 20px; |
|||
} |
|||
|
|||
.btn-primary { |
|||
color: white; |
|||
|
|||
&:hover, &:active, &:focus, &.active { |
|||
color: white; |
|||
background-color: $highlight; |
|||
border-color: $highlight; |
|||
} |
|||
} |
|||
|
|||
.hp-bar-big { |
|||
white-space: nowrap; |
|||
margin-top: 4px; |
|||
text-align: center; |
|||
direction: rtl; |
|||
margin-bottom: 10px; |
|||
transition: all ease-in-out 0.15s; |
|||
position: relative; |
|||
|
|||
div { |
|||
display: inline-block; |
|||
background-color: $primary; |
|||
width: 1%; |
|||
height: 30px; |
|||
margin: 0.3%; |
|||
border-radius: 50rem; |
|||
transition: all ease-in-out 0.15s; |
|||
|
|||
&:hover { |
|||
opacity: 0.8; |
|||
transform: scale(1.5); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,6 @@ |
|||
$primary: #5CDD8B; |
|||
$link-color: #111; |
|||
$border-radius: 50rem; |
|||
|
|||
$highlight: #7ce8a4; |
|||
$highlight-white: #e7faec; |
@ -0,0 +1,50 @@ |
|||
<template> |
|||
<div class="modal fade" tabindex="-1" ref="modal"> |
|||
<div class="modal-dialog"> |
|||
<div class="modal-content"> |
|||
<div class="modal-header"> |
|||
<h5 class="modal-title" id="exampleModalLabel">Confirm</h5> |
|||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
|||
</div> |
|||
<div class="modal-body"> |
|||
<slot></slot> |
|||
</div> |
|||
<div class="modal-footer"> |
|||
<button type="button" class="btn" :class="btnStyle" @click="yes" data-bs-dismiss="modal">Yes</button> |
|||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">No</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import { Modal } from 'bootstrap' |
|||
|
|||
export default { |
|||
props: { |
|||
btnStyle: { |
|||
type: String, |
|||
default: "btn-primary" |
|||
} |
|||
}, |
|||
data: () => ({ |
|||
modal: null |
|||
}), |
|||
mounted() { |
|||
this.modal = new Modal(this.$refs.modal) |
|||
}, |
|||
methods: { |
|||
show() { |
|||
this.modal.show() |
|||
}, |
|||
yes() { |
|||
this.$emit('yes'); |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
|
|||
</style> |
@ -0,0 +1,77 @@ |
|||
<template> |
|||
<div class="form-container"> |
|||
<div class="form"> |
|||
<form @submit.prevent="submit"> |
|||
|
|||
<h1 class="h3 mb-3 fw-normal"></h1> |
|||
|
|||
<div class="form-floating"> |
|||
<input type="text" class="form-control" id="floatingInput" placeholder="Username" v-model="username"> |
|||
<label for="floatingInput">Username</label> |
|||
</div> |
|||
|
|||
<div class="form-floating mt-3"> |
|||
<input type="password" class="form-control" id="floatingPassword" placeholder="Password" v-model="password"> |
|||
<label for="floatingPassword">Password</label> |
|||
</div> |
|||
|
|||
<div class="form-check mb-3 mt-3"> |
|||
<label> |
|||
<input type="checkbox" value="remember-me" class="form-check-input" id="remember" v-model="remember"> |
|||
|
|||
<label class="form-check-label" for="remember"> |
|||
Remember me |
|||
</label> |
|||
</label> |
|||
</div> |
|||
<button class="w-100 btn btn-primary" type="submit" :disabled="processing">Login</button> |
|||
|
|||
<div class="alert alert-danger mt-3" role="alert" v-if="res && !res.ok"> |
|||
{{ res.msg }} |
|||
</div> |
|||
</form> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
data() { |
|||
return { |
|||
processing: false, |
|||
username: "", |
|||
password: "", |
|||
remember: true, |
|||
res: null, |
|||
} |
|||
}, |
|||
methods: { |
|||
submit() { |
|||
this.processing = true; |
|||
this.$root.login(this.username, this.password, (res) => { |
|||
this.processing = false; |
|||
this.res = res; |
|||
}) |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
|
|||
.form-container { |
|||
display: flex; |
|||
align-items: center; |
|||
padding-top: 40px; |
|||
padding-bottom: 40px; |
|||
} |
|||
|
|||
.form { |
|||
|
|||
width: 100%; |
|||
max-width: 330px; |
|||
padding: 15px; |
|||
margin: auto; |
|||
text-align: center; |
|||
} |
|||
</style> |
@ -0,0 +1,69 @@ |
|||
<template> |
|||
|
|||
<div class="lost-connection" v-if="! $root.socket.connected && ! $root.socket.firstConnect"> |
|||
<div class="container-fluid"> |
|||
Lost connection to the socket server. Reconnecting... |
|||
</div> |
|||
</div> |
|||
|
|||
<header class="d-flex flex-wrap justify-content-center py-3 mb-3 border-bottom"> |
|||
|
|||
<router-link to="/" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-dark text-decoration-none"> |
|||
<svg class="bi me-2" width="40" height="32"><use xlink:href="#bootstrap"/></svg> |
|||
<span class="fs-4 title">Uptime Kuma</span> |
|||
</router-link> |
|||
|
|||
<ul class="nav nav-pills"> |
|||
<li class="nav-item"><router-link to="/dashboard" class="nav-link">📊 Dashboard</router-link></li> |
|||
<li class="nav-item"><router-link to="/settings" class="nav-link">⚙ Settings</router-link></li> |
|||
</ul> |
|||
|
|||
</header> |
|||
|
|||
<main> |
|||
<router-view v-if="$root.loggedIn" /> |
|||
<Login v-if="! $root.loggedIn && $root.allowLoginDialog" /> |
|||
</main> |
|||
|
|||
</template> |
|||
|
|||
<script> |
|||
import Login from "../components/Login.vue"; |
|||
|
|||
export default { |
|||
components: { |
|||
Login |
|||
}, |
|||
mounted() { |
|||
this.init(); |
|||
}, |
|||
watch: { |
|||
$route (to, from) { |
|||
this.init(); |
|||
} |
|||
}, |
|||
methods: { |
|||
init() { |
|||
if (this.$route.name === "root") { |
|||
this.$router.push("/dashboard") |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.title { |
|||
font-weight: bold; |
|||
} |
|||
|
|||
.nav { |
|||
margin-right: 25px; |
|||
} |
|||
|
|||
.lost-connection { |
|||
padding: 5px; |
|||
background-color: crimson; |
|||
color: white; |
|||
} |
|||
</style> |
@ -0,0 +1,78 @@ |
|||
import {createApp, h} from "vue"; |
|||
import {createRouter, createWebHistory} from 'vue-router' |
|||
|
|||
import App from './App.vue' |
|||
import Layout from './layouts/Layout.vue' |
|||
import Settings from "./pages/Settings.vue"; |
|||
import Dashboard from "./pages/Dashboard.vue"; |
|||
import DashboardHome from "./pages/DashboardHome.vue"; |
|||
import Details from "./pages/Details.vue"; |
|||
import socket from "./mixins/socket" |
|||
import "./assets/app.scss" |
|||
import EditMonitor from "./pages/EditMonitor.vue"; |
|||
import Toast from "vue-toastification"; |
|||
import "vue-toastification/dist/index.css"; |
|||
import "bootstrap" |
|||
|
|||
const routes = [ |
|||
{ |
|||
path: '/', |
|||
component: Layout, |
|||
children: [ |
|||
{ |
|||
name: "root", |
|||
path: '', |
|||
component: Dashboard, |
|||
children: [ |
|||
{ |
|||
name: "DashboardHome", |
|||
path: '/dashboard', |
|||
component: DashboardHome, |
|||
children: [ |
|||
{ |
|||
path: ':id', |
|||
component: Details, |
|||
}, |
|||
{ |
|||
path: '/add', |
|||
component: EditMonitor, |
|||
}, |
|||
{ |
|||
path: '/edit/:id', |
|||
component: EditMonitor, |
|||
}, |
|||
] |
|||
}, |
|||
{ |
|||
path: '/settings', |
|||
component: Settings, |
|||
}, |
|||
], |
|||
}, |
|||
], |
|||
} |
|||
] |
|||
|
|||
const router = createRouter({ |
|||
linkActiveClass: 'active', |
|||
history: createWebHistory(), |
|||
routes, |
|||
}) |
|||
|
|||
const app = createApp({ |
|||
mixins: [ |
|||
socket, |
|||
], |
|||
render: ()=>h(App) |
|||
}) |
|||
|
|||
app.use(router) |
|||
|
|||
const options = { |
|||
position: "bottom-right" |
|||
}; |
|||
|
|||
app.use(Toast, options); |
|||
|
|||
app.mount('#app') |
|||
|
@ -0,0 +1,121 @@ |
|||
import {io} from "socket.io-client"; |
|||
import { useToast } from 'vue-toastification' |
|||
const toast = useToast() |
|||
|
|||
let storage = localStorage; |
|||
let socket; |
|||
|
|||
export default { |
|||
|
|||
data() { |
|||
return { |
|||
socket: { |
|||
token: null, |
|||
firstConnect: true, |
|||
connected: false, |
|||
}, |
|||
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, |
|||
monitorList: [ |
|||
|
|||
], |
|||
importantHeartbeatList: [ |
|||
|
|||
] |
|||
} |
|||
}, |
|||
|
|||
created() { |
|||
socket = io("http://localhost:3001", { |
|||
transports: ['websocket'] |
|||
}); |
|||
|
|||
socket.on('monitorList', (data) => { |
|||
this.monitorList = data; |
|||
}); |
|||
|
|||
socket.on('disconnect', () => { |
|||
this.socket.connected = false; |
|||
}); |
|||
|
|||
socket.on('connect', () => { |
|||
this.socket.connected = true; |
|||
this.socket.firstConnect = false; |
|||
|
|||
if (storage.token) { |
|||
this.loginByToken(storage.token) |
|||
} else { |
|||
this.allowLoginDialog = true; |
|||
} |
|||
|
|||
}); |
|||
|
|||
}, |
|||
|
|||
methods: { |
|||
getSocket() { |
|||
return socket; |
|||
}, |
|||
toastRes(res) { |
|||
if (res.ok) { |
|||
toast.success(res.msg); |
|||
} else { |
|||
toast.error(res.msg); |
|||
} |
|||
}, |
|||
login(username, password, callback) { |
|||
socket.emit("login", { |
|||
username, |
|||
password, |
|||
}, (res) => { |
|||
|
|||
if (res.ok) { |
|||
storage.token = res.token; |
|||
this.socket.token = res.token; |
|||
this.loggedIn = true; |
|||
|
|||
// Trigger Chrome Save Password
|
|||
history.pushState({}, '') |
|||
} |
|||
|
|||
callback(res) |
|||
}) |
|||
}, |
|||
loginByToken(token) { |
|||
socket.emit("loginByToken", token, (res) => { |
|||
this.allowLoginDialog = true; |
|||
|
|||
if (! res.ok) { |
|||
this.logout() |
|||
console.log(res.msg) |
|||
} else { |
|||
this.loggedIn = true; |
|||
} |
|||
}) |
|||
}, |
|||
logout() { |
|||
storage.removeItem("token"); |
|||
this.socket.token = null; |
|||
this.loggedIn = false; |
|||
|
|||
socket.emit("logout", () => { |
|||
toast.success("Logout Successfully") |
|||
}) |
|||
}, |
|||
add(monitor, callback) { |
|||
socket.emit("add", monitor, callback) |
|||
}, |
|||
deleteMonitor(monitorID, callback) { |
|||
socket.emit("deleteMonitor", monitorID, callback) |
|||
}, |
|||
loadMonitor(monitorID) { |
|||
|
|||
} |
|||
}, |
|||
|
|||
computed: { |
|||
|
|||
} |
|||
|
|||
} |
|||
|
@ -0,0 +1,128 @@ |
|||
<template> |
|||
|
|||
<div class="container-fluid"> |
|||
<div class="row"> |
|||
<div class="col-12 col-xl-4"> |
|||
<div> |
|||
<router-link to="/add" class="btn btn-primary">Add New Monitor</router-link> |
|||
</div> |
|||
|
|||
<div class="shadow-box list"> |
|||
|
|||
<span v-if="$root.monitorList.length === 0">No Monitors, please <router-link to="/add">add one</router-link>.</span> |
|||
|
|||
<router-link :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }" v-for="item in $root.monitorList"> |
|||
|
|||
<div class="row"> |
|||
<div class="col-6"> |
|||
|
|||
<div class="info"> |
|||
<span class="badge rounded-pill bg-primary">{{ item.upRate }}%</span> |
|||
{{ item.name }} |
|||
</div> |
|||
|
|||
</div> |
|||
<div class="col-6"> |
|||
<div class="hp-bar"> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
</router-link> |
|||
|
|||
</div> |
|||
</div> |
|||
<div class="col-12 col-xl-8"> |
|||
<router-view /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
</template> |
|||
|
|||
<script> |
|||
|
|||
export default { |
|||
components: { |
|||
}, |
|||
data() { |
|||
return { |
|||
} |
|||
}, |
|||
methods: { |
|||
monitorURL(id) { |
|||
return "/dashboard/" + id; |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped lang="scss"> |
|||
@import "../assets/vars.scss"; |
|||
|
|||
.container-fluid { |
|||
width: 98% |
|||
} |
|||
|
|||
.list { |
|||
margin-top: 25px; |
|||
|
|||
.item { |
|||
display: block; |
|||
text-decoration: none; |
|||
padding: 15px 15px 12px 15px; |
|||
border-radius: 10px; |
|||
transition: all ease-in-out 0.15s; |
|||
|
|||
&.disabled { |
|||
opacity: 0.3; |
|||
} |
|||
|
|||
.info { |
|||
white-space: nowrap; |
|||
} |
|||
|
|||
&:hover { |
|||
background-color: $highlight-white; |
|||
} |
|||
|
|||
&.active { |
|||
background-color: #cdf8f4; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.hp-bar { |
|||
white-space: nowrap; |
|||
margin-top: 4px; |
|||
text-align: right; |
|||
|
|||
div { |
|||
display: inline-block; |
|||
background-color: $primary; |
|||
width: 0.35rem; |
|||
height: 1rem; |
|||
margin: 0.15rem; |
|||
border-radius: 50rem; |
|||
transition: all ease-in-out 0.15s; |
|||
|
|||
&:hover { |
|||
opacity: 0.8; |
|||
transform: scale(1.5); |
|||
} |
|||
} |
|||
} |
|||
|
|||
</style> |
@ -0,0 +1,123 @@ |
|||
<template> |
|||
|
|||
<div v-if="$route.name === 'DashboardHome'"> |
|||
<h1 class="mb-3">Quick Stats</h1> |
|||
|
|||
<div class="shadow-box big-padding text-center"> |
|||
<div class="row"> |
|||
|
|||
<div class="col-12"> |
|||
<div class="hp-bar-big"> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="col"> |
|||
<h3>Up</h3> |
|||
<span class="num">2</span> |
|||
</div> |
|||
<div class="col"> |
|||
<h3>Down</h3> |
|||
<span class="num text-danger">0</span> |
|||
</div> |
|||
<div class="col"> |
|||
<h3>Pause</h3> |
|||
<span class="num">0</span> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="row mt-4"> |
|||
<div class="col-8"> |
|||
<h4>Latest Incident</h4> |
|||
|
|||
<div class="shadow-box bg-danger text-light"> |
|||
MySQL was down. |
|||
</div> |
|||
|
|||
<div class="shadow-box bg-primary text-light"> |
|||
No issues was found. |
|||
</div> |
|||
|
|||
</div> |
|||
<div class="col-4"> |
|||
|
|||
<h4>Overall Uptime</h4> |
|||
|
|||
<div class="shadow-box"> |
|||
<div>100.00% (24 hours)</div> |
|||
<div>100.00% (7 days)</div> |
|||
<div>100.00% (30 days)</div> |
|||
</div> |
|||
|
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<router-view ref="child" /> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
computed: { |
|||
|
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped lang="scss"> |
|||
@import "../assets/vars"; |
|||
|
|||
.num { |
|||
font-size: 30px; |
|||
color: $primary; |
|||
font-weight: bold; |
|||
} |
|||
</style> |
@ -0,0 +1,162 @@ |
|||
<template> |
|||
<h1>{{ monitor.name }}</h1> |
|||
<h2>{{ monitor.url }}</h2> |
|||
|
|||
<div class="functions"> |
|||
<button class="btn btn-light" @click="pauseDialog" v-if="monitor.active">Pause</button> |
|||
<button class="btn btn-primary" @click="resumeMonitor" v-if="! monitor.active">Resume</button> |
|||
<router-link :to=" '/edit/' + monitor.id " class="btn btn-light">Edit</router-link> |
|||
<button class="btn btn-danger" @click="deleteDialog">Delete</button> |
|||
</div> |
|||
|
|||
<div class="shadow-box"> |
|||
|
|||
<div class="hp-bar-big"> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
</div> |
|||
|
|||
<div class="row"> |
|||
<div class="col-md-8"> |
|||
|
|||
</div> |
|||
<div class="col-md-4"> |
|||
|
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<Confirm ref="confirmPause" @yes="pauseMonitor"> |
|||
Are you sure want to pause? |
|||
</Confirm> |
|||
|
|||
<Confirm ref="confirmDelete" btnStyle="btn-danger" @yes="deleteMonitor"> |
|||
Are you sure want to delete this monitor? |
|||
</Confirm> |
|||
</template> |
|||
|
|||
<script> |
|||
import { useToast } from 'vue-toastification' |
|||
const toast = useToast() |
|||
import Confirm from "../components/Confirm.vue"; |
|||
|
|||
export default { |
|||
components: { |
|||
Confirm |
|||
}, |
|||
mounted() { |
|||
|
|||
}, |
|||
data() { |
|||
return { |
|||
|
|||
} |
|||
}, |
|||
computed: { |
|||
monitor() { |
|||
let id = parseInt(this.$route.params.id) |
|||
|
|||
for (let monitor of this.$root.monitorList) { |
|||
if (monitor.id === id) { |
|||
return monitor; |
|||
} |
|||
} |
|||
return {}; |
|||
}, |
|||
}, |
|||
methods: { |
|||
pauseDialog() { |
|||
this.$refs.confirmPause.show(); |
|||
}, |
|||
resumeMonitor() { |
|||
this.$root.getSocket().emit("resumeMonitor", this.monitor.id, (res) => { |
|||
this.$root.toastRes(res) |
|||
}) |
|||
}, |
|||
pauseMonitor() { |
|||
this.$root.getSocket().emit("pauseMonitor", this.monitor.id, (res) => { |
|||
this.$root.toastRes(res) |
|||
}) |
|||
}, |
|||
deleteDialog() { |
|||
this.$refs.confirmDelete.show(); |
|||
}, |
|||
deleteMonitor() { |
|||
this.$root.deleteMonitor(this.monitor.id, (res) => { |
|||
if (res.ok) { |
|||
toast.success(res.msg); |
|||
this.$router.push("/dashboard") |
|||
} else { |
|||
toast.error(res.msg); |
|||
} |
|||
}) |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
@import "../assets/vars.scss"; |
|||
|
|||
h2 { |
|||
color: $primary; |
|||
margin-bottom: 20px; |
|||
} |
|||
|
|||
.functions { |
|||
button, a { |
|||
margin-right: 20px; |
|||
} |
|||
} |
|||
|
|||
.shadow-box { |
|||
padding: 20px; |
|||
margin-top: 25px; |
|||
} |
|||
</style> |
@ -0,0 +1,123 @@ |
|||
<template> |
|||
<h1 class="mb-3">{{ pageName }}</h1> |
|||
<form @submit.prevent="submit"> |
|||
|
|||
<div class="shadow-box"> |
|||
<div class="row"> |
|||
<div class="col-md-6"> |
|||
<h2>General</h2> |
|||
|
|||
<div class="mb-3"> |
|||
<label for="type" class="form-label">Monitor Type</label> |
|||
<select class="form-select" aria-label="Default select example" id="type" v-model="monitor.type"> |
|||
<option value="http">HTTP(s)</option> |
|||
<option value="port">TCP Port</option> |
|||
<option value="ping">Ping</option> |
|||
<option value="keyword">HTTP(s) - Keyword</option> |
|||
</select> |
|||
</div> |
|||
|
|||
<div class="mb-3"> |
|||
<label for="name" class="form-label">Friendly Name</label> |
|||
<input type="text" class="form-control" id="name" v-model="monitor.name" required> |
|||
</div> |
|||
|
|||
<div class="mb-3"> |
|||
<label for="url" class="form-label">URL</label> |
|||
<input type="url" class="form-control" id="url" v-model="monitor.url" pattern="https?://.+" required> |
|||
</div> |
|||
|
|||
<div class="mb-3"> |
|||
<label for="interval" class="form-label">Heartbeat Interval (Every {{ monitor.interval }} seconds)</label> |
|||
<input type="number" class="form-control" id="interval" v-model="monitor.interval" required min="20" step="20"> |
|||
</div> |
|||
|
|||
<div> |
|||
<button class="btn btn-primary" type="submit" :disabled="processing">Save</button> |
|||
</div> |
|||
|
|||
</div> |
|||
|
|||
<div class="col-md-6"> |
|||
<h2>Notifications</h2> |
|||
<p>Not available, please setup in Settings page.</p> |
|||
<a class="btn btn-primary me-2" href="/settings" target="_blank">Go to Settings</a> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</form> |
|||
|
|||
|
|||
</template> |
|||
|
|||
<script> |
|||
import { useToast } from 'vue-toastification' |
|||
const toast = useToast() |
|||
|
|||
export default { |
|||
components: { |
|||
|
|||
}, |
|||
mounted() { |
|||
|
|||
if (this.isAdd) { |
|||
this.monitor = { |
|||
type: "http", |
|||
name: "", |
|||
url: "https://", |
|||
interval: 60, |
|||
} |
|||
} else { |
|||
this.$root.getSocket().emit("getMonitor", this.$route.params.id, (res) => { |
|||
if (res.ok) { |
|||
this.monitor = res.monitor; |
|||
} else { |
|||
toast.error(res.msg) |
|||
} |
|||
}) |
|||
} |
|||
|
|||
}, |
|||
data() { |
|||
return { |
|||
processing: false, |
|||
monitor: { } |
|||
} |
|||
}, |
|||
computed: { |
|||
pageName() { |
|||
return (this.isAdd) ? "Add New Monitor" : "Edit" |
|||
}, |
|||
isAdd() { |
|||
return this.$route.path === "/add"; |
|||
} |
|||
}, |
|||
methods: { |
|||
submit() { |
|||
this.processing = true; |
|||
|
|||
if (this.isAdd) { |
|||
this.$root.add(this.monitor, (res) => { |
|||
this.processing = false; |
|||
|
|||
if (res.ok) { |
|||
toast.success(res.msg); |
|||
this.$router.push("/dashboard/" + res.monitorID) |
|||
} else { |
|||
toast.error(res.msg); |
|||
} |
|||
|
|||
}) |
|||
} else { |
|||
|
|||
} |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.shadow-box { |
|||
padding: 20px; |
|||
} |
|||
</style> |
@ -0,0 +1,111 @@ |
|||
<template> |
|||
<h1 class="mb-3">Settings</h1> |
|||
|
|||
<div class="shadow-box"> |
|||
<div class="row"> |
|||
|
|||
<div class="col-md-6"> |
|||
<h2>General</h2> |
|||
<form class="mb-3"> |
|||
<div class="mb-3"> |
|||
<label for="timezone" class="form-label">Timezone</label> |
|||
<select class="form-select" aria-label="Default select example" id="timezone"> |
|||
<option value="1">One</option> |
|||
<option value="2">Two</option> |
|||
<option value="3">Three</option> |
|||
</select> |
|||
</div> |
|||
|
|||
<div> |
|||
<button class="btn btn-primary" type="submit">Save</button> |
|||
</div> |
|||
</form> |
|||
|
|||
<h2>Change Password</h2> |
|||
<form class="mb-3" @submit.prevent="savePassword"> |
|||
<div class="mb-3"> |
|||
<label for="current-password" class="form-label">Current Password</label> |
|||
<input type="password" class="form-control" id="current-password" required v-model="password.currentPassword"> |
|||
</div> |
|||
|
|||
<div class="mb-3"> |
|||
<label for="new-password" class="form-label">New Password</label> |
|||
<input type="password" class="form-control" id="new-password" required v-model="password.newPassword"> |
|||
</div> |
|||
|
|||
<div class="mb-3"> |
|||
<label for="repeat-new-password" class="form-label">Repeat New Password</label> |
|||
<input type="password" class="form-control" :class="{ 'is-invalid' : invalidPassword }" id="repeat-new-password" required v-model="password.repeatNewPassword"> |
|||
<div class="invalid-feedback"> |
|||
The repeat password is not match. |
|||
</div> |
|||
</div> |
|||
|
|||
<div> |
|||
<button class="btn btn-primary" type="submit">Update Password</button> |
|||
</div> |
|||
</form> |
|||
|
|||
<div> |
|||
<button class="btn btn-danger" @click="$root.logout">Logout</button> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="col-md-6"> |
|||
<h2>Notifications</h2> |
|||
<p>Empty</p> |
|||
<button class="btn btn-primary" type="submit">Add Notification</button> |
|||
</div> |
|||
|
|||
</div> |
|||
</div> |
|||
|
|||
|
|||
</template> |
|||
|
|||
<script> |
|||
|
|||
|
|||
export default { |
|||
components: { |
|||
|
|||
}, |
|||
data() { |
|||
return { |
|||
invalidPassword: false, |
|||
password: { |
|||
currentPassword: "", |
|||
newPassword: "", |
|||
repeatNewPassword: "", |
|||
} |
|||
} |
|||
}, |
|||
methods: { |
|||
savePassword() { |
|||
if (this.password.newPassword !== this.password.repeatNewPassword) { |
|||
this.invalidPassword = true; |
|||
} else { |
|||
this.$root.getSocket().emit("changePassword", this.password, (res) => { |
|||
this.$root.toastRes(res) |
|||
if (res.ok) { |
|||
this.password.currentPassword = "" |
|||
this.password.newPassword = "" |
|||
this.password.repeatNewPassword = "" |
|||
} |
|||
}) |
|||
} |
|||
}, |
|||
}, |
|||
watch: { |
|||
"password.repeatNewPassword"() { |
|||
this.invalidPassword = false; |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.shadow-box { |
|||
padding: 20px; |
|||
} |
|||
</style> |
Loading…
Reference in new issue