thelounge/src/clientManager.js

241 lines
5.8 KiB
JavaScript
Raw Normal View History

"use strict";
2017-10-15 18:05:19 +02:00
const _ = require("lodash");
2018-06-15 22:31:06 +02:00
const log = require("./log");
2018-03-02 19:28:54 +01:00
const colors = require("chalk");
2017-10-15 18:05:19 +02:00
const fs = require("fs");
const path = require("path");
const Client = require("./client");
const Helper = require("./helper");
2017-07-10 21:47:03 +02:00
const WebPush = require("./plugins/webpush");
2014-08-14 01:43:11 +02:00
module.exports = ClientManager;
function ClientManager() {
this.clients = [];
}
ClientManager.prototype.init = function(identHandler, sockets) {
this.sockets = sockets;
this.identHandler = identHandler;
2017-07-10 21:47:03 +02:00
this.webPush = new WebPush();
if (!Helper.config.public) {
this.loadUsers();
// LDAP does not have user commands, and users are dynamically
// created upon logon, so we don't need to watch for new files
if (!Helper.config.ldap.enable) {
this.autoloadUsers();
}
2016-04-26 22:41:08 +02:00
}
};
2014-08-14 01:43:11 +02:00
ClientManager.prototype.findClient = function(name) {
return this.clients.find((u) => u.name === name);
};
ClientManager.prototype.loadUsers = function() {
const users = this.getUsers();
if (users.length === 0) {
log.info(
`There are currently no users. Create one with ${colors.bold("thelounge add <name>")}.`
);
}
users.forEach((name) => this.loadUser(name));
};
ClientManager.prototype.autoloadUsers = function() {
2019-07-17 11:33:59 +02:00
fs.watch(
Helper.getUsersPath(),
_.debounce(
() => {
const loaded = this.clients.map((c) => c.name);
const updatedUsers = this.getUsers();
if (updatedUsers.length === 0) {
log.info(
`There are currently no users. Create one with ${colors.bold(
"thelounge add <name>"
)}.`
);
2019-07-17 11:33:59 +02:00
}
// 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}
)
);
2014-08-14 03:51:54 +02:00
};
ClientManager.prototype.loadUser = function(name) {
const userConfig = readUserConfig(name);
2017-10-15 18:05:19 +02:00
if (!userConfig) {
2014-08-14 03:51:54 +02:00
return;
}
2017-10-15 18:05:19 +02:00
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);
2014-09-25 00:23:54 +02:00
}
2017-10-15 18:05:19 +02:00
return client;
2014-08-14 01:43:11 +02:00
};
ClientManager.prototype.getUsers = function() {
2017-10-15 18:05:19 +02:00
return fs
.readdirSync(Helper.getUsersPath())
2017-10-15 18:05:19 +02:00
.filter((file) => file.endsWith(".json"))
.map((file) => file.slice(0, -5));
2014-08-14 01:43:11 +02:00
};
ClientManager.prototype.addUser = function(name, password, enableLog) {
2017-10-15 18:05:19 +02:00
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.`);
2014-08-14 19:25:22 +02:00
return false;
2014-08-14 01:43:11 +02:00
}
2017-10-15 18:05:19 +02:00
const user = {
password: password || "",
2018-02-24 02:07:08 +01:00
log: enableLog,
2017-10-15 18:05:19 +02:00
awayMessage: "",
networks: [],
sessions: {},
Offer optional syncing of client settings Write synced settings to localstorage. move settings and webpush init to init.js stub for server sending clientsettings get very basic setting sync working Also update client.config.clientSettings on settings:set Full setting sync with mandatory and excluded sync options Actually check client preferences. Further settings restructuring. Refactor options.js make storage act in a sane manner. Add new parameter to applySetting Do not sync if the setting is stored as a result of syncing General clean up, commenting and restructing. sync from server on checking "sync" offer initial sync Better deal with DOM being ready and instances of inital sync showing Don't try to disable autocompletion when not enabled. Restructure option.js to seperate functions from settings. More consistency in naming options vs settings Switch processSetting and applySetting names reflecting their functionality better. move options init back to configuration. simplify how settings are synced around. move options init after template building. Remove unneeded hasOwnProperty Use global for #theme and only apply theme in applySetting Return when no server side clientsettings excist. Autocompletion options to options.settings Make nocss param in url work again. Actually filter out empty highlight values. Clarify alwaysSync comment. Remove manual step for initial sync change attr to prop in options.js replace unbind with off in autocompletion.js Do not sync settings when the lounge is set to public. fix eslint error Fix merge error Do not show sync warning after page refresh when sync is enabled Move setting sync label in actual label. Improve server setting sync handling performance and failure potential. Don't give impression that the desktop notificiation is off when the browser permission is denied. Refine showing and hiding of notification warnings. rename all setting socket events to singular setting. add experimental note and icon to settingsync. fix css linting error
2017-12-11 20:01:15 +01:00
clientSettings: {},
browser: {},
2017-10-15 18:05:19 +02:00
};
try {
fs.writeFileSync(userPath, JSON.stringify(user, null, "\t"));
2015-10-01 00:39:57 +02:00
} catch (e) {
2017-10-15 18:05:19 +02:00
log.error(`Failed to create user ${colors.green(name)} (${e})`);
2014-08-14 01:43:11 +02:00
throw e;
}
2017-10-15 18:05:19 +02:00
try {
const userFolderStat = fs.statSync(Helper.getUsersPath());
const userFileStat = fs.statSync(userPath);
if (
userFolderStat &&
userFileStat &&
(userFolderStat.uid !== userFileStat.uid || userFolderStat.gid !== userFileStat.gid)
) {
log.warn(
`User ${colors.green(
name
)} has been created, but with a different uid (or gid) than expected.`
);
log.warn(
"The file owner has been changed to the expected user. " +
"To prevent any issues, please run thelounge commands " +
"as the correct user that owns the config folder."
);
log.warn(
"See https://thelounge.chat/docs/usage#using-the-correct-system-user for more information."
);
fs.chownSync(userPath, userFolderStat.uid, userFolderStat.gid);
}
} catch (e) {
// We're simply verifying file owner as a safe guard for users
// that run `thelounge add` as root, so we don't care if it fails
}
2014-08-14 19:25:22 +02:00
return true;
2014-08-14 01:43:11 +02:00
};
ClientManager.prototype.updateUser = function(name, opts, callback) {
2017-10-15 18:05:19 +02:00
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);
}
}
};
2017-10-15 18:05:19 +02:00
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;
}
2017-10-15 18:05:19 +02:00
fs.unlinkSync(userPath);
return true;
};
2017-10-15 18:05:19 +02:00
function readUserConfig(name) {
const userPath = Helper.getUserConfigPath(name);
if (!fs.existsSync(userPath)) {
log.error(`Tried to read non-existing user ${colors.green(name)}`);
2014-08-14 19:25:22 +02:00
return false;
2014-08-14 01:43:11 +02:00
}
2017-10-15 18:05:19 +02:00
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;
2017-10-15 18:05:19 +02:00
}