mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-08-09 07:39:12 +08:00
Merge branch 'master' into feature/request-with-http-proxy
# Conflicts: # package-lock.json # package.json # server/database.js # src/languages/en.js # src/mixins/socket.js
This commit is contained in:
@@ -12,6 +12,10 @@ const { loginRateLimiter } = require("./rate-limiter");
|
||||
* @returns {Promise<Bean|null>}
|
||||
*/
|
||||
exports.login = async function (username, password) {
|
||||
if (typeof username !== "string" || typeof password !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
let user = await R.findOne("user", " username = ? AND active = 1 ", [
|
||||
username,
|
||||
]);
|
||||
@@ -31,31 +35,34 @@ exports.login = async function (username, password) {
|
||||
};
|
||||
|
||||
function myAuthorizer(username, password, callback) {
|
||||
setting("disableAuth").then((result) => {
|
||||
if (result) {
|
||||
callback(null, true);
|
||||
} else {
|
||||
// Login Rate Limit
|
||||
loginRateLimiter.pass(null, 0).then((pass) => {
|
||||
if (pass) {
|
||||
exports.login(username, password).then((user) => {
|
||||
callback(null, user != null);
|
||||
// Login Rate Limit
|
||||
loginRateLimiter.pass(null, 0).then((pass) => {
|
||||
if (pass) {
|
||||
exports.login(username, password).then((user) => {
|
||||
callback(null, user != null);
|
||||
|
||||
if (user == null) {
|
||||
loginRateLimiter.removeTokens(1);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
callback(null, false);
|
||||
if (user == null) {
|
||||
loginRateLimiter.removeTokens(1);
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
callback(null, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
exports.basicAuth = basicAuth({
|
||||
authorizer: myAuthorizer,
|
||||
authorizeAsync: true,
|
||||
challenge: true,
|
||||
});
|
||||
exports.basicAuth = async function (req, res, next) {
|
||||
const middleware = basicAuth({
|
||||
authorizer: myAuthorizer,
|
||||
authorizeAsync: true,
|
||||
challenge: true,
|
||||
});
|
||||
|
||||
const disabledAuth = await setting("disableAuth");
|
||||
|
||||
if (!disabledAuth) {
|
||||
middleware(req, res, next);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
@@ -1,5 +1,6 @@
|
||||
const { setSetting } = require("./util-server");
|
||||
const { setSetting, setting } = require("./util-server");
|
||||
const axios = require("axios");
|
||||
const compareVersions = require("compare-versions");
|
||||
|
||||
exports.version = require("../package.json").version;
|
||||
exports.latestVersion = null;
|
||||
@@ -16,6 +17,19 @@ exports.startInterval = () => {
|
||||
res.data.slow = "1000.0.0";
|
||||
}
|
||||
|
||||
if (!await setting("checkUpdate")) {
|
||||
return;
|
||||
}
|
||||
|
||||
let checkBeta = await setting("checkBeta");
|
||||
|
||||
if (checkBeta && res.data.beta) {
|
||||
if (compareVersions.compare(res.data.beta, res.data.beta, ">")) {
|
||||
exports.latestVersion = res.data.beta;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (res.data.slow) {
|
||||
exports.latestVersion = res.data.slow;
|
||||
}
|
||||
|
@@ -53,6 +53,7 @@ class Database {
|
||||
"patch-2fa-invalidate-used-token.sql": true,
|
||||
"patch-notification_sent_history.sql": true,
|
||||
"patch-monitor-basic-auth.sql": true,
|
||||
"patch-status-page.sql": true,
|
||||
"patch-proxy.sql": true,
|
||||
}
|
||||
|
||||
@@ -171,6 +172,7 @@ class Database {
|
||||
}
|
||||
|
||||
await this.patch2();
|
||||
await this.migrateNewStatusPage();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -212,6 +214,74 @@ class Database {
|
||||
await setSetting("databasePatchedFiles", databasePatchedFiles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate status page value in setting to "status_page" table
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async migrateNewStatusPage() {
|
||||
|
||||
// Fix 1.13.0 empty slug bug
|
||||
await R.exec("UPDATE status_page SET slug = 'empty-slug-recover' WHERE TRIM(slug) = ''");
|
||||
|
||||
let title = await setting("title");
|
||||
|
||||
if (title) {
|
||||
console.log("Migrating Status Page");
|
||||
|
||||
let statusPageCheck = await R.findOne("status_page", " slug = 'default' ");
|
||||
|
||||
if (statusPageCheck !== null) {
|
||||
console.log("Migrating Status Page - Skip, default slug record is already existing");
|
||||
return;
|
||||
}
|
||||
|
||||
let statusPage = R.dispense("status_page");
|
||||
statusPage.slug = "default";
|
||||
statusPage.title = title;
|
||||
statusPage.description = await setting("description");
|
||||
statusPage.icon = await setting("icon");
|
||||
statusPage.theme = await setting("statusPageTheme");
|
||||
statusPage.published = !!await setting("statusPagePublished");
|
||||
statusPage.search_engine_index = !!await setting("searchEngineIndex");
|
||||
statusPage.show_tags = !!await setting("statusPageTags");
|
||||
statusPage.password = null;
|
||||
|
||||
if (!statusPage.title) {
|
||||
statusPage.title = "My Status Page";
|
||||
}
|
||||
|
||||
if (!statusPage.icon) {
|
||||
statusPage.icon = "";
|
||||
}
|
||||
|
||||
if (!statusPage.theme) {
|
||||
statusPage.theme = "light";
|
||||
}
|
||||
|
||||
let id = await R.store(statusPage);
|
||||
|
||||
await R.exec("UPDATE incident SET status_page_id = ? WHERE status_page_id IS NULL", [
|
||||
id
|
||||
]);
|
||||
|
||||
await R.exec("UPDATE [group] SET status_page_id = ? WHERE status_page_id IS NULL", [
|
||||
id
|
||||
]);
|
||||
|
||||
await R.exec("DELETE FROM setting WHERE type = 'statusPage'");
|
||||
|
||||
// Migrate Entry Page if it is status page
|
||||
let entryPage = await setting("entryPage");
|
||||
|
||||
if (entryPage === "statusPage") {
|
||||
await setSetting("entryPage", "statusPage-default", "general");
|
||||
}
|
||||
|
||||
console.log("Migrating Status Page - Done");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Used it patch2() only
|
||||
* @param sqlFilename
|
||||
|
@@ -3,12 +3,12 @@ const { R } = require("redbean-node");
|
||||
|
||||
class Group extends BeanModel {
|
||||
|
||||
async toPublicJSON() {
|
||||
async toPublicJSON(showTags = false) {
|
||||
let monitorBeanList = await this.getMonitorList();
|
||||
let monitorList = [];
|
||||
|
||||
for (let bean of monitorBeanList) {
|
||||
monitorList.push(await bean.toPublicJSON());
|
||||
monitorList.push(await bean.toPublicJSON(showTags));
|
||||
}
|
||||
|
||||
return {
|
||||
|
@@ -25,18 +25,22 @@ const apicache = require("../modules/apicache");
|
||||
class Monitor extends BeanModel {
|
||||
|
||||
/**
|
||||
* Return a object that ready to parse to JSON for public
|
||||
* Return an object that ready to parse to JSON for public
|
||||
* Only show necessary data to public
|
||||
*/
|
||||
async toPublicJSON() {
|
||||
return {
|
||||
async toPublicJSON(showTags = false) {
|
||||
let obj = {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
};
|
||||
if (showTags) {
|
||||
obj.tags = await this.getTags();
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a object that ready to parse to JSON
|
||||
* Return an object that ready to parse to JSON
|
||||
*/
|
||||
async toJSON() {
|
||||
|
||||
@@ -50,7 +54,7 @@ class Monitor extends BeanModel {
|
||||
notificationIDList[bean.notification_id] = true;
|
||||
}
|
||||
|
||||
const tags = await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ?", [this.id]);
|
||||
const tags = await this.getTags();
|
||||
|
||||
return {
|
||||
id: this.id,
|
||||
@@ -84,6 +88,10 @@ class Monitor extends BeanModel {
|
||||
};
|
||||
}
|
||||
|
||||
async getTags() {
|
||||
return await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ?", [this.id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode user and password to Base64 encoding
|
||||
* for HTTP "basic" auth, as per RFC-7617
|
||||
@@ -492,6 +500,12 @@ class Monitor extends BeanModel {
|
||||
stop() {
|
||||
clearTimeout(this.heartbeatInterval);
|
||||
this.isStop = true;
|
||||
|
||||
this.prometheus().remove();
|
||||
}
|
||||
|
||||
prometheus() {
|
||||
return new Prometheus(this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
60
server/model/status_page.js
Normal file
60
server/model/status_page.js
Normal file
@@ -0,0 +1,60 @@
|
||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
const { R } = require("redbean-node");
|
||||
|
||||
class StatusPage extends BeanModel {
|
||||
|
||||
static async sendStatusPageList(io, socket) {
|
||||
let result = {};
|
||||
|
||||
let list = await R.findAll("status_page", " ORDER BY title ");
|
||||
|
||||
for (let item of list) {
|
||||
result[item.id] = await item.toJSON();
|
||||
}
|
||||
|
||||
io.to(socket.userID).emit("statusPageList", result);
|
||||
return list;
|
||||
}
|
||||
|
||||
async toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
slug: this.slug,
|
||||
title: this.title,
|
||||
description: this.description,
|
||||
icon: this.getIcon(),
|
||||
theme: this.theme,
|
||||
published: !!this.published,
|
||||
showTags: !!this.show_tags,
|
||||
};
|
||||
}
|
||||
|
||||
async toPublicJSON() {
|
||||
return {
|
||||
slug: this.slug,
|
||||
title: this.title,
|
||||
description: this.description,
|
||||
icon: this.getIcon(),
|
||||
theme: this.theme,
|
||||
published: !!this.published,
|
||||
showTags: !!this.show_tags,
|
||||
};
|
||||
}
|
||||
|
||||
static async slugToID(slug) {
|
||||
return await R.getCell("SELECT id FROM status_page WHERE slug = ? ", [
|
||||
slug
|
||||
]);
|
||||
}
|
||||
|
||||
getIcon() {
|
||||
if (!this.icon) {
|
||||
return "/icon.svg";
|
||||
} else {
|
||||
return this.icon;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = StatusPage;
|
@@ -9,36 +9,31 @@ class Pushover extends NotificationProvider {
|
||||
let okMsg = "Sent Successfully.";
|
||||
let pushoverlink = "https://api.pushover.net/1/messages.json";
|
||||
|
||||
let data = {
|
||||
"message": "<b>Uptime Kuma Alert</b>\n\n<b>Message</b>:" + msg,
|
||||
"user": notification.pushoveruserkey,
|
||||
"token": notification.pushoverapptoken,
|
||||
"sound": notification.pushoversounds,
|
||||
"priority": notification.pushoverpriority,
|
||||
"title": notification.pushovertitle,
|
||||
"retry": "30",
|
||||
"expire": "3600",
|
||||
"html": 1,
|
||||
};
|
||||
|
||||
if (notification.pushoverdevice) {
|
||||
data.device = notification.pushoverdevice;
|
||||
}
|
||||
|
||||
try {
|
||||
if (heartbeatJSON == null) {
|
||||
let data = {
|
||||
"message": msg,
|
||||
"user": notification.pushoveruserkey,
|
||||
"token": notification.pushoverapptoken,
|
||||
"sound": notification.pushoversounds,
|
||||
"priority": notification.pushoverpriority,
|
||||
"title": notification.pushovertitle,
|
||||
"retry": "30",
|
||||
"expire": "3600",
|
||||
"html": 1,
|
||||
};
|
||||
await axios.post(pushoverlink, data);
|
||||
return okMsg;
|
||||
} else {
|
||||
data.message += "\n<b>Time (UTC)</b>:" + heartbeatJSON["time"];
|
||||
await axios.post(pushoverlink, data);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
let data = {
|
||||
"message": "<b>Uptime Kuma Alert</b>\n\n<b>Message</b>:" + msg + "\n<b>Time (UTC)</b>:" + heartbeatJSON["time"],
|
||||
"user": notification.pushoveruserkey,
|
||||
"token": notification.pushoverapptoken,
|
||||
"sound": notification.pushoversounds,
|
||||
"priority": notification.pushoverpriority,
|
||||
"title": notification.pushovertitle,
|
||||
"retry": "30",
|
||||
"expire": "3600",
|
||||
"html": 1,
|
||||
};
|
||||
await axios.post(pushoverlink, data);
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
|
@@ -86,6 +86,16 @@ class Prometheus {
|
||||
}
|
||||
}
|
||||
|
||||
remove() {
|
||||
try {
|
||||
monitor_cert_days_remaining.remove(this.monitorLabelValues);
|
||||
monitor_cert_is_valid.remove(this.monitorLabelValues);
|
||||
monitor_response_time.remove(this.monitorLabelValues);
|
||||
monitor_status.remove(this.monitorLabelValues);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
@@ -34,6 +34,14 @@ const loginRateLimiter = new KumaRateLimiter({
|
||||
errorMessage: "Too frequently, try again later."
|
||||
});
|
||||
|
||||
const twoFaRateLimiter = new KumaRateLimiter({
|
||||
tokensPerInterval: 30,
|
||||
interval: "minute",
|
||||
fireImmediately: true,
|
||||
errorMessage: "Too frequently, try again later."
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
loginRateLimiter
|
||||
loginRateLimiter,
|
||||
twoFaRateLimiter,
|
||||
};
|
||||
|
@@ -6,6 +6,7 @@ const apicache = require("../modules/apicache");
|
||||
const Monitor = require("../model/monitor");
|
||||
const dayjs = require("dayjs");
|
||||
const { UP, flipStatus, debug } = require("../../src/util");
|
||||
const StatusPage = require("../model/status_page");
|
||||
let router = express.Router();
|
||||
|
||||
let cache = apicache.middleware;
|
||||
@@ -82,110 +83,80 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Status Page Config
|
||||
router.get("/api/status-page/config", async (_request, response) => {
|
||||
// Status page config, incident, monitor list
|
||||
router.get("/api/status-page/:slug", cache("5 minutes"), async (request, response) => {
|
||||
allowDevAllOrigin(response);
|
||||
let slug = request.params.slug;
|
||||
|
||||
let config = await getSettings("statusPage");
|
||||
// Get Status Page
|
||||
let statusPage = await R.findOne("status_page", " slug = ? ", [
|
||||
slug
|
||||
]);
|
||||
|
||||
if (! config.statusPageTheme) {
|
||||
config.statusPageTheme = "light";
|
||||
if (!statusPage) {
|
||||
response.statusCode = 404;
|
||||
response.json({
|
||||
msg: "Not Found"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (! config.statusPagePublished) {
|
||||
config.statusPagePublished = true;
|
||||
}
|
||||
|
||||
if (! config.statusPageTags) {
|
||||
config.statusPageTags = false;
|
||||
}
|
||||
|
||||
if (! config.title) {
|
||||
config.title = "Uptime Kuma";
|
||||
}
|
||||
|
||||
response.json(config);
|
||||
});
|
||||
|
||||
// Status Page - Get the current Incident
|
||||
// Can fetch only if published
|
||||
router.get("/api/status-page/incident", async (_, response) => {
|
||||
allowDevAllOrigin(response);
|
||||
|
||||
try {
|
||||
await checkPublished();
|
||||
|
||||
let incident = await R.findOne("incident", " pin = 1 AND active = 1");
|
||||
// Incident
|
||||
let incident = await R.findOne("incident", " pin = 1 AND active = 1 AND status_page_id = ? ", [
|
||||
statusPage.id,
|
||||
]);
|
||||
|
||||
if (incident) {
|
||||
incident = incident.toPublicJSON();
|
||||
}
|
||||
|
||||
// Public Group List
|
||||
const publicGroupList = [];
|
||||
const showTags = !!statusPage.show_tags;
|
||||
debug("Show Tags???" + showTags);
|
||||
const list = await R.find("group", " public = 1 AND status_page_id = ? ORDER BY weight ", [
|
||||
statusPage.id
|
||||
]);
|
||||
|
||||
for (let groupBean of list) {
|
||||
let monitorGroup = await groupBean.toPublicJSON(showTags);
|
||||
publicGroupList.push(monitorGroup);
|
||||
}
|
||||
|
||||
// Response
|
||||
response.json({
|
||||
ok: true,
|
||||
config: await statusPage.toPublicJSON(),
|
||||
incident,
|
||||
publicGroupList
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
send403(response, error.message);
|
||||
}
|
||||
});
|
||||
|
||||
// Status Page - Monitor List
|
||||
// Can fetch only if published
|
||||
router.get("/api/status-page/monitor-list", cache("5 minutes"), async (_request, response) => {
|
||||
allowDevAllOrigin(response);
|
||||
|
||||
try {
|
||||
await checkPublished();
|
||||
const publicGroupList = [];
|
||||
const tagsVisible = (await getSettings("statusPage")).statusPageTags;
|
||||
const list = await R.find("group", " public = 1 ORDER BY weight ");
|
||||
for (let groupBean of list) {
|
||||
let monitorGroup = await groupBean.toPublicJSON();
|
||||
if (tagsVisible) {
|
||||
monitorGroup.monitorList = await Promise.all(monitorGroup.monitorList.map(async (monitor) => {
|
||||
// Includes tags as an array in response, allows for tags to be displayed on public status page
|
||||
const tags = await R.getAll(
|
||||
`SELECT monitor_tag.monitor_id, monitor_tag.value, tag.name, tag.color
|
||||
FROM monitor_tag
|
||||
JOIN tag
|
||||
ON monitor_tag.tag_id = tag.id
|
||||
WHERE monitor_tag.monitor_id = ?`, [monitor.id]
|
||||
);
|
||||
return {
|
||||
...monitor,
|
||||
tags: tags
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
publicGroupList.push(monitorGroup);
|
||||
}
|
||||
|
||||
response.json(publicGroupList);
|
||||
|
||||
} catch (error) {
|
||||
send403(response, error.message);
|
||||
}
|
||||
});
|
||||
|
||||
// Status Page Polling Data
|
||||
// Can fetch only if published
|
||||
router.get("/api/status-page/heartbeat", cache("5 minutes"), async (_request, response) => {
|
||||
router.get("/api/status-page/heartbeat/:slug", cache("1 minutes"), async (request, response) => {
|
||||
allowDevAllOrigin(response);
|
||||
|
||||
try {
|
||||
await checkPublished();
|
||||
|
||||
let heartbeatList = {};
|
||||
let uptimeList = {};
|
||||
|
||||
let slug = request.params.slug;
|
||||
let statusPageID = await StatusPage.slugToID(slug);
|
||||
|
||||
let monitorIDList = await R.getCol(`
|
||||
SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
|
||||
WHERE monitor_group.group_id = \`group\`.id
|
||||
AND public = 1
|
||||
`);
|
||||
AND \`group\`.status_page_id = ?
|
||||
`, [
|
||||
statusPageID
|
||||
]);
|
||||
|
||||
for (let monitorID of monitorIDList) {
|
||||
let list = await R.getAll(`
|
||||
@@ -214,22 +185,12 @@ router.get("/api/status-page/heartbeat", cache("5 minutes"), async (_request, re
|
||||
}
|
||||
});
|
||||
|
||||
async function checkPublished() {
|
||||
if (! await isPublished()) {
|
||||
throw new Error("The status page is not published");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default is published
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async function isPublished() {
|
||||
const value = await setting("statusPagePublished");
|
||||
if (value === null) {
|
||||
return true;
|
||||
}
|
||||
return value;
|
||||
return true;
|
||||
}
|
||||
|
||||
function send403(res, msg = "") {
|
||||
|
136
server/server.js
136
server/server.js
@@ -52,7 +52,7 @@ console.log("Importing this project modules");
|
||||
debug("Importing Monitor");
|
||||
const Monitor = require("./model/monitor");
|
||||
debug("Importing Settings");
|
||||
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD, errorLog } = require("./util-server");
|
||||
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD, errorLog, doubleCheckPassword } = require("./util-server");
|
||||
|
||||
debug("Importing Notification");
|
||||
const { Notification } = require("./notification");
|
||||
@@ -66,7 +66,7 @@ const Database = require("./database");
|
||||
|
||||
debug("Importing Background Jobs");
|
||||
const { initBackgroundJobs } = require("./jobs");
|
||||
const { loginRateLimiter } = require("./rate-limiter");
|
||||
const { loginRateLimiter, twoFaRateLimiter } = require("./rate-limiter");
|
||||
|
||||
const { basicAuth } = require("./auth");
|
||||
const { login } = require("./auth");
|
||||
@@ -94,6 +94,7 @@ const port = parseInt(process.env.UPTIME_KUMA_PORT || process.env.PORT || args.p
|
||||
const sslKey = process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || args["ssl-key"] || undefined;
|
||||
const sslCert = process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || args["ssl-cert"] || undefined;
|
||||
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
|
||||
const twofa_verification_opts = {
|
||||
@@ -135,6 +136,8 @@ const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sen
|
||||
const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
|
||||
const databaseSocketHandler = require("./socket-handlers/database-socket-handler");
|
||||
const TwoFA = require("./2fa");
|
||||
const StatusPage = require("./model/status_page");
|
||||
const { cloudflaredSocketHandler, autoStart: cloudflaredAutoStart } = require("./socket-handlers/cloudflared-socket-handler");
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
@@ -203,8 +206,8 @@ exports.entryPage = "dashboard";
|
||||
|
||||
// Entry Page
|
||||
app.get("/", async (_request, response) => {
|
||||
if (exports.entryPage === "statusPage") {
|
||||
response.redirect("/status");
|
||||
if (exports.entryPage && exports.entryPage.startsWith("statusPage-")) {
|
||||
response.redirect("/status/" + exports.entryPage.replace("statusPage-", ""));
|
||||
} else {
|
||||
response.redirect("/dashboard");
|
||||
}
|
||||
@@ -307,6 +310,15 @@ exports.entryPage = "dashboard";
|
||||
socket.on("login", async (data, callback) => {
|
||||
console.log("Login");
|
||||
|
||||
// Checking
|
||||
if (typeof callback !== "function") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Login Rate Limit
|
||||
if (! await loginRateLimiter.pass(callback)) {
|
||||
return;
|
||||
@@ -365,14 +377,27 @@ exports.entryPage = "dashboard";
|
||||
});
|
||||
|
||||
socket.on("logout", async (callback) => {
|
||||
// Rate Limit
|
||||
if (! await loginRateLimiter.pass(callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
socket.leave(socket.userID);
|
||||
socket.userID = null;
|
||||
callback();
|
||||
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("prepare2FA", async (callback) => {
|
||||
socket.on("prepare2FA", async (currentPassword, callback) => {
|
||||
try {
|
||||
if (! await twoFaRateLimiter.pass(callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
checkLogin(socket);
|
||||
await doubleCheckPassword(socket, currentPassword);
|
||||
|
||||
let user = await R.findOne("user", " id = ? AND active = 1 ", [
|
||||
socket.userID,
|
||||
@@ -407,14 +432,19 @@ exports.entryPage = "dashboard";
|
||||
} catch (error) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: "Error while trying to prepare 2FA.",
|
||||
msg: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("save2FA", async (callback) => {
|
||||
socket.on("save2FA", async (currentPassword, callback) => {
|
||||
try {
|
||||
if (! await twoFaRateLimiter.pass(callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
checkLogin(socket);
|
||||
await doubleCheckPassword(socket, currentPassword);
|
||||
|
||||
await R.exec("UPDATE `user` SET twofa_status = 1 WHERE id = ? ", [
|
||||
socket.userID,
|
||||
@@ -427,14 +457,19 @@ exports.entryPage = "dashboard";
|
||||
} catch (error) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: "Error while trying to change 2FA.",
|
||||
msg: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("disable2FA", async (callback) => {
|
||||
socket.on("disable2FA", async (currentPassword, callback) => {
|
||||
try {
|
||||
if (! await twoFaRateLimiter.pass(callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
checkLogin(socket);
|
||||
await doubleCheckPassword(socket, currentPassword);
|
||||
await TwoFA.disable2FA(socket.userID);
|
||||
|
||||
callback({
|
||||
@@ -444,36 +479,47 @@ exports.entryPage = "dashboard";
|
||||
} catch (error) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: "Error while trying to change 2FA.",
|
||||
msg: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("verifyToken", async (token, callback) => {
|
||||
let user = await R.findOne("user", " id = ? AND active = 1 ", [
|
||||
socket.userID,
|
||||
]);
|
||||
socket.on("verifyToken", async (token, currentPassword, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
await doubleCheckPassword(socket, currentPassword);
|
||||
|
||||
let verify = notp.totp.verify(token, user.twofa_secret, twofa_verification_opts);
|
||||
let user = await R.findOne("user", " id = ? AND active = 1 ", [
|
||||
socket.userID,
|
||||
]);
|
||||
|
||||
if (user.twofa_last_token !== token && verify) {
|
||||
callback({
|
||||
ok: true,
|
||||
valid: true,
|
||||
});
|
||||
} else {
|
||||
let verify = notp.totp.verify(token, user.twofa_secret, twofa_verification_opts);
|
||||
|
||||
if (user.twofa_last_token !== token && verify) {
|
||||
callback({
|
||||
ok: true,
|
||||
valid: true,
|
||||
});
|
||||
} else {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: "Invalid Token.",
|
||||
valid: false,
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: "Invalid Token.",
|
||||
valid: false,
|
||||
msg: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("twoFAStatus", async (callback) => {
|
||||
checkLogin(socket);
|
||||
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
let user = await R.findOne("user", " id = ? AND active = 1 ", [
|
||||
socket.userID,
|
||||
]);
|
||||
@@ -490,9 +536,10 @@ exports.entryPage = "dashboard";
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
callback({
|
||||
ok: false,
|
||||
msg: "Error while trying to get 2FA status.",
|
||||
msg: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -581,6 +628,9 @@ exports.entryPage = "dashboard";
|
||||
throw new Error("Permission denied.");
|
||||
}
|
||||
|
||||
// Reset Prometheus labels
|
||||
monitorList[monitor.id]?.prometheus()?.remove();
|
||||
|
||||
bean.name = monitor.name;
|
||||
bean.type = monitor.type;
|
||||
bean.url = monitor.url;
|
||||
@@ -939,21 +989,13 @@ exports.entryPage = "dashboard";
|
||||
throw new Error("Password is too weak. It should contain alphabetic and numeric characters. It must be at least 6 characters in length.");
|
||||
}
|
||||
|
||||
let user = await R.findOne("user", " id = ? AND active = 1 ", [
|
||||
socket.userID,
|
||||
]);
|
||||
let user = await doubleCheckPassword(socket, password.currentPassword);
|
||||
await user.resetPassword(password.newPassword);
|
||||
|
||||
if (user && passwordHash.verify(password.currentPassword, user.password)) {
|
||||
|
||||
user.resetPassword(password.newPassword);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Password has been updated successfully.",
|
||||
});
|
||||
} else {
|
||||
throw new Error("Incorrect current password");
|
||||
}
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Password has been updated successfully.",
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
@@ -980,10 +1022,14 @@ exports.entryPage = "dashboard";
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("setSettings", async (data, callback) => {
|
||||
socket.on("setSettings", async (data, currentPassword, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
if (data.disableAuth) {
|
||||
await doubleCheckPassword(socket, currentPassword);
|
||||
}
|
||||
|
||||
await setSettings("general", data);
|
||||
exports.entryPage = data.entryPage;
|
||||
|
||||
@@ -1389,6 +1435,7 @@ exports.entryPage = "dashboard";
|
||||
|
||||
// Status Page Socket Handler for admin only
|
||||
statusPageSocketHandler(socket);
|
||||
cloudflaredSocketHandler(socket);
|
||||
databaseSocketHandler(socket);
|
||||
|
||||
debug("added all socket handlers");
|
||||
@@ -1431,6 +1478,9 @@ exports.entryPage = "dashboard";
|
||||
|
||||
initBackgroundJobs(args);
|
||||
|
||||
// Start cloudflared at the end if configured
|
||||
await cloudflaredAutoStart(cloudflaredToken);
|
||||
|
||||
})();
|
||||
|
||||
async function updateMonitorNotification(monitorID, notificationIDList) {
|
||||
@@ -1475,6 +1525,8 @@ async function afterLogin(socket, user) {
|
||||
|
||||
await sleep(500);
|
||||
|
||||
await StatusPage.sendStatusPageList(io, socket);
|
||||
|
||||
for (let monitorID in monitorList) {
|
||||
await sendHeartbeatList(socket, monitorID);
|
||||
}
|
||||
|
85
server/socket-handlers/cloudflared-socket-handler.js
Normal file
85
server/socket-handlers/cloudflared-socket-handler.js
Normal file
@@ -0,0 +1,85 @@
|
||||
const { checkLogin, setSetting, setting, doubleCheckPassword } = require("../util-server");
|
||||
const { CloudflaredTunnel } = require("node-cloudflared-tunnel");
|
||||
const { io } = require("../server");
|
||||
|
||||
const prefix = "cloudflared_";
|
||||
const cloudflared = new CloudflaredTunnel();
|
||||
|
||||
cloudflared.change = (running, message) => {
|
||||
io.to("cloudflared").emit(prefix + "running", running);
|
||||
io.to("cloudflared").emit(prefix + "message", message);
|
||||
};
|
||||
|
||||
cloudflared.error = (errorMessage) => {
|
||||
io.to("cloudflared").emit(prefix + "errorMessage", errorMessage);
|
||||
};
|
||||
|
||||
module.exports.cloudflaredSocketHandler = (socket) => {
|
||||
|
||||
socket.on(prefix + "join", async () => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
socket.join("cloudflared");
|
||||
io.to(socket.userID).emit(prefix + "installed", cloudflared.checkInstalled());
|
||||
io.to(socket.userID).emit(prefix + "running", cloudflared.running);
|
||||
io.to(socket.userID).emit(prefix + "token", await setting("cloudflaredTunnelToken"));
|
||||
} catch (error) { }
|
||||
});
|
||||
|
||||
socket.on(prefix + "leave", async () => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
socket.leave("cloudflared");
|
||||
} catch (error) { }
|
||||
});
|
||||
|
||||
socket.on(prefix + "start", async (token) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
if (token && typeof token === "string") {
|
||||
await setSetting("cloudflaredTunnelToken", token);
|
||||
cloudflared.token = token;
|
||||
} else {
|
||||
cloudflared.token = null;
|
||||
}
|
||||
cloudflared.start();
|
||||
} catch (error) { }
|
||||
});
|
||||
|
||||
socket.on(prefix + "stop", async (currentPassword, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
await doubleCheckPassword(socket, currentPassword);
|
||||
cloudflared.stop();
|
||||
} catch (error) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on(prefix + "removeToken", async () => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
await setSetting("cloudflaredTunnelToken", "");
|
||||
} catch (error) { }
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
module.exports.autoStart = async (token) => {
|
||||
if (!token) {
|
||||
token = await setting("cloudflaredTunnelToken");
|
||||
} else {
|
||||
// Override the current token via args or env var
|
||||
await setSetting("cloudflaredTunnelToken", token);
|
||||
console.log("Use cloudflared token from args or env var");
|
||||
}
|
||||
|
||||
if (token) {
|
||||
console.log("Start cloudflared");
|
||||
cloudflared.token = token;
|
||||
cloudflared.start();
|
||||
}
|
||||
};
|
@@ -1,25 +1,36 @@
|
||||
const { R } = require("redbean-node");
|
||||
const { checkLogin, setSettings } = require("../util-server");
|
||||
const { checkLogin, setSettings, setSetting } = require("../util-server");
|
||||
const dayjs = require("dayjs");
|
||||
const { debug } = require("../../src/util");
|
||||
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");
|
||||
|
||||
module.exports.statusPageSocketHandler = (socket) => {
|
||||
|
||||
// Post or edit incident
|
||||
socket.on("postIncident", async (incident, callback) => {
|
||||
socket.on("postIncident", async (slug, incident, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
await R.exec("UPDATE incident SET pin = 0 ");
|
||||
let statusPageID = await StatusPage.slugToID(slug);
|
||||
|
||||
if (!statusPageID) {
|
||||
throw new Error("slug is not found");
|
||||
}
|
||||
|
||||
await R.exec("UPDATE incident SET pin = 0 WHERE status_page_id = ? ", [
|
||||
statusPageID
|
||||
]);
|
||||
|
||||
let incidentBean;
|
||||
|
||||
if (incident.id) {
|
||||
incidentBean = await R.findOne("incident", " id = ?", [
|
||||
incident.id
|
||||
incidentBean = await R.findOne("incident", " id = ? AND status_page_id = ? ", [
|
||||
incident.id,
|
||||
statusPageID
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -31,6 +42,7 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||
incidentBean.content = incident.content;
|
||||
incidentBean.style = incident.style;
|
||||
incidentBean.pin = true;
|
||||
incidentBean.status_page_id = statusPageID;
|
||||
|
||||
if (incident.id) {
|
||||
incidentBean.lastUpdatedDate = R.isoDateTime(dayjs.utc());
|
||||
@@ -52,11 +64,15 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("unpinIncident", async (callback) => {
|
||||
socket.on("unpinIncident", async (slug, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
await R.exec("UPDATE incident SET pin = 0 WHERE pin = 1");
|
||||
let statusPageID = await StatusPage.slugToID(slug);
|
||||
|
||||
await R.exec("UPDATE incident SET pin = 0 WHERE pin = 1 AND status_page_id = ? ", [
|
||||
statusPageID
|
||||
]);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
@@ -71,13 +87,23 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||
|
||||
// Save Status Page
|
||||
// imgDataUrl Only Accept PNG!
|
||||
socket.on("saveStatusPage", async (config, imgDataUrl, publicGroupList, callback) => {
|
||||
socket.on("saveStatusPage", async (slug, config, imgDataUrl, publicGroupList, callback) => {
|
||||
|
||||
try {
|
||||
checkLogin(socket);
|
||||
checkSlug(config.slug);
|
||||
|
||||
checkLogin(socket);
|
||||
apicache.clear();
|
||||
|
||||
// Save Config
|
||||
let statusPage = await R.findOne("status_page", " slug = ? ", [
|
||||
slug
|
||||
]);
|
||||
|
||||
if (!statusPage) {
|
||||
throw new Error("No slug?");
|
||||
}
|
||||
|
||||
const header = "data:image/png;base64,";
|
||||
|
||||
// Check logo format
|
||||
@@ -88,16 +114,28 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||
throw new Error("Only allowed PNG logo.");
|
||||
}
|
||||
|
||||
const filename = `logo${statusPage.id}.png`;
|
||||
|
||||
// Convert to file
|
||||
await ImageDataURI.outputFile(imgDataUrl, Database.uploadDir + "logo.png");
|
||||
config.logo = "/upload/logo.png?t=" + Date.now();
|
||||
await ImageDataURI.outputFile(imgDataUrl, Database.uploadDir + filename);
|
||||
config.logo = `/upload/${filename}?t=` + Date.now();
|
||||
|
||||
} else {
|
||||
config.icon = imgDataUrl;
|
||||
}
|
||||
|
||||
// Save Config
|
||||
await setSettings("statusPage", config);
|
||||
statusPage.slug = config.slug;
|
||||
statusPage.title = config.title;
|
||||
statusPage.description = config.description;
|
||||
statusPage.icon = config.logo;
|
||||
statusPage.theme = config.theme;
|
||||
//statusPage.published = ;
|
||||
//statusPage.search_engine_index = ;
|
||||
statusPage.show_tags = config.showTags;
|
||||
//statusPage.password = null;
|
||||
statusPage.modified_date = R.isoDateTime();
|
||||
|
||||
await R.store(statusPage);
|
||||
|
||||
// Save Public Group List
|
||||
const groupIDList = [];
|
||||
@@ -106,13 +144,15 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||
for (let group of publicGroupList) {
|
||||
let groupBean;
|
||||
if (group.id) {
|
||||
groupBean = await R.findOne("group", " id = ? AND public = 1 ", [
|
||||
group.id
|
||||
groupBean = await R.findOne("group", " id = ? AND public = 1 AND status_page_id = ? ", [
|
||||
group.id,
|
||||
statusPage.id
|
||||
]);
|
||||
} else {
|
||||
groupBean = R.dispense("group");
|
||||
}
|
||||
|
||||
groupBean.status_page_id = statusPage.id;
|
||||
groupBean.name = group.name;
|
||||
groupBean.public = true;
|
||||
groupBean.weight = groupOrder++;
|
||||
@@ -124,7 +164,6 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||
]);
|
||||
|
||||
let monitorOrder = 1;
|
||||
console.log(group.monitorList);
|
||||
|
||||
for (let monitor of group.monitorList) {
|
||||
let relationBean = R.dispense("monitor_group");
|
||||
@@ -141,7 +180,18 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||
// Delete groups that not in the list
|
||||
debug("Delete groups that not in the list");
|
||||
const slots = groupIDList.map(() => "?").join(",");
|
||||
await R.exec(`DELETE FROM \`group\` WHERE id NOT IN (${slots})`, groupIDList);
|
||||
|
||||
const data = [
|
||||
...groupIDList,
|
||||
statusPage.id
|
||||
];
|
||||
await R.exec(`DELETE FROM \`group\` WHERE id NOT IN (${slots}) AND status_page_id = ?`, data);
|
||||
|
||||
// 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;
|
||||
await setSetting("entryPage", server.entryPage, "general");
|
||||
}
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
@@ -149,7 +199,7 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
console.error(error);
|
||||
|
||||
callback({
|
||||
ok: false,
|
||||
@@ -158,4 +208,115 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Add a new status page
|
||||
socket.on("addStatusPage", async (title, slug, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
title = title?.trim();
|
||||
slug = slug?.trim();
|
||||
|
||||
// Check empty
|
||||
if (!title || !slug) {
|
||||
throw new Error("Please input all fields");
|
||||
}
|
||||
|
||||
// Make sure slug is string
|
||||
if (typeof slug !== "string") {
|
||||
throw new Error("Slug -Accept string only");
|
||||
}
|
||||
|
||||
// lower case only
|
||||
slug = slug.toLowerCase();
|
||||
|
||||
checkSlug(slug);
|
||||
|
||||
let statusPage = R.dispense("status_page");
|
||||
statusPage.slug = slug;
|
||||
statusPage.title = title;
|
||||
statusPage.theme = "light";
|
||||
statusPage.icon = "";
|
||||
await R.store(statusPage);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "OK!"
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
callback({
|
||||
ok: false,
|
||||
msg: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Delete a status page
|
||||
socket.on("deleteStatusPage", async (slug, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
let statusPageID = await StatusPage.slugToID(slug);
|
||||
|
||||
if (statusPageID) {
|
||||
|
||||
// Reset entry page if it is the default one.
|
||||
if (server.entryPage === "statusPage-" + slug) {
|
||||
server.entryPage = "dashboard";
|
||||
await setSetting("entryPage", server.entryPage, "general");
|
||||
}
|
||||
|
||||
// No need to delete records from `status_page_cname`, because it has cascade foreign key.
|
||||
// But for incident & group, it is hard to add cascade foreign key during migration, so they have to be deleted manually.
|
||||
|
||||
// Delete incident
|
||||
await R.exec("DELETE FROM incident WHERE status_page_id = ? ", [
|
||||
statusPageID
|
||||
]);
|
||||
|
||||
// Delete group
|
||||
await R.exec("DELETE FROM `group` WHERE status_page_id = ? ", [
|
||||
statusPageID
|
||||
]);
|
||||
|
||||
// Delete status_page
|
||||
await R.exec("DELETE FROM status_page WHERE id = ? ", [
|
||||
statusPageID
|
||||
]);
|
||||
|
||||
} else {
|
||||
throw new Error("Status Page is not found");
|
||||
}
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
});
|
||||
} catch (error) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Check slug a-z, 0-9, - only
|
||||
* Regex from: https://stackoverflow.com/questions/22454258/js-regex-string-validation-for-slug
|
||||
*/
|
||||
function checkSlug(slug) {
|
||||
if (typeof slug !== "string") {
|
||||
throw new Error("Slug must be string");
|
||||
}
|
||||
|
||||
slug = slug.trim();
|
||||
|
||||
if (!slug) {
|
||||
throw new Error("Slug cannot be empty");
|
||||
}
|
||||
|
||||
if (!slug.match(/^[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*$/)) {
|
||||
throw new Error("Invalid Slug");
|
||||
}
|
||||
}
|
||||
|
@@ -1,9 +1,8 @@
|
||||
const tcpp = require("tcp-ping");
|
||||
const Ping = require("./ping-lite");
|
||||
const { R } = require("redbean-node");
|
||||
const { debug } = require("../src/util");
|
||||
const { debug, genSecret } = require("../src/util");
|
||||
const passwordHash = require("./password-hash");
|
||||
const dayjs = require("dayjs");
|
||||
const { Resolver } = require("dns");
|
||||
const child_process = require("child_process");
|
||||
const iconv = require("iconv-lite");
|
||||
@@ -32,7 +31,7 @@ exports.initJWTSecret = async () => {
|
||||
jwtSecretBean.key = "jwtSecret";
|
||||
}
|
||||
|
||||
jwtSecretBean.value = passwordHash.generate(dayjs() + "");
|
||||
jwtSecretBean.value = passwordHash.generate(genSecret());
|
||||
await R.store(jwtSecretBean);
|
||||
return jwtSecretBean;
|
||||
};
|
||||
@@ -321,6 +320,28 @@ exports.checkLogin = (socket) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* For logged-in users, double-check the password
|
||||
* @param socket
|
||||
* @param currentPassword
|
||||
* @returns {Promise<Bean>}
|
||||
*/
|
||||
exports.doubleCheckPassword = async (socket, currentPassword) => {
|
||||
if (typeof currentPassword !== "string") {
|
||||
throw new Error("Wrong data type?");
|
||||
}
|
||||
|
||||
let user = await R.findOne("user", " id = ? AND active = 1 ", [
|
||||
socket.userID,
|
||||
]);
|
||||
|
||||
if (!user || !passwordHash.verify(currentPassword, user.password)) {
|
||||
throw new Error("Incorrect current password");
|
||||
}
|
||||
|
||||
return user;
|
||||
};
|
||||
|
||||
exports.startUnitTest = async () => {
|
||||
console.log("Starting unit test...");
|
||||
const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm";
|
||||
|
Reference in New Issue
Block a user