Merge pull request #735 from sharelatex/pr-affiliations-ui-adjustments

Affiliations UI, second round
This commit is contained in:
James Allen 2018-07-16 10:08:47 +01:00 committed by GitHub
commit b150a7b4ae
15 changed files with 506 additions and 195 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

@ -20,5 +20,7 @@ module.exports = Features =
return Settings.overleaf?
when 'templates'
return !Settings.overleaf?
when 'affiliations'
return Settings?.apis?.v1?.url?
else
throw new Error("unknown feature: #{feature}")

View file

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

View file

@ -9,7 +9,7 @@ block content
.page-header
h1 #{translate("account_settings")}
.account-settings(ng-controller="AccountSettingsController", ng-cloak)
if locals.showAffiliationsUI
if locals.showAffiliationsUI && hasFeature('affiliations')
include settings/user-affiliations
form-messages(for="settingsForm")
@ -22,25 +22,26 @@ block content
h3 #{translate("update_account_info")}
form(async-form="settings", name="settingsForm", method="POST", action="/user/settings", novalidate)
input(type="hidden", name="_csrf", value=csrfToken)
if !externalAuthenticationSystemUsed()
.form-group
label(for='email') #{translate("email")}
input.form-control(
type='email',
name='email',
placeholder="email@example.com"
required,
ng-model="email",
ng-init="email = "+JSON.stringify(user.email),
ng-model-options="{ updateOn: 'blur' }"
)
span.small.text-primary(ng-show="settingsForm.email.$invalid && settingsForm.email.$dirty")
| #{translate("must_be_email_address")}
else
// show the email, non-editable
.form-group
label.control-label #{translate("email")}
div.form-control(readonly="true") #{user.email}
if !(locals.showAffiliationsUI && hasFeature('affiliations'))
if !externalAuthenticationSystemUsed()
.form-group
label(for='email') #{translate("email")}
input.form-control(
type='email',
name='email',
placeholder="email@example.com"
required,
ng-model="email",
ng-init="email = "+JSON.stringify(user.email),
ng-model-options="{ updateOn: 'blur' }"
)
span.small.text-primary(ng-show="settingsForm.email.$invalid && settingsForm.email.$dirty")
| #{translate("must_be_email_address")}
else
// show the email, non-editable
.form-group
label.control-label #{translate("email")}
div.form-control(readonly="true") #{user.email}
if shouldAllowEditingDetails
.form-group

View file

