| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494 |
- const UserHandler = require('./UserHandler')
- const UserDeleter = require('./UserDeleter')
- const UserGetter = require('./UserGetter')
- const { User } = require('../../models/User')
- const NewsletterManager = require('../Newsletter/NewsletterManager')
- const UserRegistrationHandler = require('./UserRegistrationHandler')
- const logger = require('@overleaf/logger')
- const metrics = require('@overleaf/metrics')
- const AuthenticationManager = require('../Authentication/AuthenticationManager')
- const SessionManager = require('../Authentication/SessionManager')
- const Features = require('../../infrastructure/Features')
- const UserAuditLogHandler = require('./UserAuditLogHandler')
- const UserSessionsManager = require('./UserSessionsManager')
- const UserUpdater = require('./UserUpdater')
- const Errors = require('../Errors/Errors')
- const HttpErrorHandler = require('../Errors/HttpErrorHandler')
- const OError = require('@overleaf/o-error')
- const EmailHandler = require('../Email/EmailHandler')
- const UrlHelper = require('../Helpers/UrlHelper')
- const { promisify } = require('util')
- const { expressify } = require('../../util/promises')
- const {
- acceptsJson,
- } = require('../../infrastructure/RequestContentTypeDetection')
- async function _sendSecurityAlertClearedSessions(user) {
- const emailOptions = {
- to: user.email,
- actionDescribed: `active sessions were cleared on your account ${user.email}`,
- action: 'active sessions cleared',
- }
- try {
- await EmailHandler.promises.sendEmail('securityAlert', emailOptions)
- } catch (error) {
- // log error when sending security alert email but do not pass back
- logger.error(
- { error, userId: user._id },
- 'could not send security alert email when sessions cleared'
- )
- }
- }
- function _sendSecurityAlertPasswordChanged(user) {
- const emailOptions = {
- to: user.email,
- actionDescribed: `your password has been changed on your account ${user.email}`,
- action: 'password changed',
- }
- EmailHandler.sendEmail('securityAlert', emailOptions, error => {
- if (error) {
- // log error when sending security alert email but do not pass back
- logger.error(
- { error, userId: user._id },
- 'could not send security alert email when password changed'
- )
- }
- })
- }
- async function _ensureAffiliation(userId, emailData) {
- if (emailData.samlProviderId) {
- await UserUpdater.promises.confirmEmail(userId, emailData.email)
- } else {
- await UserUpdater.promises.addAffiliationForNewUser(userId, emailData.email)
- }
- }
- async function changePassword(req, res, next) {
- metrics.inc('user.password-change')
- const userId = SessionManager.getLoggedInUserId(req.session)
- const user = await AuthenticationManager.promises.authenticate(
- { _id: userId },
- req.body.currentPassword
- )
- if (!user) {
- return HttpErrorHandler.badRequest(req, res, 'Your old password is wrong')
- }
- if (req.body.newPassword1 !== req.body.newPassword2) {
- return HttpErrorHandler.badRequest(
- req,
- res,
- req.i18n.translate('password_change_passwords_do_not_match')
- )
- }
- try {
- await AuthenticationManager.promises.setUserPassword(
- user,
- req.body.newPassword1
- )
- } catch (error) {
- if (error.name === 'InvalidPasswordError') {
- return HttpErrorHandler.badRequest(req, res, error.message)
- } else {
- throw error
- }
- }
- await UserAuditLogHandler.promises.addEntry(
- user._id,
- 'update-password',
- user._id,
- req.ip
- )
- // no need to wait, errors are logged and not passed back
- _sendSecurityAlertPasswordChanged(user)
- await UserSessionsManager.promises.revokeAllUserSessions(user, [
- req.sessionID,
- ])
- return res.json({
- message: {
- type: 'success',
- email: user.email,
- text: req.i18n.translate('password_change_successful'),
- },
- })
- }
- async function clearSessions(req, res, next) {
- metrics.inc('user.clear-sessions')
- const userId = SessionManager.getLoggedInUserId(req.session)
- const user = await UserGetter.promises.getUser(userId, { email: 1 })
- const sessions = await UserSessionsManager.promises.getAllUserSessions(user, [
- req.sessionID,
- ])
- await UserAuditLogHandler.promises.addEntry(
- user._id,
- 'clear-sessions',
- user._id,
- req.ip,
- { sessions }
- )
- await UserSessionsManager.promises.revokeAllUserSessions(user, [
- req.sessionID,
- ])
- await _sendSecurityAlertClearedSessions(user)
- res.sendStatus(201)
- }
- async function ensureAffiliation(user) {
- if (!Features.hasFeature('affiliations')) {
- return
- }
- const flaggedEmails = user.emails.filter(email => email.affiliationUnchecked)
- if (flaggedEmails.length === 0) {
- return
- }
- if (flaggedEmails.length > 1) {
- logger.error(
- { userId: user._id },
- `Unexpected number of flagged emails: ${flaggedEmails.length}`
- )
- }
- await _ensureAffiliation(user._id, flaggedEmails[0])
- }
- async function ensureAffiliationMiddleware(req, res, next) {
- let user
- if (!Features.hasFeature('affiliations') || !req.query.ensureAffiliation) {
- return next()
- }
- const userId = SessionManager.getLoggedInUserId(req.session)
- try {
- user = await UserGetter.promises.getUser(userId)
- } catch (error) {
- return new Errors.UserNotFoundError({ info: { userId } })
- }
- try {
- await ensureAffiliation(user)
- } catch (error) {
- return next(error)
- }
- return next()
- }
- const UserController = {
- clearSessions: expressify(clearSessions),
- tryDeleteUser(req, res, next) {
- const userId = SessionManager.getLoggedInUserId(req.session)
- const { password } = req.body
- if (password == null || password === '') {
- logger.err(
- { userId },
- 'no password supplied for attempt to delete account'
- )
- return res.sendStatus(403)
- }
- AuthenticationManager.authenticate(
- { _id: userId },
- password,
- (err, user) => {
- if (err != null) {
- OError.tag(
- err,
- 'error authenticating during attempt to delete account',
- {
- userId,
- }
- )
- return next(err)
- }
- if (!user) {
- logger.err({ userId }, 'auth failed during attempt to delete account')
- return res.sendStatus(403)
- }
- UserDeleter.deleteUser(
- userId,
- { deleterUser: user, ipAddress: req.ip },
- err => {
- if (err) {
- const errorData = {
- message: 'error while deleting user account',
- info: { userId },
- }
- if (err instanceof Errors.SubscriptionAdminDeletionError) {
- // set info.public.error for JSON response so frontend can display
- // a specific message
- errorData.info.public = {
- error: 'SubscriptionAdminDeletionError',
- }
- logger.warn(OError.tag(err, errorData.message, errorData.info))
- return HttpErrorHandler.unprocessableEntity(
- req,
- res,
- errorData.message,
- errorData.info.public
- )
- } else {
- return next(OError.tag(err, errorData.message, errorData.info))
- }
- }
- const sessionId = req.sessionID
- if (typeof req.logout === 'function') {
- req.logout()
- }
- req.session.destroy(err => {
- if (err != null) {
- OError.tag(err, 'error destroying session')
- return next(err)
- }
- UserSessionsManager.untrackSession(user, sessionId, () => {})
- res.sendStatus(200)
- })
- }
- )
- }
- )
- },
- unsubscribe(req, res, next) {
- const userId = SessionManager.getLoggedInUserId(req.session)
- UserGetter.getUser(userId, (err, user) => {
- if (err != null) {
- return next(err)
- }
- NewsletterManager.unsubscribe(user, err => {
- if (err != null) {
- logger.warn(
- { err, user },
- 'Failed to unsubscribe user from newsletter'
- )
- }
- res.sendStatus(200)
- })
- })
- },
- updateUserSettings(req, res, next) {
- const userId = SessionManager.getLoggedInUserId(req.session)
- User.findById(userId, (err, user) => {
- if (err != null || user == null) {
- logger.err({ err, userId }, 'problem updaing user settings')
- return res.sendStatus(500)
- }
- if (req.body.first_name != null) {
- user.first_name = req.body.first_name.trim()
- }
- if (req.body.last_name != null) {
- user.last_name = req.body.last_name.trim()
- }
- if (req.body.role != null) {
- user.role = req.body.role.trim()
- }
- if (req.body.institution != null) {
- user.institution = req.body.institution.trim()
- }
- if (req.body.mode != null) {
- user.ace.mode = req.body.mode
- }
- if (req.body.editorTheme != null) {
- user.ace.theme = req.body.editorTheme
- }
- if (req.body.overallTheme != null) {
- user.ace.overallTheme = req.body.overallTheme
- }
- if (req.body.fontSize != null) {
- user.ace.fontSize = req.body.fontSize
- }
- if (req.body.autoComplete != null) {
- user.ace.autoComplete = req.body.autoComplete
- }
- if (req.body.autoPairDelimiters != null) {
- user.ace.autoPairDelimiters = req.body.autoPairDelimiters
- }
- if (req.body.spellCheckLanguage != null) {
- user.ace.spellCheckLanguage = req.body.spellCheckLanguage
- }
- if (req.body.pdfViewer != null) {
- user.ace.pdfViewer = req.body.pdfViewer
- }
- if (req.body.syntaxValidation != null) {
- user.ace.syntaxValidation = req.body.syntaxValidation
- }
- if (req.body.fontFamily != null) {
- user.ace.fontFamily = req.body.fontFamily
- }
- if (req.body.lineHeight != null) {
- user.ace.lineHeight = req.body.lineHeight
- }
- user.save(err => {
- if (err != null) {
- return next(err)
- }
- const newEmail =
- req.body.email != null
- ? req.body.email.trim().toLowerCase()
- : undefined
- if (
- newEmail == null ||
- newEmail === user.email ||
- req.externalAuthenticationSystemUsed()
- ) {
- // end here, don't update email
- SessionManager.setInSessionUser(req.session, {
- first_name: user.first_name,
- last_name: user.last_name,
- })
- res.sendStatus(200)
- } else if (newEmail.indexOf('@') === -1) {
- // email invalid
- res.sendStatus(400)
- } else {
- // update the user email
- const auditLog = {
- initiatorId: userId,
- ipAddress: req.ip,
- }
- UserUpdater.changeEmailAddress(userId, newEmail, auditLog, err => {
- if (err) {
- if (err instanceof Errors.EmailExistsError) {
- const translation = req.i18n.translate(
- 'email_already_registered'
- )
- return HttpErrorHandler.conflict(req, res, translation)
- } else {
- return HttpErrorHandler.legacyInternal(
- req,
- res,
- req.i18n.translate('problem_changing_email_address'),
- OError.tag(err, 'problem_changing_email_address', {
- userId,
- newEmail,
- })
- )
- }
- }
- User.findById(userId, (err, user) => {
- if (err != null) {
- logger.err(
- { err, userId },
- 'error getting user for email update'
- )
- return res.sendStatus(500)
- }
- SessionManager.setInSessionUser(req.session, {
- email: user.email,
- first_name: user.first_name,
- last_name: user.last_name,
- })
- UserHandler.populateTeamInvites(user, err => {
- // need to refresh this in the background
- if (err != null) {
- logger.err({ err }, 'error populateTeamInvites')
- }
- res.sendStatus(200)
- })
- })
- })
- }
- })
- })
- },
- doLogout(req, cb) {
- metrics.inc('user.logout')
- const user = SessionManager.getSessionUser(req.session)
- logger.log({ user }, 'logging out')
- const sessionId = req.sessionID
- if (typeof req.logout === 'function') {
- req.logout()
- } // passport logout
- req.session.destroy(err => {
- if (err) {
- OError.tag(err, 'error destroying session')
- return cb(err)
- }
- if (user != null) {
- UserSessionsManager.untrackSession(user, sessionId, () => {})
- }
- cb()
- })
- },
- logout(req, res, next) {
- const requestedRedirect = req.body.redirect
- ? UrlHelper.getSafeRedirectPath(req.body.redirect)
- : undefined
- const redirectUrl = requestedRedirect || '/login'
- UserController.doLogout(req, err => {
- if (err != null) {
- return next(err)
- }
- if (acceptsJson(req)) {
- res.status(200).json({ redir: redirectUrl })
- } else {
- res.redirect(redirectUrl)
- }
- })
- },
- expireDeletedUser(req, res, next) {
- const userId = req.params.userId
- UserDeleter.expireDeletedUser(userId, error => {
- if (error) {
- return next(error)
- }
- res.sendStatus(204)
- })
- },
- expireDeletedUsersAfterDuration(req, res, next) {
- UserDeleter.expireDeletedUsersAfterDuration(error => {
- if (error) {
- return next(error)
- }
- res.sendStatus(204)
- })
- },
- register(req, res, next) {
- const { email } = req.body
- if (email == null || email === '') {
- return res.sendStatus(422) // Unprocessable Entity
- }
- UserRegistrationHandler.registerNewUserAndSendActivationEmail(
- email,
- (error, user, setNewPasswordUrl) => {
- if (error != null) {
- return next(error)
- }
- res.json({
- email: user.email,
- setNewPasswordUrl,
- })
- }
- )
- },
- changePassword: expressify(changePassword),
- }
- UserController.promises = {
- doLogout: promisify(UserController.doLogout),
- ensureAffiliation,
- ensureAffiliationMiddleware,
- }
- module.exports = UserController
|