"use strict"; const _ = require("lodash"); const log = require("./log"); const colors = require("chalk"); const fs = require("fs"); const path = require("path"); const Client = require("./client"); const Helper = require("./helper"); const WebPush = require("./plugins/webpush"); module.exports = ClientManager; function ClientManager() { this.clients = []; } ClientManager.prototype.init = function(identHandler, sockets) { this.sockets = sockets; this.identHandler = identHandler; this.webPush = new WebPush(); if (!Helper.config.public && !Helper.config.ldap.enable) { this.autoloadUsers(); } }; ClientManager.prototype.findClient = function(name) { return this.clients.find((u) => u.name === name); }; ClientManager.prototype.autoloadUsers = function() { const users = this.getUsers(); const noUsersWarning = `There are currently no users. Create one with ${colors.bold("thelounge add ")}.`; if (users.length === 0) { log.info(noUsersWarning); } users.forEach((name) => this.loadUser(name)); fs.watch(Helper.getUsersPath(), _.debounce(() => { const loaded = this.clients.map((c) => c.name); const updatedUsers = this.getUsers(); if (updatedUsers.length === 0) { log.info(noUsersWarning); } // Reload all users. Existing users will only have their passwords reloaded. updatedUsers.forEach((name) => this.loadUser(name)); // Existing users removed since last time users were loaded _.difference(loaded, updatedUsers).forEach((name) => { const client = _.find(this.clients, {name}); if (client) { client.quit(true); this.clients = _.without(this.clients, client); log.info(`User ${colors.bold(name)} disconnected and removed.`); } }); }, 1000, {maxWait: 10000})); }; ClientManager.prototype.loadUser = function(name) { const userConfig = readUserConfig(name); if (!userConfig) { return; } let client = this.findClient(name); if (client) { if (userConfig.password !== client.config.password) { /** * If we happen to reload an existing client, make super duper sure we * have their latest password. We're not replacing the entire config * object, because that could have undesired consequences. * * @see https://github.com/thelounge/thelounge/issues/598 */ client.config.password = userConfig.password; log.info(`Password for user ${colors.bold(name)} was reset.`); } } else { client = new Client(this, name, userConfig); this.clients.push(client); } return client; }; ClientManager.prototype.getUsers = function() { return fs .readdirSync(Helper.getUsersPath()) .filter((file) => file.endsWith(".json")) .map((file) => file.slice(0, -5)); }; ClientManager.prototype.addUser = function(name, password, enableLog) { if (path.basename(name) !== name) { throw new Error(`${name} is an invalid username.`); } const userPath = Helper.getUserConfigPath(name); if (fs.existsSync(userPath)) { log.error(`User ${colors.green(name)} already exists.`); return false; } const user = { password: password || "", log: enableLog, awayMessage: "", networks: [], sessions: {}, clientSettings: {}, browser: {}, }; try { fs.writeFileSync(userPath, JSON.stringify(user, null, "\t")); } catch (e) { log.error(`Failed to create user ${colors.green(name)} (${e})`); throw e; } return true; }; ClientManager.prototype.updateUser = function(name, opts, callback) { const user = readUserConfig(name); if (!user) { return callback ? callback(true) : false; } const currentUser = JSON.stringify(user, null, "\t"); _.assign(user, opts); const newUser = JSON.stringify(user, null, "\t"); // Do not touch the disk if object has not changed if (currentUser === newUser) { return callback ? callback() : true; } try { fs.writeFileSync(Helper.getUserConfigPath(name), newUser); return callback ? callback() : true; } catch (e) { log.error(`Failed to update user ${colors.green(name)} (${e})`); if (callback) { callback(e); } } }; ClientManager.prototype.removeUser = function(name) { const userPath = Helper.getUserConfigPath(name); if (!fs.existsSync(userPath)) { log.error(`Tried to remove non-existing user ${colors.green(name)}.`); return false; } fs.unlinkSync(userPath); return true; }; function readUserConfig(name) { const userPath = Helper.getUserConfigPath(name); if (!fs.existsSync(userPath)) { log.error(`Tried to read non-existing user ${colors.green(name)}`); return false; } try { const data = fs.readFileSync(userPath, "utf-8"); return JSON.parse(data); } catch (e) { log.error(`Failed to read user ${colors.bold(name)}: ${e}`); } return false; }