@ -3,44 +3,96 @@ form.row(
name="affiliationsForm"
)
.col-md-12
h3 Emails and Affiliations
p.small Add additional email addresses to your account to access any upgrades your university or institution has, to make it easier for collaborators to find you, and to make sure you can recover your account.
h3 #{translate("emails_and_affiliations_title")}
p.small #{translate("emails_and_affiliations_explanation")}
table.table.affiliations-table
thead
tr
th.affiliations-table-email Email
th.affiliations-table-institution Institution and role
th.affiliations-table-email #{translate("email")}
th.affiliations-table-institution #{translate("institution_and_role")}
th.affiliations-table-inline-actions
tbody
tbody(
ng-if="!ui.hasError"
)
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") {{ userEmail.affiliation.institution.name }}
div(ng-if="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 }}
td
a(
href
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.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)"
)
affiliation-form(
affiliation-data="affiliationToChange"
show-university-and-country="false"
show-role-and-department="true"
)
.affiliation-change-actions.small
a(
href
ng-click="saveAffiliationChange();"
) #{translate("save_or_cancel-save")}
|  #{translate("save_or_cancel-or" )} 
a(
href
ng-click="cancelAffiliationChange();"
) #{translate("save_or_cancel-cancel")}
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")}
|  
button.btn.btn-sm.btn-danger.affiliations-table-inline-action(
ng-if="!userEmail.default"
ng-click="setDefaultUserEmail(userEmail.email)"
) Make default
br
a(
href
ng-if="!userEmail.default"
ng-click="removeUserEmail(userEmail.email)"
) Remove
ng-click="removeUserEmail(userEmail)"
tooltip=translate("remove")
)
i.fa.fa-fw.fa-trash
tr.affiliations-table-highlighted-row(
ng-if="ui.isLoadingEmails"
)
td.text-center(colspan="3")
i.fa.fa-fw.fa-spin.fa-refresh
|  Loading...
|  #{translate("loading")}...
tr.affiliations-table-highlighted-row(
ng-if="!ui.showAddEmailUI && !ui.isLoadingEmails"
)
@ -48,7 +100,7 @@ form.row(
a(
href
ng-click="showAddEmailForm()"
) Add another email
) #{translate("add_another_email")}
tr.affiliations-table-highlighted-row(
ng-if="ui.showAddEmailUI"
@ -67,106 +119,130 @@ form.row(
input-required="true"
)
td
.affiliations-table-label(
p.affiliations-table-label(
ng-if="newAffiliation.university && !ui.showManualUniversitySelectionUI"
)
| {{ newAffiliation.university.name }} (
a(
href
ng-click="selectUniversityManually();"
) change
| )
| {{ newAffiliation.university.name }}
span.small
| (
a(
href
ng-click="selectUniversityManually();"
) #{translate("change")}
| )
.affiliations-table-label(
ng-if="!newAffiliation.university && !ui.isValidEmail && !ui.showManualUniversitySelectionUI"
) Start by adding your email address.
) #{translate("start_by_adding_your_email")}
.affiliations-table-label(
ng-if="!newAffiliation.university && ui.isValidEmail && !ui.isBlacklistedEmail && !ui.showManualUniversitySelectionUI"
)
| Is your email affiliated with an institution?
| #{translate("is_email_affiliated")}
br
a(
href
ng-click="selectUniversityManually();"
) Let us know
.affiliations-form-group(
ng-if="ui.showManualUniversitySelectionUI"
) #{translate("let_us_know")}
affiliation-form(
affiliation-data="newAffiliation"
show-university-and-country="ui.showManualUniversitySelectionUI"
show-role-and-department="ui.isValidEmail && newAffiliation.university"
)
ui-select(
ng-model="newAffiliation.country"
)
ui-select-match(
placeholder="Country"
) {{ $select.selected.name }}
ui-select-choices(
repeat="country in countries | filter: $select.search"
)
span(
ng-bind="country.name"
s)
.affiliations-form-group(
ng-if="ui.showManualUniversitySelectionUI"
)
ui-select(
ng-model="newAffiliation.university"
ng-disabled="!newAffiliation.country"
tagging="addUniversityToSelection"
tagging-label="false"
)
ui-select-match(
placeholder="Institution"
) {{ $select.selected.name }}
ui-select-choices(
repeat="university in universities | filter: $select.search"
)
span(
ng-bind="university.name"
)
.affiliations-form-group(
ng-if="ui.isValidEmail && newAffiliation.university"
)
ui-select(
ng-model="newAffiliation.role"
tagging
tagging-label="false"
)
ui-select-match(
placeholder="Role"
) {{ $select.selected }}
ui-select-choices(
repeat="role in roles | filter: $select.search"
)
span(
ng-bind="role"
)
.affiliations-form-group(
ng-if="ui.isValidEmail && newAffiliation.university"
)
ui-select(
ng-model="newAffiliation.department"
tagging
tagging-label="false"
)
ui-select-match(
placeholder="Department"
) {{ $select.selected }}
ui-select-choices(
repeat="department in departments | filter: $select.search"
)
span(
ng-bind="department"
)
td
button.btn.btn-primary(
button.btn.btn-sm.btn-primary(
ng-disabled="affiliationsForm.$invalid || ui.isAddingNewEmail"
ng-click="addNewEmail()"
)
span(
ng-if="!ui.isAddingNewEmail"
) Add new email
) #{translate("add_new_email")}
span(
ng-if="ui.isAddingNewEmail"
)
i.fa.fa-fw.fa-spin.fa-refresh
|  Adding...
hr
|  #{translate("adding")}...
tbody(
ng-if="ui.hasError"
)
tr.affiliations-table-error-row(
ng-if="true"
)
td.text-center(colspan="3")
div
i.fa.fa-fw.fa-exclamation-triangle
|  #{translate("error_performing_request")}
div
button.btn.btn-xs(
ng-click="acknowledgeError();"
) #{translate("reload_emails_and_affiliations")}
hr
script(type="text/ng-template", id="affiliationFormTpl")
.affiliations-form-group(
ng-if="$ctrl.showUniversityAndCountry"
)
ui-select(
ng-model="$ctrl.affiliationData.country"
)
ui-select-match(
placeholder="Country"
) {{ $select.selected.name }}
ui-select-choices(
repeat="country in $ctrl.countries | filter: $select.search"
)
span(
ng-bind="country.name"
)
.affiliations-form-group(
ng-if="$ctrl.showUniversityAndCountry"
)
ui-select(
ng-model="$ctrl.affiliationData.university"
ng-disabled="!$ctrl.affiliationData.country"
tagging="$ctrladdUniversityToSelection"
tagging-label="false"
)
ui-select-match(
placeholder="Institution"
) {{ $select.selected.name }}
ui-select-choices(
repeat="university in $ctrl.universities | filter: $select.search"
)
span(
ng-bind="university.name"
)
.affiliations-form-group(
ng-if="$ctrl.showRoleAndDepartment"
)
ui-select(
ng-model="$ctrl.affiliationData.role"
tagging
tagging-label="false"
)
ui-select-match(
placeholder="Role"
) {{ $select.selected }}
ui-select-choices(
repeat="role in $ctrl.roles | filter: $select.search"
)
span(
ng-bind="role"
)
.affiliations-form-group(
ng-if="$ctrl.showRoleAndDepartment"
)
ui-select(
ng-model="$ctrl.affiliationData.department"
tagging
tagging-label="false"
)
ui-select-match(
placeholder="Department"
) {{ $select.selected }}
ui-select-choices(
repeat="department in $ctrl.departments | filter: $select.search"
)
span(
ng-bind="department"
)

View file

@ -20,6 +20,7 @@ define [
"main/subscription/team-invite-controller"
"main/contact-us"
"main/learn"
"main/affiliations/components/affiliationForm"
"main/affiliations/controllers/UserAffiliationsController"
"main/affiliations/factories/UserAffiliationsDataService"
"main/keys"

View file

@ -0,0 +1,52 @@
define [
"base"
], (App) ->
affiliationFormController = ($scope, $element, $attrs, UserAffiliationsDataService) ->
ctrl = @
ctrl.roles = []
ctrl.departments = []
ctrl.countries = []
ctrl.universities = []
_defaultDepartments = []
ctrl.addUniversityToSelection = (universityName) ->
{ name: universityName, isUserSuggested: true }
# Populates the countries dropdown
UserAffiliationsDataService
.getCountries()
.then (countries) -> ctrl.countries = countries
# Populates the roles dropdown
UserAffiliationsDataService
.getDefaultRoleHints()
.then (roles) -> ctrl.roles = roles
# Fetches the default department hints
UserAffiliationsDataService
.getDefaultDepartmentHints()
.then (departments) ->
_defaultDepartments = departments
# Populates the universities dropdown (after selecting a country)
$scope.$watch "$ctrl.affiliationData.country", (newSelectedCountry, prevSelectedCountry) ->
if newSelectedCountry? and newSelectedCountry != prevSelectedCountry
ctrl.affiliationData.university = null
ctrl.affiliationData.role = null
ctrl.affiliationData.department = null
UserAffiliationsDataService
.getUniversitiesFromCountry(newSelectedCountry)
.then (universities) -> ctrl.universities = universities
# Populates the departments dropdown (after selecting a university)
$scope.$watch "$ctrl.affiliationData.university", (newSelectedUniversity, prevSelectedUniversity) ->
if newSelectedUniversity? and newSelectedUniversity != prevSelectedUniversity and newSelectedUniversity.departments?.length > 0
ctrl.departments = _.uniq newSelectedUniversity.departments
else
ctrl.departments = _defaultDepartments
return
App.component "affiliationForm", {
bindings:
affiliationData: "="
showUniversityAndCountry: "<"
showRoleAndDepartment: "<"
controller: affiliationFormController
templateUrl: "affiliationFormTpl"
}

View file

@ -3,12 +3,6 @@ define [
], (App) ->
App.controller "UserAffiliationsController", ["$scope", "UserAffiliationsDataService", "$q", "_", ($scope, UserAffiliationsDataService, $q, _) ->
$scope.userEmails = []
$scope.countries = []
$scope.universities = []
$scope.roles = []
$scope.departments = []
_defaultDepartments = []
LOCAL_AND_DOMAIN_REGEX = /([^@]+)@(.+)/
EMAIL_REGEX = /^([A-Za-z0-9_\-\.]+)@([^\.]+)\.([A-Za-z0-9_\-\.]+)([^\.])$/
@ -20,9 +14,6 @@ define [
else
{ local: null, domain: null }
$scope.addUniversityToSelection = (universityName) ->
{ name: universityName, isUserSuggested: true }
$scope.getEmailSuggestion = (userInput) ->
userInputLocalAndDomain = _matchLocalAndDomain(userInput)
$scope.ui.isValidEmail = EMAIL_REGEX.test userInput
@ -55,6 +46,38 @@ define [
$scope.newAffiliation.department = null
$scope.ui.showManualUniversitySelectionUI = true
$scope.changeAffiliation = (userEmail) ->
if userEmail.affiliation?.institution?.id?
UserAffiliationsDataService.getUniversityDetails userEmail.affiliation.institution.id
.then (universityDetails) -> $scope.affiliationToChange.university = universityDetails
$scope.affiliationToChange.email = userEmail.email
$scope.affiliationToChange.role = userEmail.affiliation.role
$scope.affiliationToChange.department = userEmail.affiliation.department
$scope.saveAffiliationChange = () ->
$scope.ui.isLoadingEmails = true
UserAffiliationsDataService
.addRoleAndDepartment(
$scope.affiliationToChange.email,
$scope.affiliationToChange.role,
$scope.affiliationToChange.department
)
.then () ->
_reset()
_getUserEmails()
.catch () ->
$scope.ui.hasError = true
$scope.cancelAffiliationChange = (email) ->
$scope.affiliationToChange.email = ""
$scope.affiliationToChange.university = null
$scope.affiliationToChange.role = null
$scope.affiliationToChange.department = null
$scope.isChangingAffiliation = (email) ->
$scope.affiliationToChange.email == email
$scope.showAddEmailForm = () ->
$scope.ui.showAddEmailUI = true
@ -81,27 +104,40 @@ define [
$scope.newAffiliation.role,
$scope.newAffiliation.department
)
addEmailPromise.then () ->
_reset()
_getUserEmails()
addEmailPromise
.then () ->
_reset()
_getUserEmails()
.catch () ->
$scope.ui.hasError = true
$scope.setDefaultUserEmail = (email) ->
$scope.setDefaultUserEmail = (userEmail) ->
$scope.ui.isLoadingEmails = true
UserAffiliationsDataService
.setDefaultUserEmail email
.setDefaultUserEmail userEmail.email
.then () -> _getUserEmails()
.catch () -> $scope.ui.hasError = true
$scope.removeUserEmail = (email) ->
$scope.removeUserEmail = (userEmail) ->
$scope.ui.isLoadingEmails = true
userEmailIdx = _.indexOf $scope.userEmails, userEmail
if userEmailIdx > -1
$scope.userEmails.splice userEmailIdx, 1
UserAffiliationsDataService
.removeUserEmail userEmail.email
.then () -> _getUserEmails()
.catch () -> $scope.ui.hasError = true
$scope.resendConfirmationEmail = (userEmail) ->
$scope.ui.isLoadingEmails = true
UserAffiliationsDataService
.removeUserEmail email
.resendConfirmationEmail userEmail.email
.then () -> _getUserEmails()
.catch () -> $scope.ui.hasError = true
$scope.getDepartments = () ->
if $scope.newAffiliation.university?.departments.length > 0
_.uniq $scope.newAffiliation.university.departments
else
UserAffiliationsDataService.getDefaultDepartmentHints()
$scope.acknowledgeError = () ->
_reset()
_getUserEmails()
_reset = () ->
$scope.newAffiliation =
@ -111,12 +147,19 @@ define [
role: null
department: null
$scope.ui =
hasError: false
showChangeAffiliationUI: false
showManualUniversitySelectionUI: false
isLoadingEmails: false
isAddingNewEmail: false
showAddEmailUI: false
isValidEmail: false
isBlacklistedEmail: false
$scope.affiliationToChange =
email: ""
university: null
role: null
department: null
_reset()
# Populates the emails table
@ -127,40 +170,9 @@ define [
.then (emails) ->
$scope.userEmails = emails
$scope.ui.isLoadingEmails = false
.catch () ->
$scope.ui.hasError = true
_getUserEmails()
# Populates the countries dropdown
UserAffiliationsDataService
.getCountries()
.then (countries) -> $scope.countries = countries
# Populates the roles dropdown
UserAffiliationsDataService
.getDefaultRoleHints()
.then (roles) -> $scope.roles = roles
# Fetches the default department hints
UserAffiliationsDataService
.getDefaultDepartmentHints()
.then (departments) ->
_defaultDepartments = departments
# Populates the universities dropdown (after selecting a country)
$scope.$watch "newAffiliation.country", (newSelectedCountry, prevSelectedCountry) ->
if newSelectedCountry? and newSelectedCountry != prevSelectedCountry
$scope.newAffiliation.university = null
$scope.newAffiliation.role = null
$scope.newAffiliation.department = null
UserAffiliationsDataService
.getUniversitiesFromCountry(newSelectedCountry)
.then (universities) -> $scope.universities = universities
# Populates the departments dropdown (after selecting a university)
$scope.$watch "newAffiliation.university", (newSelectedUniversity, prevSelectedUniversity) ->
if newSelectedUniversity? and newSelectedUniversity != prevSelectedUniversity
if newSelectedUniversity.departments?.length > 0
$scope.departments = _.uniq newSelectedUniversity.departments
else
$scope.departments = _defaultDepartments
]

View file

@ -27,7 +27,7 @@ define [
getDefaultDepartmentHints = () ->
$q.resolve defaultDepartmentHints
getUserEmails = () ->
getUserEmails = () ->
$http.get "/user/emails"
.then (response) -> response.data
@ -53,6 +53,10 @@ define [
else
$q.reject null
getUniversityDetails = (universityId) ->
$http.get "/institutions/list/#{ universityId }"
.then (response) -> response.data
addUserEmail = (email) ->
$http.post "/user/emails", {
email,
@ -80,8 +84,17 @@ define [
_csrf: window.csrfToken
}
addRoleAndDepartment = (email, role, department) ->
$http.post "/user/emails/endorse", {
email,
role,
department,
_csrf: window.csrfToken
}
setDefaultUserEmail = (email) ->
$http.post "/user/emails/default", {
email,
_csrf: window.csrfToken
}
@ -91,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
@ -101,11 +120,14 @@ define [
getUserEmails
getUniversitiesFromCountry
getUniversityDomainFromPartialDomainInput
getUniversityDetails
addUserEmail
addUserAffiliationWithUnknownUniversity
addUserAffiliation
addRoleAndDepartment
setDefaultUserEmail
removeUserEmail
resendConfirmationEmail
isDomainBlacklisted
}
]

View file

@ -24,18 +24,36 @@
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%);
}
.affiliations-table-error-row {
background-color: @alert-danger-bg;
color: @alert-danger-text;
.btn {
margin-top: @table-cell-padding;
.button-variant(@btn-danger-color; darken(@btn-danger-bg, 8%); @btn-danger-border);
}
}
.affiliations-form-group {
margin-top: @table-cell-padding;
&:first-child {
margin-top: 0;
}
}
.affiliation-change-container,
.affiliation-change-actions {
margin-top: @table-cell-padding;
}
.affiliations-table-label {
padding-top: 4px;
}

View file

@ -10,7 +10,7 @@
.input-suggestions-shadow {
background-color: @input-bg;
padding-top: 4px;
padding-top: @input-suggestion-v-offset;
}
.input-suggestions-shadow-existing {
color: transparent;

View file

@ -35,12 +35,14 @@
> .btn {
border-color: @input-border-focus;
box-shadow: inset 0 1px 1px rgba(0,0,0,0.075), 0 0 8px fade(@input-border-focus, 60%);
padding-top: @input-suggestion-v-offset;
}
}
> .btn {
color: @input-color;
background-color: @input-bg;
border: 1px solid @input-border;
padding-top: @input-suggestion-v-offset;
&[disabled] {
cursor: not-allowed;
background-color: @input-bg-disabled;
@ -53,6 +55,7 @@
.ui-select-container[tagging] {
.ui-select-toggle {
cursor: text;
padding-top: @input-suggestion-v-offset;
> i.caret.pull-right {
display: none;
}

View file

@ -995,3 +995,5 @@
@history-toolbar-bg-color : @toolbar-alt-bg-color;
@history-toolbar-color : @text-color;
// Input suggestions
@input-suggestion-v-offset : 6px;

View file

@ -289,6 +289,8 @@
@sys-msg-color : #FFF;
@sys-msg-border : solid 1px lighten(@ol-blue, 10%);
@input-suggestion-v-offset : 4px;
//== Colors
//
//## Gray and brand colors for use across Bootstrap.

View file

@ -17,7 +17,7 @@ describe "UserEmails", ->
token = null
async.series [
(cb) =>
@user.request {
@user.request {
method: 'POST',
url: '/user/emails',
json:
@ -45,7 +45,7 @@ describe "UserEmails", ->
token = tokens[0].token
cb()
(cb) =>
@user.request {
@user.request {
method: 'POST',
url: '/user/emails/confirm',
json:
@ -80,7 +80,7 @@ describe "UserEmails", ->
(cb) => @user2.login cb
(cb) =>
# Create email for first user
@user.request {
@user.request {
method: 'POST',
url: '/user/emails',
json: {@email}
@ -99,21 +99,21 @@ describe "UserEmails", ->
cb()
(cb) =>
# Delete the email from the first user
@user.request {
@user.request {
method: 'POST',
url: '/user/emails/delete',
json: {@email}
}, cb
(cb) =>
# Create email for second user
@user2.request {
@user2.request {
method: 'POST',
url: '/user/emails',
json: {@email}
}, cb
(cb) =>
# Original confirmation token should no longer work
@user.request {
@user.request {
method: 'POST',
url: '/user/emails/confirm',
json:
@ -158,7 +158,7 @@ describe "UserEmails", ->
token = null
async.series [
(cb) =>
@user.request {
@user.request {
method: 'POST',
url: '/user/emails',
json:
@ -183,12 +183,12 @@ describe "UserEmails", ->
db.tokens.update {
token: token
}, {
$set: {
$set: {
expiresAt: new Date(Date.now() - 1000000)
}
}, cb
(cb) =>
@user.request {
@user.request {
method: 'POST',
url: '/user/emails/confirm',
json:
@ -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