Merge pull request #2251 from overleaf/jel-dashboard-institution-notifications
Institution SSO dashboard notifications GitOrigin-RevId: c80117c2732fafdc9d09eed69db06a26ad28a663
This commit is contained in:
committed by
sharelatex
parent
9e651d77b2
commit
28a4be296f
@@ -412,7 +412,7 @@ const ProjectController = {
|
||||
user(cb) {
|
||||
User.findById(
|
||||
userId,
|
||||
'featureSwitches overleaf awareOfV2 features lastLoginIp',
|
||||
'emails featureSwitches overleaf awareOfV2 features lastLoginIp',
|
||||
cb
|
||||
)
|
||||
},
|
||||
@@ -428,27 +428,92 @@ const ProjectController = {
|
||||
logger.warn({ err }, 'error getting data for project list page')
|
||||
return next(err)
|
||||
}
|
||||
const { notifications, user, userAffiliations } = results
|
||||
const v1Tags =
|
||||
(results.v1Projects != null ? results.v1Projects.tags : undefined) ||
|
||||
[]
|
||||
const tags = results.tags.concat(v1Tags)
|
||||
const notifications = results.notifications
|
||||
for (const notification of notifications) {
|
||||
notification.html = req.i18n.translate(
|
||||
notification.templateKey,
|
||||
notification.messageOpts
|
||||
)
|
||||
}
|
||||
|
||||
// Institution SSO Notifications
|
||||
if (Features.hasFeature('saml') || req.session.samlBeta) {
|
||||
const samlSession = req.session.saml
|
||||
// Notification: SSO Available
|
||||
// Could have multiple emails at the same institution, and if any are
|
||||
// linked to the institution then do not show notification for others
|
||||
const linkedInstitutionIds = []
|
||||
const linkedInstitutionEmails = []
|
||||
user.emails.forEach(email => {
|
||||
if (email.samlProviderId) {
|
||||
linkedInstitutionEmails.push(email.email)
|
||||
linkedInstitutionIds.push(email.samlProviderId)
|
||||
}
|
||||
})
|
||||
if (Array.isArray(userAffiliations)) {
|
||||
userAffiliations.forEach(affiliation => {
|
||||
if (
|
||||
affiliation.institution &&
|
||||
affiliation.institution.ssoEnabled &&
|
||||
linkedInstitutionEmails.indexOf(affiliation.email) === -1 &&
|
||||
linkedInstitutionIds.indexOf(
|
||||
affiliation.institution.id.toString()
|
||||
) === -1
|
||||
) {
|
||||
notifications.push({
|
||||
email: affiliation.email,
|
||||
institutionId: affiliation.institution.id,
|
||||
institutionName: affiliation.institution.name,
|
||||
templateKey: 'notification_institution_sso_available'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (samlSession) {
|
||||
// Notification: After SSO Linked
|
||||
if (samlSession.linked) {
|
||||
notifications.push({
|
||||
email: samlSession.institutionEmail,
|
||||
institutionName: samlSession.linked.universityName,
|
||||
templateKey: 'notification_institution_sso_linked'
|
||||
})
|
||||
}
|
||||
|
||||
// Notification: After SSO Linked or Logging in
|
||||
// The requested email does not match primary email returned from
|
||||
// the institution
|
||||
if (samlSession.emailNonCanonical) {
|
||||
notifications.push({
|
||||
institutionEmail: samlSession.emailNonCanonical,
|
||||
requestedEmail: samlSession.requestedEmail,
|
||||
templateKey: 'notification_institution_sso_non_canonical'
|
||||
})
|
||||
}
|
||||
|
||||
// Notification: Tried to register, but account already existed
|
||||
if (samlSession.registerIntercept) {
|
||||
notifications.push({
|
||||
email: samlSession.institutionEmail,
|
||||
templateKey: 'notification_institution_sso_already_registered'
|
||||
})
|
||||
}
|
||||
}
|
||||
delete req.session.saml
|
||||
}
|
||||
|
||||
const portalTemplates = ProjectController._buildPortalTemplatesList(
|
||||
results.userAffiliations
|
||||
userAffiliations
|
||||
)
|
||||
const projects = ProjectController._buildProjectList(
|
||||
results.projects,
|
||||
userId,
|
||||
results.v1Projects != null ? results.v1Projects.projects : undefined
|
||||
)
|
||||
const { user } = results
|
||||
const { userAffiliations } = results
|
||||
const warnings = ProjectController._buildWarningsList(
|
||||
results.v1Projects
|
||||
)
|
||||
|
||||
@@ -115,7 +115,16 @@ const UserPagesController = {
|
||||
delete req.session.ssoError
|
||||
}
|
||||
// Institution SSO
|
||||
const institutionLinked = _.get(req.session, ['saml', 'linked'])
|
||||
let institutionLinked = _.get(req.session, ['saml', 'linked'])
|
||||
if (institutionLinked) {
|
||||
// copy object if exists because _.get does not
|
||||
institutionLinked = Object.assign(
|
||||
{
|
||||
hasEntitlement: _.get(req.session, ['saml', 'hasEntitlement'])
|
||||
},
|
||||
institutionLinked
|
||||
)
|
||||
}
|
||||
const institutionNotLinked = _.get(req.session, ['saml', 'notLinked'])
|
||||
const institutionEmailNonCanonical = _.get(req.session, [
|
||||
'saml',
|
||||
|
||||
@@ -89,8 +89,11 @@ block content
|
||||
include ./list/project-list
|
||||
|
||||
.project-list-empty.row(ng-if="projects.length === 0")
|
||||
.col-md-offset-2.col-md-8.col-md-offset-2.col-xs-8
|
||||
.project-list-empty-col.col-md-offset-2.col-md-8.col-md-offset-2.col-xs-8.col-xs-offset-2
|
||||
include ./list/empty-project-list
|
||||
.row.row-spaced
|
||||
.col-sm-12
|
||||
include ./list/notifications
|
||||
|
||||
include ./list/modals
|
||||
|
||||
|
||||
@@ -1,77 +1,106 @@
|
||||
span(ng-controller="NotificationsController").userNotifications
|
||||
ul.list-unstyled.notifications-list(
|
||||
.user-notifications(ng-controller="NotificationsController")
|
||||
ul.list-unstyled(
|
||||
ng-if="notifications.length > 0",
|
||||
ng-cloak
|
||||
)
|
||||
li.notification_entry(
|
||||
li.notification-entry(
|
||||
ng-repeat="notification in notifications",
|
||||
)
|
||||
.row(ng-hide="notification.hide")
|
||||
.col-xs-12
|
||||
div(ng-switch="notification.templateKey")
|
||||
.alert.alert-info(ng-switch-when="notification_project_invite", ng-controller="ProjectInviteNotificationController")
|
||||
div.notification_inner
|
||||
.notification_body(ng-show="!notification.accepted")
|
||||
| !{translate("notification_project_invite_message", { userName: "{{ userName }}", projectName: "{{ projectName }}" })}
|
||||
a.pull-right.btn.btn-sm.btn-info(href, ng-click="accept()", ng-disabled="notification.inflight")
|
||||
span(ng-show="!notification.inflight") #{translate("join_project")}
|
||||
span(ng-show="notification.inflight")
|
||||
i.fa.fa-fw.fa-spinner.fa-spin(aria-hidden="true")
|
||||
|
|
||||
| #{translate("joining")}...
|
||||
.notification_body(ng-show="notification.accepted")
|
||||
| !{translate("notification_project_invite_accepted_message", { projectName: "{{ projectName }}" })}
|
||||
a.pull-right.btn.btn-sm.btn-info(href="/project/{{ notification.messageOpts.projectId }}") #{translate("open_project")}
|
||||
span().notification_close
|
||||
button(ng-click="dismiss(notification)").close.pull-right
|
||||
span(aria-hidden="true") ×
|
||||
span.sr-only #{translate("close")}
|
||||
.alert.alert-info(ng-switch-when="notification_ip_matched_affiliation")
|
||||
div.notification_inner
|
||||
.notification_body
|
||||
| It looks like you're at
|
||||
strong {{ notification.messageOpts.university_name }}! <br/>
|
||||
| Did you know that {{notification.messageOpts.university_name}} is providing
|
||||
strong free Overleaf Professional accounts
|
||||
| to everyone at {{notification.messageOpts.university_name}}? <br/>
|
||||
| Add an institutional email address to claim your account.
|
||||
a.pull-right.btn.btn-sm.btn-info(href="/user/settings")
|
||||
| Add Affiliation
|
||||
span().notification_close
|
||||
button(ng-click="dismiss(notification)").close.pull-right
|
||||
span(aria-hidden="true") ×
|
||||
span.sr-only #{translate("close")}
|
||||
.alert.alert-info(ng-switch-default)
|
||||
div.notification_inner
|
||||
span(ng-bind-html="notification.html").notification_body
|
||||
span().notification_close
|
||||
button(ng-click="dismiss(notification)").close.pull-right
|
||||
span(aria-hidden="true") ×
|
||||
span.sr-only #{translate("close")}
|
||||
div(ng-switch="notification.templateKey" ng-hide="notification.hide")
|
||||
.alert.alert-info(ng-switch-when="notification_project_invite", ng-controller="ProjectInviteNotificationController")
|
||||
.notification-body(ng-show="!notification.accepted")
|
||||
| !{translate("notification_project_invite_message", { userName: "{{ userName }}", projectName: "{{ projectName }}" })}
|
||||
a.pull-right.btn.btn-sm.btn-info(href, ng-click="accept()", ng-disabled="notification.inflight")
|
||||
span(ng-show="!notification.inflight") #{translate("join_project")}
|
||||
span(ng-show="notification.inflight")
|
||||
i.fa.fa-fw.fa-spinner.fa-spin(aria-hidden="true")
|
||||
|
|
||||
| #{translate("joining")}...
|
||||
.notification-body(ng-show="notification.accepted")
|
||||
| !{translate("notification_project_invite_accepted_message", { projectName: "{{ projectName }}" })}
|
||||
a.pull-right.btn.btn-sm.btn-info(href="/project/{{ notification.messageOpts.projectId }}") #{translate("open_project")}
|
||||
.notification-close
|
||||
button(ng-click="dismiss(notification)").close.pull-right
|
||||
span(aria-hidden="true") ×
|
||||
span.sr-only #{translate("close")}
|
||||
.alert.alert-info(ng-switch-when="notification_ip_matched_affiliation")
|
||||
.notification-body
|
||||
| It looks like you're at
|
||||
strong {{ notification.messageOpts.university_name }}! <br/>
|
||||
| Did you know that {{notification.messageOpts.university_name}} is providing
|
||||
strong free Overleaf Professional accounts
|
||||
| to everyone at {{notification.messageOpts.university_name}}? <br/>
|
||||
| Add an institutional email address to claim your account.
|
||||
a.pull-right.btn.btn-sm.btn-info(href="/user/settings")
|
||||
| Add Affiliation
|
||||
.notification-close
|
||||
button(ng-click="dismiss(notification)").close.pull-right
|
||||
span(aria-hidden="true") ×
|
||||
span.sr-only #{translate("close")}
|
||||
.alert.alert-info(ng-switch-when="notification_institution_sso_available")
|
||||
.notification-body
|
||||
p !{translate("can_link_institution_email_acct_to_institution_acct", {appName: settings.appName, email: "{{notification.email}}", institutionName: "{{notification.institutionName}}"})}
|
||||
div !{translate("doing_this_allow_log_in_through_institution", {appName: settings.appName})}
|
||||
.notification-action
|
||||
a.btn.btn-info(href="{{samlInitPath}}?university_id={{notification.institutionId}}&auto=project&email={{notification.email}}")
|
||||
| #{translate('link_account')}
|
||||
.notification-close
|
||||
button(ng-click="dismiss(notification)").close.pull-right
|
||||
span(aria-hidden="true") ×
|
||||
span.sr-only #{translate("close")}
|
||||
.alert.alert-info(ng-switch-when="notification_institution_sso_linked")
|
||||
.notification-body
|
||||
div !{translate("account_has_been_link_to_institution_account", {appName: settings.appName, email: "{{notification.email}}", institutionName: "{{notification.institutionName}}"})}
|
||||
.notification-close
|
||||
button(ng-click="dismiss(notification)").close.pull-right
|
||||
span(aria-hidden="true") ×
|
||||
span.sr-only #{translate("close")}
|
||||
.alert.alert-warning(ng-switch-when="notification_institution_sso_non_canonical")
|
||||
.notification-body
|
||||
div
|
||||
i.fa.fa-fw.fa-exclamation-triangle(aria-hidden="true")
|
||||
| !{translate("tried_to_log_in_with_email", {email: "{{notification.requestedEmail}}"})} !{translate("in_order_to_match_institutional_metadata_associated", {email: "{{notification.institutionEmail}}"})}
|
||||
.notification-close
|
||||
button(ng-click="dismiss(notification)").close.pull-right
|
||||
span(aria-hidden="true") ×
|
||||
span.sr-only #{translate("close")}
|
||||
.alert.alert-info(ng-switch-when="notification_institution_sso_already_registered")
|
||||
.notification-body
|
||||
| !{translate("tried_to_register_with_email", {appName: settings.appName, email: "{{notification.email}}"})}
|
||||
| #{translate("we_logged_you_in")}
|
||||
.notification-action
|
||||
a.btn.btn-info(href="/")
|
||||
| #{translate("find_out_more")}
|
||||
.notification-close
|
||||
button(ng-click="dismiss(notification)").close.pull-right
|
||||
span(aria-hidden="true") ×
|
||||
span.sr-only #{translate("close")}
|
||||
.alert.alert-info(ng-switch-default)
|
||||
span(ng-bind-html="notification.html").notification-body
|
||||
.notification-close
|
||||
button(ng-click="dismiss(notification)").close.pull-right
|
||||
span(aria-hidden="true") ×
|
||||
span.sr-only #{translate("close")}
|
||||
|
||||
ul.list-unstyled.notifications-list(
|
||||
|
||||
ul.list-unstyled(
|
||||
ng-controller="EmailNotificationController",
|
||||
ng-cloak
|
||||
)
|
||||
li.notification_entry(
|
||||
li.notification-entry(
|
||||
ng-repeat="userEmail in userEmails",
|
||||
ng-if="showConfirmEmail(userEmail)"
|
||||
)
|
||||
.row
|
||||
.col-xs-12
|
||||
.alert.alert-warning
|
||||
.notification_inner(
|
||||
ng-if="!userEmail.confirmationInflight"
|
||||
)
|
||||
| #{translate("please_confirm_email", {emailAddress: "{{ userEmail.email }}"})}
|
||||
|
|
||||
a(
|
||||
href
|
||||
ng-click="resendConfirmationEmail(userEmail)"
|
||||
) (#{translate('resend_confirmation_email')})
|
||||
.notification_inner(
|
||||
ng-if="userEmail.confirmationInflight"
|
||||
)
|
||||
i.fa.fa-spinner.fa-spin(aria-hidden="true")
|
||||
|
|
||||
| #{translate('resending_confirmation_email')}…
|
||||
.alert.alert-warning(ng-if="!userEmail.confirmationInflight")
|
||||
.notification-body
|
||||
| #{translate("please_confirm_email", {emailAddress: "{{ userEmail.email }}"})}
|
||||
|
|
||||
a(
|
||||
href
|
||||
ng-click="resendConfirmationEmail(userEmail)"
|
||||
) (#{translate('resend_confirmation_email')})
|
||||
.alert.alert-warning(ng-if="userEmail.confirmationInflight")
|
||||
.notification-body
|
||||
i.fa.fa-spinner.fa-spin(aria-hidden="true")
|
||||
|
|
||||
| #{translate('resending_confirmation_email')}…
|
||||
@@ -212,7 +212,7 @@ form.row(
|
||||
span(ng-if="ui.errorMessage") {{ui.errorMessage}}
|
||||
if institutionLinked
|
||||
tr.affiliations-table-info-row(ng-if="!hideInstitutionNotifications.info")
|
||||
td(colspan="3").text-center(aria-live="assertive")
|
||||
td.text-center(aria-live="assertive" colspan="3")
|
||||
button.close(
|
||||
type="button"
|
||||
data-dismiss="modal"
|
||||
@@ -225,7 +225,7 @@ form.row(
|
||||
.small !{translate("this_grants_access_to_features", {featureType: translate("professional")})}
|
||||
if institutionEmailNonCanonical
|
||||
tr.affiliations-table-warning-row(ng-if="!hideInstitutionNotifications.warning")
|
||||
td(colspan="3").text-center(aria-live="assertive")
|
||||
td.text-center(aria-live="assertive" colspan="3")
|
||||
button.close(
|
||||
type="button"
|
||||
data-dismiss="modal"
|
||||
@@ -239,7 +239,7 @@ form.row(
|
||||
| !{translate("in_order_to_match_institutional_metadata", {email: institutionEmailNonCanonical})}
|
||||
if institutionNotLinked
|
||||
tr.affiliations-table-error-row(ng-if="!hideInstitutionNotifications.error")
|
||||
td(colspan="3").text-center(aria-live="assertive")
|
||||
td.text-center(aria-live="assertive" colspan="3")
|
||||
button.close(
|
||||
type="button"
|
||||
data-dismiss="modal"
|
||||
|
||||
@@ -17,6 +17,8 @@ define(['base'], function(App) {
|
||||
notification.hide = false
|
||||
}
|
||||
|
||||
$scope.samlInitPath = ExposedSettings.samlInitPath
|
||||
|
||||
return ($scope.dismiss = notification =>
|
||||
$http({
|
||||
url: `/notifications/${notification._id}`,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,8 +17,10 @@ describe('ProjectController', function() {
|
||||
|
||||
this.user = {
|
||||
_id: ObjectId('123456123456123456123456'),
|
||||
email: 'test@overleaf.com',
|
||||
first_name: 'bjkdsjfk',
|
||||
features: {}
|
||||
features: {},
|
||||
emails: [{ email: 'test@overleaf.com' }]
|
||||
}
|
||||
this.settings = {
|
||||
apis: {
|
||||
@@ -107,13 +109,27 @@ describe('ProjectController', function() {
|
||||
fire: sinon.stub()
|
||||
}
|
||||
}
|
||||
this.Features = { hasFeature: sinon.stub() }
|
||||
this.Features = {
|
||||
hasFeature: sinon
|
||||
.stub()
|
||||
.withArgs('saml')
|
||||
.returns(false)
|
||||
}
|
||||
this.BrandVariationsHandler = {
|
||||
getBrandVariationById: sinon
|
||||
.stub()
|
||||
.callsArgWith(1, null, this.brandVariationDetails)
|
||||
}
|
||||
this.getUserAffiliations = sinon.stub().callsArgWith(1, null, [])
|
||||
this.getUserAffiliations = sinon.stub().callsArgWith(1, null, [
|
||||
{
|
||||
email: 'test@overleaf.com',
|
||||
institution: {
|
||||
id: 1,
|
||||
name: 'Overleaf',
|
||||
ssoEnabled: true
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
this.ProjectController = SandboxedModule.require(MODULE_PATH, {
|
||||
globals: {
|
||||
@@ -622,6 +638,99 @@ describe('ProjectController', function() {
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
})
|
||||
|
||||
describe('When Institution SSO is released', function() {
|
||||
beforeEach(function(done) {
|
||||
this.institutionEmail = 'test@overleaf.com'
|
||||
this.institutionName = 'Overleaf'
|
||||
this.Features.hasFeature.withArgs('saml').returns(true)
|
||||
done()
|
||||
})
|
||||
it('should show institution SSO available notification', function() {
|
||||
this.res.render = (pageName, opts) => {
|
||||
expect(opts.notifications).to.deep.include({
|
||||
email: 'test@overleaf.com',
|
||||
institutionId: 1,
|
||||
institutionName: 'Overleaf',
|
||||
templateKey: 'notification_institution_sso_available'
|
||||
})
|
||||
}
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
it('should show a linked notification', function() {
|
||||
this.req.session.saml = {
|
||||
institutionEmail: this.institutionEmail,
|
||||
linked: {
|
||||
hasEntitlement: false,
|
||||
universityName: this.institutionName
|
||||
}
|
||||
}
|
||||
this.res.render = (pageName, opts) => {
|
||||
expect(opts.notifications).to.deep.include({
|
||||
email: this.institutionEmail,
|
||||
institutionName: this.institutionName,
|
||||
templateKey: 'notification_institution_sso_linked'
|
||||
})
|
||||
}
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
it('should show a linked another email notification', function() {
|
||||
// when they request to link an email but the institution returns
|
||||
// a different email
|
||||
this.res.render = (pageName, opts) => {
|
||||
expect(opts.notifications).to.deep.include({
|
||||
institutionEmail: this.institutionEmail,
|
||||
requestedEmail: 'requested@overleaf.com',
|
||||
templateKey: 'notification_institution_sso_non_canonical'
|
||||
})
|
||||
}
|
||||
this.req.session.saml = {
|
||||
emailNonCanonical: this.institutionEmail,
|
||||
institutionEmail: this.institutionEmail,
|
||||
requestedEmail: 'requested@overleaf.com',
|
||||
linked: {
|
||||
hasEntitlement: false,
|
||||
universityName: this.institutionName
|
||||
}
|
||||
}
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
it('should show a notification when intent was to register via SSO but account existed', function() {
|
||||
this.res.render = (pageName, opts) => {
|
||||
expect(opts.notifications).to.deep.include({
|
||||
email: this.institutionEmail,
|
||||
templateKey: 'notification_institution_sso_already_registered'
|
||||
})
|
||||
}
|
||||
this.req.session.saml = {
|
||||
institutionEmail: this.institutionEmail,
|
||||
linked: {
|
||||
hasEntitlement: false,
|
||||
universityName: 'Overleaf'
|
||||
},
|
||||
registerIntercept: true
|
||||
}
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
})
|
||||
|
||||
describe('When Institution SSO is not released', function() {
|
||||
beforeEach(function(done) {
|
||||
this.Features.hasFeature.withArgs('saml').returns(false)
|
||||
done()
|
||||
})
|
||||
it('should not show institution sso available notification', function() {
|
||||
this.res.render = (pageName, opts) => {
|
||||
expect(opts.notifications).to.deep.not.include({
|
||||
email: 'test@overleaf.com',
|
||||
institutionId: 1,
|
||||
institutionName: 'Overleaf',
|
||||
templateKey: 'notification_institution_sso_available'
|
||||
})
|
||||
}
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('projectListPage with duplicate projects', function() {
|
||||
|
||||
Reference in New Issue
Block a user