• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

NodeBB / NodeBB / 23441317402

23 Mar 2026 02:03PM UTC coverage: 85.419% (-0.009%) from 85.428%
23441317402

push

github

nodebb-misty
chore(i18n): fallback strings for new resources: nodebb.topic

13439 of 18444 branches covered (72.86%)

28384 of 33229 relevant lines covered (85.42%)

3336.71 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

77.78
/src/controllers/authentication.js
1
'use strict';
2

3
const winston = require('winston');
4✔
4
const passport = require('passport');
4✔
5
const nconf = require('nconf');
4✔
6
const validator = require('validator');
4✔
7
const _ = require('lodash');
4✔
8
const util = require('util');
4✔
9

10
const db = require('../database');
4✔
11
const meta = require('../meta');
4✔
12
const analytics = require('../analytics');
4✔
13
const user = require('../user');
4✔
14
const plugins = require('../plugins');
4✔
15
const utils = require('../utils');
4✔
16
const slugify = require('../slugify');
4✔
17
const helpers = require('./helpers');
4✔
18
const privileges = require('../privileges');
4✔
19
const sockets = require('../socket.io');
4✔
20

21
const authenticationController = module.exports;
4✔
22

23
async function registerAndLoginUser(req, res, userData) {
24
        if (!userData.hasOwnProperty('email')) {
76✔
25
                userData.updateEmail = true;
20✔
26
        }
27

28
        const data = await user.interstitials.get(req, userData);
76✔
29

30
        // If interstitials are found, save registration attempt into session and abort
31
        const deferRegistration = data.interstitials.length;
76✔
32
        if (deferRegistration) {
76✔
33
                userData.register = true;
24✔
34
                req.session.registration = userData;
24✔
35
                const next = `${nconf.get('relative_path')}/register/complete`;
24✔
36
                if (req.body?.noscript === 'true') {
24!
37
                        res.redirect(next);
×
38
                        return;
×
39
                }
40
                res.json({ next });
24✔
41
                return;
24✔
42
        }
43

44
        const { queued, uid, message } = await user.createOrQueue(req, userData);
52✔
45
        if (queued) {
44✔
46
                return { message };
16✔
47
        }
48

49
        if (res.locals.processLogin) {
28!
50
                const hasLoginPrivilege = await privileges.global.can('local:login', uid);
28✔
51
                if (hasLoginPrivilege) {
28!
52
                        await authenticationController.doLogin(req, uid);
28✔
53
                }
54
        }
55

56
        // Distinguish registrations through invites from direct ones
57
        if (userData.token) {
28✔
58
                // Token has to be verified at this point
59
                await Promise.all([
4✔
60
                        user.confirmIfInviteEmailIsUsed(userData.token, userData.email, uid),
61
                        user.joinGroupsFromInvitation(uid, userData.token),
62
                        user.setInviterUid(uid, userData.token),
63
                ]);
64
        }
65
        await user.deleteInvitationKey(userData.email, userData.token);
28✔
66
        let next = req.session.returnTo || `${nconf.get('relative_path')}/`;
28✔
67
        if (req.loggedIn && next === `${nconf.get('relative_path')}/login`) {
28!
68
                next = `${nconf.get('relative_path')}/`;
×
69
        }
70
        const complete = await plugins.hooks.fire('filter:register.complete', { uid: uid, next: next });
28✔
71
        req.session.returnTo = complete.next;
28✔
72
        return complete;
28✔
73
}
74

75
// POST /register
76
authenticationController.register = async function (req, res) {
4✔
77
        const registrationType = meta.config.registrationType || 'normal';
100!
78

79
        if (registrationType === 'disabled') {
100✔
80
                return res.sendStatus(403);
4✔
81
        }
82

83
        const userData = req.body;
96✔
84
        try {
96✔
85
                if (userData.token || registrationType === 'invite-only' || registrationType === 'admin-invite-only') {
96✔
86
                        await user.verifyInvitation(userData);
8✔
87
                }
88

89
                user.checkUsernameLength(userData.username);
92✔
90

91
                if (userData.password !== userData['password-confirm']) {
64!
92
                        throw new Error('[[user:change-password-error-match]]');
×
93
                }
94

95
                user.isPasswordValid(userData.password);
64✔
96

97
                await plugins.hooks.fire('filter:password.check', { password: userData.password, uid: 0, userData: userData });
64✔
98

99
                res.locals.processLogin = true; // set it to false in plugin if you wish to just register only
64✔
100
                await plugins.hooks.fire('filter:register.check', { req: req, res: res, userData: userData });
64✔
101

102
                const data = await registerAndLoginUser(req, res, userData);
64✔
103
                if (data) {
56✔
104
                        if (data.uid && req.body?.userLang) {
32✔
105
                                await user.setSetting(data.uid, 'userLang', req.body.userLang);
4✔
106
                        }
107
                        res.json(data);
32✔
108
                }
109
        } catch (err) {
110
                helpers.noScriptErrors(req, res, err.message, 400);
40✔
111
        }
112
};
113

