Merge pull request #752 from sharelatex/ja-confirmation-ui-and-reconfirm

Show confirmation status in email UI, with button to resend confirmation
This commit is contained in:
James Allen 2018-07-16 09:58:11 +01:00 committed by GitHub
commit a5b943d5b6
7 changed files with 195 additions and 37 deletions

View file

@ -61,6 +61,19 @@ module.exports = UserEmailsController =
return next(error) if error?
res.sendStatus 204
resendConfirmation: (req, res, next) ->
userId = AuthenticationController.getLoggedInUserId(req)
email = EmailHelper.parseEmail(req.body.email)
return res.sendStatus 422 unless email?
UserGetter.getUserByAnyEmail email, {_id:1}, (error, user) ->
return next(error) if error?
if !user? or user?._id?.toString() != userId
logger.log {userId, email, foundUserId: user?._id}, "email doesn't match logged in user"
return res.sendStatus 422
logger.log {userId, email}, 'resending email confirmation token'
UserEmailsConfirmationHandler.sendConfirmationEmail userId, email, (error) ->
return next(error) if error?
res.sendStatus 200
showConfirm: (req, res, next) ->
res.render 'user/confirm_email', {

View file

@ -115,6 +115,9 @@ module.exports = class Router
UserEmailsController.showConfirm
webRouter.post '/user/emails/confirm',
UserEmailsController.confirm
webRouter.post '/user/emails/resend_confirmation',
AuthenticationController.requireLogin(),
UserEmailsController.resendConfirmation
if Features.hasFeature 'affiliations'
webRouter.post '/user/emails',

View file

@ -17,27 +17,37 @@ form.row(
tr(
ng-repeat="userEmail in userEmails"
)
td {{ userEmail.email + (userEmail.default ? ' (default)' : '') }}
td
| {{ userEmail.email + (userEmail.default ? ' (default)' : '') }}
div(ng-if="!userEmail.confirmedAt").small
strong #{translate('unconfirmed')}.
|
| #{translate('please_check_your_inbox')}.
br
a(
href,
ng-click="resendConfirmationEmail(userEmail)"
) #{translate('resend_confirmation_email')}
td
div(ng-if="userEmail.affiliation.institution")
div {{ userEmail.affiliation.institution.name }}
span.small
a(
href
ng-if="!isChangingAffiliation(userEmail.email) && !userEmail.affiliation.role && !userEmail.affiliation.department"
ng-click="changeAffiliation(userEmail);"
) #{translate("add_role_and_department")}
div(
div.small(
ng-if="!isChangingAffiliation(userEmail.email) && (userEmail.affiliation.role || userEmail.affiliation.department)"
)
span(ng-if="userEmail.affiliation.role") {{ userEmail.affiliation.role }}
span(ng-if="userEmail.affiliation.role && userEmail.affiliation.department") ,
span(ng-if="userEmail.affiliation.department") {{ userEmail.affiliation.department }}
| (
br
a(
href
ng-click="changeAffiliation(userEmail);"
) #{translate("change")}
| )
.affiliation-change-container(
ng-if="isChangingAffiliation(userEmail.email)"
)
@ -46,7 +56,7 @@ form.row(
show-university-and-country="false"
show-role-and-department="true"
)
.affiliation-change-actions
.affiliation-change-actions.small
a(
href
ng-click="saveAffiliationChange();"
@ -56,18 +66,27 @@ form.row(
href
ng-click="cancelAffiliationChange();"
) #{translate("save_or_cancel-cancel")}
td
a.affiliations-table-inline-action(
href
ng-if="!userEmail.default"
td.affiliations-table-inline-actions
// Disabled buttons don't work with tooltips, due to pointer-events: none,
// so create a wrapper for the tooltip
div.affiliations-table-inline-action-disabled-wrapper(
tooltip=translate("please_confirm_your_email_before_making_it_default")
ng-if="!userEmail.default && !userEmail.confirmedAt"
)
button.btn.btn-sm.btn-success.affiliations-table-inline-action(
disabled
) #{translate("make_default")}
button.btn.btn-sm.btn-success.affiliations-table-inline-action(
ng-if="!userEmail.default && userEmail.confirmedAt"
ng-click="setDefaultUserEmail(userEmail)"
) #{translate("make_default")}
br
a.affiliations-table-inline-action(
href
|  
button.btn.btn-sm.btn-danger.affiliations-table-inline-action(
ng-if="!userEmail.default"
ng-click="removeUserEmail(userEmail)"
) #{translate("remove")}
tooltip=translate("remove")
)
i.fa.fa-fw.fa-trash
tr.affiliations-table-highlighted-row(
ng-if="ui.isLoadingEmails"
)
@ -100,10 +119,12 @@ form.row(
input-required="true"
)
td
.affiliations-table-label(
p.affiliations-table-label(
ng-if="newAffiliation.university && !ui.showManualUniversitySelectionUI"
)
| {{ newAffiliation.university.name }} (
| {{ newAffiliation.university.name }}
span.small
| (
a(
href
ng-click="selectUniversityManually();"
@ -127,7 +148,7 @@ form.row(
show-role-and-department="ui.isValidEmail && newAffiliation.university"
)
td
button.btn.btn-primary(
button.btn.btn-sm.btn-primary(
ng-disabled="affiliationsForm.$invalid || ui.isAddingNewEmail"
ng-click="addNewEmail()"
)

View file

@ -128,6 +128,13 @@ define [
.then () -> _getUserEmails()
.catch () -> $scope.ui.hasError = true
$scope.resendConfirmationEmail = (userEmail) ->
$scope.ui.isLoadingEmails = true
UserAffiliationsDataService
.resendConfirmationEmail userEmail.email
.then () -> _getUserEmails()
.catch () -> $scope.ui.hasError = true
$scope.acknowledgeError = () ->
_reset()
_getUserEmails()

View file

@ -104,6 +104,12 @@ define [
_csrf: window.csrfToken
}
resendConfirmationEmail = (email) ->
$http.post "/user/emails/resend_confirmation", {
email,
_csrf: window.csrfToken
}
isDomainBlacklisted = (domain) ->
domain.toLowerCase() of domainsBlackList
@ -121,6 +127,7 @@ define [
addRoleAndDepartment
setDefaultUserEmail
removeUserEmail
resendConfirmationEmail
isDomainBlacklisted
}
]

View file

@ -24,11 +24,14 @@
width: 40%;
}
.affiliations-table-inline-actions {
width: 20%;
text-align: right;
}
.affiliations-table-inline-action {
text-transform: capitalize;
}
.affiliations-table-inline-action-disabled-wrapper {
display: inline-block;
}
.affiliations-table-highlighted-row {
background-color: tint(@content-alt-bg-color, 6%);
}

View file

@ -198,3 +198,107 @@ describe "UserEmails", ->
expect(response.statusCode).to.equal 404
cb()
], done
describe 'resending the confirmation', ->
it 'should generate a new token', (done) ->
async.series [
(cb) =>
@user.request {
method: 'POST',
url: '/user/emails',
json:
email: 'reconfirmation-email@example.com'
}, (error, response, body) =>
return done(error) if error?
expect(response.statusCode).to.equal 204
cb()
(cb) =>
db.tokens.find {
use: 'email_confirmation',
'data.user_id': @user._id,
usedAt: { $exists: false }
}, (error, tokens) =>
# There should only be one confirmation token at the moment
expect(tokens.length).to.equal 1
expect(tokens[0].data.email).to.equal 'reconfirmation-email@example.com'
expect(tokens[0].data.user_id).to.equal @user._id
cb()
(cb) =>
@user.request {
method: 'POST',
url: '/user/emails/resend_confirmation',
json:
email: 'reconfirmation-email@example.com'
}, (error, response, body) =>
return done(error) if error?
expect(response.statusCode).to.equal 200
cb()
(cb) =>
db.tokens.find {
use: 'email_confirmation',
'data.user_id': @user._id,
usedAt: { $exists: false }
}, (error, tokens) =>
# There should be two tokens now
expect(tokens.length).to.equal 2
expect(tokens[0].data.email).to.equal 'reconfirmation-email@example.com'
expect(tokens[0].data.user_id).to.equal @user._id
expect(tokens[1].data.email).to.equal 'reconfirmation-email@example.com'
expect(tokens[1].data.user_id).to.equal @user._id
cb()
], done
it 'should create a new token if none exists', (done) ->
# This should only be for users that have sign up with their main
# emails before the confirmation system existed
async.series [
(cb) =>
db.tokens.remove {
use: 'email_confirmation',
'data.user_id': @user._id,
usedAt: { $exists: false }
}, cb
(cb) =>
@user.request {
method: 'POST',
url: '/user/emails/resend_confirmation',
json:
email: @user.email
}, (error, response, body) =>
return done(error) if error?
expect(response.statusCode).to.equal 200
cb()
(cb) =>
db.tokens.find {
use: 'email_confirmation',
'data.user_id': @user._id,
usedAt: { $exists: false }
}, (error, tokens) =>
# There should still only be one confirmation token
expect(tokens.length).to.equal 1
expect(tokens[0].data.email).to.equal @user.email
expect(tokens[0].data.user_id).to.equal @user._id
cb()
], done
it "should not allow reconfirmation if the email doesn't match the user", (done) ->
async.series [
(cb) =>
@user.request {
method: 'POST',
url: '/user/emails/resend_confirmation',
json:
email: 'non-matching-email@example.com'
}, (error, response, body) =>
return done(error) if error?
expect(response.statusCode).to.equal 422
cb()
(cb) =>
db.tokens.find {
use: 'email_confirmation',
'data.user_id': @user._id,
usedAt: { $exists: false }
}, (error, tokens) =>
expect(tokens.length).to.equal 0
cb()
], done