Просмотр исходного кода

Merge pull request #13591 from overleaf/ab-account-enrollment

[web] Managed users - account enrollment

GitOrigin-RevId: 6fc9ef8c44394eb92d3afd2022433b0b6d508503
Davinder Singh 3 лет назад
Родитель
Сommit
9e8e124113

+ 10 - 2
services/web/app/src/Features/Authorization/PermissionsManager.js

@@ -133,8 +133,16 @@ function registerPolicy(name, capabilities, options = {}) {
  * @returns {Array} An array of policy names that are enforced.
  * @returns {Array} An array of policy names that are enforced.
  */
  */
 function getEnforcedPolicyNames(groupPolicy = {}) {
 function getEnforcedPolicyNames(groupPolicy = {}) {
-  return Object.keys(groupPolicy).filter(
-    policyName => groupPolicy[policyName] !== false
+  if (!groupPolicy) {
+    return []
+  }
+  return Object.keys(
+    typeof groupPolicy.toObject === 'function'
+      ? groupPolicy.toObject()
+      : groupPolicy
+  ).filter(
+    policyName =>
+      !['__v', '_id'].includes(policyName) && groupPolicy[policyName] !== false
   ) // filter out the policies that are not enforced
   ) // filter out the policies that are not enforced
 }
 }
 
 

+ 31 - 17
services/web/app/src/Features/Project/ProjectListController.js

@@ -32,7 +32,6 @@ const SplitTestHandler = require('../SplitTests/SplitTestHandler')
 /** @typedef {import("../../../../types/project/dashboard/api").Sort} Sort */
 /** @typedef {import("../../../../types/project/dashboard/api").Sort} Sort */
 /** @typedef {import("./types").AllUsersProjects} AllUsersProjects */
 /** @typedef {import("./types").AllUsersProjects} AllUsersProjects */
 /** @typedef {import("./types").MongoProject} MongoProject */
 /** @typedef {import("./types").MongoProject} MongoProject */
-
 /** @typedef {import("../Tags/types").Tag} Tag */
 /** @typedef {import("../Tags/types").Tag} Tag */
 
 
 const _ssoAvailable = (affiliation, session, linkedInstitutionIds) => {
 const _ssoAvailable = (affiliation, session, linkedInstitutionIds) => {
@@ -90,6 +89,10 @@ async function projectListPage(req, res, next) {
   // - object - the subscription data object
   // - object - the subscription data object
   let usersBestSubscription
   let usersBestSubscription
   let survey
   let survey
+  let userIsMemberOfGroupSubscription = false
+  let groupSubscriptionsPendingEnrollment = []
+
+  const isSaas = Features.hasFeature('saas')
 
 
   const userId = SessionManager.getLoggedInUserId(req.session)
   const userId = SessionManager.getLoggedInUserId(req.session)
   const projectsBlobPending = _getProjects(userId).catch(err => {
   const projectsBlobPending = _getProjects(userId).catch(err => {
@@ -98,7 +101,9 @@ async function projectListPage(req, res, next) {
   })
   })
   const user = await User.findById(
   const user = await User.findById(
     userId,
     userId,
-    'email emails features lastPrimaryEmailCheck signUpDate'
+    `email emails features lastPrimaryEmailCheck signUpDate${
+      isSaas ? ' enrollment' : ''
+    }`
   )
   )
 
 
   // Handle case of deleted user
   // Handle case of deleted user
@@ -107,7 +112,7 @@ async function projectListPage(req, res, next) {
     return
     return
   }
   }
 
 
-  if (Features.hasFeature('saas')) {
+  if (isSaas) {
     try {
     try {
       usersBestSubscription =
       usersBestSubscription =
         await SubscriptionViewModelBuilder.promises.getBestSubscription({
         await SubscriptionViewModelBuilder.promises.getBestSubscription({
@@ -119,6 +124,24 @@ async function projectListPage(req, res, next) {
         "Failed to get user's best subscription"
         "Failed to get user's best subscription"
       )
       )
     }
     }
+    try {
+      const { isMember, subscriptions } =
+        await LimitationsManager.promises.userIsMemberOfGroupSubscription(user)
+
+      userIsMemberOfGroupSubscription = isMember
+
+      // TODO use helper function
+      if (!user.enrollment?.managedBy) {
+        groupSubscriptionsPendingEnrollment = subscriptions.filter(
+          subscription => subscription.groupPlan && subscription.groupPolicy
+        )
+      }
+    } catch (error) {
+      logger.error(
+        { err: error },
+        'Failed to check whether user is a member of group subscription'
+      )
+    }
 
 
     try {
     try {
       survey = await SurveyHandler.promises.getSurvey(userId)
       survey = await SurveyHandler.promises.getSurvey(userId)
@@ -280,20 +303,6 @@ async function projectListPage(req, res, next) {
     status: prefetchedProjectsBlob ? 'success' : 'too-slow',
     status: prefetchedProjectsBlob ? 'success' : 'too-slow',
   })
   })
 
 
-  let userIsMemberOfGroupSubscription = false
-  try {
-    const userIsMemberOfGroupSubscriptionPromise =
-      await LimitationsManager.promises.userIsMemberOfGroupSubscription(user)
-
-    userIsMemberOfGroupSubscription =
-      userIsMemberOfGroupSubscriptionPromise.isMember
-  } catch (error) {
-    logger.error(
-      { err: error },
-      'Failed to check whether user is a member of group subscription'
-    )
-  }
-
   // in v2 add notifications for matching university IPs
   // in v2 add notifications for matching university IPs
   if (Settings.overleaf != null && req.ip !== user.lastLoginIp) {
   if (Settings.overleaf != null && req.ip !== user.lastLoginIp) {
     try {
     try {
@@ -395,6 +404,11 @@ async function projectListPage(req, res, next) {
     showINRBanner,
     showINRBanner,
     projectDashboardReact: true, // used in navbar
     projectDashboardReact: true, // used in navbar
     welcomePageRedesignVariant: welcomePageRedesignAssignment.variant,
     welcomePageRedesignVariant: welcomePageRedesignAssignment.variant,
+    groupSubscriptionsPendingEnrollment:
+      groupSubscriptionsPendingEnrollment.map(subscription => ({
+        groupId: subscription._id,
+        groupName: subscription.teamName,
+      })),
   })
   })
 }
 }
 
 

+ 1 - 0
services/web/app/src/Features/User/UserPagesController.js

@@ -126,6 +126,7 @@ async function settingsPage(req, res) {
     showPersonalAccessToken,
     showPersonalAccessToken,
     personalAccessTokens,
     personalAccessTokens,
     emailAddressLimit: Settings.emailAddressLimit,
     emailAddressLimit: Settings.emailAddressLimit,
+    isManagedAccount: !!user.enrollment?.managedBy,
   })
   })
 }
 }
 
 

+ 11 - 0
services/web/app/src/Features/UserMembership/UserMembershipAuthorization.js

@@ -22,5 +22,16 @@ const UserMembershipAuthorization = {
       )
       )
     }
     }
   },
   },