114
// POST /register/complete
115
authenticationController.registerComplete = async function (req, res) {
4✔
116
        try {
16✔
117
                // For the interstitials that respond, execute the callback with the form body
118
                const data = await user.interstitials.get(req, req.session.registration);
16✔
119
                const callbacks = data.interstitials.reduce((memo, cur) => {
16✔
120
                        if (cur.hasOwnProperty('callback') && typeof cur.callback === 'function') {
32!
121
                                req.body.files = req.files;
32✔
122
                                if (
32✔
123
                                        (cur.callback.constructor && cur.callback.constructor.name === 'AsyncFunction') ||
80✔
124
                                        cur.callback.length === 2 // non-async function w/o callback
125
                                ) {
126
                                        memo.push(cur.callback);
16✔
127
                                } else {
128
                                        memo.push(util.promisify(cur.callback));
16✔
129
                                }
130
                        }
131

132
                        return memo;
32✔
133
                }, []);
134

135
                const done = function (data) {
16✔
136
                        delete req.session.registration;
12✔
137
                        const relative_path = nconf.get('relative_path');
12✔
138
                        if (data && data.message) {
12!
139
                                return res.redirect(`${relative_path}/?register=${encodeURIComponent(data.message)}`);
×
140
                        }
141

142
                        if (req.session.returnTo) {
12!
143
                                res.redirect(relative_path + req.session.returnTo.replace(new RegExp(`^${relative_path}`), ''));
12✔
144
                        } else {
145
                                res.redirect(`${relative_path}/`);
×
146
                        }
147
                };
148

149
                const results = await Promise.allSettled(callbacks.map(async (cb) => {
16✔
150
                        await cb(req.session.registration, req.body);
32✔
151
                }));
152
                const errors = results.map(result => result.status === 'rejected' && result.reason && result.reason.message).filter(Boolean);
32✔
153
                if (errors.length) {
16✔
154
                        req.flash('errors', errors);
4✔
155
                        return req.session.save(() => {
4✔
156
                                res.redirect(`${nconf.get('relative_path')}/register/complete`);
4✔
157
                        });
158
                }
159

160
                if (req.session.registration.register === true) {
12!
161
                        res.locals.processLogin = true;
12✔
162
                        req.body.noscript = 'true'; // trigger full page load on error
12✔
163

164
                        const data = await registerAndLoginUser(req, res, req.session.registration);
12✔
165
                        if (!data) {
12!
166
                                return winston.warn('[register] Interstitial callbacks processed with no errors, but one or more interstitials remain. This is likely an issue with one of the interstitials not properly handling a null case or invalid value.');
×
167
                        }
168
                        done(data);
12✔
169
                } else {
170
                        // Update user hash, clear registration data in session
171
                        const payload = req.session.registration;
×
172
                        const { uid } = payload;
×
173
                        delete payload.uid;
×
174
                        delete payload.returnTo;
×
175

176
                        Object.keys(payload).forEach((prop) => {
×
177
                                if (typeof payload[prop] === 'boolean') {
×
178
                                        payload[prop] = payload[prop] ? 1 : 0;
×
179
                                }
180
                        });
181

182
                        await user.setUserFields(uid, payload);
×
183
                        done();
×
184
                }
185
        } catch (err) {
186
                delete req.session.registration;
×
187
                res.redirect(`${nconf.get('relative_path')}/?register=${encodeURIComponent(err.message)}`);
×
188
        }
189
};
190

