Merge pull request #1741 from overleaf/jel-manage-linked-accounts

Manage linked accounts via v2

GitOrigin-RevId: 09a7af361e1fc3c30bd01f7876fc17d651f60f06
This commit is contained in:
Jessica Lawshe 2019-05-20 10:57:58 -05:00 committed by sharelatex
parent 004272619e
commit e03b5ae691
6 changed files with 154 additions and 46 deletions

View file

@ -71,6 +71,8 @@ module.exports = UserPagesController =
user_id = AuthenticationController.getLoggedInUserId(req)
logger.log user: user_id, "loading settings page"
shouldAllowEditingDetails = !(Settings?.ldap?.updateUserDetailsOnLogin) and !(Settings?.saml?.updateUserDetailsOnLogin)
oauthProviders = Settings.oauthProviders || {}
UserGetter.getUser user_id, (err, user)->
return next(err) if err?
@ -83,7 +85,10 @@ module.exports = UserPagesController =
hasPassword: passwordPresent,
shouldAllowEditingDetails: shouldAllowEditingDetails
languages: Settings.languages,
accountSettingsTabActive: true
accountSettingsTabActive: true,
oauthProviders: UserPagesController._translateProviderDescriptions(oauthProviders, req),
thirdPartyIds: UserPagesController._restructureThirdPartyIds(user),
previewOauth: req.query.prvw?
sessionsPage: (req, res, next) ->
user = AuthenticationController.getSessionUser(req)
@ -110,4 +115,21 @@ module.exports = UserPagesController =
else if body?.has_password
return callback(err, true)
return callback(err, false)
_restructureThirdPartyIds: (user) ->
# 3rd party identifiers are an array of objects
# this turn them into a single object, which
# makes data easier to use in template
return null if !user.thirdPartyIdentifiers || user.thirdPartyIdentifiers.length == 0
user.thirdPartyIdentifiers.reduce (obj, identifier) ->
obj[identifier.providerId] = identifier.externalUserId
obj
, {}
_translateProviderDescriptions: (providers, req) ->
result = {}
if providers
for provider, data of providers
data.description = req.i18n.translate(data.descriptionKey, data.descriptionOptions)
result[provider] = data
return result

View file

@ -168,14 +168,14 @@ block content
div
a(id="sessions-link", href="/user/sessions") #{translate("manage_sessions")}
if settings.overleaf && !hasFeature('oauth')
if settings.overleaf && !hasFeature('oauth') && !previewOauth
hr
p
| To manage your account's connection to Google, Twitter, ORCID and IEEE, please
|
a(href="/sign_in_to_v1?return_to=/users/edit#linked-accounts") click here
| .
else if hasFeature('oauth')
else if hasFeature('oauth') || previewOauth
hr
include settings/user-oauth

View file

