From ddb6163b6598bd4c721f357678b9be25392ff212 Mon Sep 17 00:00:00 2001 From: Jessica Lawshe Date: Mon, 22 Feb 2021 09:01:02 -0600 Subject: [PATCH] Merge pull request #3623 from overleaf/jel-ui-reconfirm-settings Add reconfirm UI to settings GitOrigin-RevId: cf9c4648cab07784782e24d752154089dc32196a --- .../src/Features/User/UserPagesController.js | 5 ++ .../views/_mixins/reconfirm_affiliation.pug | 39 +++++++++++++ services/web/app/views/user/settings.pug | 8 ++- .../views/user/settings/user-affiliations.pug | 21 ++++++- services/web/frontend/js/main.js | 1 + .../controllers/UserAffiliationsController.js | 2 + .../UserAffiliationsReconfirmController.js | 56 +++++++++++++++++++ .../frontend/stylesheets/_style_includes.less | 1 + .../stylesheets/components/notifications.less | 41 ++++++++++++++ services/web/locales/en.json | 12 +++- 10 files changed, 182 insertions(+), 4 deletions(-) create mode 100644 services/web/app/views/_mixins/reconfirm_affiliation.pug create mode 100644 services/web/frontend/js/main/affiliations/controllers/UserAffiliationsReconfirmController.js create mode 100644 services/web/frontend/stylesheets/components/notifications.less diff --git a/services/web/app/src/Features/User/UserPagesController.js b/services/web/app/src/Features/User/UserPagesController.js index 4e8f9f5b7a..5feff12638 100644 --- a/services/web/app/src/Features/User/UserPagesController.js +++ b/services/web/app/src/Features/User/UserPagesController.js @@ -66,6 +66,7 @@ const UserPagesController = { settingsPage(req, res, next) { const userId = AuthenticationController.getLoggedInUserId(req) + const reconfirmationRemoveEmail = req.query.remove // SSO const ssoError = req.session.ssoError if (ssoError) { @@ -91,6 +92,8 @@ const UserPagesController = { 'saml', 'requestedEmail' ]) + + const reconfirmedViaSAML = _.get(req.session, ['saml', 'reconfirmed']) delete req.session.saml let shouldAllowEditingDetails = true if (Settings.ldap && Settings.ldap.updateUserDetailsOnLogin) { @@ -123,6 +126,8 @@ const UserPagesController = { institutionEmailNonCanonical && institutionRequestedEmail ? institutionEmailNonCanonical : undefined, + reconfirmedViaSAML, + reconfirmationRemoveEmail, samlBeta: req.session.samlBeta, ssoError: ssoError, thirdPartyIds: UserPagesController._restructureThirdPartyIds(user) diff --git a/services/web/app/views/_mixins/reconfirm_affiliation.pug b/services/web/app/views/_mixins/reconfirm_affiliation.pug new file mode 100644 index 0000000000..1a00999eed --- /dev/null +++ b/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')} diff --git a/services/web/app/views/user/settings.pug b/services/web/app/views/user/settings.pug index 05ea39e0ef..b77006afd2 100644 --- a/services/web/app/views/user/settings.pug +++ b/services/web/app/views/user/settings.pug @@ -281,7 +281,13 @@ block content ) span(ng-hide="state.inflight") #{translate("delete")} 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'). window.usersEmail = !{StringHelper.stringifyJsonForScript(user.email)}; window.passwordStrengthOptions = !{StringHelper.stringifyJsonForScript(settings.passwordStrengthOptions || {})} diff --git a/services/web/app/views/user/settings/user-affiliations.pug b/services/web/app/views/user/settings/user-affiliations.pug index 6d3901e39a..afa5eb0a29 100644 --- a/services/web/app/views/user/settings/user-affiliations.pug +++ b/services/web/app/views/user/settings/user-affiliations.pug @@ -1,3 +1,5 @@ +include ../../_mixins/reconfirm_affiliation + mixin aboutInstitutionLink() a(href="/learn/how-to/Institutional_Login") #{translate("find_out_more_about_institution_login")}. @@ -116,7 +118,7 @@ form.row( ) #{translate("make_primary")} |   +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(ng-attr-colspan="{{userEmail.samlProviderId ? '2' : '1'}}" ng-class="institutionAlreadyLinked(userEmail) ? '' : 'with-border'") p.small(ng-if="userEmail.samlProviderId") @@ -137,6 +139,23 @@ form.row( type="button" ) | #{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( ng-if="!ui.showAddEmailUI && !ui.isMakingRequest" ) diff --git a/services/web/frontend/js/main.js b/services/web/frontend/js/main.js index cd87c2aeef..ec37527ddb 100644 --- a/services/web/frontend/js/main.js +++ b/services/web/frontend/js/main.js @@ -33,6 +33,7 @@ import './main/affiliations/components/affiliationForm' import './main/affiliations/components/inputSuggestions' import './main/affiliations/controllers/UserAffiliationsController' import './main/affiliations/factories/UserAffiliationsDataService' +import './main/affiliations/controllers/UserAffiliationsReconfirmController' import './main/oauth/controllers/UserOauthController' import './main/keys' import './main/importing' diff --git a/services/web/frontend/js/main/affiliations/controllers/UserAffiliationsController.js b/services/web/frontend/js/main/affiliations/controllers/UserAffiliationsController.js index 10a7659d1a..92012902e1 100644 --- a/services/web/frontend/js/main/affiliations/controllers/UserAffiliationsController.js +++ b/services/web/frontend/js/main/affiliations/controllers/UserAffiliationsController.js @@ -22,6 +22,8 @@ export default App.controller('UserAffiliationsController', function( } $scope.samlBetaSession = ExposedSettings.hasSamlBeta $scope.samlInitPath = ExposedSettings.samlInitPath + $scope.reconfirmationRemoveEmail = $window.data.reconfirmationRemoveEmail + $scope.reconfirmedViaSAML = $window.data.reconfirmedViaSAML 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,}))$/ diff --git a/services/web/frontend/js/main/affiliations/controllers/UserAffiliationsReconfirmController.js b/services/web/frontend/js/main/affiliations/controllers/UserAffiliationsReconfirmController.js new file mode 100644 index 0000000000..2b36aa3b72 --- /dev/null +++ b/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) + } + } +}) diff --git a/services/web/frontend/stylesheets/_style_includes.less b/services/web/frontend/stylesheets/_style_includes.less index e852fbaeea..b0ee05685b 100644 --- a/services/web/frontend/stylesheets/_style_includes.less +++ b/services/web/frontend/stylesheets/_style_includes.less @@ -36,6 +36,7 @@ @import 'components/footer.less'; //@import "components/breadcrumbs.less"; //@import "components/pagination.less"; +@import 'components/notifications.less'; @import 'components/pager.less'; @import 'components/labels.less'; //@import "components/badges.less"; diff --git a/services/web/frontend/stylesheets/components/notifications.less b/services/web/frontend/stylesheets/components/notifications.less new file mode 100644 index 0000000000..0c90b82563 --- /dev/null +++ b/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; + } + } + } + } + } +} diff --git a/services/web/locales/en.json b/services/web/locales/en.json index f2de760170..3f72139253 100644 --- a/services/web/locales/en.json +++ b/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 to everyone at __institutionName__?", "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", - "dropbox_duplicate_project_names": "We detected an update from Dropbox to <0>__projectName__, 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." -} \ No newline at end of file + "dropbox_duplicate_project_names": "We detected an update from Dropbox to <0>__projectName__, 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 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__?", + "confirm_affiliation": "Confirm Affiliation", + "please_check_your_inbox_to_confirm": "Please check your email inbox to confirm your <0>__institutionName__ affiliation.", + "resend_confirmation_email": "Resend confirmation email", + "your_affiliation_is_confirmed": "Your <0>__institutionName__ affiliation is confirmed.", + "thank_you": "Thank you!" +}