191
// POST /register/abort
192
authenticationController.registerAbort = async (req, res) => {
4✔
193
        if (req.uid && req.session.registration) {
24✔
194
                // Email is the only cancelable interstitial
195
                delete req.session.registration.updateEmail;
20✔
196

197
                const { interstitials } = await user.interstitials.get(req, req.session.registration);
20✔
198
                if (!interstitials.length) {
20✔
199
                        delete req.session.registration;
16✔
200
                        return res.redirect(nconf.get('relative_path') + (req.session.returnTo || '/'));
16!
201
                }
202
        }
203

204
        // End the session and redirect to home
205
        req.session.destroy(() => {
8✔
206
                res.clearCookie(nconf.get('sessionKey'), meta.configs.cookie.get());
8✔
207
                res.redirect(`${nconf.get('relative_path')}/`);
8✔
208
        });
209
};
210

211
// POST /login
212
authenticationController.login = async (req, res, next) => {
4✔
213
        let { strategy } = await plugins.hooks.fire('filter:login.override', { req, strategy: 'local' });
356✔
214
        if (!passport._strategy(strategy)) {
356!
215
                winston.error(`[auth/override] Requested login strategy "${strategy}" not found, reverting back to local login strategy.`);
×
216
                strategy = 'local';
×
217
        }
218

219
        if (plugins.hooks.hasListeners('action:auth.overrideLogin')) {
356!
220
                return continueLogin(strategy, req, res, next);
×
221
        }
222

223
        const loginWith = meta.config.allowLoginWith || 'username-email';
356✔
224
        req.body.username = String(req.body.username).trim();
356✔
225
        const errorHandler = res.locals.noScriptErrors || helpers.noScriptErrors;
356✔
226
        try {
356✔
227
                await plugins.hooks.fire('filter:login.check', { req: req, res: res, userData: req.body });
356✔
228
        } catch (err) {
229
                return errorHandler(req, res, err.message, 403);
×
230
        }
231
        try {
356✔
232
                const isEmailLogin = loginWith.includes('email') && req.body.username && utils.isEmailValid(req.body.username);
356✔
233
                const isUsernameLogin = loginWith.includes('username') && !validator.isEmail(req.body.username);
356✔
234
                if (isEmailLogin) {
356✔
235
                        const username = await user.getUsernameByEmail(req.body.username);
12✔
236
                        if (username !== '[[global:guest]]') {
12✔
237
                                req.body.username = username;
8✔
238
                        } else {
239
                                return errorHandler(req, res, '[[error:invalid-email]]', 400);
4✔
240
                        }
241
                }
242
                if (isEmailLogin || isUsernameLogin) {
352✔
243
                        continueLogin(strategy, req, res, next);
348✔
244
                } else {
245
                        errorHandler(req, res, `[[error:wrong-login-type-${loginWith}]]`, 400);
4✔
246
                }
247
        } catch (err) {
248
                return errorHandler(req, res, err.message, 500);
×
249
        }
250
};
251

252
function continueLogin(strategy, req, res, next) {
253
        passport.authenticate(strategy, async (err, userData, info) => {
348✔
254
                if (err) {
348✔
255
                        plugins.hooks.fire('action:login.continue', { req, strategy, userData, error: err });
60✔
256
                        return helpers.noScriptErrors(req, res, err.data || err.message, 403);
60✔
257
                }
258

259
                if (!userData) {
288✔
260
                        if (info instanceof Error) {
12!
261
                                info = info.message;
×
262
                        } else if (typeof info === 'object') {
12!
263
                                info = '[[error:invalid-username-or-password]]';
12✔
264
                        }
265

266
                        plugins.hooks.fire('action:login.continue', { req, strategy, userData, error: new Error(info) });
12✔
267
                        return helpers.noScriptErrors(req, res, info, 403);
12✔
268
                }
269

270
                // Alter user cookie depending on passed-in option
271
                if (req.body?.remember === 'on') {
276✔
272
                        const duration = meta.getSessionTTLSeconds() * 1000;
12✔
273
                        req.session.cookie.maxAge = duration;
12✔
274
                        req.session.cookie.expires = new Date(Date.now() + duration);
12✔
275
                } else {
276
                        const duration = meta.config.sessionDuration * 1000;
264✔
277
                        req.session.cookie.maxAge = duration || false;
264✔
278
                        req.session.cookie.expires = duration ? new Date(Date.now() + duration) : false;
264✔
279
                }
280

281
                plugins.hooks.fire('action:login.continue', { req, strategy, userData, error: null });
276✔
282

283
                if (userData.passwordExpiry && userData.passwordExpiry < Date.now()) {
276!
284
                        winston.verbose(`[auth] Triggering password reset for uid ${userData.uid} due to password policy`);
×
285
                        req.session.passwordExpired = true;
×
286

287
                        const code = await user.reset.generate(userData.uid);
×
288
                        (res.locals.redirectAfterLogin || redirectAfterLogin)(req, res, `${nconf.get('relative_path')}/reset/${code}`);
×
289
                } else {
290
                        delete req.query.lang;
276✔
291
                        await authenticationController.doLogin(req, userData.uid);
276✔
292
                        let destination;
293
                        if (req.session.returnTo) {
276!
294
                                destination = req.session.returnTo.startsWith('http') ?
×
295
                                        req.session.returnTo :
296
                                        nconf.get('relative_path') + req.session.returnTo;
297
                                delete req.session.returnTo;
×
298
                        } else {
299
                                destination = `${nconf.get('relative_path')}/`;
276✔
300
                        }
301

302
                        (res.locals.redirectAfterLogin || redirectAfterLogin)(req, res, destination);
276✔
303
                }
304
        })(req, res, next);
305
}
306