@ -1,35 +1,63 @@
form.row(
block scripts
script(type='text/javascript').
window.oauthFallback = #{oauthFallback}
window.oauthProviders = !{StringHelper.stringifyJsonForScript(oauthProviders)}
window.thirdPartyIds = !{StringHelper.stringifyJsonForScript(thirdPartyIds)}
mixin providerList()
ul.list-like-table
li(ng-repeat="(key, provider) in providers")
.row(ng-if="key != 'collabratec' || (key == 'collabratec' && v2ThirdPartyIds[key])")
.col-xs-12.col-sm-8.col-md-10
h4 {{provider.name}}
p.small(ng-bind-html="provider.description")
.col-xs-2.col-sm-4.col-md-2.text-right
//- Unlink
button.btn.btn-default(
ng-click="unlink(key)"
ng-disabled="providers[key].ui.isProcessing"
ng-if="thirdPartyIds[key] || (key == 'collabratec' && v2ThirdPartyIds[key])"
)
span(ng-if="!providers[key].ui.isProcessing") #{translate("unlink")}
span(ng-if="providers[key].ui.isProcessing") #{translate("processing")}
//- Link
a.btn.btn-primary(
href="{{provider.linkPath}}?intent=link"
ng-if="!thirdPartyIds[key] && !provider.hideWhenNotLinked"
) #{translate("link")}
//- unlink error
.row(
ng-if="providers[key].ui.hasError"
)
.col-sm-12
//- to do: fix CSS so that we don't need inline styling
.alert.alert-danger(
ng-if="providers[key].ui.hasError"
style="display: block; margin-bottom: 10px;"
)
i.fa.fa-fw.fa-exclamation-triangle(aria-hidden="true")  
| {{providers[key].ui.errorMessage}}
.row(
ng-controller="UserOauthController"
name="oauthForm"
ng-cloak
)
.col-xs-12
h3.text-capitalize #{translate("linked_accounts")}
h3.text-capitalize#linked-accounts #{translate("linked_accounts")}
p.small #{translate("linked_accounts_explained", {appName:'{{settings.appName}}'})}
div.text-center(ng-if="ui.isLoadingProviders")
i.fa.fa-fw.fa-spin.fa-refresh(aria-hidden="true")
|  #{translate("loading")}...
if settings.oauthFallback
div.text-center(ng-if="ui.isLoadingV1Ids")
i.fa.fa-fw.fa-spin.fa-refresh(aria-hidden="true")
|  #{translate("loading")}...
ul(
class="list-like-table"
ng-if="ui.isLoadingProviders == false && ui.hasError == false"
)
li(ng-repeat="provider in providers")
.row
.col-xs-12.col-sm-8.col-md-10
h4 {{provider.name}}
p.small(ng-if="provider.key != 'orcid'")
| #{translate("login_with_service", {service:'{{provider.name}}'})}
p.small(ng-if="provider.key == 'orcid'")
| !{translate('oauth_orcid_description', {link: '/blog/434'})}
.col-xs-2.col-sm-4.col-md-2.text-right
button.btn.btn-default(ng-if="userProviders[provider.key]") #{translate("unlink")}
button.btn.btn-primary(ng-if="!userProviders[provider.key]") #{translate("link")}
div(ng-if="ui.isLoadingV1Ids == false && ui.hasError == false")
+providerList()
.alert.alert-danger(
ng-if="ui.hasError"
)
i.fa.fa-fw.fa-exclamation-triangle(aria-hidden="true")
span(ng-if="!ui.errorMessage")  #{translate("error_performing_request")}
span(ng-if="ui.errorMessage")  {{ui.errorMessage}}
.alert.alert-danger(
ng-if="ui.hasError"
)
i.fa.fa-fw.fa-exclamation-triangle(aria-hidden="true")
span(ng-if="!ui.errorMessage")  #{translate("error_performing_request")}
span(ng-if="ui.errorMessage")  {{ui.errorMessage}}
else
+providerList()

View file

@ -1,18 +1,14 @@
define(['base'], App =>
App.controller('UserOauthController', [
'$http',
'$scope',
'$q',
'_',
'UserOauthDataService',
function($scope, $q, _, UserOauthDataService) {
$scope.providers = [
{ key: 'google', name: 'Google' },
{ key: 'orcid', name: 'Orcid' },
{ key: 'twitter', name: 'Twitter' }
]
function($http, $scope, $q, _, UserOauthDataService) {
const _monitorRequest = function(promise) {
$scope.ui.hasError = false
$scope.ui.isLoadingProviders = true
$scope.ui.isLoadingV1Ids = true
promise
.catch(response => {
$scope.ui.hasError = true
@ -22,7 +18,7 @@ define(['base'], App =>
: 'error'
})
.finally(() => {
$scope.ui.isLoadingProviders = false
$scope.ui.isLoadingV1Ids = false
})
return promise
}
@ -30,19 +26,65 @@ define(['base'], App =>
$scope.ui = {
hasError: false,
errorMessage: '',
isLoadingProviders: false
isLoadingV1Ids: false
}
$scope.userProviders = {}
$scope.providers = window.oauthProviders
$scope.thirdPartyIds = window.thirdPartyIds
// $scope.v2ThirdPartyIds can be removed post user-c11n
// until v1 is authoritative we will use v1 SSO data for providers
// except collabratec, which will only write to v2.
// post user-c11n, oauthFallback setting will be removed and
// we will only use data from v2
$scope.v2ThirdPartyIds = window.thirdPartyIds
}
const _getUserV1OauthProviders = () => {
$scope.ui.isLoadingProviders = true
$scope.ui.isLoadingV1Ids = true
return _monitorRequest(UserOauthDataService.getUserOauthV1()).then(
userProviders => {
$scope.userProviders = userProviders
thirdPartyIds => {
$scope.thirdPartyIds = thirdPartyIds
}
)
}
const _unlinkError = (providerId, err) => {
$scope.providers[providerId].ui.hasError = true
$scope.providers[providerId].ui.errorMessage =
err && err.data && err.data.message ? err.data.message : 'error'
}
$scope.unlink = providerId => {
if (window.ExposedSettings.isOverleaf) {
// UI
$scope.providers[providerId].ui = {
hasError: false,
isProcessing: true
}
// Data for update
const data = {
link: false,
providerId
}
$http
.post('/user/oauth-unlink', data)
.catch(error => {
$scope.providers[providerId].ui.isProcessing = false
_unlinkError(providerId, error)
})
.then(response => {
$scope.providers[providerId].ui.isProcessing = false
if (response.status === 200) {
$scope.thirdPartyIds[providerId] = null
// v2thirdPartyIds below can be removed post user c11n
$scope.v2ThirdPartyIds[providerId] = null
} else {
_unlinkError(providerId, response)
}
})
}
}
_reset()
return _getUserV1OauthProviders()
if (window.oauthFallback) {
_getUserV1OauthProviders()
}
}
]))

View file

@ -3,7 +3,7 @@
border-radius: @border-radius-base;
list-style: none;
margin: 0;
padding: @padding-sm;
padding: 0 @padding-sm;
li {
border-top: 1px solid @hr-border;
div {
@ -13,6 +13,7 @@
}
.row {
display: table;
margin: 0;
width: 100%;
}
&:first-child {

View file

@ -21,6 +21,12 @@ describe "UserPagesController", ->
_id: @user_id = "kwjewkl"
features:{}
email: "joe@example.com"
thirdPartyIdentifiers: [
{
"providerId": "google",
"externalUserId": "testId"
}
]
@UserGetter = getUser: sinon.stub()
@UserSessionsManager =
@ -161,6 +167,15 @@ describe "UserPagesController", ->
done()
@UserPagesController.settingsPage @req, @res
it "should restructure thirdPartyIdentifiers data for template use", (done)->
expectedResult = {
google: "testId"
}
@res.render = (page, opts)=>
expect(opts.thirdPartyIds).to.include expectedResult
done()
@UserPagesController.settingsPage @req, @res
describe 'when ldap.updateUserDetailsOnLogin is true', ->
beforeEach ->