diff --git a/src/controllers/accounts/edit.js b/src/controllers/accounts/edit.js index c44ee5cce4..39a5f94e25 100644 --- a/src/controllers/accounts/edit.js +++ b/src/controllers/accounts/edit.js @@ -1,212 +1,156 @@ 'use strict'; -var async = require('async'); +const user = require('../../user'); +const meta = require('../../meta'); +const plugins = require('../../plugins'); +const helpers = require('../helpers'); +const groups = require('../../groups'); +const accountHelpers = require('./helpers'); +const privileges = require('../../privileges'); +const file = require('../../file'); -var db = require('../../database'); -var user = require('../../user'); -var meta = require('../../meta'); -var plugins = require('../../plugins'); -var helpers = require('../helpers'); -var groups = require('../../groups'); -var accountHelpers = require('./helpers'); -var privileges = require('../../privileges'); -var file = require('../../file'); +const editController = module.exports; -var editController = module.exports; +editController.get = async function (req, res, next) { + const [userData, canUseSignature] = await Promise.all([ + accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid), + privileges.global.can('signature', req.uid), + ]); + if (!userData) { + return next(); + } + userData.maximumSignatureLength = meta.config.maximumSignatureLength; + userData.maximumAboutMeLength = meta.config.maximumAboutMeLength; + userData.maximumProfileImageSize = meta.config.maximumProfileImageSize; + userData.allowProfilePicture = !userData.isSelf || !!meta.config['reputation:disabled'] || userData.reputation >= meta.config['min:rep:profile-picture']; + userData.allowCoverPicture = !userData.isSelf || !!meta.config['reputation:disabled'] || userData.reputation >= meta.config['min:rep:cover-picture']; + userData.allowProfileImageUploads = meta.config.allowProfileImageUploads; + userData.allowMultipleBadges = meta.config.allowMultipleBadges === 1; + userData.allowAccountDelete = meta.config.allowAccountDelete === 1; + userData.allowWebsite = !userData.isSelf || !!meta.config['reputation:disabled'] || userData.reputation >= meta.config['min:rep:website']; + userData.allowAboutMe = !userData.isSelf || !!meta.config['reputation:disabled'] || userData.reputation >= meta.config['min:rep:aboutme']; + userData.allowSignature = canUseSignature && (!userData.isSelf || !!meta.config['reputation:disabled'] || userData.reputation >= meta.config['min:rep:signature']); + userData.profileImageDimension = meta.config.profileImageDimension; + userData.defaultAvatar = user.getDefaultAvatar(); -editController.get = function (req, res, callback) { - async.waterfall([ - function (next) { - async.parallel({ - userData: function (next) { - accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, next); - }, - canUseSignature: function (next) { - privileges.global.can('signature', req.uid, next); - }, - }, next); + userData.groups = userData.groups.filter(g => g && g.userTitleEnabled && !groups.isPrivilegeGroup(g.name) && g.name !== 'registered-users'); + + if (!userData.allowMultipleBadges) { + userData.groupTitle = userData.groupTitleArray[0]; + } + userData.groups.forEach(function (group) { + group.selected = userData.groupTitleArray.includes(group.name); + }); + + userData.title = '[[pages:account/edit, ' + userData.username + ']]'; + userData.breadcrumbs = helpers.buildBreadcrumbs([ + { + text: userData.username, + url: '/user/' + userData.userslug, }, - function (results, next) { - var userData = results.userData; - if (!userData) { - return callback(); - } - userData.maximumSignatureLength = meta.config.maximumSignatureLength; - userData.maximumAboutMeLength = meta.config.maximumAboutMeLength; - userData.maximumProfileImageSize = meta.config.maximumProfileImageSize; - userData.allowProfilePicture = !userData.isSelf || !!meta.config['reputation:disabled'] || userData.reputation >= meta.config['min:rep:profile-picture']; - userData.allowCoverPicture = !userData.isSelf || !!meta.config['reputation:disabled'] || userData.reputation >= meta.config['min:rep:cover-picture']; - userData.allowProfileImageUploads = meta.config.allowProfileImageUploads; - userData.allowMultipleBadges = meta.config.allowMultipleBadges === 1; - userData.allowAccountDelete = meta.config.allowAccountDelete === 1; - userData.allowWebsite = !userData.isSelf || !!meta.config['reputation:disabled'] || userData.reputation >= meta.config['min:rep:website']; - userData.allowAboutMe = !userData.isSelf || !!meta.config['reputation:disabled'] || userData.reputation >= meta.config['min:rep:aboutme']; - userData.allowSignature = results.canUseSignature && (!userData.isSelf || !!meta.config['reputation:disabled'] || userData.reputation >= meta.config['min:rep:signature']); - userData.profileImageDimension = meta.config.profileImageDimension; - userData.defaultAvatar = user.getDefaultAvatar(); - - userData.groups = userData.groups.filter(function (group) { - return group && group.userTitleEnabled && !groups.isPrivilegeGroup(group.name) && group.name !== 'registered-users'; - }); - - if (!userData.allowMultipleBadges) { - userData.groupTitle = userData.groupTitleArray[0]; - } - userData.groups.forEach(function (group) { - group.selected = userData.groupTitleArray.includes(group.name); - }); - - userData.title = '[[pages:account/edit, ' + userData.username + ']]'; - userData.breadcrumbs = helpers.buildBreadcrumbs([ - { - text: userData.username, - url: '/user/' + userData.userslug, - }, - { - text: '[[user:edit]]', - }, - ]); - userData.editButtons = []; - - plugins.fireHook('filter:user.account.edit', userData, next); + { + text: '[[user:edit]]', }, - function (userData) { - res.render('account/edit', userData); - }, - ], callback); + ]); + userData.editButtons = []; + + const result = await plugins.fireHook('filter:user.account.edit', userData); + res.render('account/edit', result); }; -editController.password = function (req, res, next) { - renderRoute('password', req, res, next); +editController.password = async function (req, res, next) { + await renderRoute('password', req, res, next); }; -editController.username = function (req, res, next) { - renderRoute('username', req, res, next); +editController.username = async function (req, res, next) { + await renderRoute('username', req, res, next); }; -editController.email = function (req, res, next) { - renderRoute('email', req, res, next); +editController.email = async function (req, res, next) { + await renderRoute('email', req, res, next); }; -function renderRoute(name, req, res, next) { - async.waterfall([ - function (next) { - getUserData(req, next, next); +async function renderRoute(name, req, res, next) { + const userData = await getUserData(req, next); + if (!userData) { + return next(); + } + if (meta.config[name + ':disableEdit'] && !userData.isAdmin) { + return helpers.notAllowed(req, res); + } + + if (name === 'password') { + userData.minimumPasswordLength = meta.config.minimumPasswordLength; + userData.minimumPasswordStrength = meta.config.minimumPasswordStrength; + } + + userData.title = '[[pages:account/edit/' + name + ', ' + userData.username + ']]'; + userData.breadcrumbs = helpers.buildBreadcrumbs([ + { + text: userData.username, + url: '/user/' + userData.userslug, }, - function (userData) { - if (!userData) { - return next(); - } - - if (meta.config[name + ':disableEdit'] && !userData.isAdmin) { - return helpers.notAllowed(req, res); - } - - if (name === 'password') { - userData.minimumPasswordLength = meta.config.minimumPasswordLength; - userData.minimumPasswordStrength = meta.config.minimumPasswordStrength; - } - - userData.title = '[[pages:account/edit/' + name + ', ' + userData.username + ']]'; - userData.breadcrumbs = helpers.buildBreadcrumbs([ - { - text: userData.username, - url: '/user/' + userData.userslug, - }, - { - text: '[[user:edit]]', - url: '/user/' + userData.userslug + '/edit', - }, - { - text: '[[user:' + name + ']]', - }, - ]); - - res.render('account/edit/' + name, userData); + { + text: '[[user:edit]]', + url: '/user/' + userData.userslug + '/edit', }, - ], next); + { + text: '[[user:' + name + ']]', + }, + ]); + + res.render('account/edit/' + name, userData); } -function getUserData(req, next, callback) { - var userData; - async.waterfall([ - function (next) { - accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, next); - }, - function (data, next) { - userData = data; - if (!userData) { - return callback(null, null); - } - db.getObjectField('user:' + userData.uid, 'password', next); - }, - function (password, next) { - userData.hasPassword = !!password; - next(null, userData); - }, - ], callback); +async function getUserData(req) { + const userData = await accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid); + if (!userData) { + return null; + } + + userData.hasPassword = await user.hasPassword(userData.uid); + return userData; } -editController.uploadPicture = function (req, res, next) { - var userPhoto = req.files.files[0]; - - var updateUid; - - async.waterfall([ - function (next) { - user.getUidByUserslug(req.params.userslug, next); - }, - function (uid, next) { - updateUid = uid; - - privileges.users.canEdit(req.uid, uid, next); - }, - function (isAllowed, next) { - if (!isAllowed) { - return helpers.notAllowed(req, res); - } - user.checkMinReputation(req.uid, updateUid, 'min:rep:profile-picture', next); - }, - function (next) { - user.uploadCroppedPicture({ - uid: updateUid, - file: userPhoto, - }, next); - }, - ], function (err, image) { - file.delete(userPhoto.path); - if (err) { - return next(err); +editController.uploadPicture = async function (req, res, next) { + const userPhoto = req.files.files[0]; + try { + const updateUid = await user.getUidByUserslug(req.params.userslug); + const isAllowed = await privileges.users.canEdit(req.uid, updateUid); + if (!isAllowed) { + return helpers.notAllowed(req, res); } - + await user.checkMinReputation(req.uid, updateUid, 'min:rep:profile-picture'); + const image = await user.uploadCroppedPicture({ + uid: updateUid, + file: userPhoto, + }); res.json([{ name: userPhoto.name, url: image.url, }]); - }); + } catch (err) { + next(err); + } finally { + file.delete(userPhoto.path); + } }; -editController.uploadCoverPicture = function (req, res, next) { +editController.uploadCoverPicture = async function (req, res, next) { var params = JSON.parse(req.body.params); var coverPhoto = req.files.files[0]; - - async.waterfall([ - function (next) { - user.checkMinReputation(req.uid, params.uid, 'min:rep:cover-picture', next); - }, - function (next) { - user.updateCoverPicture({ - file: coverPhoto, - uid: params.uid, - }, next); - }, - ], function (err, image) { - file.delete(coverPhoto.path); - if (err) { - return next(err); - } + try { + await user.checkMinReputation(req.uid, params.uid, 'min:rep:cover-picture'); + const image = await user.updateCoverPicture({ + file: coverPhoto, + uid: params.uid, + }); res.json([{ url: image.url, }]); - }); + } catch (err) { + next(err); + } finally { + file.delete(coverPhoto.path); + } }; diff --git a/src/controllers/accounts/notifications.js b/src/controllers/accounts/notifications.js index 3ec4708e7a..3ec7c893c8 100644 --- a/src/controllers/accounts/notifications.js +++ b/src/controllers/accounts/notifications.js @@ -1,16 +1,14 @@ 'use strict'; -var async = require('async'); +const user = require('../../user'); +const helpers = require('../helpers'); +const plugins = require('../../plugins'); +const pagination = require('../../pagination'); -var user = require('../../user'); -var helpers = require('../helpers'); -var plugins = require('../../plugins'); -var pagination = require('../../pagination'); +const notificationsController = module.exports; -var notificationsController = module.exports; - -notificationsController.get = function (req, res, next) { - var regularFilters = [ +notificationsController.get = async function (req, res, next) { + const regularFilters = [ { name: '[[notifications:all]]', filter: '' }, { name: '[[global:topics]]', filter: 'new-topic' }, { name: '[[notifications:replies]]', filter: 'new-reply' }, @@ -19,84 +17,56 @@ notificationsController.get = function (req, res, next) { { name: '[[notifications:upvote]]', filter: 'upvote' }, ]; - var moderatorFilters = [ + const moderatorFilters = [ { name: '[[notifications:new-flags]]', filter: 'new-post-flag' }, { name: '[[notifications:my-flags]]', filter: 'my-flags' }, { name: '[[notifications:bans]]', filter: 'ban' }, ]; - var filter = req.query.filter || ''; - var page = Math.max(1, req.query.page || 1); - var itemsPerPage = 20; - var start = (page - 1) * itemsPerPage; - var stop = start + itemsPerPage - 1; - var selectedFilter; - var pageCount = 1; - var allFilters = []; + const filter = req.query.filter || ''; + const page = Math.max(1, req.query.page || 1); + const itemsPerPage = 20; + const start = (page - 1) * itemsPerPage; + const stop = start + itemsPerPage - 1; - async.waterfall([ - function (next) { - async.parallel({ - filters: function (next) { - plugins.fireHook('filter:notifications.addFilters', { - regularFilters: regularFilters, - moderatorFilters: moderatorFilters, - uid: req.uid, - }, next); - }, - isPrivileged: function (next) { - user.isPrivileged(req.uid, next); - }, - }, next); - }, - function (data, _next) { - allFilters = data.filters.regularFilters; + const [filters, isPrivileged] = await Promise.all([ + plugins.fireHook('filter:notifications.addFilters', { + regularFilters: regularFilters, + moderatorFilters: moderatorFilters, + uid: req.uid, + }), + user.isPrivileged(req.uid), + ]); - if (data.isPrivileged) { - allFilters = allFilters.concat([ - { separator: true }, - ]).concat(data.filters.moderatorFilters); - } + let allFilters = filters.regularFilters; + if (isPrivileged) { + allFilters = allFilters.concat([ + { separator: true }, + ]).concat(filters.moderatorFilters); + } + const selectedFilter = allFilters.find(function (filterData) { + filterData.selected = filterData.filter === filter; + return filterData.selected; + }); + if (!selectedFilter) { + return next(); + } + let nids = await user.notifications.getAll(req.uid, selectedFilter.filter); + const pageCount = Math.max(1, Math.ceil(nids.length / itemsPerPage)); + nids = nids.slice(start, stop + 1); - selectedFilter = allFilters.find(function (filterData) { - filterData.selected = filterData.filter === filter; - return filterData.selected; - }); - - if (!selectedFilter) { - return next(); - } - - user.notifications.getAll(req.uid, selectedFilter.filter, _next); - }, - function (nids, next) { - pageCount = Math.max(1, Math.ceil(nids.length / itemsPerPage)); - nids = nids.slice(start, stop + 1); - - user.notifications.getNotifications(nids, req.uid, next); - }, - function (notifications, next) { - plugins.fireHook('filter:notifications.get', { - notifications: notifications, - }, function (err, data) { - if (err) { - return next(err); - } - - next(null, data.notifications); - }); - }, - function (notifications) { - res.render('notifications', { - notifications: notifications, - pagination: pagination.create(page, pageCount, req.query), - filters: allFilters, - regularFilters: regularFilters, - moderatorFilters: moderatorFilters, - selectedFilter: selectedFilter, - title: '[[pages:notifications]]', - breadcrumbs: helpers.buildBreadcrumbs([{ text: '[[pages:notifications]]' }]), - }); - }, - ], next); + const notifications = await user.notifications.getNotifications(nids, req.uid); + const data = await plugins.fireHook('filter:notifications.get', { + notifications: notifications, + }); + res.render('notifications', { + notifications: data.notifications, + pagination: pagination.create(page, pageCount, req.query), + filters: allFilters, + regularFilters: regularFilters, + moderatorFilters: moderatorFilters, + selectedFilter: selectedFilter, + title: '[[pages:notifications]]', + breadcrumbs: helpers.buildBreadcrumbs([{ text: '[[pages:notifications]]' }]), + }); }; diff --git a/src/controllers/accounts/profile.js b/src/controllers/accounts/profile.js index f84d6107f5..55aeb67f26 100644 --- a/src/controllers/accounts/profile.js +++ b/src/controllers/accounts/profile.js @@ -1,23 +1,22 @@ 'use strict'; -var nconf = require('nconf'); -var async = require('async'); +const nconf = require('nconf'); const db = require('../../database'); -var user = require('../../user'); -var posts = require('../../posts'); +const user = require('../../user'); +const posts = require('../../posts'); const categories = require('../../categories'); -var plugins = require('../../plugins'); -var meta = require('../../meta'); -var accountHelpers = require('./helpers'); -var helpers = require('../helpers'); -var messaging = require('../../messaging'); -var utils = require('../../utils'); +const plugins = require('../../plugins'); +const meta = require('../../meta'); +const accountHelpers = require('./helpers'); +const helpers = require('../helpers'); +const messaging = require('../../messaging'); +const utils = require('../../utils'); -var profileController = module.exports; +const profileController = module.exports; -profileController.get = function (req, res, callback) { - var lowercaseSlug = req.params.userslug.toLowerCase(); +profileController.get = async function (req, res, next) { + const lowercaseSlug = req.params.userslug.toLowerCase(); if (req.params.userslug !== lowercaseSlug) { if (res.locals.isAPI) { @@ -27,97 +26,71 @@ profileController.get = function (req, res, callback) { } } - var userData; - async.waterfall([ - function (next) { - accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, next); - }, - function (_userData, next) { - if (!_userData) { - return callback(); - } - userData = _userData; + const userData = await accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid); + if (!userData) { + return next(); + } - if (req.uid >= 0) { - req.session.uids_viewed = req.session.uids_viewed || {}; + await incrementProfileViews(req, userData); - if (req.uid !== userData.uid && (!req.session.uids_viewed[userData.uid] || req.session.uids_viewed[userData.uid] < Date.now() - 3600000)) { - user.incrementUserFieldBy(userData.uid, 'profileviews', 1); - req.session.uids_viewed[userData.uid] = Date.now(); - } - } + const [hasPrivateChat, latestPosts, bestPosts] = await Promise.all([ + messaging.hasPrivateChat(req.uid, userData.uid), + getLatestPosts(req.uid, userData), + getBestPosts(req.uid, userData), + posts.parseSignature(userData, req.uid), + ]); - async.parallel({ - hasPrivateChat: function (next) { - messaging.hasPrivateChat(req.uid, userData.uid, next); - }, - latestPosts: function (next) { - getLatestPosts(req.uid, userData, next); - }, - bestPosts: function (next) { - getBestPosts(req.uid, userData, next); - }, - signature: function (next) { - posts.parseSignature(userData, req.uid, next); - }, - }, next); - }, - function (results, next) { - if (meta.config['reputation:disabled']) { - delete userData.reputation; - } + if (meta.config['reputation:disabled']) { + delete userData.reputation; + } - userData.posts = results.latestPosts; // for backwards compat. - userData.latestPosts = results.latestPosts; - userData.bestPosts = results.bestPosts; - userData.hasPrivateChat = results.hasPrivateChat; - userData.breadcrumbs = helpers.buildBreadcrumbs([{ text: userData.username }]); - userData.title = userData.username; - userData.allowCoverPicture = !userData.isSelf || !!meta.config['reputation:disabled'] || userData.reputation >= meta.config['min:rep:cover-picture']; + userData.posts = latestPosts; // for backwards compat. + userData.latestPosts = latestPosts; + userData.bestPosts = bestPosts; + userData.hasPrivateChat = hasPrivateChat; + userData.breadcrumbs = helpers.buildBreadcrumbs([{ text: userData.username }]); + userData.title = userData.username; + userData.allowCoverPicture = !userData.isSelf || !!meta.config['reputation:disabled'] || userData.reputation >= meta.config['min:rep:cover-picture']; - if (!userData.profileviews) { - userData.profileviews = 1; - } + if (!userData.profileviews) { + userData.profileviews = 1; + } - addMetaTags(res, userData); + addMetaTags(res, userData); - userData.selectedGroup = userData.groups.filter(function (group) { - return group && userData.groupTitleArray.includes(group.name); - }); + userData.selectedGroup = userData.groups.filter(function (group) { + return group && userData.groupTitleArray.includes(group.name); + }); - plugins.fireHook('filter:user.account', { userData: userData, uid: req.uid }, next); - }, - function (results) { - res.render('account/profile', results.userData); - }, - ], callback); + const results = await plugins.fireHook('filter:user.account', { userData: userData, uid: req.uid }); + res.render('account/profile', results.userData); }; -function getLatestPosts(callerUid, userData, callback) { - getPosts(callerUid, userData, 'pids', callback); +async function incrementProfileViews(req, userData) { + if (req.uid >= 0) { + req.session.uids_viewed = req.session.uids_viewed || {}; + + if (req.uid !== userData.uid && (!req.session.uids_viewed[userData.uid] || req.session.uids_viewed[userData.uid] < Date.now() - 3600000)) { + await user.incrementUserFieldBy(userData.uid, 'profileviews', 1); + req.session.uids_viewed[userData.uid] = Date.now(); + } + } } -function getBestPosts(callerUid, userData, callback) { - getPosts(callerUid, userData, 'pids:votes', callback); +async function getLatestPosts(callerUid, userData) { + return await getPosts(callerUid, userData, 'pids'); } -function getPosts(callerUid, userData, setSuffix, callback) { - async.waterfall([ - function (next) { - categories.getCidsByPrivilege('categories:cid', callerUid, 'topics:read', next); - }, - function (cids, next) { - const keys = cids.map(c => 'cid:' + c + ':uid:' + userData.uid + ':' + setSuffix); - db.getSortedSetRevRange(keys, 0, 9, next); - }, - function (pids, next) { - posts.getPostSummaryByPids(pids, callerUid, { stripTags: false }, next); - }, - function (posts, next) { - posts = posts.filter(p => p && !p.deleted); - next(null, posts); - }, - ], callback); +async function getBestPosts(callerUid, userData) { + return await getPosts(callerUid, userData, 'pids:votes'); +} + +async function getPosts(callerUid, userData, setSuffix) { + const cids = await categories.getCidsByPrivilege('categories:cid', callerUid, 'topics:read'); + const keys = cids.map(c => 'cid:' + c + ':uid:' + userData.uid + ':' + setSuffix); + const pids = await db.getSortedSetRevRange(keys, 0, 9); + const postData = await posts.getPostSummaryByPids(pids, callerUid, { stripTags: false }); + return postData.filter(p => p && !p.deleted); } function addMetaTags(res, userData) { diff --git a/src/controllers/accounts/sessions.js b/src/controllers/accounts/sessions.js index 1609fa2383..fbea034afc 100644 --- a/src/controllers/accounts/sessions.js +++ b/src/controllers/accounts/sessions.js @@ -1,80 +1,59 @@ 'use strict'; -var async = require('async'); +const util = require('util'); -var db = require('../../database'); -var user = require('../../user'); -var helpers = require('../helpers'); -var accountHelpers = require('./helpers'); +const db = require('../../database'); +const user = require('../../user'); +const helpers = require('../helpers'); +const accountHelpers = require('./helpers'); -var sessionController = module.exports; +const sessionController = module.exports; -sessionController.get = function (req, res, callback) { - var userData; - - async.waterfall([ - function (next) { - accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, next); - }, - function (_userData, next) { - userData = _userData; - if (!userData) { - return callback(); - } - - user.auth.getSessions(userData.uid, req.sessionID, next); - }, - function (sessions) { - userData.sessions = sessions; - - userData.title = '[[pages:account/sessions]]'; - userData.breadcrumbs = helpers.buildBreadcrumbs([{ text: userData.username, url: '/user/' + userData.userslug }, { text: '[[pages:account/sessions]]' }]); - - res.render('account/sessions', userData); - }, - ], callback); -}; - -sessionController.revoke = function (req, res, next) { - if (!req.params.hasOwnProperty('uuid')) { +sessionController.get = async function (req, res, next) { + const userData = await accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid); + if (!userData) { return next(); } - var _id; - var uid = res.locals.uid; - async.waterfall([ - function (next) { - if (!uid) { - return next(new Error('[[error:no-session-found]]')); - } - db.getSortedSetRange('uid:' + uid + ':sessions', 0, -1, next); - }, - function (sids, done) { - async.eachSeries(sids, function (sid, next) { - db.sessionStore.get(sid, function (err, sessionObj) { - if (err) { - return next(err); - } - if (sessionObj && sessionObj.meta && sessionObj.meta.uuid === req.params.uuid) { - _id = sid; - done(); - } else { - next(); - } - }); - }, next); - }, - function (next) { - if (!_id) { - return next(new Error('[[error:no-session-found]]')); - } + userData.sessions = await user.auth.getSessions(userData.uid, req.sessionID); + userData.title = '[[pages:account/sessions]]'; + userData.breadcrumbs = helpers.buildBreadcrumbs([{ text: userData.username, url: '/user/' + userData.userslug }, { text: '[[pages:account/sessions]]' }]); - user.auth.revokeSession(_id, uid, next); - }, - ], function (err) { - if (err) { - return res.status(500).send(err.message); - } - return res.sendStatus(200); - }); + res.render('account/sessions', userData); +}; + +const getSessionAsync = util.promisify(function (sid, callback) { + db.sessionStore.get(sid, (err, sessionObj) => callback(err, sessionObj || null)); +}); + +sessionController.revoke = async function (req, res, next) { + if (!req.params.hasOwnProperty('uuid')) { + return next(); + } + try { + const uid = await user.getUidByUserslug(req.params.userslug); + if (!uid) { + throw new Error('[[error:no-session-found]]'); + } + const sids = await db.getSortedSetRange('uid:' + uid + ':sessions', 0, -1); + let _id; + for (const sid of sids) { + /* eslint-disable no-await-in-loop */ + const sessionObj = await getSessionAsync(sid); + if (sessionObj && sessionObj.meta && sessionObj.meta.uuid === req.params.uuid) { + _id = sid; + break; + } + } + + if (!_id) { + throw new Error('[[error:no-session-found]]'); + } + + await user.auth.revokeSession(_id, uid); + } catch (err) { + return res.status(500).send(err.message); + } + + res.sendStatus(200); }; diff --git a/src/controllers/accounts/settings.js b/src/controllers/accounts/settings.js index 991b0a8e24..1f0645fd77 100644 --- a/src/controllers/accounts/settings.js +++ b/src/controllers/accounts/settings.js @@ -1,356 +1,306 @@ 'use strict'; -var async = require('async'); -var nconf = require('nconf'); -var winston = require('winston'); -var _ = require('lodash'); -var jwt = require('jsonwebtoken'); +const nconf = require('nconf'); +const winston = require('winston'); +const _ = require('lodash'); +const jwt = require('jsonwebtoken'); +const util = require('util'); -var user = require('../../user'); -var languages = require('../../languages'); -var meta = require('../../meta'); -var plugins = require('../../plugins'); -var privileges = require('../../privileges'); -var categories = require('../../categories'); -var notifications = require('../../notifications'); -var db = require('../../database'); -var helpers = require('../helpers'); -var accountHelpers = require('./helpers'); +const user = require('../../user'); +const languages = require('../../languages'); +const meta = require('../../meta'); +const plugins = require('../../plugins'); +const privileges = require('../../privileges'); +const categories = require('../../categories'); +const notifications = require('../../notifications'); +const db = require('../../database'); +const helpers = require('../helpers'); +const accountHelpers = require('./helpers'); -var settingsController = module.exports; +const settingsController = module.exports; -settingsController.get = function (req, res, callback) { - var userData; - async.waterfall([ - function (next) { - accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, next); - }, - function (_userData, next) { - userData = _userData; - if (!userData) { - return callback(); - } - async.parallel({ - settings: function (next) { - user.getSettings(userData.uid, next); - }, - languages: function (next) { - languages.list(next); - }, - soundsMapping: function (next) { - meta.sounds.getUserSoundMap(userData.uid, next); - }, - }, next); - }, - function (results, next) { - userData.settings = results.settings; - userData.languages = results.languages; - if (userData.isAdmin && userData.isSelf) { - userData.acpLanguages = _.cloneDeep(results.languages); - } +settingsController.get = async function (req, res, next) { + const userData = await accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid); + if (!userData) { + return next(); + } + const [settings, languagesData, soundsMapping] = await Promise.all([ + user.getSettings(userData.uid), + languages.list(), + meta.sounds.getUserSoundMap(userData.uid), - var types = [ - 'notification', - 'chat-incoming', - 'chat-outgoing', - ]; - var aliases = { - notification: 'notificationSound', - 'chat-incoming': 'incomingChatSound', - 'chat-outgoing': 'outgoingChatSound', - }; + ]); - types.forEach(function (type) { - var soundpacks = plugins.soundpacks.map(function (pack) { - var sounds = Object.keys(pack.sounds).map(function (soundName) { - var value = pack.name + ' | ' + soundName; - return { - name: soundName, - value: value, - selected: value === results.soundsMapping[type], - }; - }); + userData.settings = settings; + userData.languages = languagesData; + if (userData.isAdmin && userData.isSelf) { + userData.acpLanguages = _.cloneDeep(languagesData); + } - return { - name: pack.name, - sounds: sounds, - }; - }); + addSoundSettings(userData, soundsMapping); - userData[type + '-sound'] = soundpacks; - // fallback - userData[aliases[type]] = soundpacks.concat.apply([], soundpacks.map(function (pack) { - return pack.sounds.map(function (sound) { - return { - name: sound.value, - selected: sound.selected, - }; - }); - })); - }); + const data = await plugins.fireHook('filter:user.customSettings', { + settings: settings, + customSettings: [], + uid: req.uid, + }); - plugins.fireHook('filter:user.customSettings', { settings: results.settings, customSettings: [], uid: req.uid }, next); - }, - function (data, next) { - userData.customSettings = data.customSettings; - async.parallel({ - notificationSettings: function (next) { - getNotificationSettings(userData, next); - }, - routes: function (next) { - getHomePageRoutes(userData, next); - }, - }, next); - }, - function (results) { - userData.homePageRoutes = results.routes; - userData.notificationSettings = results.notificationSettings; - userData.disableEmailSubscriptions = meta.config.disableEmailSubscriptions; + const [notificationSettings, routes] = await Promise.all([ + getNotificationSettings(userData), + getHomePageRoutes(userData), + ]); - userData.dailyDigestFreqOptions = [ - { value: 'off', name: '[[user:digest_off]]', selected: userData.settings.dailyDigestFreq === 'off' }, - { value: 'day', name: '[[user:digest_daily]]', selected: userData.settings.dailyDigestFreq === 'day' }, - { value: 'week', name: '[[user:digest_weekly]]', selected: userData.settings.dailyDigestFreq === 'week' }, - { value: 'month', name: '[[user:digest_monthly]]', selected: userData.settings.dailyDigestFreq === 'month' }, - ]; + userData.customSettings = data.customSettings; + userData.homePageRoutes = routes; + userData.notificationSettings = notificationSettings; + userData.disableEmailSubscriptions = meta.config.disableEmailSubscriptions; - userData.bootswatchSkinOptions = [ - { name: 'Default', value: '' }, - { name: 'Cerulean', value: 'cerulean' }, - { name: 'Cosmo', value: 'cosmo' }, - { name: 'Cyborg', value: 'cyborg' }, - { name: 'Darkly', value: 'darkly' }, - { name: 'Flatly', value: 'flatly' }, - { name: 'Journal', value: 'journal' }, - { name: 'Lumen', value: 'lumen' }, - { name: 'Paper', value: 'paper' }, - { name: 'Readable', value: 'readable' }, - { name: 'Sandstone', value: 'sandstone' }, - { name: 'Simplex', value: 'simplex' }, - { name: 'Slate', value: 'slate' }, - { name: 'Spacelab', value: 'spacelab' }, - { name: 'Superhero', value: 'superhero' }, - { name: 'United', value: 'united' }, - { name: 'Yeti', value: 'yeti' }, - ]; + userData.dailyDigestFreqOptions = [ + { value: 'off', name: '[[user:digest_off]]', selected: userData.settings.dailyDigestFreq === 'off' }, + { value: 'day', name: '[[user:digest_daily]]', selected: userData.settings.dailyDigestFreq === 'day' }, + { value: 'week', name: '[[user:digest_weekly]]', selected: userData.settings.dailyDigestFreq === 'week' }, + { value: 'month', name: '[[user:digest_monthly]]', selected: userData.settings.dailyDigestFreq === 'month' }, + ]; - userData.bootswatchSkinOptions.forEach(function (skin) { - skin.selected = skin.value === userData.settings.bootswatchSkin; - }); + userData.bootswatchSkinOptions = [ + { name: 'Default', value: '' }, + { name: 'Cerulean', value: 'cerulean' }, + { name: 'Cosmo', value: 'cosmo' }, + { name: 'Cyborg', value: 'cyborg' }, + { name: 'Darkly', value: 'darkly' }, + { name: 'Flatly', value: 'flatly' }, + { name: 'Journal', value: 'journal' }, + { name: 'Lumen', value: 'lumen' }, + { name: 'Paper', value: 'paper' }, + { name: 'Readable', value: 'readable' }, + { name: 'Sandstone', value: 'sandstone' }, + { name: 'Simplex', value: 'simplex' }, + { name: 'Slate', value: 'slate' }, + { name: 'Spacelab', value: 'spacelab' }, + { name: 'Superhero', value: 'superhero' }, + { name: 'United', value: 'united' }, + { name: 'Yeti', value: 'yeti' }, + ]; - userData.languages.forEach(function (language) { - language.selected = language.code === userData.settings.userLang; - }); + userData.bootswatchSkinOptions.forEach(function (skin) { + skin.selected = skin.value === userData.settings.bootswatchSkin; + }); - if (userData.isAdmin && userData.isSelf) { - userData.acpLanguages.forEach(function (language) { - language.selected = language.code === userData.settings.acpLang; - }); - } + userData.languages.forEach(function (language) { + language.selected = language.code === userData.settings.userLang; + }); - var notifFreqOptions = [ - 'all', - 'first', - 'everyTen', - 'threshold', - 'logarithmic', - 'disabled', - ]; + if (userData.isAdmin && userData.isSelf) { + userData.acpLanguages.forEach(function (language) { + language.selected = language.code === userData.settings.acpLang; + }); + } - userData.upvoteNotifFreq = notifFreqOptions.map(function (name) { + const notifFreqOptions = [ + 'all', + 'first', + 'everyTen', + 'threshold', + 'logarithmic', + 'disabled', + ]; + + userData.upvoteNotifFreq = notifFreqOptions.map(name => ({ name: name, selected: name === userData.settings.upvoteNotifFreq })); + + userData.categoryWatchState = { [userData.settings.categoryWatchState]: true }; + + userData.disableCustomUserSkins = meta.config.disableCustomUserSkins; + + userData.allowUserHomePage = meta.config.allowUserHomePage; + + userData.hideFullname = meta.config.hideFullname; + userData.hideEmail = meta.config.hideEmail; + + userData.inTopicSearchAvailable = plugins.hasListeners('filter:topic.search'); + + userData.maxTopicsPerPage = meta.config.maxTopicsPerPage; + userData.maxPostsPerPage = meta.config.maxPostsPerPage; + + userData.title = '[[pages:account/settings]]'; + userData.breadcrumbs = helpers.buildBreadcrumbs([{ text: userData.username, url: '/user/' + userData.userslug }, { text: '[[user:settings]]' }]); + + res.render('account/settings', userData); +}; + +function addSoundSettings(userData, soundsMapping) { + const types = [ + 'notification', + 'chat-incoming', + 'chat-outgoing', + ]; + const aliases = { + notification: 'notificationSound', + 'chat-incoming': 'incomingChatSound', + 'chat-outgoing': 'outgoingChatSound', + }; + + types.forEach(function (type) { + const soundpacks = plugins.soundpacks.map(function (pack) { + const sounds = Object.keys(pack.sounds).map(function (soundName) { + const value = pack.name + ' | ' + soundName; return { - name: name, - selected: name === userData.settings.upvoteNotifFreq, + name: soundName, + value: value, + selected: value === soundsMapping[type], }; }); - userData.categoryWatchState = { [userData.settings.categoryWatchState]: true }; + return { + name: pack.name, + sounds: sounds, + }; + }); - userData.disableCustomUserSkins = meta.config.disableCustomUserSkins; + userData[type + '-sound'] = soundpacks; + // fallback + userData[aliases[type]] = soundpacks.concat.apply([], soundpacks.map(function (pack) { + return pack.sounds.map(function (sound) { + return { + name: sound.value, + selected: sound.selected, + }; + }); + })); + }); +} - userData.allowUserHomePage = meta.config.allowUserHomePage; +const jwtVerifyAsync = util.promisify(function (token, callback) { + jwt.verify(token, nconf.get('secret'), (err, payload) => callback(err, payload)); +}); - userData.hideFullname = meta.config.hideFullname; - userData.hideEmail = meta.config.hideEmail; - - userData.inTopicSearchAvailable = plugins.hasListeners('filter:topic.search'); - - userData.maxTopicsPerPage = meta.config.maxTopicsPerPage; - userData.maxPostsPerPage = meta.config.maxPostsPerPage; - - userData.title = '[[pages:account/settings]]'; - userData.breadcrumbs = helpers.buildBreadcrumbs([{ text: userData.username, url: '/user/' + userData.userslug }, { text: '[[user:settings]]' }]); - - res.render('account/settings', userData); - }, - ], callback); -}; - -settingsController.unsubscribe = function (req, res) { +settingsController.unsubscribe = async function (req, res) { if (!req.params.token) { return res.sendStatus(404); } - - jwt.verify(req.params.token, nconf.get('secret'), function (err, payload) { - if (err) { - return res.sendStatus(403); + let payload; + try { + payload = await jwtVerifyAsync(req.params.token); + if (!payload || (payload.template !== 'notification' && payload.template !== 'digest')) { + return res.sendStatus(404); } - - switch (payload.template) { - case 'digest': - async.parallel([ - async.apply(user.setSetting, payload.uid, 'dailyDigestFreq', 'off'), - async.apply(user.updateDigestSetting, payload.uid, 'off'), - ], function (err) { - if (err) { - winston.error('[settings/unsubscribe] One-click unsubscribe failed with error: ' + err.message); - return res.sendStatus(500); - } - - return res.sendStatus(200); - }); - break; - case 'notification': - async.waterfall([ - async.apply(db.getObjectField, 'user:' + payload.uid + ':settings', 'notificationType_' + payload.type), - (current, next) => { - user.setSetting(payload.uid, 'notificationType_' + payload.type, (current === 'notificationemail' ? 'notification' : 'none'), next); - }, - ], function (err) { - if (err) { - winston.error('[settings/unsubscribe] One-click unsubscribe failed with error: ' + err.message); - return res.sendStatus(500); - } - - return res.sendStatus(200); - }); - break; - default: - res.sendStatus(404); - break; + } catch (err) { + return res.sendStatus(403); + } + try { + if (payload.template === 'digest') { + await Promise.all([ + user.setSetting(payload.uid, 'dailyDigestFreq', 'off'), + user.updateDigestSetting(payload.uid, 'off'), + ]); + } else if (payload.template === 'notification') { + const current = await db.getObjectField('user:' + payload.uid + ':settings', 'notificationType_' + payload.type); + await user.setSetting(payload.uid, 'notificationType_' + payload.type, (current === 'notificationemail' ? 'notification' : 'none')); } - }); + res.sendStatus(200); + } catch (err) { + winston.error('[settings/unsubscribe] One-click unsubscribe failed with error: ' + err.message); + res.sendStatus(500); + } }; -function getNotificationSettings(userData, callback) { - var privilegedTypes = []; +async function getNotificationSettings(userData) { + const privilegedTypes = []; - async.waterfall([ - function (next) { - user.getPrivileges(userData.uid, next); - }, - function (privileges, next) { - if (privileges.isAdmin) { - privilegedTypes.push('notificationType_new-register'); - } - if (privileges.isAdmin || privileges.isGlobalMod || privileges.isModeratorOfAnyCategory) { - privilegedTypes.push('notificationType_post-queue', 'notificationType_new-post-flag'); - } - if (privileges.isAdmin || privileges.isGlobalMod) { - privilegedTypes.push('notificationType_new-user-flag'); - } - plugins.fireHook('filter:user.notificationTypes', { - types: notifications.baseTypes.slice(), - privilegedTypes: privilegedTypes, - }, next); - }, - function (results, next) { - function modifyType(type) { - var setting = userData.settings[type]; + const privileges = await user.getPrivileges(userData.uid); + if (privileges.isAdmin) { + privilegedTypes.push('notificationType_new-register'); + } + if (privileges.isAdmin || privileges.isGlobalMod || privileges.isModeratorOfAnyCategory) { + privilegedTypes.push('notificationType_post-queue', 'notificationType_new-post-flag'); + } + if (privileges.isAdmin || privileges.isGlobalMod) { + privilegedTypes.push('notificationType_new-user-flag'); + } + const results = await plugins.fireHook('filter:user.notificationTypes', { + types: notifications.baseTypes.slice(), + privilegedTypes: privilegedTypes, + }); - return { - name: type, - label: '[[notifications:' + type + ']]', - none: setting === 'none', - notification: setting === 'notification', - email: setting === 'email', - notificationemail: setting === 'notificationemail', - }; - } + function modifyType(type) { + const setting = userData.settings[type]; + return { + name: type, + label: '[[notifications:' + type + ']]', + none: setting === 'none', + notification: setting === 'notification', + email: setting === 'email', + notificationemail: setting === 'notificationemail', + }; + } - if (meta.config.disableChat) { - results.types = results.types.filter(type => type !== 'notificationType_new-chat'); - } + if (meta.config.disableChat) { + results.types = results.types.filter(type => type !== 'notificationType_new-chat'); + } - var notificationSettings = results.types.map(modifyType).concat(results.privilegedTypes.map(modifyType)); - next(null, notificationSettings); - }, - ], callback); + return results.types.map(modifyType).concat(results.privilegedTypes.map(modifyType)); } -function getHomePageRoutes(userData, callback) { - async.waterfall([ - function (next) { - db.getSortedSetRange('cid:0:children', 0, -1, next); +async function getHomePageRoutes(userData) { + let cids = await categories.getAllCidsFromSet('cid:0:children'); + cids = await privileges.categories.filterCids('find', cids, userData.uid); + let categoryData = await categories.getCategoriesFields(cids, ['name', 'slug']); + + categoryData = categoryData.map(function (category) { + return { + route: 'category/' + category.slug, + name: 'Category: ' + category.name, + }; + }); + + const data = await plugins.fireHook('filter:homepage.get', { routes: [ + { + route: 'categories', + name: 'Categories', }, - function (cids, next) { - privileges.categories.filterCids('find', cids, 0, next); + { + route: 'unread', + name: 'Unread', }, - function (cids, next) { - categories.getCategoriesFields(cids, ['name', 'slug'], next); + { + route: 'recent', + name: 'Recent', }, - function (categoryData, next) { - categoryData = categoryData.map(function (category) { - return { - route: 'category/' + category.slug, - name: 'Category: ' + category.name, - }; - }); - - categoryData = categoryData || []; - - plugins.fireHook('filter:homepage.get', { routes: [ - { - route: 'categories', - name: 'Categories', - }, - { - route: 'unread', - name: 'Unread', - }, - { - route: 'recent', - name: 'Recent', - }, - { - route: 'top', - name: 'Top', - }, - { - route: 'popular', - name: 'Popular', - }, - ].concat(categoryData, [ - { - route: 'custom', - name: 'Custom', - }, - ]) }, next); + { + route: 'top', + name: 'Top', }, - function (data, next) { - // Set selected for each route - var customIdx; - var hasSelected = false; - data.routes = data.routes.map(function (route, idx) { - if (route.route === userData.settings.homePageRoute) { - route.selected = true; - hasSelected = true; - } else { - route.selected = false; - } - - if (route.route === 'custom') { - customIdx = idx; - } - - return route; - }); - - if (!hasSelected && customIdx && userData.settings.homePageRoute !== 'none') { - data.routes[customIdx].selected = true; - } - - next(null, data.routes); + { + route: 'popular', + name: 'Popular', }, - ], callback); + ].concat(categoryData, [ + { + route: 'custom', + name: 'Custom', + }, + ]) }); + + // Set selected for each route + var customIdx; + var hasSelected = false; + data.routes = data.routes.map(function (route, idx) { + if (route.route === userData.settings.homePageRoute) { + route.selected = true; + hasSelected = true; + } else { + route.selected = false; + } + + if (route.route === 'custom') { + customIdx = idx; + } + + return route; + }); + + if (!hasSelected && customIdx && userData.settings.homePageRoute !== 'none') { + data.routes[customIdx].selected = true; + } + + return data.routes; } diff --git a/src/controllers/accounts/uploads.js b/src/controllers/accounts/uploads.js index 899ed866b5..02cc1e9ca7 100644 --- a/src/controllers/accounts/uploads.js +++ b/src/controllers/accounts/uploads.js @@ -1,57 +1,40 @@ 'use strict'; +const nconf = require('nconf'); -var async = require('async'); -var nconf = require('nconf'); +const db = require('../../database'); +const helpers = require('../helpers'); +const meta = require('../../meta'); +const pagination = require('../../pagination'); +const accountHelpers = require('./helpers'); -var db = require('../../database'); -var helpers = require('../helpers'); -var meta = require('../../meta'); -var pagination = require('../../pagination'); -var accountHelpers = require('./helpers'); +const uploadsController = module.exports; -var uploadsController = module.exports; +uploadsController.get = async function (req, res, next) { + const userData = await accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid); + if (!userData) { + return next(); + } -uploadsController.get = function (req, res, callback) { - var userData; + const page = Math.max(1, parseInt(req.query.page, 10) || 1); + const itemsPerPage = 25; + const start = (page - 1) * itemsPerPage; + const stop = start + itemsPerPage - 1; + const [itemCount, uploadNames] = await Promise.all([ + db.sortedSetCard('uid:' + userData.uid + ':uploads'), + db.getSortedSetRevRange('uid:' + userData.uid + ':uploads', start, stop), + ]); - var page = Math.max(1, parseInt(req.query.page, 10) || 1); - var itemsPerPage = 25; - - async.waterfall([ - function (next) { - accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, next); - }, - function (_userData, next) { - userData = _userData; - if (!userData) { - return callback(); - } - - var start = (page - 1) * itemsPerPage; - var stop = start + itemsPerPage - 1; - async.parallel({ - itemCount: function (next) { - db.sortedSetCard('uid:' + userData.uid + ':uploads', next); - }, - uploadNames: function (next) { - db.getSortedSetRevRange('uid:' + userData.uid + ':uploads', start, stop, next); - }, - }, next); - }, - function (results) { - userData.uploads = results.uploadNames.map(function (uploadName) { - return { - name: uploadName, - url: nconf.get('upload_url') + uploadName, - }; - }); - var pageCount = Math.ceil(results.itemCount / itemsPerPage); - userData.pagination = pagination.create(page, pageCount, req.query); - userData.privateUploads = meta.config.privateUploads === 1; - userData.title = '[[pages:account/uploads, ' + userData.username + ']]'; - userData.breadcrumbs = helpers.buildBreadcrumbs([{ text: userData.username, url: '/user/' + userData.userslug }, { text: '[[global:uploads]]' }]); - res.render('account/uploads', userData); - }, - ], callback); + userData.uploads = uploadNames.map(function (uploadName) { + return { + name: uploadName, + url: nconf.get('upload_url') + uploadName, + }; + }); + const pageCount = Math.ceil(itemCount / itemsPerPage); + userData.pagination = pagination.create(page, pageCount, req.query); + userData.privateUploads = meta.config.privateUploads === 1; + userData.title = '[[pages:account/uploads, ' + userData.username + ']]'; + userData.breadcrumbs = helpers.buildBreadcrumbs([{ text: userData.username, url: '/user/' + userData.userslug }, { text: '[[global:uploads]]' }]); + res.render('account/uploads', userData); }; diff --git a/src/plugins/hooks.js b/src/plugins/hooks.js index f5934add23..0adeadb8e9 100644 --- a/src/plugins/hooks.js +++ b/src/plugins/hooks.js @@ -7,6 +7,9 @@ const utils = require('../utils'); module.exports = function (Plugins) { Plugins.deprecatedHooks = { 'filter:controllers.topic.get': 'filter:topic.build', + 'filter:user.account': 'filter:account/profile.build', + 'filter:user.account.edit': 'filter:account/edit.build', + 'filter:notifications.get': 'filter:notifications.build', }; Plugins.internals = {