diff --git a/client/css/style.css b/client/css/style.css
index aaed0a37..807aae85 100644
--- a/client/css/style.css
+++ b/client/css/style.css
@@ -252,6 +252,14 @@ kbd {
color: #7f8c8d;
}
+.session-list strong {
+ display: block;
+}
+
+.session-list p {
+ margin-bottom: 10px;
+}
+
#chat .invite .from::before {
content: "\f003"; /* http://fontawesome.io/icon/envelope-o/ */
color: #2ecc40;
@@ -668,6 +676,7 @@ kbd {
width: 100%;
}
+#windows p,
#windows label,
#settings .error {
font-size: 14px;
diff --git a/client/index.html b/client/index.html
index 5b170e63..26f4c4f4 100644
--- a/client/index.html
+++ b/client/index.html
@@ -198,15 +198,14 @@
-
+
+
Settings
+
-
-
Settings
-
Messages
@@ -380,6 +379,18 @@
+
+ {{#unless public}}
+
+
Sessions
+
+
Current session
+
+
+
Other sessions
+
+
+ {{/unless}}
diff --git a/client/js/lounge.js b/client/js/lounge.js
index 009871bb..9daad56b 100644
--- a/client/js/lounge.js
+++ b/client/js/lounge.js
@@ -383,8 +383,9 @@ $(function() {
}
document.title = title;
+ const type = chan.data("type");
var placeholder = "";
- if (chan.data("type") === "channel" || chan.data("type") === "query") {
+ if (type === "channel" || type === "query") {
placeholder = `Write to ${chan.data("title")}`;
}
input.attr("placeholder", placeholder);
@@ -404,6 +405,11 @@ $(function() {
socket.emit("names", {target: self.data("id")});
}
+ if (type === "settings") {
+ $("#session-list").html("Loading…
");
+ socket.emit("sessions:get");
+ }
+
focus();
});
diff --git a/client/js/socket-events/index.js b/client/js/socket-events/index.js
index 23a5e961..b0064665 100644
--- a/client/js/socket-events/index.js
+++ b/client/js/socket-events/index.js
@@ -17,3 +17,4 @@ require("./sync_sort");
require("./topic");
require("./users");
require("./sign_out");
+require("./sessions_list");
diff --git a/client/js/socket-events/sessions_list.js b/client/js/socket-events/sessions_list.js
new file mode 100644
index 00000000..9d77df46
--- /dev/null
+++ b/client/js/socket-events/sessions_list.js
@@ -0,0 +1,31 @@
+"use strict";
+
+const $ = require("jquery");
+const socket = require("../socket");
+const templates = require("../../views");
+
+socket.on("sessions:list", function(data) {
+ data.sort((a, b) => b.lastUse - a.lastUse);
+
+ let html = "";
+ data.forEach((connection) => {
+ if (connection.current) {
+ $("#session-current").html(templates.session(connection));
+ return;
+ }
+
+ html += templates.session(connection);
+ });
+
+ if (html.length === 0) {
+ html = "You are not currently logged in to any other device.
";
+ }
+
+ $("#session-list").html(html);
+});
+
+$("#settings").on("click", ".remove-session", function() {
+ socket.emit("sign-out", $(this).data("token"));
+
+ return false;
+});
diff --git a/client/views/index.js b/client/views/index.js
index 50f6a93f..118fa60c 100644
--- a/client/views/index.js
+++ b/client/views/index.js
@@ -34,6 +34,7 @@ module.exports = {
msg_unhandled: require("./msg_unhandled.tpl"),
network: require("./network.tpl"),
image_viewer: require("./image_viewer.tpl"),
+ session: require("./session.tpl"),
unread_marker: require("./unread_marker.tpl"),
user: require("./user.tpl"),
user_filtered: require("./user_filtered.tpl"),
diff --git a/client/views/session.tpl b/client/views/session.tpl
new file mode 100644
index 00000000..d3d5cbd8
--- /dev/null
+++ b/client/views/session.tpl
@@ -0,0 +1,17 @@
+
+{{#if current}}
+ {{agent}}
+ {{ip}}
+{{else}}
+
+
+ {{agent}}
+ {{ip}}
+
+ {{#if active}}
+ Currently active
+ {{else}}
+ Last used on
+ {{/if}}
+{{/if}}
+
diff --git a/src/client.js b/src/client.js
index d4cd3ccc..a70b9472 100644
--- a/src/client.js
+++ b/src/client.js
@@ -304,11 +304,15 @@ Client.prototype.updateSession = function(token, ip, request) {
friendlyAgent += ` on ${agent.os.name} ${agent.os.version}`;
}
- client.config.sessions[token] = _.assign({
+ client.config.sessions[token] = _.assign(client.config.sessions[token], {
lastUse: Date.now(),
ip: ip,
agent: friendlyAgent,
- }, client.config.sessions[token]);
+ });
+
+ client.manager.updateUser(client.name, {
+ sessions: client.config.sessions
+ });
};
Client.prototype.setPassword = function(hash, callback) {
@@ -318,7 +322,6 @@ Client.prototype.setPassword = function(hash, callback) {
password: hash
}, function(err) {
if (err) {
- log.error("Failed to update password of", client.name, err);
return callback(false);
}
diff --git a/src/clientManager.js b/src/clientManager.js
index f8425182..da7f663d 100644
--- a/src/clientManager.js
+++ b/src/clientManager.js
@@ -153,7 +153,7 @@ ClientManager.prototype.updateUser = function(name, opts, callback) {
fs.writeFile(Helper.getUserConfigPath(name), newUser, (err) => {
if (err) {
- log.error("Failed to update user", err);
+ log.error(`Failed to update user ${colors.green(name)} (${err})`);
}
if (callback) {
diff --git a/src/server.js b/src/server.js
index e43cc70b..de8c90e5 100644
--- a/src/server.js
+++ b/src/server.js
@@ -237,7 +237,6 @@ function initializeClient(socket, client, token, lastMessage) {
socket.on("disconnect", function() {
client.clientDetach(socket.id);
});
- client.clientAttach(socket.id, token);
socket.on(
"input",
@@ -378,23 +377,59 @@ function initializeClient(socket, client, token, lastMessage) {
client.unregisterPushSubscription(token);
});
- socket.on("sign-out", () => {
- delete client.config.sessions[token];
+ const sendSessionList = () => {
+ const sessions = _.map(client.config.sessions, (session, sessionToken) => ({
+ current: sessionToken === token,
+ active: _.find(client.attachedClients, (u) => u.token === sessionToken) !== undefined,
+ lastUse: session.lastUse,
+ ip: session.ip,
+ agent: session.agent,
+ token: sessionToken, // TODO: Ideally don't expose actual tokens to the client
+ }));
+
+ socket.emit("sessions:list", sessions);
+ };
+
+ socket.on("sessions:get", sendSessionList);
+
+ socket.on("sign-out", (tokenToSignOut) => {
+ // If no token provided, sign same client out
+ if (!tokenToSignOut) {
+ tokenToSignOut = token;
+ }
+
+ if (!(tokenToSignOut in client.config.sessions)) {
+ return;
+ }
+
+ delete client.config.sessions[tokenToSignOut];
client.manager.updateUser(client.name, {
sessions: client.config.sessions
- }, (err) => {
- if (err) {
- log.error("Failed to update sessions for", client.name, err);
- }
});
- socket.emit("sign-out");
+ _.map(client.attachedClients, (attachedClient, socketId) => {
+ if (attachedClient.token !== tokenToSignOut) {
+ return;
+ }
+
+ const socketToRemove = manager.sockets.of("/").connected[socketId];
+
+ socketToRemove.emit("sign-out");
+ socketToRemove.disconnect();
+ });
+
+ // Do not send updated session list if user simply logs out
+ if (tokenToSignOut !== token) {
+ sendSessionList();
+ }
});
socket.join(client.id);
const sendInitEvent = (tokenToSend) => {
+ client.clientAttach(socket.id, token);
+
let networks = client.networks;
if (lastMessage > -1) {
@@ -423,14 +458,6 @@ function initializeClient(socket, client, token, lastMessage) {
client.updateSession(token, getClientIp(socket.request), socket.request);
- client.manager.updateUser(client.name, {
- sessions: client.config.sessions
- }, (err) => {
- if (err) {
- log.error("Failed to update sessions for", client.name, err);
- }
- });
-
sendInitEvent(token);
});
} else {