307
function redirectAfterLogin(req, res, destination) {
308
        if (req.body?.noscript === 'true') {
272!
309
                res.redirect(`${destination}?loggedin`);
×
310
        } else {
311
                res.status(200).send({
272✔
312
                        next: destination,
313
                });
314
        }
315
}
316

317
authenticationController.doLogin = async function (req, uid) {
4✔
318
        if (!uid) {
304!
319
                return;
×
320
        }
321
        const loginAsync = util.promisify(req.login).bind(req);
304✔
322
        await loginAsync({ uid: uid }, { keepSessionInfo: req.res.locals.reroll !== false });
304✔
323
        await authenticationController.onSuccessfulLogin(req, uid);
304✔
324
};
325

326
authenticationController.onSuccessfulLogin = async function (req, uid, trackSession = true) {
4✔
327
        /*
328
         * Older code required that this method be called from within the SSO plugin.
329
         * That behaviour is no longer required, onSuccessfulLogin is now automatically
330
         * called in NodeBB core. However, if already called, return prematurely
331
         */
332
        if (req.loggedIn && !req.session.forceLogin) {
312✔
333
                return true;
4✔
334
        }
335

336
        try {
308✔
337
                const uuid = utils.generateUUID();
308✔
338

339
                req.uid = uid;
308✔
340
                req.loggedIn = true;
308✔
341
                await meta.blacklist.test(req.ip);
308✔
342
                await user.logIP(uid, req.ip);
308✔
343
                await user.bans.unbanIfExpired([uid]);
308✔
344
                await user.reset.cleanByUid(uid);
308✔
345

346
                req.session.meta = {};
308✔
347

348
                delete req.session.forceLogin;
308✔
349
                // Associate IP used during login with user account
350
                req.session.meta.ip = req.ip;
308✔
351

352
                // Associate metadata retrieved via user-agent
353
                req.session.meta = _.extend(req.session.meta, {
308✔
354
                        uuid: uuid,
355
                        datetime: Date.now(),
356
                        platform: req.useragent.platform,
357
                        browser: req.useragent.browser,
358
                        version: req.useragent.version,
359
                });
360
                await Promise.all([
308✔
361
                        new Promise((resolve) => {
362
                                req.session.save(resolve);
308✔
363
                        }),
364
                        trackSession ? user.auth.addSession(uid, req.sessionID) : undefined,
308✔
365
                        user.updateLastOnlineTime(uid),
366
                        user.onUserOnline(uid, Date.now()),
367
                        analytics.increment('logins'),
368
                        db.incrObjectFieldBy('global', 'loginCount', 1),
369
                ]);
370

371
                // Force session check for all connected socket.io clients with the same session id
372
                sockets.in(`sess_${req.sessionID}`).emit('checkSession', uid);
308✔
373

374
                plugins.hooks.fire('action:user.loggedIn', { uid: uid, req: req });
308✔
375
        } catch (err) {
376
                req.session.destroy();
×
377
                throw err;
×
378
        }
379
};
380

381
const destroyAsync = util.promisify((req, callback) => req.session.destroy(callback));
4✔
382
const logoutAsync = util.promisify((req, callback) => req.logout(callback));
4✔
383

