diff --git a/.eslintrc.js b/.eslintrc.js index 2ed0fc26d..da81c8e6d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -104,9 +104,10 @@ module.exports = { "jsdoc/require-returns-type": "off", "jsdoc/require-param-type": "off", "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/ban-ts-comment": "off", "prefer-const": "off", "@typescript-eslint/no-unused-vars": "warn", - "eqeqeq": "off", + eqeqeq: "off", }, }, ], diff --git a/extra/reset-password.js b/extra/reset-password.js index 29aad642f..48a40c699 100644 --- a/extra/reset-password.js +++ b/extra/reset-password.js @@ -4,7 +4,6 @@ const Database = require("../server/database"); const { R } = require("redbean-node"); const readline = require("readline"); const { passwordStrength } = require("check-password-strength"); -const { initJWTSecret } = require("../server/util-server"); const User = require("../server/model/user"); const { io } = require("socket.io-client"); const { localWebSocketURL } = require("../server/config"); @@ -62,8 +61,8 @@ const main = async () => { if (!("dry-run" in args)) { await User.resetPassword(user.id, password); - // Reset all sessions by reset jwt secret - await initJWTSecret(); + // TODO: Reset all sessions by reset jwt secret + // await initJWTSecret(); // Disconnect all other socket clients of the user await disconnectAllSocketClients(user.username, password); diff --git a/server/better-auth.ts b/server/better-auth.ts index 89d4afa3a..52b0945c6 100644 --- a/server/better-auth.ts +++ b/server/better-auth.ts @@ -1,4 +1,5 @@ import { betterAuth } from "better-auth"; +// @ts-ignore import * as Database from "./database.js"; import { genSecret, log } from "../src/util"; import { R } from "redbean-node"; @@ -64,7 +65,20 @@ export function getAuthSecret() { } /** - * + * Get session from cookie + * @param cookie Cookie string + * @returns Session Object + */ +export function getSession(cookie: string) { + const context = { + headers: new Headers(), + }; + context.headers.set("cookie", cookie || ""); + return auth.api.getSession(); +} + +/** + * TODO */ export async function createUser() { await auth.api.signUpEmail({ @@ -76,9 +90,10 @@ export async function createUser() { }); } +// TODO createUser(); /** - * + * TODO */ export async function migrateUser() {} diff --git a/server/model/user.js b/server/model/user.js index ac82e56f9..b019213f1 100644 --- a/server/model/user.js +++ b/server/model/user.js @@ -31,22 +31,6 @@ class User extends BeanModel { this.password = hashedPassword; } - - /** - * Create a new JWT for a user - * @param {User} user The User to create a JsonWebToken for - * @param {string} jwtSecret The key used to sign the JsonWebToken - * @returns {string} the JsonWebToken as a string - */ - static createJWT(user, jwtSecret) { - return jwt.sign( - { - username: user.username, - h: shake256(user.password, SHAKE256_LENGTH), - }, - jwtSecret - ); - } } module.exports = User; diff --git a/server/monitor-types/real-browser-monitor-type.js b/server/monitor-types/real-browser-monitor-type.js index 32fa36f1a..3c654f682 100644 --- a/server/monitor-types/real-browser-monitor-type.js +++ b/server/monitor-types/real-browser-monitor-type.js @@ -274,6 +274,7 @@ class RealBrowserMonitorType extends MonitorType { await page.waitForTimeout(monitor.screenshot_delay); } + // TODO fix without jwtSecret let filename = jwt.sign(monitor.id, server.jwtSecret) + ".png"; await page.screenshot({ diff --git a/server/server.js b/server/server.js index 467a32036..67ea79e3e 100644 --- a/server/server.js +++ b/server/server.js @@ -105,7 +105,6 @@ const { getSettings, setSettings, setting, - initJWTSecret, checkLogin, doubleCheckPassword, shake256, @@ -227,7 +226,7 @@ let needSetup = false; } // Init Better Auth - const { auth } = await import("./better-auth"); + const { auth, getSession } = await import("./better-auth"); // Database should be ready now await server.initAfterDatabaseReady(); @@ -377,59 +376,14 @@ let needSetup = false; socket.emit("setup"); } + // Auth Session + const session = await getSession(socket.request.headers.cookie); + // *************************** // Public Socket API // *************************** - socket.on("loginByToken", async (token, callback) => { - const clientIP = await server.getClientIP(socket); - - log.info("auth", `Login by token. IP=${clientIP}`); - - try { - let decoded = jwt.verify(token, server.jwtSecret); - - log.info("auth", "Username from JWT: " + decoded.username); - - let user = await R.findOne("user", " username = ? AND active = 1 ", [decoded.username]); - - if (user) { - // Check if the password changed - if (decoded.h !== shake256(user.password, SHAKE256_LENGTH)) { - throw new Error("The token is invalid due to password change or old token"); - } - - log.debug("auth", "afterLogin"); - await afterLogin(socket, user); - log.debug("auth", "afterLogin ok"); - - log.info("auth", `Successfully logged in user ${decoded.username}. IP=${clientIP}`); - - callback({ - ok: true, - }); - } else { - log.info("auth", `Inactive or deleted user ${decoded.username}. IP=${clientIP}`); - - callback({ - ok: false, - msg: "authUserInactiveOrDeleted", - msgi18n: true, - }); - } - } catch (error) { - log.error("auth", `Invalid token. IP=${clientIP}`); - if (error.message) { - log.error("auth", error.message, `IP=${clientIP}`); - } - callback({ - ok: false, - msg: "authInvalidToken", - msgi18n: true, - }); - } - }); - + // TODO: better-auth socket.on("login", async (data, callback) => { const clientIP = await server.getClientIP(socket); @@ -1412,6 +1366,7 @@ let needSetup = false; } }); + // TODO: better-auth socket.on("changePassword", async (password, callback) => { try { checkLogin(socket); @@ -1838,24 +1793,6 @@ async function initDatabase(testMode = false) { // Patch the database await Database.patch(port, hostname); - - let jwtSecretBean = await R.findOne("setting", " `key` = ? ", ["jwtSecret"]); - - if (!jwtSecretBean) { - log.info("server", "JWT secret is not found, generate one."); - jwtSecretBean = await initJWTSecret(); - log.info("server", "Stored JWT secret into database"); - } else { - log.debug("server", "Load JWT secret from database."); - } - - // If there is no record in user table, it is a new Uptime Kuma instance, need to setup - if ((await R.knex("user").count("id as count").first()).count === 0) { - log.info("server", "No user, need setup"); - needSetup = true; - } - - server.jwtSecret = jwtSecretBean.value; } /** diff --git a/server/uptime-kuma-server.js b/server/uptime-kuma-server.js index 030ee0787..a47e315eb 100644 --- a/server/uptime-kuma-server.js +++ b/server/uptime-kuma-server.js @@ -4,7 +4,7 @@ const fs = require("fs"); const http = require("http"); const { Server } = require("socket.io"); const { R } = require("redbean-node"); -const { log, isDev } = require("../src/util"); +const { log, isDev, devOriginList } = require("../src/util"); const Database = require("./database"); const util = require("util"); const { Settings } = require("./settings"); @@ -54,12 +54,6 @@ class UptimeKumaServer { */ static monitorTypeList = {}; - /** - * Use for decode the auth object - * @type {null} - */ - jwtSecret = null; - /** * Get the current instance of the server if it exists, otherwise * create a new instance. @@ -135,12 +129,15 @@ class UptimeKumaServer { let cors = undefined; if (isDev) { cors = { - origin: "*", + origin: devOriginList, + credentials: true, + methods: ["GET", "POST"], }; } this.io = new Server(this.httpServer, { cors, + cookie: true, allowRequest: async (req, callback) => { let transport; // It should be always true, but just in case, because this property is not documented diff --git a/server/util-server.js b/server/util-server.js index a22d4d09e..0c697fda3 100644 --- a/server/util-server.js +++ b/server/util-server.js @@ -34,31 +34,6 @@ const { Kafka, SASLOptions } = require("kafkajs"); const crypto = require("crypto"); const isWindows = process.platform === /^win/.test(process.platform); -/** - * Init or reset JWT secret - * @returns {Promise} JWT secret - */ -exports.initJWTSecret = async () => { - let jwtSecretBean = await R.findOne("setting", " `key` = ? ", ["jwtSecret"]); - - if (!jwtSecretBean) { - jwtSecretBean = R.dispense("setting"); - jwtSecretBean.key = "jwtSecret"; - } - - jwtSecretBean.value = await passwordHash.generate(genSecret()); - await R.store(jwtSecretBean); - return jwtSecretBean; -}; - -/** - * Decodes a jwt and returns the payload portion without verifying the jwt. - * @param {string} jwt The input jwt as a string - * @returns {object} Decoded jwt payload object - */ -exports.decodeJwt = (jwt) => { - return JSON.parse(Buffer.from(jwt.split(".")[1], "base64").toString()); -}; /** * Gets an Access Token from an oidc/oauth2 provider diff --git a/src/mixins/socket.js b/src/mixins/socket.js index 00574af3b..ccd2c9cf4 100644 --- a/src/mixins/socket.js +++ b/src/mixins/socket.js @@ -117,7 +117,9 @@ export default { url = undefined; } - socket = io(url); + socket = io(url, { + withCredentials: true, + }); socket.on("info", (info) => { this.info = info; diff --git a/src/util.ts b/src/util.ts index bd6f062f5..b54e1348a 100644 --- a/src/util.ts +++ b/src/util.ts @@ -28,6 +28,13 @@ export const isNode = typeof process !== "undefined" && process?.versions?.node; */ const dayjs = isNode ? require("dayjs") : dayjsFrontend; +export const devOriginList = [ + "http://127.0.0.1:3000", + "http://127.0.0.1:3001", + "http://localhost:3000", + "http://localhost:3001", +]; + export const appName = "Uptime Kuma"; export const DOWN = 0; export const UP = 1;