UserPagesController.mjs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. import UserGetter from './UserGetter.js'
  2. import OError from '@overleaf/o-error'
  3. import UserSessionsManager from './UserSessionsManager.js'
  4. import logger from '@overleaf/logger'
  5. import Settings from '@overleaf/settings'
  6. import AuthenticationController from '../Authentication/AuthenticationController.js'
  7. import SessionManager from '../Authentication/SessionManager.js'
  8. import NewsletterManager from '../Newsletter/NewsletterManager.js'
  9. import SubscriptionLocator from '../Subscription/SubscriptionLocator.js'
  10. import _ from 'lodash'
  11. import { expressify } from '@overleaf/promise-utils'
  12. import Features from '../../infrastructure/Features.js'
  13. import SplitTestHandler from '../SplitTests/SplitTestHandler.js'
  14. import Modules from '../../infrastructure/Modules.js'
  15. async function settingsPage(req, res) {
  16. const userId = SessionManager.getLoggedInUserId(req.session)
  17. const reconfirmationRemoveEmail = req.query.remove
  18. // SSO
  19. const ssoError = req.session.ssoError
  20. if (ssoError) {
  21. delete req.session.ssoError
  22. }
  23. const ssoErrorMessage = req.session.ssoErrorMessage
  24. if (ssoErrorMessage) {
  25. delete req.session.ssoErrorMessage
  26. }
  27. const projectSyncSuccessMessage = req.session.projectSyncSuccessMessage
  28. if (projectSyncSuccessMessage) {
  29. delete req.session.projectSyncSuccessMessage
  30. }
  31. // Institution SSO
  32. let institutionLinked = _.get(req.session, ['saml', 'linked'])
  33. if (institutionLinked) {
  34. // copy object if exists because _.get does not
  35. institutionLinked = Object.assign(
  36. {
  37. hasEntitlement: _.get(req.session, ['saml', 'hasEntitlement']),
  38. },
  39. institutionLinked
  40. )
  41. }
  42. const samlError = _.get(req.session, ['saml', 'error'])
  43. const institutionEmailNonCanonical = _.get(req.session, [
  44. 'saml',
  45. 'emailNonCanonical',
  46. ])
  47. const institutionRequestedEmail = _.get(req.session, [
  48. 'saml',
  49. 'requestedEmail',
  50. ])
  51. const reconfirmedViaSAML = _.get(req.session, ['saml', 'reconfirmed'])
  52. delete req.session.saml
  53. let shouldAllowEditingDetails = true
  54. if (Settings.ldap && Settings.ldap.updateUserDetailsOnLogin) {
  55. shouldAllowEditingDetails = false
  56. }
  57. if (Settings.saml && Settings.saml.updateUserDetailsOnLogin) {
  58. shouldAllowEditingDetails = false
  59. }
  60. const oauthProviders = Settings.oauthProviders || {}
  61. const user = await UserGetter.promises.getUser(userId)
  62. if (!user) {
  63. // The user has just deleted their account.
  64. return UserSessionsManager.removeSessionsFromRedis(
  65. { _id: userId },
  66. null,
  67. () => res.redirect('/')
  68. )
  69. }
  70. let personalAccessTokens
  71. try {
  72. const results = await Modules.promises.hooks.fire(
  73. 'listPersonalAccessTokens',
  74. user._id
  75. )
  76. personalAccessTokens = results?.[0] ?? []
  77. } catch (error) {
  78. const err = OError.tag(error, 'listPersonalAccessTokens hook failed')
  79. logger.error({ err, userId }, err.message)
  80. }
  81. let currentManagedUserAdminEmail
  82. try {
  83. currentManagedUserAdminEmail =
  84. await SubscriptionLocator.promises.getAdminEmail(req.managedBy)
  85. } catch (err) {
  86. logger.error({ err }, 'error getting subscription admin email')
  87. }
  88. let memberOfSSOEnabledGroups = []
  89. try {
  90. memberOfSSOEnabledGroups =
  91. (
  92. await Modules.promises.hooks.fire(
  93. 'getUserGroupsSSOEnrollmentStatus',
  94. user._id,
  95. { teamName: 1 },
  96. ['email']
  97. )
  98. )?.[0] || []
  99. memberOfSSOEnabledGroups = memberOfSSOEnabledGroups.map(group => {
  100. return {
  101. groupId: group._id.toString(),
  102. linked: group.linked,
  103. groupName: group.teamName,
  104. adminEmail: group.admin_id?.email,
  105. }
  106. })
  107. } catch (error) {
  108. logger.error(
  109. { err: error },
  110. 'error fetching groups with Group SSO enabled the user may be member of'
  111. )
  112. }
  113. // Get the users write-and-cite assignment to switch between translation strings
  114. await SplitTestHandler.promises.getAssignment(req, res, 'write-and-cite')
  115. // Get the users papers-integration assignment to show the linking widget
  116. await SplitTestHandler.promises.getAssignment(req, res, 'papers-integration')
  117. res.render('user/settings', {
  118. title: 'account_settings',
  119. user: {
  120. id: user._id,
  121. isAdmin: user.isAdmin,
  122. email: user.email,
  123. allowedFreeTrial: user.allowedFreeTrial,
  124. first_name: user.first_name,
  125. last_name: user.last_name,
  126. alphaProgram: user.alphaProgram,
  127. betaProgram: user.betaProgram,
  128. labsProgram: user.labsProgram,
  129. features: {
  130. dropbox: user.features.dropbox,
  131. github: user.features.github,
  132. mendeley: user.features.mendeley,
  133. zotero: user.features.zotero,
  134. papers: user.features.papers,
  135. references: user.features.references,
  136. },
  137. refProviders: {
  138. mendeley: Boolean(user.refProviders?.mendeley),
  139. zotero: Boolean(user.refProviders?.zotero),
  140. papers: Boolean(user.refProviders?.papers),
  141. },
  142. writefull: {
  143. enabled: Boolean(user.writefull?.enabled),
  144. },
  145. aiErrorAssistant: {
  146. enabled: Boolean(user.aiErrorAssistant?.enabled),
  147. },
  148. },
  149. hasPassword: !!user.hashedPassword,
  150. shouldAllowEditingDetails,
  151. oauthProviders: UserPagesController._translateProviderDescriptions(
  152. oauthProviders,
  153. req
  154. ),
  155. institutionLinked,
  156. samlError,
  157. institutionEmailNonCanonical:
  158. institutionEmailNonCanonical && institutionRequestedEmail
  159. ? institutionEmailNonCanonical
  160. : undefined,
  161. reconfirmedViaSAML,
  162. reconfirmationRemoveEmail,
  163. samlBeta: req.session.samlBeta,
  164. ssoErrorMessage,
  165. thirdPartyIds: UserPagesController._restructureThirdPartyIds(user),
  166. projectSyncSuccessMessage,
  167. personalAccessTokens,
  168. emailAddressLimit: Settings.emailAddressLimit,
  169. isManagedAccount: !!req.managedBy,
  170. userRestrictions: Array.from(req.userRestrictions || []),
  171. currentManagedUserAdminEmail,
  172. gitBridgeEnabled: Settings.enableGitBridge,
  173. isSaas: Features.hasFeature('saas'),
  174. memberOfSSOEnabledGroups,
  175. })
  176. }
  177. async function accountSuspended(req, res) {
  178. if (SessionManager.isUserLoggedIn(req.session)) {
  179. return res.redirect('/project')
  180. }
  181. res.render('user/accountSuspended', {
  182. title: 'your_account_is_suspended',
  183. })
  184. }
  185. async function reconfirmAccountPage(req, res) {
  186. const pageData = {
  187. reconfirm_email: req.session.reconfirm_email,
  188. }
  189. const { variant } = await SplitTestHandler.promises.getAssignment(
  190. req,
  191. res,
  192. 'auth-pages-bs5'
  193. )
  194. const template =
  195. variant === 'enabled' ? 'user/reconfirm-bs5' : 'user/reconfirm'
  196. res.render(template, pageData)
  197. }
  198. const UserPagesController = {
  199. accountSuspended: expressify(accountSuspended),
  200. registerPage(req, res) {
  201. const sharedProjectData = req.session.sharedProjectData || {}
  202. const newTemplateData = {}
  203. if (req.session.templateData != null) {
  204. newTemplateData.templateName = req.session.templateData.templateName
  205. }
  206. res.render('user/register', {
  207. title: 'register',
  208. sharedProjectData,
  209. newTemplateData,
  210. samlBeta: req.session.samlBeta,
  211. })
  212. },
  213. loginPage(req, res) {
  214. // if user is being sent to /login with explicit redirect (redir=/foo),
  215. // such as being sent from the editor to /login, then set the redirect explicitly
  216. if (
  217. req.query.redir != null &&
  218. AuthenticationController.getRedirectFromSession(req) == null
  219. ) {
  220. AuthenticationController.setRedirectInSession(req, req.query.redir)
  221. }
  222. res.render('user/login', {
  223. title: Settings.nav?.login_support_title || 'login',
  224. login_support_text: Settings.nav?.login_support_text,
  225. })
  226. },
  227. /**
  228. * Landing page for users who may have received one-time login
  229. * tokens from the read-only maintenance site.
  230. *
  231. * We tell them that Overleaf is back up and that they can login normally.
  232. */
  233. oneTimeLoginPage(req, res, next) {
  234. res.render('user/one_time_login')
  235. },
  236. renderReconfirmAccountPage: expressify(reconfirmAccountPage),
  237. settingsPage: expressify(settingsPage),
  238. sessionsPage(req, res, next) {
  239. const user = SessionManager.getSessionUser(req.session)
  240. logger.debug({ userId: user._id }, 'loading sessions page')
  241. const currentSession = {
  242. ip_address: user.ip_address,
  243. session_created: user.session_created,
  244. }
  245. UserSessionsManager.getAllUserSessions(
  246. user,
  247. [req.sessionID],
  248. (err, sessions) => {
  249. if (err != null) {
  250. OError.tag(err, 'error getting all user sessions', {
  251. userId: user._id,
  252. })
  253. return next(err)
  254. }
  255. res.render('user/sessions', {
  256. title: 'sessions',
  257. currentSession,
  258. sessions,
  259. })
  260. }
  261. )
  262. },
  263. emailPreferencesPage(req, res, next) {
  264. const userId = SessionManager.getLoggedInUserId(req.session)
  265. UserGetter.getUser(
  266. userId,
  267. { _id: 1, email: 1, first_name: 1, last_name: 1 },
  268. (err, user) => {
  269. if (err != null) {
  270. return next(err)
  271. }
  272. NewsletterManager.subscribed(user, (err, subscribed) => {
  273. if (err != null) {
  274. OError.tag(err, 'error getting newsletter subscription status')
  275. return next(err)
  276. }
  277. res.render('user/email-preferences', {
  278. title: 'newsletter_info_title',
  279. subscribed,
  280. })
  281. })
  282. }
  283. )
  284. },
  285. async compromisedPasswordPage(req, res) {
  286. // Populates splitTestVariants with a value for the split test name and allows
  287. // Pug to read it
  288. await SplitTestHandler.promises.getAssignment(
  289. req,
  290. res,
  291. 'bs5-misc-pages-platform'
  292. )
  293. res.render('user/compromised_password')
  294. },
  295. _restructureThirdPartyIds(user) {
  296. // 3rd party identifiers are an array of objects
  297. // this turn them into a single object, which
  298. // makes data easier to use in template
  299. if (
  300. !user.thirdPartyIdentifiers ||
  301. user.thirdPartyIdentifiers.length === 0
  302. ) {
  303. return null
  304. }
  305. return user.thirdPartyIdentifiers.reduce((obj, identifier) => {
  306. obj[identifier.providerId] = identifier.externalUserId
  307. return obj
  308. }, {})
  309. },
  310. _translateProviderDescriptions(providers, req) {
  311. const result = {}
  312. if (providers) {
  313. for (const provider in providers) {
  314. const data = providers[provider]
  315. data.description = req.i18n.translate(
  316. data.descriptionKey,
  317. Object.assign({}, data.descriptionOptions)
  318. )
  319. result[provider] = data
  320. }
  321. }
  322. return result
  323. },
  324. }
  325. export default UserPagesController