UserPagesController.mjs 9.9 KB

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