+
+  isEntityMember() {
+    return req => {
+      if (!req.entity) {
+        return false
+      }
+      return req.entity[req.entityConfig.fields.membership].some(accessUserId =>
+        accessUserId.equals(req.user._id)
+      )
+    }
+  },
 }
 }
 module.exports = UserMembershipAuthorization
 module.exports = UserMembershipAuthorization

+ 4 - 0
services/web/app/src/Features/UserMembership/UserMembershipEntityConfigs.js

@@ -8,6 +8,7 @@ module.exports = {
       read: ['invited_emails', 'teamInvites', 'member_ids'],
       read: ['invited_emails', 'teamInvites', 'member_ids'],
       write: null,
       write: null,
       access: 'manager_ids',
       access: 'manager_ids',
+      membership: 'member_ids',
       name: 'teamName',
       name: 'teamName',
     },
     },
     baseQuery: {
     baseQuery: {
@@ -47,6 +48,7 @@ module.exports = {
       read: ['manager_ids'],
       read: ['manager_ids'],
       write: 'manager_ids',
       write: 'manager_ids',
       access: 'manager_ids',
       access: 'manager_ids',
+      membership: 'member_ids',
       name: 'teamName',
       name: 'teamName',
     },
     },
     baseQuery: {
     baseQuery: {
@@ -72,6 +74,7 @@ module.exports = {
       read: ['managerIds'],
       read: ['managerIds'],
       write: 'managerIds',
       write: 'managerIds',
       access: 'managerIds',
       access: 'managerIds',
+      membership: 'member_ids',
       name: 'name',
       name: 'name',
     },
     },
     translations: {
     translations: {
@@ -95,6 +98,7 @@ module.exports = {
       read: ['managerIds'],
       read: ['managerIds'],
       write: 'managerIds',
       write: 'managerIds',
       access: 'managerIds',
       access: 'managerIds',
+      membership: 'member_ids',
       name: 'name',
       name: 'name',
     },
     },
     translations: {
     translations: {

+ 9 - 0
services/web/app/src/Features/UserMembership/UserMembershipMiddleware.js

@@ -22,6 +22,15 @@ const UserMembershipMiddleware = {
     ]),
     ]),
   ],
   ],
 
 
+  requireGroup: [fetchEntityConfig('group'), fetchEntity(), requireEntity()],
+
+  requireGroupAccess: [
+    AuthenticationController.requireLogin(),
+    fetchEntityConfig('group'),
+    fetchEntity(),
+    requireEntity(),
+  ],
+
   requireGroupManagementAccess: [
   requireGroupManagementAccess: [
     AuthenticationController.requireLogin(),
     AuthenticationController.requireLogin(),
     fetchEntityConfig('group'),
     fetchEntityConfig('group'),

+ 1 - 0
services/web/app/views/project/list-react.pug

@@ -30,6 +30,7 @@ block append meta
 	meta(name="ol-groupsAndEnterpriseBannerVariant" data-type="string" content=groupsAndEnterpriseBannerVariant)
 	meta(name="ol-groupsAndEnterpriseBannerVariant" data-type="string" content=groupsAndEnterpriseBannerVariant)
 	meta(name="ol-showINRBanner" data-type="boolean" content=showINRBanner)
 	meta(name="ol-showINRBanner" data-type="boolean" content=showINRBanner)
 	meta(name="ol-welcomePageRedesignVariant" data-type="string" content=welcomePageRedesignVariant)
 	meta(name="ol-welcomePageRedesignVariant" data-type="string" content=welcomePageRedesignVariant)
+	meta(name="ol-groupSubscriptionsPendingEnrollment" data-type="json" content=groupSubscriptionsPendingEnrollment)
 
 
 block content
 block content
 	main.content.content-alt.project-list-react#project-list-root
 	main.content.content-alt.project-list-react#project-list-root

+ 1 - 0
services/web/app/views/user/settings.pug

@@ -25,6 +25,7 @@ block append meta
 	meta(name="ol-showPersonalAccessToken", data-type="boolean" content=showPersonalAccessToken)
 	meta(name="ol-showPersonalAccessToken", data-type="boolean" content=showPersonalAccessToken)
 	meta(name="ol-personalAccessTokens", data-type="json" content=personalAccessTokens)
 	meta(name="ol-personalAccessTokens", data-type="json" content=personalAccessTokens)
 	meta(name="ol-emailAddressLimit", data-type="json", content=emailAddressLimit)
 	meta(name="ol-emailAddressLimit", data-type="json", content=emailAddressLimit)
+	meta(name="ol-isManagedAccount" data-type="boolean" content=isManagedAccount)
 
 
 block content
 block content
 	main.content.content-alt#settings-page-root
 	main.content.content-alt#settings-page-root

+ 1 - 0
services/web/config/settings.defaults.js

@@ -808,6 +808,7 @@ module.exports = {
     editorLeftMenuSync: [],
     editorLeftMenuSync: [],
     editorLeftMenuManageTemplate: [],
     editorLeftMenuManageTemplate: [],
     oauth2Server: [],
     oauth2Server: [],
+    managedGroupSubscriptionEnrollmentNotification: [],
   },
   },
 
 
   moduleImportSequence: ['launchpad', 'server-ce-scripts', 'user-activate'],
   moduleImportSequence: ['launchpad', 'server-ce-scripts', 'user-activate'],

+ 25 - 0
services/web/frontend/extracted-translations.json

@@ -14,10 +14,12 @@
   "about_to_leave_projects": "",
   "about_to_leave_projects": "",
   "about_to_trash_projects": "",
   "about_to_trash_projects": "",
   "accept": "",
   "accept": "",
+  "accept_invitation": "",
   "accepted_invite": "",
   "accepted_invite": "",
   "access_denied": "",
   "access_denied": "",
   "account_has_been_link_to_institution_account": "",
   "account_has_been_link_to_institution_account": "",
   "account_has_past_due_invoice_change_plan_warning": "",
   "account_has_past_due_invoice_change_plan_warning": "",
+  "account_managed_by_group_administrator": "",
   "account_not_linked_to_dropbox": "",
   "account_not_linked_to_dropbox": "",
   "account_settings": "",
   "account_settings": "",
   "acct_linked_to_institution_acct_2": "",
   "acct_linked_to_institution_acct_2": "",
@@ -117,6 +119,7 @@
   "change_plan": "",
   "change_plan": "",
   "change_primary_email_address_instructions": "",
   "change_primary_email_address_instructions": "",
   "change_project_owner": "",
   "change_project_owner": "",
+  "change_the_ownership_of_your_personal_projects": "",
   "change_to_group_plan": "",
   "change_to_group_plan": "",
   "change_to_this_plan": "",
   "change_to_this_plan": "",
   "changing_the_position_of_your_figure": "",
   "changing_the_position_of_your_figure": "",
@@ -220,6 +223,7 @@
   "discount_of": "",
   "discount_of": "",
   "dismiss": "",
   "dismiss": "",
   "dismiss_error_popup": "",
   "dismiss_error_popup": "",
+  "do_this_later": "",
   "do_you_want_to_change_your_primary_email_address_to": "",
   "do_you_want_to_change_your_primary_email_address_to": "",
   "do_you_want_to_overwrite_them": "",
   "do_you_want_to_overwrite_them": "",
   "documentation": "",
   "documentation": "",
@@ -403,6 +407,8 @@
   "go_to_pdf_location_in_code": "",
   "go_to_pdf_location_in_code": "",
   "go_to_settings": "",
   "go_to_settings": "",
   "group_admin": "",
   "group_admin": "",
+  "group_admin_or_managers_can_reassign_projects": "",
+  "group_managed_by_group_administrator": "",
   "group_plan_tooltip": "",
   "group_plan_tooltip": "",
   "group_plan_with_name_tooltip": "",
   "group_plan_with_name_tooltip": "",
   "group_subscription": "",
   "group_subscription": "",
@@ -506,6 +512,7 @@
   "join_project": "",
   "join_project": "",
   "joining": "",
   "joining": "",
   "keep_current_plan": "",
   "keep_current_plan": "",
+  "keep_personal_projects_separate": "",
   "keybindings": "",
   "keybindings": "",
   "labels_help_you_to_easily_reference_your_figures": "",
   "labels_help_you_to_easily_reference_your_figures": "",
   "labs_program_already_participating": "",
   "labs_program_already_participating": "",
@@ -525,6 +532,7 @@
   "learn_more": "",
   "learn_more": "",
   "learn_more_about_link_sharing": "",
   "learn_more_about_link_sharing": "",
   "leave": "",
   "leave": "",
+  "leave_any_group_subscriptions": "",
   "leave_group": "",
   "leave_group": "",
   "leave_now": "",
   "leave_now": "",
   "leave_projects": "",
   "leave_projects": "",
@@ -560,6 +568,7 @@
   "log_hint_extra_info": "",
   "log_hint_extra_info": "",
   "log_in_with_primary_email_address": "",
   "log_in_with_primary_email_address": "",
   "log_viewer_error": "",
   "log_viewer_error": "",
+  "login_to_transfer_account": "",
   "login_with_service": "",
   "login_with_service": "",
   "login_with_service_will_stop_working_soon": "",
   "login_with_service_will_stop_working_soon": "",
   "logs_and_output_files": "",
   "logs_and_output_files": "",
@@ -616,6 +625,8 @@
   "navigate_log_source": "",
   "navigate_log_source": "",
   "navigation": "",
   "navigation": "",
   "need_anything_contact_us_at": "",
   "need_anything_contact_us_at": "",
+  "need_contact_group_admin_to_make_changes": "",
+  "need_make_changes": "",
   "need_more_than_x_licenses": "",
   "need_more_than_x_licenses": "",
   "need_to_add_new_primary_before_remove": "",
   "need_to_add_new_primary_before_remove": "",
   "need_to_leave": "",
   "need_to_leave": "",
@@ -667,6 +678,7 @@
   "ok": "",
   "ok": "",
   "on": "",
   "on": "",
   "on_free_plan_upgrade_to_access_features": "",
   "on_free_plan_upgrade_to_access_features": "",
+  "only_group_admin_or_managers_can_delete_your_account": "",
   "open_project": "",
   "open_project": "",
   "optional": "",
   "optional": "",
   "or": "",
   "or": "",
@@ -804,6 +816,7 @@
   "remove_from_group": "",
   "remove_from_group": "",
   "remove_manager": "",
   "remove_manager": "",
   "remove_or_replace_figure": "",
   "remove_or_replace_figure": "",
+  "remove_secondary_email_addresses": "",
   "remove_tag": "",
   "remove_tag": "",
   "removing": "",
   "removing": "",
   "rename": "",
   "rename": "",
@@ -896,6 +909,7 @@
   "session_expired_redirecting_to_login": "",
   "session_expired_redirecting_to_login": "",
   "sessions": "",
   "sessions": "",
   "settings": "",
   "settings": "",
+  "setup_another_account_under_a_personal_email_address": "",
   "share": "",
   "share": "",
   "share_project": "",
   "share_project": "",
   "share_with_your_collabs": "",
   "share_with_your_collabs": "",
@@ -999,6 +1013,7 @@
   "to_add_email_accounts_need_to_be_linked_2": "",
   "to_add_email_accounts_need_to_be_linked_2": "",
   "to_add_more_collaborators": "",
   "to_add_more_collaborators": "",
   "to_change_access_permissions": "",
   "to_change_access_permissions": "",
+  "to_confirm_transfer_enter_email_address": "",
   "to_modify_your_subscription_go_to": "",
   "to_modify_your_subscription_go_to": "",
   "toggle_compile_options_menu": "",
   "toggle_compile_options_menu": "",
   "token": "",
   "token": "",
@@ -1040,6 +1055,11 @@
   "track_changes_is_on": "",
   "track_changes_is_on": "",
   "tracked_change_added": "",
   "tracked_change_added": "",
   "tracked_change_deleted": "",
   "tracked_change_deleted": "",
+  "transfer_account": "",
+  "transfer_management_of_your_account": "",
+  "transfer_management_of_your_account_to_x": "",
+  "transfer_management_resolve_following_issues": "",
+  "transferring": "",
   "trash": "",
   "trash": "",
   "trash_projects": "",
   "trash_projects": "",
   "trashed": "",
   "trashed": "",
@@ -1072,6 +1092,8 @@
   "unlink_dropbox_warning": "",
   "unlink_dropbox_warning": "",
   "unlink_github_repository": "",
   "unlink_github_repository": "",
   "unlink_github_warning": "",
   "unlink_github_warning": "",
+  "unlink_linked_accounts": "",
+  "unlink_linked_google_account": "",
   "unlink_provider_account_title": "",
   "unlink_provider_account_title": "",
   "unlink_provider_account_warning": "",
   "unlink_provider_account_warning": "",
   "unlink_reference": "",
   "unlink_reference": "",
@@ -1123,6 +1145,7 @@
   "we_logged_you_in": "",
   "we_logged_you_in": "",
   "wed_love_you_to_stay": "",
   "wed_love_you_to_stay": "",
   "welcome_to_sl": "",
   "welcome_to_sl": "",
+  "what_does_this_mean_for_you": "",
   "when_you_tick_the_include_caption_box": "",
   "when_you_tick_the_include_caption_box": "",
   "wide": "",
   "wide": "",
   "with_premium_subscription_you_also_get": "",
   "with_premium_subscription_you_also_get": "",
@@ -1150,6 +1173,8 @@
   "you_can_now_log_in_sso": "",
   "you_can_now_log_in_sso": "",
   "you_dont_have_any_repositories": "",
   "you_dont_have_any_repositories": "",
   "you_have_added_x_of_group_size_y": "",
   "you_have_added_x_of_group_size_y": "",
+  "you_have_been_invited_to_transfer_management_of_your_account": "",
+  "you_have_been_invited_to_transfer_management_of_your_account_to": "",
   "your_affiliation_is_confirmed": "",
   "your_affiliation_is_confirmed": "",
   "your_browser_does_not_support_this_feature": "",
   "your_browser_does_not_support_this_feature": "",
   "your_git_access_info": "",
   "your_git_access_info": "",

+ 28 - 1
services/web/frontend/js/features/project-list/components/notifications/user-notifications.tsx

@@ -1,3 +1,4 @@
+import { JSXElementConstructor } from 'react'
 import Common from './groups/common'
 import Common from './groups/common'
 import Institution from './groups/institution'
 import Institution from './groups/institution'
 import ConfirmEmail from './groups/confirm-email'
 import ConfirmEmail from './groups/confirm-email'
@@ -6,13 +7,39 @@ import GroupsAndEnterpriseBanner from './groups-and-enterprise-banner'
 import WritefullPromoBanner from './writefull-promo-banner'
 import WritefullPromoBanner from './writefull-promo-banner'
 import INRBanner from './ads/inr-banner'
 import INRBanner from './ads/inr-banner'
 import getMeta from '../../../../utils/meta'
 import getMeta from '../../../../utils/meta'
+import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
+
+type Subscription = {
+  groupId: string
+  groupName: string
+}
+
+const [enrollmentNotificationModule] = importOverleafModules(
+  'managedGroupSubscriptionEnrollmentNotification'
+)
+const EnrollmentNotification: JSXElementConstructor<{
+  groupId: string
+  groupName: string
+}> = enrollmentNotificationModule?.import.default
 
 
 function UserNotifications() {
 function UserNotifications() {
-  const showIRNBanner = getMeta('ol-showINRBanner')
+  const showIRNBanner = getMeta('ol-showINRBanner', false)
+  const groupSubscriptionsPendingEnrollment: Subscription[] = getMeta(
+    'ol-groupSubscriptionsPendingEnrollment',
+    []
+  )
 
 
   return (
   return (
     <div className="user-notifications">
     <div className="user-notifications">
       <ul className="list-unstyled">
       <ul className="list-unstyled">
+        {EnrollmentNotification &&
+          groupSubscriptionsPendingEnrollment.map(subscription => (
+            <EnrollmentNotification
+              groupId={subscription.groupId}
+              groupName={subscription.groupName}
+              key={subscription.groupId}
+            />
+          ))}
         <Common />
         <Common />
         <Institution />
         <Institution />
         <ConfirmEmail />
         <ConfirmEmail />

+ 31 - 0
services/web/frontend/js/features/settings/components/managed-account-alert.tsx

@@ -0,0 +1,31 @@
+import { Trans, useTranslation } from 'react-i18next'
+import getMeta from '../../../utils/meta'
+
+export default function ManagedAccountAlert() {
+  const { t } = useTranslation()
+  const isManaged = getMeta('ol-isManagedAccount', false)
+
+  if (!isManaged) {
+    return null
+  }
+
+  return (
+    <div className="enrollment-alert">
+      <div className="icon">
+        <span className="info-badge" />
+      </div>
+      <div>
+        <div>
+          <strong>{t('account_managed_by_group_administrator')}</strong>
+        </div>
+        <div>
+          <Trans
+            i18nKey="need_contact_group_admin_to_make_changes"
+            // TODO update with actual wiki link once created
+            components={[<a href="/learn/how-to/Managed_Users" />]} // eslint-disable-line jsx-a11y/anchor-has-content, react/jsx-key
+          />
+        </div>
+      </div>
+    </div>
+  )
+}

+ 2 - 0
services/web/frontend/js/features/settings/components/root.tsx

@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next'
 import getMeta from '../../../utils/meta'
 import getMeta from '../../../utils/meta'
 import EmailsSection from './emails-section'
 import EmailsSection from './emails-section'
 import AccountInfoSection from './account-info-section'
 import AccountInfoSection from './account-info-section'
+import ManagedAccountAlert from './managed-account-alert'
 import PasswordSection from './password-section'
 import PasswordSection from './password-section'
 import LinkingSection from './linking-section'
 import LinkingSection from './linking-section'
 import BetaProgramSection from './beta-program-section'
 import BetaProgramSection from './beta-program-section'
@@ -50,6 +51,7 @@ function SettingsPageContent() {
           <h1>{t('account_settings')}</h1>
           <h1>{t('account_settings')}</h1>
         </div>
         </div>
         <div>
         <div>
+          <ManagedAccountAlert />
           <EmailsSection />
           <EmailsSection />
           <SSOAlert />
           <SSOAlert />
           <div className="row">
           <div className="row">

+ 19 - 0
services/web/frontend/stylesheets/app/account-settings.less

@@ -233,3 +233,22 @@ tbody > tr.affiliations-table-warning-row > td {
 .linking-git-bridge-revoke-button {
 .linking-git-bridge-revoke-button {
   padding: 2px 4px;
   padding: 2px 4px;
 }
 }
+
+.enrollment-alert {
+  width: 100%;
+  border-radius: 4px;
+  padding: 12px;
+  background-color: @blue-10;
+  border: 1px solid @blue-20;
+  display: flex;
+  flex-direction: row;
+
+  .icon {
+    flex: 1 1 auto;
+    padding: 0 16px 0 4px;
+  }
+
+  a {
+    text-decoration: underline;
+  }
+}

+ 3 - 0
services/web/frontend/stylesheets/core/variables.less

@@ -37,6 +37,7 @@
 
 
 @blue: #405ebf;
 @blue: #405ebf;
 @blue-10: #f1f4f9;
 @blue-10: #f1f4f9;
+@blue-20: #c3d0e3;
 @blue-30: #97b6e5;
 @blue-30: #97b6e5;
 @blue-40: #6597e0;
 @blue-40: #6597e0;
 @blue-70: #214475;
 @blue-70: #214475;
@@ -46,6 +47,8 @@
 @green-30: #8cca86;
 @green-30: #8cca86;
 @red: #a93529;
 @red: #a93529;
 @yellow: #a1a729;
 @yellow: #a1a729;
+@yellow-10: #fcf1e3;
+@yellow-50: #8f5514;
 @orange: #f89406;
 @orange: #f89406;
 @orange-dark: #9e5e04;
 @orange-dark: #9e5e04;
 @pink: #c3325f;
 @pink: #c3325f;

+ 67 - 2
services/web/frontend/stylesheets/modules/managed-users.less

@@ -1,6 +1,4 @@
 .group-settings-title {
 .group-settings-title {
-  min-width: 0;
-  color: @content-secondary;
   font-family: Lato, sans-serif;
   font-family: Lato, sans-serif;
   font-size: @font-size-large;
   font-size: @font-size-large;
   line-height: 28px;
   line-height: 28px;
@@ -9,6 +7,73 @@
   text-overflow: ellipsis;
   text-overflow: ellipsis;
 }
 }
 
 
+.enrollment-invite {
+  color: @neutral-90;
+
+  .inner-card {
+    background-color: @gray-lightest;
+    margin: 16px 0;
+    padding: 16px;
+
+    &.warning {
+      background-color: @yellow-10;
+    }
+
+    .list-row {
+      display: flex;
+      flex-direction: row;
+
+      .icon {
+        display: flex;
+        flex: 1 1 10%;
+        > span {
+          font-size: 16px;
+        }
+        padding-right: 12px;
+        font-size: 24px;
+
+        &.error {
+          color: @yellow-50;
+          margin-top: 2px;
+        }
+      }
+    }
+
+    a {
+      text-decoration: underline;
+    }
+  }
+
+  .title {
+    font-family: Lato, sans-serif;
+    font-size: @font-size-large;
+    line-height: 28px;
+    font-weight: bold;
+    overflow: hidden;
+    text-overflow: ellipsis;
+  }
+
+  .subtitle {
+    font-family: Lato, sans-serif;
+    font-size: @font-size-base;
+    line-height: 24px;
+    font-weight: bold;
+    overflow: hidden;
+    text-overflow: ellipsis;
+  }
+
+  .label {
+    color: @gray-dark;
+    font-size: @font-size-small;
+    padding-left: 0;
+  }
+
+  .btn {
+    width: 100%;
+    margin: 8px 0;
+  }
+}
+
 .managed-users-enabled {
 .managed-users-enabled {
   font-family: Lato, sans-serif;
   font-family: Lato, sans-serif;
   color: @ol-green;
   color: @ol-green;

+ 24 - 0
services/web/locales/en.json

@@ -37,6 +37,7 @@
   "account_has_been_link_to_institution_account": "Your __appName__ account on <b>__email__</b> has been linked to your <b>__institutionName__</b> institutional account.",
   "account_has_been_link_to_institution_account": "Your __appName__ account on <b>__email__</b> has been linked to your <b>__institutionName__</b> institutional account.",
   "account_has_past_due_invoice_change_plan_warning": "Your account currently has a past due invoice. You will not be able to change your plan until this is resolved.",
   "account_has_past_due_invoice_change_plan_warning": "Your account currently has a past due invoice. You will not be able to change your plan until this is resolved.",
   "account_linking": "Account Linking",
   "account_linking": "Account Linking",
+  "account_managed_by_group_administrator": "Your account is managed by your group administrator",
   "account_not_linked_to_dropbox": "Your account is not linked to Dropbox",
   "account_not_linked_to_dropbox": "Your account is not linked to Dropbox",
   "account_settings": "Account Settings",
   "account_settings": "Account Settings",
   "account_with_email_exists": "It looks like an <b>__appName__</b> account with the email <b>__email__</b> already exists.",
   "account_with_email_exists": "It looks like an <b>__appName__</b> account with the email <b>__email__</b> already exists.",
@@ -202,6 +203,7 @@
   "change_plan": "Change plan",
   "change_plan": "Change plan",
   "change_primary_email_address_instructions": "To change your primary email, please add your new primary email address first (by clicking <0>Add another email</0>) and confirm it. Then click the <0>Make Primary</0> button. <1>Learn more</1> about managing your __appName__ emails.",
   "change_primary_email_address_instructions": "To change your primary email, please add your new primary email address first (by clicking <0>Add another email</0>) and confirm it. Then click the <0>Make Primary</0> button. <1>Learn more</1> about managing your __appName__ emails.",
   "change_project_owner": "Change Project Owner",
   "change_project_owner": "Change Project Owner",
+  "change_the_ownership_of_your_personal_projects": "Change the ownership of your personal projects to the new account. <0>Find out how to change project owner.</0>",
   "change_to_annual_billing_and_save": "Get <0>__percentage__</0> off with annual billing. If you switch now you’ll save <1>__yearlySaving__</1> per year.",
   "change_to_annual_billing_and_save": "Get <0>__percentage__</0> off with annual billing. If you switch now you’ll save <1>__yearlySaving__</1> per year.",
   "change_to_group_plan": "Change to a group plan",
   "change_to_group_plan": "Change to a group plan",
   "change_to_this_plan": "Change to this plan",
   "change_to_this_plan": "Change to this plan",
@@ -359,6 +361,7 @@
   "dismiss_error_popup": "Dismiss first error alert",
   "dismiss_error_popup": "Dismiss first error alert",
   "do_not_have_acct_or_do_not_want_to_link": "If you don’t have an <b>__appName__</b> account, or if you don’t want to link to your <b>__institutionName__</b> account, please click <b>__clickText__</b>.",
   "do_not_have_acct_or_do_not_want_to_link": "If you don’t have an <b>__appName__</b> account, or if you don’t want to link to your <b>__institutionName__</b> account, please click <b>__clickText__</b>.",
   "do_not_link_accounts": "Don’t link accounts",
   "do_not_link_accounts": "Don’t link accounts",
+  "do_this_later": "I’ll do this later",
   "do_you_want_to_change_your_primary_email_address_to": "Do you want to change your primary email address to <b>__email__</b>?",
   "do_you_want_to_change_your_primary_email_address_to": "Do you want to change your primary email address to <b>__email__</b>?",
   "do_you_want_to_overwrite_them": "Do you want to overwrite them?",
   "do_you_want_to_overwrite_them": "Do you want to overwrite them?",
   "document_history": "Document history",
   "document_history": "Document history",
@@ -655,9 +658,11 @@
   "go_to_pdf_location_in_code": "Go to PDF location in code (Tip: double click on the PDF for best results)",
   "go_to_pdf_location_in_code": "Go to PDF location in code (Tip: double click on the PDF for best results)",
   "go_to_settings": "Go to settings",
   "go_to_settings": "Go to settings",
   "group_admin": "Group admin",
   "group_admin": "Group admin",
+  "group_admin_or_managers_can_reassign_projects": "Your group admin and group managers will be able to reassign ownership of your projects to another group member.",
   "group_admins_get_access_to": "Group admins get access to",
   "group_admins_get_access_to": "Group admins get access to",
   "group_admins_get_access_to_info": "Special features available only on group plans.",
   "group_admins_get_access_to_info": "Special features available only on group plans.",
   "group_full": "This group is already full",
   "group_full": "This group is already full",
+  "group_managed_by_group_administrator": "This group is managed by the group administrator",
   "group_members_and_collaborators_get_access_to": "Group members and their project collaborators get access to",
   "group_members_and_collaborators_get_access_to": "Group members and their project collaborators get access to",
   "group_members_and_collaborators_get_access_to_info": "These features are available to group members and their collaborators (other Overleaf users invited to projects owned a group member).",
   "group_members_and_collaborators_get_access_to_info": "These features are available to group members and their collaborators (other Overleaf users invited to projects owned a group member).",
   "group_members_get_access_to": "Group members get access to",
   "group_members_get_access_to": "Group members get access to",
@@ -822,6 +827,7 @@
   "june": "June",
   "june": "June",
   "kb_suggestions_enquiry": "Have you checked our <0>__kbLink__</0>?",
   "kb_suggestions_enquiry": "Have you checked our <0>__kbLink__</0>?",
   "keep_current_plan": "Keep my current plan",
   "keep_current_plan": "Keep my current plan",
+  "keep_personal_projects_separate": "Keep personal projects separate",
   "keep_your_account_safe": "Keep your account safe",
   "keep_your_account_safe": "Keep your account safe",
   "keep_your_email_updated": "Keep your email updated so that you don’t lose access to your account and data.",
   "keep_your_email_updated": "Keep your email updated so that you don’t lose access to your account and data.",
   "keybindings": "Keybindings",
   "keybindings": "Keybindings",
@@ -857,6 +863,7 @@
   "learn_more_about_link_sharing": "Learn more about Link Sharing",
   "learn_more_about_link_sharing": "Learn more about Link Sharing",
   "learn_more_lowercase": "learn more",
   "learn_more_lowercase": "learn more",
   "leave": "Leave",
   "leave": "Leave",
+  "leave_any_group_subscriptions": "Leave any group subscriptions other than the one that will be managing your account. <0>Leave them from the Subscription page.</0>",
   "leave_group": "Leave group",
   "leave_group": "Leave group",
   "leave_now": "Leave now",
   "leave_now": "Leave now",
   "leave_projects": "Leave Projects",
   "leave_projects": "Leave Projects",
@@ -919,6 +926,7 @@
   "login_register_or": "or",
   "login_register_or": "or",
   "login_to_accept_invitation": "Login to accept invitation",
   "login_to_accept_invitation": "Login to accept invitation",
   "login_to_overleaf": "Log in to Overleaf",
   "login_to_overleaf": "Log in to Overleaf",
+  "login_to_transfer_account": "Log in to transfer account",
   "login_with_email": "Log in with your email",
   "login_with_email": "Log in with your email",
   "login_with_service": "Log in with __service__",
   "login_with_service": "Log in with __service__",
   "login_with_service_will_stop_working_soon": "Login with __service__ will stop working soon. If you log in with __service__, please take a moment to set up another SSO provider or a password.",
   "login_with_service_will_stop_working_soon": "Login with __service__ will stop working soon. If you log in with __service__, please take a moment to set up another SSO provider or a password.",
@@ -1000,6 +1008,8 @@
   "navigation": "Navigation",
   "navigation": "Navigation",
   "nearly_activated": "You’re one step away from activating your __appName__ account!",
   "nearly_activated": "You’re one step away from activating your __appName__ account!",
   "need_anything_contact_us_at": "If there is anything you ever need please feel free to contact us directly at",
   "need_anything_contact_us_at": "If there is anything you ever need please feel free to contact us directly at",
+  "need_contact_group_admin_to_make_changes": "You’ll need to contact your group admin if you wish to make certain changes to your account. <0>Read more about managed users.</0>",
+  "need_make_changes": "You need to make some changes",
   "need_more_than_x_licenses": "Need more than __x__ licenses?",
   "need_more_than_x_licenses": "Need more than __x__ licenses?",
   "need_to_add_new_primary_before_remove": "You’ll need to add a new primary email address before you can remove this one.",
   "need_to_add_new_primary_before_remove": "You’ll need to add a new primary email address before you can remove this one.",
   "need_to_leave": "Need to leave?",
   "need_to_leave": "Need to leave?",
@@ -1085,6 +1095,7 @@
   "one_collaborator": "Only one collaborator",
   "one_collaborator": "Only one collaborator",
   "one_free_collab": "One free collaborator",
   "one_free_collab": "One free collaborator",
   "online_latex_editor": "Online LaTeX Editor",
   "online_latex_editor": "Online LaTeX Editor",
+  "only_group_admin_or_managers_can_delete_your_account": "Only your group admin or group managers will be able to delete your account.",
   "open_a_file_on_the_left": "Open a file on the left",
   "open_a_file_on_the_left": "Open a file on the left",
   "open_as_template": "Open as Template",
   "open_as_template": "Open as Template",
   "open_project": "Open Project",
   "open_project": "Open Project",
@@ -1304,6 +1315,7 @@
   "remove_from_group": "Remove from group",
   "remove_from_group": "Remove from group",
   "remove_manager": "Remove manager",
   "remove_manager": "Remove manager",
   "remove_or_replace_figure": "Remove or replace figure",
   "remove_or_replace_figure": "Remove or replace figure",
+  "remove_secondary_email_addresses": "Remove any secondary email addresses associated with your account. <0>Remove them in account settings.</0>",
   "remove_tag": "Remove tag __tagName__",
   "remove_tag": "Remove tag __tagName__",
   "removed": "removed",
   "removed": "removed",
   "removing": "Removing",
   "removing": "Removing",
@@ -1432,6 +1444,7 @@
   "set_new_password": "Set new password",
   "set_new_password": "Set new password",
   "set_password": "Set Password",
   "set_password": "Set Password",
   "settings": "Settings",
   "settings": "Settings",
+  "setup_another_account_under_a_personal_email_address": "Set up another Overleaf account under a personal email address.",
   "share": "Share",
   "share": "Share",
   "share_project": "Share Project",
   "share_project": "Share Project",
   "share_sl_to_get_rewards": "Share __appName__ with your friends and colleagues and unlock the rewards below",
   "share_sl_to_get_rewards": "Share __appName__ with your friends and colleagues and unlock the rewards below",
@@ -1600,6 +1613,7 @@
   "to_add_email_accounts_need_to_be_linked_2": "To add this email, your <0>__appName__</0> and <0>__institutionName__</0> accounts will need to be linked.",
   "to_add_email_accounts_need_to_be_linked_2": "To add this email, your <0>__appName__</0> and <0>__institutionName__</0> accounts will need to be linked.",
   "to_add_more_collaborators": "To add more collaborators or turn on link sharing, please ask the project owner",
   "to_add_more_collaborators": "To add more collaborators or turn on link sharing, please ask the project owner",
   "to_change_access_permissions": "To change access permissions, please ask the project owner",
   "to_change_access_permissions": "To change access permissions, please ask the project owner",
+  "to_confirm_transfer_enter_email_address": "To confirm the transfer, enter the email address linked to your account.",
   "to_many_login_requests_2_mins": "This account has had too many login requests. Please wait 2 minutes before trying to log in again",
   "to_many_login_requests_2_mins": "This account has had too many login requests. Please wait 2 minutes before trying to log in again",
   "to_modify_your_subscription_go_to": "To modify your subscription go to",
   "to_modify_your_subscription_go_to": "To modify your subscription go to",
   "toggle_compile_options_menu": "Toggle compile options menu",
   "toggle_compile_options_menu": "Toggle compile options menu",
@@ -1651,6 +1665,11 @@
   "track_changes_is_on": "Track changes is <strong>on</strong>",
   "track_changes_is_on": "Track changes is <strong>on</strong>",
   "tracked_change_added": "Added",
   "tracked_change_added": "Added",
   "tracked_change_deleted": "Deleted",
   "tracked_change_deleted": "Deleted",
+  "transfer_account": "Transfer account",
+  "transfer_management_of_your_account": "Transfer management of your Overleaf account",
+  "transfer_management_of_your_account_to_x": "Transfer management of your Overleaf account to __groupName__",
+  "transfer_management_resolve_following_issues": "To transfer the management of your account, you need to resolve the following issues:",
+  "transferring": "Transferring",
   "trash": "Trash",
   "trash": "Trash",
   "trash_projects": "Trash Projects",
   "trash_projects": "Trash Projects",
   "trashed": "Trashed",
   "trashed": "Trashed",
@@ -1695,6 +1714,8 @@
   "unlink_dropbox_warning": "Any projects that you have synced with Dropbox will be disconnected and no longer kept in sync with Dropbox. Are you sure you want to unlink your Dropbox account?",
   "unlink_dropbox_warning": "Any projects that you have synced with Dropbox will be disconnected and no longer kept in sync with Dropbox. Are you sure you want to unlink your Dropbox account?",
   "unlink_github_repository": "Unlink GitHub Repository",
   "unlink_github_repository": "Unlink GitHub Repository",
   "unlink_github_warning": "Any projects that you have synced with GitHub will be disconnected and no longer kept in sync with GitHub. Are you sure you want to unlink your GitHub account?",
   "unlink_github_warning": "Any projects that you have synced with GitHub will be disconnected and no longer kept in sync with GitHub. Are you sure you want to unlink your GitHub account?",
+  "unlink_linked_accounts": "Unlink any linked accounts (such as ORCID ID, IEEE). <0>Remove them in Account Settings (under Linked Accounts).</0>",
+  "unlink_linked_google_account": "Unlink your Google account. <0>Remove it in Account Settings (under Linked Accounts).</0>",
   "unlink_provider_account_title": "Unlink __provider__ Account",
   "unlink_provider_account_title": "Unlink __provider__ Account",
   "unlink_provider_account_warning": "Warning: When you unlink your account from __provider__ you will not be able to sign in using __provider__ anymore.",
   "unlink_provider_account_warning": "Warning: When you unlink your account from __provider__ you will not be able to sign in using __provider__ anymore.",
   "unlink_reference": "Unlink References Provider",
   "unlink_reference": "Unlink References Provider",
@@ -1768,6 +1789,7 @@
   "website_status": "Website status",
   "website_status": "Website status",
   "wed_love_you_to_stay": "We’d love you to stay",
   "wed_love_you_to_stay": "We’d love you to stay",
   "welcome_to_sl": "Welcome to __appName__!",
   "welcome_to_sl": "Welcome to __appName__!",
+  "what_does_this_mean_for_you": "What does this mean for you?",
   "when_you_tick_the_include_caption_box": "When you tick the box “Include caption” the image will be inserted into your document with a placeholder caption. To edit it, you simply select the placeholder text and type to replace it with your own.",
   "when_you_tick_the_include_caption_box": "When you tick the box “Include caption” the image will be inserted into your document with a placeholder caption. To edit it, you simply select the placeholder text and type to replace it with your own.",
   "wide": "Wide",
   "wide": "Wide",
   "will_need_to_log_out_from_and_in_with": "You will need to <b>log out</b> from your <b>__email1__</b> account and then log in with <b>__email2__</b>.",
   "will_need_to_log_out_from_and_in_with": "You will need to <b>log out</b> from your <b>__email1__</b> account and then log in with <b>__email2__</b>.",
@@ -1814,6 +1836,8 @@
   "you_get_access_to": "You get access to",
   "you_get_access_to": "You get access to",
   "you_get_access_to_info": "These features are available only to you (the subscriber).",
   "you_get_access_to_info": "These features are available only to you (the subscriber).",
   "you_have_added_x_of_group_size_y": "You have added <0>__addedUsersSize__</0> of <1>__groupSize__</1> available members",
   "you_have_added_x_of_group_size_y": "You have added <0>__addedUsersSize__</0> of <1>__groupSize__</1> available members",
+  "you_have_been_invited_to_transfer_management_of_your_account": "You have been invited to transfer management of your account.",
+  "you_have_been_invited_to_transfer_management_of_your_account_to": "You have been invited to transfer management of your account to __groupName__.",
   "you_introed_high_number": " You’ve introduced <0>__numberOfPeople__</0> people to __appName__. Good job!",
   "you_introed_high_number": " You’ve introduced <0>__numberOfPeople__</0> people to __appName__. Good job!",
   "you_introed_small_number": " You’ve introduced <0>__numberOfPeople__</0> person to __appName__. Good job, but can you get some more?",
   "you_introed_small_number": " You’ve introduced <0>__numberOfPeople__</0> person to __appName__. Good job, but can you get some more?",
   "you_not_introed_anyone_to_sl": "You’ve not introduced anyone to __appName__ yet. Get sharing!",
   "you_not_introed_anyone_to_sl": "You’ve not introduced anyone to __appName__ yet. Get sharing!",