123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307 |
- import { SocketHandler } from "../socket-handler.js";
- import { DockgeServer } from "../dockge-server";
- import { log } from "../log";
- import { R } from "redbean-node";
- import { loginRateLimiter, twoFaRateLimiter } from "../rate-limiter";
- import { generatePasswordHash, needRehashPassword, shake256, SHAKE256_LENGTH, verifyPassword } from "../password-hash";
- import { User } from "../models/user";
- import { checkLogin, DockgeSocket, doubleCheckPassword, JWTDecoded } from "../util-server";
- import { passwordStrength } from "check-password-strength";
- import jwt from "jsonwebtoken";
- import { Settings } from "../settings";
- export class MainSocketHandler extends SocketHandler {
- create(socket : DockgeSocket, server : DockgeServer) {
- // ***************************
- // Public Socket API
- // ***************************
- // Setup
- socket.on("setup", async (username, password, callback) => {
- try {
- if (passwordStrength(password).value === "Too weak") {
- throw new Error("Password is too weak. It should contain alphabetic and numeric characters. It must be at least 6 characters in length.");
- }
- if ((await R.knex("user").count("id as count").first()).count !== 0) {
- throw new Error("Dockge has been initialized. If you want to run setup again, please delete the database.");
- }
- const user = R.dispense("user");
- user.username = username;
- user.password = generatePasswordHash(password);
- await R.store(user);
- server.needSetup = false;
- callback({
- ok: true,
- msg: "successAdded",
- msgi18n: true,
- });
- } catch (e) {
- if (e instanceof Error) {
- callback({
- ok: false,
- msg: e.message,
- });
- }
- }
- });
- // Login by token
- socket.on("loginByToken", async (token, callback) => {
- const clientIP = await server.getClientIP(socket);
- log.info("auth", `Login by token. IP=${clientIP}`);
- try {
- const decoded = jwt.verify(token, server.jwtSecret) as JWTDecoded;
- log.info("auth", "Username from JWT: " + decoded.username);
- const user = await R.findOne("user", " username = ? AND active = 1 ", [
- decoded.username,
- ]) as User;
- 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 server.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) {
- if (!(error instanceof Error)) {
- console.error("Unknown error:", error);
- return;
- }
- log.error("auth", `Invalid token. IP=${clientIP}`);
- if (error.message) {
- log.error("auth", error.message + ` IP=${clientIP}`);
- }
- callback({
- ok: false,
- msg: "authInvalidToken",
- msgi18n: true,
- });
- }
- });
- // Login
- socket.on("login", async (data, callback) => {
- const clientIP = await server.getClientIP(socket);
- log.info("auth", `Login by username + password. IP=${clientIP}`);
- // Checking
- if (typeof callback !== "function") {
- return;
- }
- if (!data) {
- return;
- }
- // Login Rate Limit
- if (!await loginRateLimiter.pass(callback)) {
- log.info("auth", `Too many failed requests for user ${data.username}. IP=${clientIP}`);
- return;
- }
- const user = await this.login(data.username, data.password);
- if (user) {
- if (user.twofa_status === 0) {
- server.afterLogin(socket, user);
- log.info("auth", `Successfully logged in user ${data.username}. IP=${clientIP}`);
- callback({
- ok: true,
- token: User.createJWT(user, server.jwtSecret),
- });
- }
- if (user.twofa_status === 1 && !data.token) {
- log.info("auth", `2FA token required for user ${data.username}. IP=${clientIP}`);
- callback({
- tokenRequired: true,
- });
- }
- if (data.token) {
- // @ts-ignore
- const verify = notp.totp.verify(data.token, user.twofa_secret, twoFAVerifyOptions);
- if (user.twofa_last_token !== data.token && verify) {
- server.afterLogin(socket, user);
- await R.exec("UPDATE `user` SET twofa_last_token = ? WHERE id = ? ", [
- data.token,
- socket.userID,
- ]);
- log.info("auth", `Successfully logged in user ${data.username}. IP=${clientIP}`);
- callback({
- ok: true,
- token: User.createJWT(user, server.jwtSecret),
- });
- } else {
- log.warn("auth", `Invalid token provided for user ${data.username}. IP=${clientIP}`);
- callback({
- ok: false,
- msg: "authInvalidToken",
- msgi18n: true,
- });
- }
- }
- } else {
- log.warn("auth", `Incorrect username or password for user ${data.username}. IP=${clientIP}`);
- callback({
- ok: false,
- msg: "authIncorrectCreds",
- msgi18n: true,
- });
- }
- });
- // Change Password
- socket.on("changePassword", async (password, callback) => {
- try {
- checkLogin(socket);
- if (! password.newPassword) {
- throw new Error("Invalid new password");
- }
- if (passwordStrength(password.newPassword).value === "Too weak") {
- 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 doubleCheckPassword(socket, password.currentPassword);
- await user.resetPassword(password.newPassword);
- callback({
- ok: true,
- msg: "Password has been updated successfully.",
- });
- } catch (e) {
- if (e instanceof Error) {
- callback({
- ok: false,
- msg: e.message,
- });
- }
- }
- });
- socket.on("getSettings", async (callback) => {
- try {
- checkLogin(socket);
- const data = await Settings.getSettings("general");
- callback({
- ok: true,
- data: data,
- });
- } catch (e) {
- if (e instanceof Error) {
- callback({
- ok: false,
- msg: e.message,
- });
- }
- }
- });
- socket.on("setSettings", async (data, currentPassword, callback) => {
- try {
- checkLogin(socket);
- // If currently is disabled auth, don't need to check
- // Disabled Auth + Want to Disable Auth => No Check
- // Disabled Auth + Want to Enable Auth => No Check
- // Enabled Auth + Want to Disable Auth => Check!!
- // Enabled Auth + Want to Enable Auth => No Check
- const currentDisabledAuth = await Settings.get("disableAuth");
- if (!currentDisabledAuth && data.disableAuth) {
- await doubleCheckPassword(socket, currentPassword);
- }
- console.log(data);
- await Settings.setSettings("general", data);
- callback({
- ok: true,
- msg: "Saved"
- });
- server.sendInfo(socket);
- } catch (e) {
- if (e instanceof Error) {
- callback({
- ok: false,
- msg: e.message,
- });
- }
- }
- });
- }
- async login(username : string, password : string) : Promise<User | null> {
- if (typeof username !== "string" || typeof password !== "string") {
- return null;
- }
- const user = await R.findOne("user", " username = ? AND active = 1 ", [
- username,
- ]) as User;
- if (user && verifyPassword(password, user.password)) {
- // Upgrade the hash to bcrypt
- if (needRehashPassword(user.password)) {
- await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
- generatePasswordHash(password),
- user.id,
- ]);
- }
- return user;
- }
- return null;
- }
- }
|