Selaa lähdekoodia

Merge pull request #3623 from overleaf/jel-ui-reconfirm-settings

Add reconfirm UI to settings

GitOrigin-RevId: cf9c4648cab07784782e24d752154089dc32196a
Jessica Lawshe 5 vuotta sitten
vanhempi
sitoutus
ddb6163b65

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

@@ -66,6 +66,7 @@ const UserPagesController = {
 
 
   settingsPage(req, res, next) {
   settingsPage(req, res, next) {
     const userId = AuthenticationController.getLoggedInUserId(req)
     const userId = AuthenticationController.getLoggedInUserId(req)
+    const reconfirmationRemoveEmail = req.query.remove
     // SSO
     // SSO
     const ssoError = req.session.ssoError
     const ssoError = req.session.ssoError
     if (ssoError) {
     if (ssoError) {
@@ -91,6 +92,8 @@ const UserPagesController = {
       'saml',
       'saml',
       'requestedEmail'
       'requestedEmail'
     ])
     ])
+
+    const reconfirmedViaSAML = _.get(req.session, ['saml', 'reconfirmed'])
     delete req.session.saml
     delete req.session.saml
     let shouldAllowEditingDetails = true
     let shouldAllowEditingDetails = true
     if (Settings.ldap && Settings.ldap.updateUserDetailsOnLogin) {
     if (Settings.ldap && Settings.ldap.updateUserDetailsOnLogin) {
@@ -123,6 +126,8 @@ const UserPagesController = {
           institutionEmailNonCanonical && institutionRequestedEmail
           institutionEmailNonCanonical && institutionRequestedEmail
             ? institutionEmailNonCanonical
             ? institutionEmailNonCanonical
             : undefined,
             : undefined,
+        reconfirmedViaSAML,
+        reconfirmationRemoveEmail,
         samlBeta: req.session.samlBeta,
         samlBeta: req.session.samlBeta,
         ssoError: ssoError,
         ssoError: ssoError,
         thirdPartyIds: UserPagesController._restructureThirdPartyIds(user)
         thirdPartyIds: UserPagesController._restructureThirdPartyIds(user)

+ 39 - 0
services/web/app/views/_mixins/reconfirm_affiliation.pug

@@ -0,0 +1,39 @@
+mixin reconfirmAffiliationNotification(location)
+	.reconfirm-notification(ng-controller="UserAffiliationsReconfirmController")
+		div(ng-if="!reconfirm[userEmail.email].reconfirmationSent" style="width:100%;")
+			i.fa.fa-warning
+
+			button.btn-reconfirm.btn.btn-sm.btn-info(
+				data-location=location
+				ng-if="!ui.sentReconfirmation"
+				ng-click="requestReconfirmation($event, userEmail)"
+				ng-disabled="ui.isMakingRequest"
+			) #{translate("confirm_affiliation")}
+
+			| !{translate("are_you_still_at", {institutionName: '{{userEmail.affiliation.institution.name}}'}, ['strong'])}
+			|  
+
+			if location == '/user/settings'
+				| !{translate('please_reconfirm_institutional_email', {}, [{ name: 'span' }])}
+				span(ng-if="userEmail.default")  #{translate('need_to_add_new_primary_before_remove')}
+			else
+				| !{translate("please_reconfirm_institutional_email", {}, [{name: 'a', attrs: {href: '/user/settings?remove={{userEmail.email}}' }}])}
+
+			|  
+			a(href="/learn/how-to/Reconfirm_an_institutional_email_address") #{translate("learn_more")}
+
+		div(ng-if="reconfirm[userEmail.email].reconfirmationSent")
+			| !{translate("please_check_your_inbox_to_confirm", {institutionName: '{{userEmail.affiliation.institution.name}}'}, ['strong'])}
+			|  
+			a(
+				href
+				ng-click="requestReconfirmation($event, userEmail)"
+				ng-disabled="ui.isMakingRequest"
+			) #{translate('resend_confirmation_email')}
+
+mixin reconfirmedAffiliationNotification()
+	.alert.alert-info
+		.reconfirm-notification
+			div
+				//- extra div for flex styling
+				| !{translate("your_affiliation_is_confirmed", {institutionName: '{{userEmail.affiliation.institution.name}}'}, ['strong'])} #{translate('thank_you')}

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

@@ -281,7 +281,13 @@ block content
 			)
 			)
 				span(ng-hide="state.inflight") #{translate("delete")}
 				span(ng-hide="state.inflight") #{translate("delete")}
 				span(ng-show="state.inflight") #{translate("deleting")}…
 				span(ng-show="state.inflight") #{translate("deleting")}…
-	
+
+	script#data(type="application/json").
+		!{StringHelper.stringifyJsonForScript({ reconfirmationRemoveEmail, reconfirmedViaSAML })}
+
+	script(type="text/javascript").
+		window.data = JSON.parse(document.querySelector("#data").text);
+
 	script(type='text/javascript').
 	script(type='text/javascript').
 		window.usersEmail = !{StringHelper.stringifyJsonForScript(user.email)};
 		window.usersEmail = !{StringHelper.stringifyJsonForScript(user.email)};
 		window.passwordStrengthOptions = !{StringHelper.stringifyJsonForScript(settings.passwordStrengthOptions || {})}
 		window.passwordStrengthOptions = !{StringHelper.stringifyJsonForScript(settings.passwordStrengthOptions || {})}

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

@@ -1,3 +1,5 @@
+include ../../_mixins/reconfirm_affiliation
+
 mixin aboutInstitutionLink()
 mixin aboutInstitutionLink()
 	a(href="/learn/how-to/Institutional_Login") #{translate("find_out_more_about_institution_login")}.
 	a(href="/learn/how-to/Institutional_Login") #{translate("find_out_more_about_institution_login")}.
 
 
@@ -116,7 +118,7 @@ form.row(
 						) #{translate("make_primary")}
 						) #{translate("make_primary")}
 						|  
 						|  
 						+btnRemoveEmail()
 						+btnRemoveEmail()
-				tr.affiliations-table-saml-row(ng-repeat-end ng-if="userEmail.affiliation && userEmail.affiliation && userEmail.ssoAvailable")
+				tr.affiliations-table-saml-row(ng-if="userEmail.affiliation && userEmail.affiliation && userEmail.ssoAvailable")
 					td
 					td
 					td(ng-attr-colspan="{{userEmail.samlProviderId ? '2' : '1'}}" ng-class="institutionAlreadyLinked(userEmail) ? '' : 'with-border'")
 					td(ng-attr-colspan="{{userEmail.samlProviderId ? '2' : '1'}}" ng-class="institutionAlreadyLinked(userEmail) ? '' : 'with-border'")
 						p.small(ng-if="userEmail.samlProviderId")
 						p.small(ng-if="userEmail.samlProviderId")
@@ -137,6 +139,23 @@ form.row(
 							type="button"
 							type="button"
 						)
 						)
 							| #{translate("link_accounts")}
 							| #{translate("link_accounts")}
+				tr(
+					class="reconfirm-row"
+					ng-if="userEmail.samlIdentifier && userEmail.samlIdentifier.providerId === reconfirmedViaSAML"
+				)
+					td(colspan="3")
+						+reconfirmedAffiliationNotification()
+				tr(
+					class="reconfirm-row"
+					ng-repeat-end
+				)
+					td(
+						colspan="3"
+						ng-if="userEmail.affiliation && userEmail.affiliation.inReconfirmNotificationPeriod"
+					)
+						div(ng-class="{'alert alert-info': reconfirmationRemoveEmail === userEmail.email}")
+							+reconfirmAffiliationNotification('/user/settings')
+
 				tr.affiliations-table-highlighted-row(
 				tr.affiliations-table-highlighted-row(
 					ng-if="!ui.showAddEmailUI && !ui.isMakingRequest"
 					ng-if="!ui.showAddEmailUI && !ui.isMakingRequest"
 				)
 				)

+ 1 - 0
services/web/frontend/js/main.js

@@ -33,6 +33,7 @@ import './main/affiliations/components/affiliationForm'
 import './main/affiliations/components/inputSuggestions'
 import './main/affiliations/components/inputSuggestions'
 import './main/affiliations/controllers/UserAffiliationsController'
 import './main/affiliations/controllers/UserAffiliationsController'
 import './main/affiliations/factories/UserAffiliationsDataService'
 import './main/affiliations/factories/UserAffiliationsDataService'
+import './main/affiliations/controllers/UserAffiliationsReconfirmController'
 import './main/oauth/controllers/UserOauthController'
 import './main/oauth/controllers/UserOauthController'
 import './main/keys'
 import './main/keys'
 import './main/importing'
 import './main/importing'

+ 2 - 0
services/web/frontend/js/main/affiliations/controllers/UserAffiliationsController.js

@@ -22,6 +22,8 @@ export default App.controller('UserAffiliationsController', function(
   }
   }
   $scope.samlBetaSession = ExposedSettings.hasSamlBeta
   $scope.samlBetaSession = ExposedSettings.hasSamlBeta
   $scope.samlInitPath = ExposedSettings.samlInitPath
   $scope.samlInitPath = ExposedSettings.samlInitPath
+  $scope.reconfirmationRemoveEmail = $window.data.reconfirmationRemoveEmail
+  $scope.reconfirmedViaSAML = $window.data.reconfirmedViaSAML
 
 
   const LOCAL_AND_DOMAIN_REGEX = /([^@]+)@(.+)/
   const LOCAL_AND_DOMAIN_REGEX = /([^@]+)@(.+)/
   const EMAIL_REGEX = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\ ".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA -Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
   const EMAIL_REGEX = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\ ".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA -Z\-0-9]+\.)+[a-zA-Z]{2,}))$/

+ 56 - 0
services/web/frontend/js/main/affiliations/controllers/UserAffiliationsReconfirmController.js

@@ -0,0 +1,56 @@
+import _ from 'lodash'
+import App from '../../../base'
+
+export default App.controller('UserAffiliationsReconfirmController', function(
+  $scope,
+  UserAffiliationsDataService,
+  $window
+) {
+  $scope.reconfirm = {}
+  // For portals:
+  $scope.userEmails = window.data.userEmails
+  // For settings page:
+  $scope.ui = $scope.ui || {} // $scope.ui inherited on settings page
+  // For dashboard:
+  $scope.allInReconfirmNotificationPeriods =
+    window.data.allInReconfirmNotificationPeriods
+
+  function sendReconfirmEmail(email) {
+    $scope.ui.hasError = false
+    $scope.ui.isMakingRequest = true
+    UserAffiliationsDataService.resendConfirmationEmail(email)
+      .then(() => {
+        $scope.reconfirm[email].reconfirmationSent = true
+      })
+      .catch(error => {
+        $scope.ui.hasError = true
+      })
+      .finally(() => ($scope.ui.isMakingRequest = false))
+  }
+
+  $scope.reconfirmationRemoveEmail = $window.data.reconfirmationRemoveEmail
+  $scope.reconfirmedViaSAML = $window.data.reconfirmedViaSAML
+
+  $scope.requestReconfirmation = function(obj, userEmail) {
+    const email = userEmail.email
+    // For the settings page, disable other parts of affiliation UI
+    $scope.ui.isMakingRequest = true
+    $scope.ui.isProcessing = true
+    // create UI scope for requested email
+    $scope.reconfirm[email] = $scope.reconfirm[email] || {} // keep existing scope for resend email requests
+
+    const location = obj.currentTarget.getAttribute('data-location')
+    const institutionId = _.get(userEmail, ['affiliation', 'institution', 'id'])
+    const ssoEnabled = _.get(userEmail, [
+      'affiliation',
+      'institution',
+      'ssoEnabled'
+    ])
+
+    if (ssoEnabled) {
+      $window.location.href = `${$scope.samlInitPath}?university_id=${institutionId}&reconfirm=${location}`
+    } else {
+      sendReconfirmEmail(email)
+    }
+  }
+})

+ 1 - 0
services/web/frontend/stylesheets/_style_includes.less

@@ -36,6 +36,7 @@
 @import 'components/footer.less';
 @import 'components/footer.less';
 //@import "components/breadcrumbs.less";
 //@import "components/breadcrumbs.less";
 //@import "components/pagination.less";
 //@import "components/pagination.less";
+@import 'components/notifications.less';
 @import 'components/pager.less';
 @import 'components/pager.less';
 @import 'components/labels.less';
 @import 'components/labels.less';
 //@import "components/badges.less";
 //@import "components/badges.less";

+ 41 - 0
services/web/frontend/stylesheets/components/notifications.less

@@ -0,0 +1,41 @@
+.reconfirm-notification {
+  display: flex;
+  width: 100%;
+  .fa-warning {
+    margin-right: @margin-sm;
+  }
+  .btn-reconfirm {
+    float: right;
+    margin-left: @margin-sm;
+    text-transform: capitalize;
+  }
+}
+
+// Settings page
+.affiliations-table {
+  .reconfirm-notification {
+    margin: 0px auto @margin-sm auto !important;
+    padding: @padding-md;
+  }
+
+  .reconfirm-row {
+    td {
+      border: 0;
+
+      .alert {
+        border: 0;
+        padding: 0;
+      }
+
+      :not(.alert) {
+        .reconfirm-notification {
+          background-color: @ol-blue-gray-0;
+          border-radius: @border-radius-base;
+          .fa-warning {
+            color: @brand-warning;
+          }
+        }
+      }
+    }
+  }
+}

+ 10 - 2
services/web/locales/en.json

@@ -1347,5 +1347,13 @@
   "did_you_know_institution_providing_professional": "Did you know that __institutionName__ is providing <0>free __appName__ Professional features</0> to everyone at __institutionName__?",
   "did_you_know_institution_providing_professional": "Did you know that __institutionName__ is providing <0>free __appName__ Professional features</0> to everyone at __institutionName__?",
   "add_email_to_claim_features": "Add an institutional email address to claim your features.",
   "add_email_to_claim_features": "Add an institutional email address to claim your features.",
   "please_change_primary_to_remove": "Please change your primary email in order to remove",
   "please_change_primary_to_remove": "Please change your primary email in order to remove",
-  "dropbox_duplicate_project_names": "We detected an update from Dropbox to <0>__projectName__</0>, but you have multiple projects with that name. We are unable to process this, so have unlinked your Dropbox account. Please ensure your project names are unique across your active, archived and trashed projects, and then re-link your Dropbox account."
-}
+  "dropbox_duplicate_project_names": "We detected an update from Dropbox to <0>__projectName__</0>, but you have multiple projects with that name. We are unable to process this, so have unlinked your Dropbox account. Please ensure your project names are unique across your active, archived and trashed projects, and then re-link your Dropbox account.",
+  "please_reconfirm_institutional_email": "Please take a moment to confirm your institutional email address or <0>remove it</0> from your account.",
+  "need_to_add_new_primary_before_remove": "You'll need to add a new primary email address before you can remove this one.",
+  "are_you_still_at": "Are you still at <0>__institutionName__</0>?",
+  "confirm_affiliation": "Confirm Affiliation",
+  "please_check_your_inbox_to_confirm": "Please check your email inbox to confirm your <0>__institutionName__</0> affiliation.",
+  "resend_confirmation_email": "Resend confirmation email",
+  "your_affiliation_is_confirmed": "Your <0>__institutionName__</0> affiliation is confirmed.",
+  "thank_you": "Thank you!"
+}