384
authenticationController.localLogin = async function (req, username, password, next) {
4✔
385
        if (!username) {
336!
386
                return next(new Error('[[error:invalid-username]]'));
×
387
        }
388

389
        if (!password || !utils.isPasswordValid(password)) {
336!
390
                return next(new Error('[[error:invalid-password]]'));
×
391
        }
392

393
        if (password.length > 512) {
336✔
394
                return next(new Error('[[error:password-too-long]]'));
4✔
395
        }
396

397
        const userslug = slugify(username);
332✔
398
        if (!utils.isUserNameValid(username) || !userslug) {
332!
399
                return next(new Error('[[error:invalid-username]]'));
×
400
        }
401

402
        const uid = await user.getUidByUserslug(userslug);
332✔
403
        try {
332✔
404
                const [userData, isAdminOrGlobalMod, canLoginIfBanned] = await Promise.all([
332✔
405
                        user.getUserFields(uid, ['uid', 'passwordExpiry']),
406
                        user.isAdminOrGlobalMod(uid),
407
                        user.bans.canLoginIfBanned(uid),
408
                ]);
409

410
                userData.isAdminOrGlobalMod = isAdminOrGlobalMod;
332✔
411

412
                try {
332✔
413
                        const passwordMatch = await user.isPasswordCorrect(uid, password, req.ip);
332✔
414
                        if (!passwordMatch) {
324✔
415
                                return next(new Error('[[error:invalid-login-credentials]]'));
32✔
416
                        }
417
                        if (!canLoginIfBanned) {
292✔
418
                                return next(await getBanError(uid));
12✔
419
                        }
420

421
                        // Doing this after the ban check, because user's privileges might change after a ban expires
422
                        const hasLoginPrivilege = await privileges.global.can('local:login', uid);
280✔
423
                        if (parseInt(uid, 10) && !hasLoginPrivilege) {
280✔
424
                                return next(new Error('[[error:local-login-disabled]]'));
4✔
425
                        }
426
                } catch (e) {
427
                        if (req.loggedIn) {
8!
428
                                await logoutAsync(req);
×
429
                                await destroyAsync(req);
×
430
                        }
431
                        throw e;
8✔
432
                }
433

434
                next(null, userData, '[[success:authentication-successful]]');
276✔
435
        } catch (err) {
436
                next(err);
8✔
437
        }
438
};
439

440
authenticationController.logout = async function (req, res) {
4✔
441
        if (!req.loggedIn || !req.sessionID) {
12!
442
                res.clearCookie(nconf.get('sessionKey'), meta.configs.cookie.get());
12✔
443
                return res.status(200).send('not-logged-in');
12✔
444
        }
445
        const { uid } = req;
×
446
        const { sessionID } = req;
×
447

448
        try {
×
449
                await user.auth.revokeSession(sessionID, uid);
×
450
                await logoutAsync(req);
×
451
                await destroyAsync(req);
×
452
                res.clearCookie(nconf.get('sessionKey'), meta.configs.cookie.get());
×
453

454
                await user.setUserField(uid, 'lastonline', Date.now() - (meta.config.onlineCutoff * 60000));
×
455
                await db.sortedSetAdd('users:online', Date.now() - (meta.config.onlineCutoff * 60000), uid);
×
456
                await plugins.hooks.fire('static:user.loggedOut', { req, res, uid, sessionID });
×
457

458
                // Force session check for all connected socket.io clients with the same session id
459
                sockets.in(`sess_${sessionID}`).emit('checkSession', 0);
×
460
                const payload = {
×
461
                        next: `${nconf.get('relative_path')}/`,
462
                };
463
                await plugins.hooks.fire('filter:user.logout', payload);
×
464

465
                if (req.body?.noscript === 'true' || res.locals.logoutRedirect === true) {
×
466
                        return res.redirect(payload.next);
×
467
                }
468
                res.status(200).send(payload);
×
469
        } catch (err) {
470
                winston.error(`${req.method} ${req.originalUrl}\n${err.stack}`);
×
471
                res.status(500).send(err.message);
×
472
        }
473
};
474

475
async function getBanError(uid) {
476
        try {
12✔
477
                const banInfo = await user.getLatestBanInfo(uid);
12✔
478

479
                if (!banInfo.reason) {
12✔
480
                        banInfo.reason = '[[user:info.banned-no-reason]]';
4✔
481
                }
482
                const err = new Error(banInfo.reason);
12✔
483
                err.data = banInfo;
12✔
484
                return err;
12✔
485
        } catch (err) {
486
                if (err.message === 'no-ban-info') {
×
487
                        return new Error('[[error:user-banned]]');
×
488
                }
489
                throw err;
×
490
        }
491
}
492

493
require('../promisify')(authenticationController, ['register', 'registerComplete', 'registerAbort', 'login', 'localLogin', 'logout']);
4✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc