nodebb-plugin-session-sharing 4.7.2 → 5.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/controllers.js +30 -28
- package/library.js +321 -378
- package/package.json +5 -3
- package/plugin.json +3 -2
- package/static/lib/admin.js +30 -28
- package/static/templates/admin/plugins/session-sharing.tpl +30 -1
- package/test/index.js +183 -0
package/library.js
CHANGED
|
@@ -1,26 +1,24 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
var groups = require.main.require('./src/groups');
|
|
6
|
-
var SocketPlugins = require.main.require('./src/socket.io/plugins');
|
|
3
|
+
const winston = module.parent.require('winston');
|
|
4
|
+
const nconf = module.parent.require('nconf');
|
|
7
5
|
|
|
8
|
-
|
|
9
|
-
var nconf = module.parent.require('nconf');
|
|
6
|
+
const _ = require('lodash');
|
|
10
7
|
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
const meta = require.main.require('./src/meta');
|
|
9
|
+
const user = require.main.require('./src/user');
|
|
10
|
+
const groups = require.main.require('./src/groups');
|
|
11
|
+
const SocketPlugins = require.main.require('./src/socket.io/plugins');
|
|
12
|
+
const db = require.main.require('./src/database');
|
|
13
|
+
const plugins = require.main.require('./src/plugins');
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
var plugins = require.main.require('./src/plugins');
|
|
15
|
+
const jwt = require('jsonwebtoken');
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
var controllers = require('./lib/controllers');
|
|
20
|
-
var nbbAuthController = require.main.require('./src/controllers/authentication');
|
|
17
|
+
const controllers = require('./lib/controllers');
|
|
18
|
+
const nbbAuthController = require.main.require('./src/controllers/authentication');
|
|
21
19
|
|
|
22
20
|
/* all the user profile fields that can be passed to user.updateProfile */
|
|
23
|
-
|
|
21
|
+
const profileFields = [
|
|
24
22
|
'username',
|
|
25
23
|
'email',
|
|
26
24
|
'fullname',
|
|
@@ -31,7 +29,7 @@ var profileFields = [
|
|
|
31
29
|
'signature',
|
|
32
30
|
'aboutme',
|
|
33
31
|
];
|
|
34
|
-
|
|
32
|
+
const payloadKeys = profileFields.concat([
|
|
35
33
|
'id', // the uniq identifier of that account
|
|
36
34
|
'firstName', // for backwards compatibillity
|
|
37
35
|
'lastName', // dto.
|
|
@@ -39,7 +37,7 @@ var payloadKeys = profileFields.concat([
|
|
|
39
37
|
'groups',
|
|
40
38
|
]);
|
|
41
39
|
|
|
42
|
-
|
|
40
|
+
const plugin = {
|
|
43
41
|
ready: false,
|
|
44
42
|
settings: {
|
|
45
43
|
name: 'appId',
|
|
@@ -50,6 +48,7 @@ var plugin = {
|
|
|
50
48
|
adminRevalidate: 'off',
|
|
51
49
|
noRegistration: 'off',
|
|
52
50
|
payloadParent: undefined,
|
|
51
|
+
allowBannedUsers: false,
|
|
53
52
|
},
|
|
54
53
|
};
|
|
55
54
|
|
|
@@ -57,7 +56,7 @@ payloadKeys.forEach(function (key) {
|
|
|
57
56
|
plugin.settings['payload:' + key] = key;
|
|
58
57
|
});
|
|
59
58
|
|
|
60
|
-
plugin.init =
|
|
59
|
+
plugin.init = async (params) => {
|
|
61
60
|
var router = params.router;
|
|
62
61
|
var hostMiddleware = params.middleware;
|
|
63
62
|
|
|
@@ -71,10 +70,10 @@ plugin.init = function (params, callback) {
|
|
|
71
70
|
router.get('/debug/session', plugin.generate);
|
|
72
71
|
}
|
|
73
72
|
|
|
74
|
-
plugin.reloadSettings(
|
|
73
|
+
await plugin.reloadSettings();
|
|
75
74
|
};
|
|
76
75
|
|
|
77
|
-
plugin.appendConfig =
|
|
76
|
+
plugin.appendConfig = async (config) => {
|
|
78
77
|
config.sessionSharing = {
|
|
79
78
|
logoutRedirect: plugin.settings.logoutRedirect,
|
|
80
79
|
loginOverride: plugin.settings.loginOverride,
|
|
@@ -83,45 +82,30 @@ plugin.appendConfig = function (config, callback) {
|
|
|
83
82
|
hostWhitelist: plugin.settings.hostWhitelist,
|
|
84
83
|
};
|
|
85
84
|
|
|
86
|
-
|
|
85
|
+
return config;
|
|
87
86
|
};
|
|
88
87
|
|
|
89
88
|
/* Websocket Listeners */
|
|
90
89
|
|
|
91
90
|
SocketPlugins.sessionSharing = {};
|
|
92
91
|
|
|
93
|
-
SocketPlugins.sessionSharing.showUserIds =
|
|
92
|
+
SocketPlugins.sessionSharing.showUserIds = async (socket, data) => {
|
|
94
93
|
// Retrieve the hash and find matches
|
|
95
|
-
|
|
96
|
-
var payload = [];
|
|
97
|
-
|
|
98
|
-
payload.length = uids.length;
|
|
99
|
-
|
|
100
|
-
if (uids.length) {
|
|
101
|
-
async.map(uids, function (uid, next) {
|
|
102
|
-
db.getSortedSetRangeByScore(plugin.settings.name + ':uid', 0, -1, uid, uid, next);
|
|
103
|
-
}, function (err, remoteIds) {
|
|
104
|
-
if (err) {
|
|
105
|
-
return callback(err);
|
|
106
|
-
}
|
|
94
|
+
const { uids } = data;
|
|
107
95
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
callback(null, payload);
|
|
113
|
-
});
|
|
114
|
-
} else {
|
|
115
|
-
callback(new Error('no-uids-supplied'));
|
|
96
|
+
if (!uids.length) {
|
|
97
|
+
throw new Error('no-uids-supplied');
|
|
116
98
|
}
|
|
99
|
+
|
|
100
|
+
return Promise.all(uids.map(async uid => db.getSortedSetRangeByScore(plugin.settings.name + ':uid', 0, -1, uid, uid)));
|
|
117
101
|
};
|
|
118
102
|
|
|
119
|
-
SocketPlugins.sessionSharing.findUserByRemoteId =
|
|
120
|
-
if (data.remoteId) {
|
|
121
|
-
|
|
122
|
-
} else {
|
|
123
|
-
callback(new Error('no-remote-id-supplied'));
|
|
103
|
+
SocketPlugins.sessionSharing.findUserByRemoteId = async (socket, data) => {
|
|
104
|
+
if (!data.remoteId) {
|
|
105
|
+
throw new Error('no-remote-id-supplied');
|
|
124
106
|
}
|
|
107
|
+
|
|
108
|
+
return plugin.getUser(data.remoteId);
|
|
125
109
|
};
|
|
126
110
|
|
|
127
111
|
/* End Websocket Listeners */
|
|
@@ -129,32 +113,28 @@ SocketPlugins.sessionSharing.findUserByRemoteId = function (socket, data, callba
|
|
|
129
113
|
/*
|
|
130
114
|
* Given a remoteId, show user data
|
|
131
115
|
*/
|
|
132
|
-
plugin.getUser =
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
},
|
|
142
|
-
], callback);
|
|
116
|
+
plugin.getUser = async (remoteId) => {
|
|
117
|
+
const uid = await db.sortedSetScore(plugin.settings.name + ':uid', remoteId);
|
|
118
|
+
|
|
119
|
+
if (!uid) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return user.getUserFields(uid, ['username', 'userslug', 'picture']);
|
|
143
124
|
};
|
|
144
125
|
|
|
145
|
-
plugin.process =
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
], callback);
|
|
126
|
+
plugin.process = async (token) => {
|
|
127
|
+
const payload = await jwt.verify(token, plugin.settings.secret);
|
|
128
|
+
const userData = await plugin.normalizePayload(payload);
|
|
129
|
+
const [uid, isNewUser] = await plugin.findOrCreateUser(userData);
|
|
130
|
+
await plugin.updateUserProfile(uid, userData, isNewUser);
|
|
131
|
+
await plugin.updateUserGroups(uid, userData);
|
|
132
|
+
await plugin.verifyUser(token, uid, isNewUser);
|
|
133
|
+
return uid;
|
|
154
134
|
};
|
|
155
135
|
|
|
156
|
-
plugin.normalizePayload =
|
|
157
|
-
|
|
136
|
+
plugin.normalizePayload = async (payload) => {
|
|
137
|
+
const userData = {};
|
|
158
138
|
|
|
159
139
|
if (plugin.settings.payloadParent) {
|
|
160
140
|
payload = payload[plugin.settings.payloadParent];
|
|
@@ -162,11 +142,11 @@ plugin.normalizePayload = function (payload, callback) {
|
|
|
162
142
|
|
|
163
143
|
if (typeof payload !== 'object') {
|
|
164
144
|
winston.warn('[session-sharing] the payload is not an object', payload);
|
|
165
|
-
|
|
145
|
+
throw new Error('payload-invalid');
|
|
166
146
|
}
|
|
167
147
|
|
|
168
148
|
payloadKeys.forEach(function (key) {
|
|
169
|
-
|
|
149
|
+
const propName = plugin.settings['payload:' + key];
|
|
170
150
|
if (payload[propName]) {
|
|
171
151
|
userData[key] = payload[propName];
|
|
172
152
|
}
|
|
@@ -174,7 +154,7 @@ plugin.normalizePayload = function (payload, callback) {
|
|
|
174
154
|
|
|
175
155
|
if (!userData.id) {
|
|
176
156
|
winston.warn('[session-sharing] No user id was given in payload');
|
|
177
|
-
|
|
157
|
+
throw new Error('payload-invalid');
|
|
178
158
|
}
|
|
179
159
|
|
|
180
160
|
userData.fullname = (userData.fullname || [userData.firstName, userData.lastName].join(' ')).trim();
|
|
@@ -188,7 +168,7 @@ plugin.normalizePayload = function (payload, callback) {
|
|
|
188
168
|
|
|
189
169
|
if (!userData.username) {
|
|
190
170
|
winston.warn('[session-sharing] No valid username could be determined');
|
|
191
|
-
|
|
171
|
+
throw new Error('payload-invalid');
|
|
192
172
|
}
|
|
193
173
|
|
|
194
174
|
if (userData.hasOwnProperty('groups') && !Array.isArray(userData.groups)) {
|
|
@@ -197,218 +177,185 @@ plugin.normalizePayload = function (payload, callback) {
|
|
|
197
177
|
}
|
|
198
178
|
|
|
199
179
|
winston.verbose('[session-sharing] Payload verified');
|
|
200
|
-
plugins.
|
|
180
|
+
const data = await plugins.hooks.fire('filter:sessionSharing.normalizePayload', {
|
|
201
181
|
payload: payload,
|
|
202
182
|
userData: userData,
|
|
203
|
-
}, function (err, data) {
|
|
204
|
-
callback(err, data.userData);
|
|
205
183
|
});
|
|
184
|
+
|
|
185
|
+
return data.userData;
|
|
206
186
|
};
|
|
207
187
|
|
|
208
|
-
plugin.verifyUser =
|
|
209
|
-
plugins.
|
|
188
|
+
plugin.verifyUser = async (token, uid, isNewUser) => {
|
|
189
|
+
await plugins.hooks.fire('static:sessionSharing.verifyUser', {
|
|
210
190
|
uid: uid,
|
|
211
191
|
isNewUser: isNewUser,
|
|
212
192
|
token: token,
|
|
213
|
-
}, function (err) {
|
|
214
|
-
if (err) {
|
|
215
|
-
return callback(err);
|
|
216
|
-
}
|
|
217
|
-
// Check ban state of user, reject if banned
|
|
218
|
-
user.bans.isBanned(uid, function (err, banned) {
|
|
219
|
-
callback(err || banned ? new Error('banned') : null, uid);
|
|
220
|
-
});
|
|
221
193
|
});
|
|
194
|
+
|
|
195
|
+
// Check ban state of user
|
|
196
|
+
const isBanned = await user.bans.isBanned(uid);
|
|
197
|
+
|
|
198
|
+
// Reject if banned and settings dont allow banned users to login
|
|
199
|
+
if (isBanned && !plugin.settings.allowBannedUsers) {
|
|
200
|
+
throw new Error('banned');
|
|
201
|
+
}
|
|
222
202
|
};
|
|
223
203
|
|
|
224
|
-
plugin.findOrCreateUser =
|
|
225
|
-
|
|
204
|
+
plugin.findOrCreateUser = async (userData) => {
|
|
205
|
+
const { id } = userData;
|
|
206
|
+
let isNewUser = false;
|
|
207
|
+
let userId = null;
|
|
208
|
+
let queries = [db.sortedSetScore(plugin.settings.name + ':uid', userData.id)];
|
|
209
|
+
|
|
226
210
|
if (userData.email && userData.email.length) {
|
|
227
|
-
queries
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
return plugin.createUser(userData, function (err, uid) {
|
|
271
|
-
next(err, uid, userData, true);
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
setImmediate(next, null, uid, userData, false);
|
|
275
|
-
},
|
|
276
|
-
], callback);
|
|
277
|
-
});
|
|
211
|
+
queries = [...queries, db.sortedSetScore('email:uid', userData.email)];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
let [uid, mergeUid] = await Promise.all(queries);
|
|
215
|
+
uid = parseInt(uid, 10);
|
|
216
|
+
mergeUid = parseInt(mergeUid, 10);
|
|
217
|
+
|
|
218
|
+
/* check if found something to work with */
|
|
219
|
+
if (uid && !isNaN(uid)) {
|
|
220
|
+
try {
|
|
221
|
+
/* check if the user with the given id actually exists */
|
|
222
|
+
const exists = await user.exists(uid);
|
|
223
|
+
|
|
224
|
+
if (exists) {
|
|
225
|
+
userId = uid;
|
|
226
|
+
} else {
|
|
227
|
+
/* reference is outdated, user got deleted */
|
|
228
|
+
await db.sortedSetRemove(plugin.settings.name + ':uid', id);
|
|
229
|
+
}
|
|
230
|
+
} catch (error) {
|
|
231
|
+
/* ignore errors, but assume the user doesn't exist */
|
|
232
|
+
winston.warn('[session-sharing] Error while testing user existance', error);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (!userId && mergeUid && !isNaN(mergeUid)) {
|
|
237
|
+
winston.info('[session-sharing] Found user via their email, associating this id (' + id + ') with their NodeBB account');
|
|
238
|
+
await db.sortedSetAdd(plugin.settings.name + ':uid', mergeUid, id);
|
|
239
|
+
userId = mergeUid;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/* create the user from payload if necessary */
|
|
243
|
+
winston.debug('createUser?', !userId);
|
|
244
|
+
if (!userId) {
|
|
245
|
+
if (plugin.settings.noRegistration === 'on') {
|
|
246
|
+
throw new Error('no-match');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
userId = await plugin.createUser(userData);
|
|
250
|
+
isNewUser = true;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return [userId, isNewUser];
|
|
278
254
|
};
|
|
279
255
|
|
|
280
|
-
plugin.updateUserProfile =
|
|
256
|
+
plugin.updateUserProfile = async (uid, userData, isNewUser) => {
|
|
281
257
|
winston.debug('consider updateProfile?', isNewUser || plugin.settings.updateProfile === 'on');
|
|
258
|
+
let userObj = {};
|
|
282
259
|
|
|
283
260
|
/* even update the profile on a new account, since some fields are not initialized by NodeBB */
|
|
284
261
|
if (!isNewUser && plugin.settings.updateProfile !== 'on') {
|
|
285
|
-
return
|
|
262
|
+
return;
|
|
286
263
|
}
|
|
287
264
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
var obj = {};
|
|
265
|
+
const existingFields = await user.getUserFields(uid, profileFields);
|
|
266
|
+
const obj = profileFields.reduce((result, field) => {
|
|
267
|
+
if (typeof userData[field] !== 'undefined' && existingFields[field] !== userData[field]) {
|
|
268
|
+
result[field] = userData[field];
|
|
269
|
+
}
|
|
294
270
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
obj[field] = userData[field];
|
|
298
|
-
}
|
|
299
|
-
});
|
|
271
|
+
return result;
|
|
272
|
+
}, {});
|
|
300
273
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
next(null, userObj || existingFields);
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
setImmediate(next, null, {});
|
|
314
|
-
},
|
|
315
|
-
function (userObj, next) {
|
|
316
|
-
if (userData.picture) {
|
|
317
|
-
return db.setObjectField('user:' + uid, 'picture', userData.picture, next);
|
|
274
|
+
if (Object.keys(obj).length) {
|
|
275
|
+
winston.debug('[session-sharing] Updating profile fields:', obj);
|
|
276
|
+
obj.uid = uid;
|
|
277
|
+
try {
|
|
278
|
+
userObj = await user.updateProfile(uid, obj);
|
|
279
|
+
|
|
280
|
+
// If it errors out, not that big of a deal, continue anyway.
|
|
281
|
+
if (!userObj) {
|
|
282
|
+
userObj = existingFields;
|
|
318
283
|
}
|
|
284
|
+
} catch (error) {
|
|
285
|
+
winston.warn('[session-sharing] Unable to update profile information for uid: ' + uid + '(' + error.message + ')');
|
|
286
|
+
}
|
|
287
|
+
}
|
|
319
288
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
return callback(err, uid, userData, isNewUser);
|
|
324
|
-
});
|
|
289
|
+
if (userData.picture) {
|
|
290
|
+
await db.setObjectField('user:' + uid, 'picture', userData.picture);
|
|
291
|
+
}
|
|
325
292
|
};
|
|
326
293
|
|
|
327
|
-
plugin.updateUserGroups =
|
|
294
|
+
plugin.updateUserGroups = async (uid, userData) => {
|
|
328
295
|
if (!userData.groups || !Array.isArray(userData.groups)) {
|
|
329
|
-
return
|
|
296
|
+
return;
|
|
330
297
|
}
|
|
331
298
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
// Normalize user group data to just group names
|
|
337
|
-
groups = groups[0].map(function (groupObj) {
|
|
338
|
-
return groupObj.name;
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
// Build join and leave arrays
|
|
342
|
-
var join = userData.groups.filter(function (name) {
|
|
343
|
-
return !groups.includes(name);
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
if (plugin.settings.syncGroupList === 'on') {
|
|
347
|
-
join = join.filter(group => plugin.settings.syncGroups.includes(group));
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
var leave = groups.filter(function (name) {
|
|
351
|
-
// `registered-users` is always a joined group
|
|
352
|
-
if (name === 'registered-users') {
|
|
353
|
-
return false;
|
|
354
|
-
}
|
|
299
|
+
// Retrieve user groups
|
|
300
|
+
let [userGroups] = await groups.getUserGroupsFromSet('groups:createtime', [uid]);
|
|
301
|
+
// Normalize user group data to just group names
|
|
302
|
+
userGroups = userGroups.map(groupObj => groupObj.name);
|
|
355
303
|
|
|
356
|
-
|
|
357
|
-
|
|
304
|
+
// Build join and leave arrays
|
|
305
|
+
let join = userData.groups.filter(name => !userGroups.includes(name));
|
|
306
|
+
if (plugin.settings.syncGroupList === 'on') {
|
|
307
|
+
join = join.filter(group => plugin.settings.syncGroups.includes(group));
|
|
308
|
+
}
|
|
358
309
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
310
|
+
let leave = userGroups.filter((name) => {
|
|
311
|
+
// `registered-users` is always a joined group
|
|
312
|
+
if (name === 'registered-users') {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
362
315
|
|
|
363
|
-
|
|
364
|
-
},
|
|
365
|
-
], function (err) {
|
|
366
|
-
return callback(err, uid, isNewUser);
|
|
316
|
+
return !userData.groups.includes(name);
|
|
367
317
|
});
|
|
318
|
+
if (plugin.settings.syncGroupList === 'on') {
|
|
319
|
+
leave = leave.filter(group => plugin.settings.syncGroups.includes(group));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
await executeJoinLeave(uid, join, leave);
|
|
368
323
|
};
|
|
369
324
|
|
|
370
|
-
function executeJoinLeave(uid, join, leave
|
|
371
|
-
|
|
372
|
-
|
|
325
|
+
async function executeJoinLeave(uid, join, leave) {
|
|
326
|
+
await Promise.all([
|
|
327
|
+
(async () => {
|
|
373
328
|
if (plugin.settings.syncGroupJoin !== 'on') {
|
|
374
|
-
return
|
|
329
|
+
return;
|
|
375
330
|
}
|
|
376
331
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
},
|
|
381
|
-
function (next) {
|
|
332
|
+
await Promise.all(join.map(name => groups.join(name, uid)));
|
|
333
|
+
})(),
|
|
334
|
+
(async () => {
|
|
382
335
|
if (plugin.settings.syncGroupLeave !== 'on') {
|
|
383
|
-
return
|
|
336
|
+
return;
|
|
384
337
|
}
|
|
385
338
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
},
|
|
390
|
-
], callback);
|
|
339
|
+
await Promise.all(leave.map(name => groups.leave(name, uid)));
|
|
340
|
+
})(),
|
|
341
|
+
]);
|
|
391
342
|
}
|
|
392
343
|
|
|
393
|
-
plugin.createUser =
|
|
344
|
+
plugin.createUser = async (userData) => {
|
|
394
345
|
winston.verbose('[session-sharing] No user found, creating a new user for this login');
|
|
395
346
|
|
|
396
|
-
user.create(_.pick(userData, profileFields)
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
db.sortedSetAdd(plugin.settings.name + ':uid', uid, userData.id, function (err) {
|
|
400
|
-
callback(err, uid);
|
|
401
|
-
});
|
|
402
|
-
});
|
|
347
|
+
const uid = await user.create(_.pick(userData, profileFields));
|
|
348
|
+
await db.sortedSetAdd(plugin.settings.name + ':uid', uid, userData.id);
|
|
349
|
+
return uid;
|
|
403
350
|
};
|
|
404
351
|
|
|
405
|
-
plugin.addMiddleware = async function (req, res
|
|
352
|
+
plugin.addMiddleware = async function ({ req, res }) {
|
|
406
353
|
const { hostWhitelist, guestRedirect, editOverride, loginOverride, registerOverride } = await meta.settings.get('session-sharing');
|
|
407
354
|
|
|
408
355
|
if (hostWhitelist) {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
for (
|
|
356
|
+
const hosts = hostWhitelist.split(',') || [hostWhitelist];
|
|
357
|
+
let whitelisted = false;
|
|
358
|
+
for (const host of hosts) {
|
|
412
359
|
if (req.headers.host.includes(host)) {
|
|
413
360
|
whitelisted = true;
|
|
414
361
|
break;
|
|
@@ -416,35 +363,33 @@ plugin.addMiddleware = async function (req, res, next) {
|
|
|
416
363
|
}
|
|
417
364
|
|
|
418
365
|
if (!whitelisted) {
|
|
419
|
-
return
|
|
366
|
+
return;
|
|
420
367
|
}
|
|
421
368
|
}
|
|
422
369
|
|
|
423
|
-
function handleGuest(req, res
|
|
370
|
+
function handleGuest(req, res) {
|
|
424
371
|
if (guestRedirect && !req.originalUrl.startsWith(nconf.get('relative_path') + '/login?local=1')) {
|
|
425
372
|
// If a guest redirect is specified, follow it
|
|
426
373
|
res.redirect(guestRedirect.replace('%1', encodeURIComponent(req.protocol + '://' + req.get('host') + req.originalUrl)));
|
|
427
374
|
} else if (res.locals.fullRefresh === true) {
|
|
428
375
|
res.redirect(nconf.get('relative_path') + req.url);
|
|
429
|
-
} else {
|
|
430
|
-
next();
|
|
431
376
|
}
|
|
432
377
|
}
|
|
433
378
|
|
|
434
379
|
// Only respond to page loads by guests, not api or asset calls
|
|
435
|
-
|
|
436
|
-
|
|
380
|
+
const hasSession = req.hasOwnProperty('user') && req.user.hasOwnProperty('uid') && parseInt(req.user.uid, 10) > 0;
|
|
381
|
+
const hasLoginLock = req.session.hasOwnProperty('loginLock');
|
|
437
382
|
|
|
438
383
|
if (
|
|
439
384
|
!plugin.ready || // plugin not ready
|
|
440
385
|
(plugin.settings.behaviour === 'trust' && hasSession) || // user logged in + "trust" behaviour
|
|
441
|
-
(plugin.settings.behaviour === 'revalidate' && hasLoginLock) ||
|
|
386
|
+
((plugin.settings.behaviour === 'revalidate' || plugin.settings.behaviour === 'update') && hasLoginLock) ||
|
|
442
387
|
req.originalUrl.startsWith(nconf.get('relative_path') + '/api') // api routes
|
|
443
388
|
) {
|
|
444
|
-
// Let requests through under "revalidate" behaviour only if they're logging in for the first time
|
|
445
|
-
delete req.session.loginLock; // remove login lock for "revalidate" logins
|
|
389
|
+
// Let requests through under "update" or "revalidate" behaviour only if they're logging in for the first time
|
|
390
|
+
delete req.session.loginLock; // remove login lock for "update" or "revalidate" logins
|
|
446
391
|
|
|
447
|
-
return
|
|
392
|
+
return;
|
|
448
393
|
}
|
|
449
394
|
|
|
450
395
|
if (editOverride && hasSession && req.originalUrl.match(/\/user\/.*\/edit$/)) {
|
|
@@ -458,89 +403,78 @@ plugin.addMiddleware = async function (req, res, next) {
|
|
|
458
403
|
}
|
|
459
404
|
|
|
460
405
|
// Hook into ip blacklist functionality in core
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
406
|
+
try {
|
|
407
|
+
await meta.blacklist.test(req.ip);
|
|
408
|
+
} catch (error) {
|
|
409
|
+
if (hasSession) {
|
|
410
|
+
req.logout();
|
|
411
|
+
res.locals.fullRefresh = true;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
await plugin.cleanup({ res: res });
|
|
415
|
+
return handleGuest.call(null, req, res);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (Object.keys(req.cookies).length && req.cookies.hasOwnProperty(plugin.settings.cookieName) && req.cookies[plugin.settings.cookieName].length) {
|
|
419
|
+
try {
|
|
420
|
+
const uid = await plugin.process(req.cookies[plugin.settings.cookieName]);
|
|
421
|
+
winston.verbose('[session-sharing] Processing login for uid ' + uid + ', path ' + req.originalUrl);
|
|
422
|
+
req.uid = uid;
|
|
423
|
+
|
|
424
|
+
if (plugin.settings.behaviour === 'revalidate') {
|
|
425
|
+
res.locals.reroll = false; // disable session rerolling in core
|
|
466
426
|
}
|
|
467
427
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
return plugins.fireHook('filter:sessionSharing.error', {
|
|
497
|
-
error: err,
|
|
498
|
-
uid: uid,
|
|
499
|
-
res: res,
|
|
500
|
-
settings: plugin.settings,
|
|
501
|
-
handleAsGuest: handleAsGuest,
|
|
502
|
-
}, function (err, data) {
|
|
503
|
-
if (data.handleAsGuest) {
|
|
504
|
-
return handleGuest.call(err, req, res, next);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
next(err);
|
|
508
|
-
});
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
winston.verbose('[session-sharing] Processing login for uid ' + uid + ', path ' + req.originalUrl);
|
|
512
|
-
req.uid = uid;
|
|
513
|
-
|
|
514
|
-
if (plugin.settings.behaviour === 'revalidate') {
|
|
515
|
-
res.locals.reroll = false; // disable session rerolling in core
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
nbbAuthController.doLogin(req, uid, function () {
|
|
519
|
-
req.session.loginLock = true;
|
|
520
|
-
const url = req.session.returnTo || req.originalUrl.replace(nconf.get('relative_path'), '');
|
|
521
|
-
delete req.session.returnTo;
|
|
522
|
-
res.redirect(nconf.get('relative_path') + url);
|
|
523
|
-
});
|
|
524
|
-
});
|
|
525
|
-
} else if (hasSession) {
|
|
526
|
-
// Has login session but no cookie, can assume "revalidate" behaviour
|
|
527
|
-
user.isAdministrator(req.user.uid, function (err, isAdmin) {
|
|
528
|
-
if (plugin.settings.adminRevalidate === 'on' || !isAdmin) {
|
|
529
|
-
req.logout();
|
|
530
|
-
res.locals.fullRefresh = true;
|
|
531
|
-
handleGuest(req, res, next);
|
|
532
|
-
} else {
|
|
533
|
-
// Admins can bypass
|
|
534
|
-
return next(err);
|
|
535
|
-
}
|
|
428
|
+
await nbbAuthController.doLogin(req, uid);
|
|
429
|
+
|
|
430
|
+
req.session.loginLock = true;
|
|
431
|
+
const url = req.session.returnTo || req.originalUrl.replace(nconf.get('relative_path'), '');
|
|
432
|
+
delete req.session.returnTo;
|
|
433
|
+
res.redirect(nconf.get('relative_path') + url);
|
|
434
|
+
} catch (error) {
|
|
435
|
+
let handleAsGuest = false;
|
|
436
|
+
|
|
437
|
+
switch (error.message) {
|
|
438
|
+
case 'payload-invalid':
|
|
439
|
+
winston.warn('[session-sharing] The passed-in payload was invalid and could not be processed');
|
|
440
|
+
break;
|
|
441
|
+
case 'no-match':
|
|
442
|
+
winston.info('[session-sharing] Payload valid, but local account not found. Assuming guest.');
|
|
443
|
+
handleAsGuest = true;
|
|
444
|
+
break;
|
|
445
|
+
default:
|
|
446
|
+
winston.warn('[session-sharing] Error encountered while parsing token: ' + error.message);
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const data = await plugins.hooks.fire('filter:sessionSharing.error', {
|
|
451
|
+
error,
|
|
452
|
+
res: res,
|
|
453
|
+
settings: plugin.settings,
|
|
454
|
+
handleAsGuest: handleAsGuest,
|
|
536
455
|
});
|
|
537
|
-
|
|
538
|
-
|
|
456
|
+
|
|
457
|
+
if (data.handleAsGuest) {
|
|
458
|
+
return handleGuest.call(error, req, res);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
throw error;
|
|
539
462
|
}
|
|
540
|
-
})
|
|
463
|
+
} else if (hasSession) {
|
|
464
|
+
// Has login session but no cookie, can assume "revalidate" behaviour
|
|
465
|
+
const isAdmin = await user.isAdministrator(req.user.uid);
|
|
466
|
+
|
|
467
|
+
if (plugin.settings.behaviour !== 'update' && (plugin.settings.adminRevalidate === 'on' || !isAdmin)) {
|
|
468
|
+
req.logout();
|
|
469
|
+
res.locals.fullRefresh = true;
|
|
470
|
+
return handleGuest(req, res);
|
|
471
|
+
}
|
|
472
|
+
} else {
|
|
473
|
+
return handleGuest.call(null, req, res);
|
|
474
|
+
}
|
|
541
475
|
};
|
|
542
476
|
|
|
543
|
-
plugin.cleanup =
|
|
477
|
+
plugin.cleanup = async (data) => {
|
|
544
478
|
if (plugin.settings.cookieDomain) {
|
|
545
479
|
winston.verbose('[session-sharing] Clearing cookie');
|
|
546
480
|
data.res.clearCookie(plugin.settings.cookieName, {
|
|
@@ -550,15 +484,21 @@ plugin.cleanup = function (data, callback) {
|
|
|
550
484
|
});
|
|
551
485
|
}
|
|
552
486
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
}
|
|
487
|
+
data.res.clearCookie('nbb_token', {
|
|
488
|
+
domain: plugin.settings.cookieDomain,
|
|
489
|
+
expires: new Date(),
|
|
490
|
+
path: '/',
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
return true;
|
|
558
494
|
};
|
|
559
495
|
|
|
560
496
|
plugin.generate = function (req, res) {
|
|
561
|
-
|
|
497
|
+
if (!plugin.ready) {
|
|
498
|
+
return res.sendStatus(404);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
let payload = {};
|
|
562
502
|
payload[plugin.settings['payload:id']] = 1;
|
|
563
503
|
payload[plugin.settings['payload:username']] = 'testUser';
|
|
564
504
|
payload[plugin.settings['payload:email']] = 'testUser@example.org';
|
|
@@ -573,13 +513,13 @@ plugin.generate = function (req, res) {
|
|
|
573
513
|
payload[plugin.settings['payload:groups']] = ['test-group'];
|
|
574
514
|
|
|
575
515
|
if (plugin.settings.payloadParent || plugin.settings['payload:parent']) {
|
|
576
|
-
|
|
577
|
-
|
|
516
|
+
const parentKey = plugin.settings.payloadParent || plugin.settings['payload:parent'];
|
|
517
|
+
const newPayload = {};
|
|
578
518
|
newPayload[parentKey] = payload;
|
|
579
519
|
payload = newPayload;
|
|
580
520
|
}
|
|
581
521
|
|
|
582
|
-
|
|
522
|
+
const token = jwt.sign(payload, plugin.settings.secret);
|
|
583
523
|
res.cookie(plugin.settings.cookieName, token, {
|
|
584
524
|
maxAge: 1000 * 60 * 60 * 24 * 21,
|
|
585
525
|
httpOnly: true,
|
|
@@ -589,74 +529,77 @@ plugin.generate = function (req, res) {
|
|
|
589
529
|
res.sendStatus(200);
|
|
590
530
|
};
|
|
591
531
|
|
|
592
|
-
plugin.addAdminNavigation =
|
|
532
|
+
plugin.addAdminNavigation = async (header) => {
|
|
593
533
|
header.plugins.push({
|
|
594
534
|
route: '/plugins/session-sharing',
|
|
595
535
|
icon: 'fa-user-secret',
|
|
596
536
|
name: 'Session Sharing',
|
|
597
537
|
});
|
|
598
538
|
|
|
599
|
-
|
|
539
|
+
return header;
|
|
600
540
|
};
|
|
601
541
|
|
|
602
|
-
plugin.reloadSettings =
|
|
603
|
-
// If
|
|
604
|
-
if (
|
|
542
|
+
plugin.reloadSettings = async (data) => {
|
|
543
|
+
// If data argument is truthy, then it is the action hook from core
|
|
544
|
+
if (data && data.plugin !== 'session-sharing') {
|
|
605
545
|
return;
|
|
606
546
|
}
|
|
607
547
|
|
|
608
|
-
meta.settings.get('session-sharing'
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
if (!settings.hasOwnProperty('secret') || !settings.secret.length) {
|
|
614
|
-
winston.error('[session-sharing] JWT Secret not found, session sharing disabled.');
|
|
615
|
-
return callback();
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
// If "payload:parent" is found, but payloadParent is not, update the latter and delete the former
|
|
619
|
-
if (!settings.payloadParent && settings['payload:parent']) {
|
|
620
|
-
winston.verbose('[session-sharing] Migrating payload:parent to payloadParent');
|
|
621
|
-
settings.payloadParent = settings['payload:parent'];
|
|
622
|
-
db.setObjectField('settings:session-sharing', 'payloadParent', settings.payloadParent);
|
|
623
|
-
db.deleteObjectField('settings:session-sharing', 'payload:parent');
|
|
624
|
-
}
|
|
548
|
+
const settings = await meta.settings.get('session-sharing');
|
|
549
|
+
if (!settings.hasOwnProperty('secret') || !settings.secret.length) {
|
|
550
|
+
winston.error('[session-sharing] JWT Secret not found, session sharing disabled.');
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
625
553
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
554
|
+
// If "payload:parent" is found, but payloadParent is not, update the latter and delete the former
|
|
555
|
+
if (!settings.payloadParent && settings['payload:parent']) {
|
|
556
|
+
winston.verbose('[session-sharing] Migrating payload:parent to payloadParent');
|
|
557
|
+
settings.payloadParent = settings['payload:parent'];
|
|
558
|
+
await db.setObjectField('settings:session-sharing', 'payloadParent', settings.payloadParent);
|
|
559
|
+
await db.deleteObjectField('settings:session-sharing', 'payload:parent');
|
|
560
|
+
}
|
|
629
561
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
562
|
+
if (!settings['payload:username'] && !settings['payload:firstName'] && !settings['payload:lastName'] && !settings['payload:fullname']) {
|
|
563
|
+
settings['payload:username'] = 'username';
|
|
564
|
+
}
|
|
633
565
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
});
|
|
566
|
+
winston.info('[session-sharing] Settings OK');
|
|
567
|
+
plugin.settings = _.defaults(_.pickBy(settings, Boolean), plugin.settings);
|
|
568
|
+
plugin.ready = true;
|
|
638
569
|
};
|
|
639
570
|
|
|
640
|
-
plugin.appendTemplate = (data
|
|
641
|
-
if (data.req.session.sessionSharing
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
return callback(err);
|
|
645
|
-
}
|
|
571
|
+
plugin.appendTemplate = async (data) => {
|
|
572
|
+
if (!data.req.session.sessionSharing || !data.req.session.sessionSharing.banned) {
|
|
573
|
+
return data;
|
|
574
|
+
}
|
|
646
575
|
|
|
647
|
-
|
|
648
|
-
ban: info,
|
|
649
|
-
banned: true,
|
|
650
|
-
};
|
|
576
|
+
const info = await user.getLatestBanInfo(data.req.session.sessionSharing.uid);
|
|
651
577
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
578
|
+
data.templateData.sessionSharingBan = {
|
|
579
|
+
ban: info,
|
|
580
|
+
banned: true,
|
|
581
|
+
};
|
|
655
582
|
|
|
656
|
-
|
|
583
|
+
delete data.req.session.sessionSharing;
|
|
584
|
+
return data;
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
plugin.saveReverseToken = async ({ req, userData: data }) => {
|
|
588
|
+
if (!plugin.ready || plugin.settings.reverseToken !== 'on') {
|
|
589
|
+
return; // no reverse token if secret not set
|
|
657
590
|
}
|
|
658
591
|
|
|
659
|
-
|
|
592
|
+
const res = req.res;
|
|
593
|
+
const userData = await user.getUserFields(data.uid, ['uid', 'username', 'picture', 'reputation', 'postcount', 'banned']);
|
|
594
|
+
const token = jwt.sign(userData, plugin.settings.secret);
|
|
595
|
+
|
|
596
|
+
res.cookie('nbb_token', token, {
|
|
597
|
+
maxAge: meta.getSessionTTLSeconds() * 1000,
|
|
598
|
+
httpOnly: !req.secure,
|
|
599
|
+
domain: plugin.settings.cookieDomain,
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
winston.info(`[plugins/session-sharing] Saving reverse cookie for uid ${userData.uid}, session: ${req.session.id}`);
|
|
660
603
|
};
|
|
661
604
|
|
|
662
605
|
module.exports = plugin;
|