Commit 54dfa394 authored by Randall Leeds's avatar Randall Leeds

Squash session and profile; auth and account

These resources are one resource on the backend. The pattern of
preserving object identity for the session response is unnecessary.
The identity module now listens for events on the root scope instead
of watching the session. The auth controller publishes the session
change directly and the auth directive is completely removed. Timeout
is handled in the controller.

Include the account and auth forms via a macro in the blocks template
so that all the dialogs can be overridden together and get rid of the
``show-account`` directive.
parent 7e1a4673
...@@ -4,14 +4,14 @@ imports = [ ...@@ -4,14 +4,14 @@ imports = [
class AuthController class AuthController
this.$inject = ['$scope', '$timeout', 'session', 'formHelpers'] this.$inject = ['$scope', '$timeout', 'flash', 'session', 'formHelpers']
constructor: ( $scope, $timeout, session, formHelpers ) -> constructor: ( $scope, $timeout, flash, session, formHelpers ) ->
timeout = null timeout = null
success = -> success = (data) ->
$scope.tab = if $scope.tab is 'forgot' then 'activate' else null $scope.tab = if $scope.tab is 'forgot' then 'activate' else null
$scope.model = null $scope.model = null
$scope.$broadcast 'success' $scope.$emit 'session', data
failure = (form, response) -> failure = (form, response) ->
{errors, reason} = response.data {errors, reason} = response.data
...@@ -23,18 +23,13 @@ class AuthController ...@@ -23,18 +23,13 @@ class AuthController
return unless form.$valid return unless form.$valid
data = {} $scope.$broadcast 'formState', form.$name, 'loading'
method = '$' + form.$name session[form.$name] $scope.model, success,
angular.copy $scope.model, session
session.$promise = session[method] success,
angular.bind(this, failure, form) angular.bind(this, failure, form)
session.$resolved = false .$promise.finally -> $scope.$broadcast 'formState', form.$name, ''
# Update status btn $scope.model = null
$scope.$broadcast 'formState', form.$name, 'loading' $scope.tab = 'login'
session.$promise.finally ->
$scope.$broadcast 'formState', form.$name, ''
$scope.$on '$destroy', -> $scope.$on '$destroy', ->
if timeout if timeout
...@@ -48,57 +43,12 @@ class AuthController ...@@ -48,57 +43,12 @@ class AuthController
# If the model is not empty, start the timeout # If the model is not empty, start the timeout
if value and not angular.equals(value, {}) if value and not angular.equals(value, {})
timeout = $timeout -> timeout = $timeout ->
$scope.form?.$setPristine()
$scope.model = null $scope.model = null
$scope.$broadcast 'timeout' flash 'info',
'For your security, the forms have been reset due to inactivity.'
, 300000 , 300000
authDirective = ['$timeout', ($timeout) ->
controller: 'AuthController'
link: (scope, elem, attrs, [auth, form]) ->
elem.on 'submit', (event) ->
scope.$apply ->
$target = angular.element event.target
$form = $target.controller('form')
auth.submit($form)
scope.model = {}
scope.$on 'authorize', ->
scope.tab = 'login'
scope.$on 'error', (event) ->
scope.onError()
scope.$on 'success', (event) ->
form.$setPristine()
scope.onSuccess()
scope.$on 'timeout', (event) ->
form.$setPristine()
scope.onTimeout()
scope.$watch 'tab', (name) ->
$timeout ->
elem
.find('form')
.filter(-> this.name is name)
.find('input')
.filter(-> this.type isnt 'hidden')
.first()
.focus()
require: ['auth', 'form']
restrict: 'C'
scope:
onError: '&'
onSuccess: '&'
onTimeout: '&'
session: '='
tab: '=ngModel'
templateUrl: 'auth.html'
]
angular.module('h.auth', imports) angular.module('h.auth', imports)
.controller('AuthController', AuthController) .controller('AuthController', AuthController)
.directive('auth', authDirective)
...@@ -12,53 +12,47 @@ identityFactory = [ ...@@ -12,53 +12,47 @@ identityFactory = [
onlogout = null onlogout = null
onmatch = null onmatch = null
$rootScope.session = session $rootScope.$on 'session', (event, session) ->
# Get the userid and convert it to the persona format.
$rootScope.$watch 'session.$promise', (promise) -> persona = session.userid?.replace(/^acct:/, '') or null
# Wait for any pending action to resolve.
promise.finally -> # Fire callbacks as appropriate.
# Get the userid and convert it to the persona format. # Consult the state matrix in the `navigator.id.watch` documentation.
persona = session.userid?.replace(/^acct:/, '') or null # https://developer.mozilla.org/en-US/docs/Web/API/navigator.id.watch
if loggedInUser is null
# Fire callbacks as appropriate. if persona
# Consult the state matrix in the `navigator.id.watch` documentation. loggedInUser = persona
# https://developer.mozilla.org/en-US/docs/Web/API/navigator.id.watch onlogin?(session.csrf)
if loggedInUser is null else
if persona onmatch?()
loggedInUser = persona else if loggedInUser
onlogin?(session.csrf) if persona
else if loggedInUser is persona
onmatch?() onmatch?()
else if loggedInUser
if persona
if loggedInUser is persona
onmatch?()
else
loggedInUser = persona
onlogin?(session.csrf)
else else
loggedInUser = null
onlogout?()
else
if persona
loggedInUser = persona loggedInUser = persona
onlogin?(session.csrf) onlogin?(session.csrf)
else else
loggedInUser = null loggedInUser = null
onlogout?() onlogout?()
else
if persona
loggedInUser = persona
onlogin?(session.csrf)
else
loggedInUser = null
onlogout?()
logout: -> logout: ->
# Clear the session but preserve its identity and give it a new promise. session.logout({}).$promise.then ->
$promise = session.$logout() $rootScope.$emit 'session', {}
$resolved = false
angular.copy({$promise, $resolved}, session)
$rootScope.$broadcast 'logout'
request: -> request: ->
$rootScope.$broadcast 'authorize' $rootScope.$broadcast 'authorize'
watch: (options) -> watch: (options) ->
{loggedInUser, onlogin, onlogout, onmatch} = options {loggedInUser, onlogin, onlogout, onmatch} = options
session.load().$promise.then (data) -> $rootScope.$emit 'session', data
] ]
......
...@@ -12,6 +12,7 @@ ACTION = [ ...@@ -12,6 +12,7 @@ ACTION = [
'forgot' 'forgot'
'activate' 'activate'
'edit_profile' 'edit_profile'
'disable_user'
] ]
ACTION_OPTION = ACTION_OPTION =
...@@ -90,37 +91,9 @@ class SessionProvider ...@@ -90,37 +91,9 @@ class SessionProvider
actions[name].transformResponse = process actions[name].transformResponse = process
endpoint = documentHelpers.absoluteURI('/app') endpoint = documentHelpers.absoluteURI('/app')
$resource(endpoint, {}, actions).load() $resource(endpoint, {}, actions)
] ]
# Function providing a server-side user profile resource.
#
# This function provides an angular $resource factory
# for manipulating server-side account-profile settings. It defines the
# actions (such as 'login', 'register') as REST-ish actions
profileProvider = [
'$q', '$resource', 'documentHelpers',
($q, $resource, documentHelpers) ->
defaults =
email: ""
password: ""
actions =
edit_profile:
method: 'POST'
params:
__formid__: "edit_profile"
withCredentials: true
disable_user:
method: 'POST'
params:
__formid__: "disable_user"
withCredentials: true
endpoint = documentHelpers.absoluteURI('/app')
$resource(endpoint, {}, actions)
]
configure = ['$httpProvider', ($httpProvider) -> configure = ['$httpProvider', ($httpProvider) ->
defaults = $httpProvider.defaults defaults = $httpProvider.defaults
...@@ -143,4 +116,3 @@ configure = ['$httpProvider', ($httpProvider) -> ...@@ -143,4 +116,3 @@ configure = ['$httpProvider', ($httpProvider) ->
angular.module('h.session', imports, configure) angular.module('h.session', imports, configure)
.provider('session', SessionProvider) .provider('session', SessionProvider)
.factory('profile', profileProvider)
...@@ -246,6 +246,8 @@ class App ...@@ -246,6 +246,8 @@ class App
# Do not rely on the identity service to invoke callbacks within an # Do not rely on the identity service to invoke callbacks within an
# angular digest cycle. # angular digest cycle.
$scope.$evalAsync -> $scope.$evalAsync ->
$scope.dialog.visible = false
# Update any edits in progress. # Update any edits in progress.
for draft in drafts.all() for draft in drafts.all()
annotator.publish 'beforeAnnotationCreated', draft annotator.publish 'beforeAnnotationCreated', draft
...@@ -307,15 +309,12 @@ class App ...@@ -307,15 +309,12 @@ class App
$scope.updater.then (sock) -> $scope.updater.then (sock) ->
sock.send(JSON.stringify(sockmsg)) sock.send(JSON.stringify(sockmsg))
$scope.authTimeout = ->
flash 'info',
'For your security, the forms have been reset due to inactivity.'
$scope.clearSelection = -> $scope.clearSelection = ->
$scope.search.query = '' $scope.search.query = ''
$scope.selectedAnnotations = null $scope.selectedAnnotations = null
$scope.selectedAnnotationsCount = 0 $scope.selectedAnnotationsCount = 0
$scope.dialog = visible: false
$scope.id = identity $scope.id = identity
$scope.model = persona: undefined $scope.model = persona: undefined
......
class AccountManagement class AccountManagement
@inject = ['$scope', '$rootScope', '$filter', 'flash', 'profile', @inject = ['$scope', '$filter', 'flash', 'session', 'identity', 'formHelpers']
'identity', 'formHelpers']
constructor: ($scope, $rootScope, $filter, flash, profile, constructor: ($scope, $filter, flash, session, identity, formHelpers) ->
identity, formHelpers) ->
persona_filter = $filter('persona') persona_filter = $filter('persona')
onSuccess = (form, response) -> onSuccess = (form, response) ->
...@@ -36,15 +34,12 @@ class AccountManagement ...@@ -36,15 +34,12 @@ class AccountManagement
$scope.changePassword = {} $scope.changePassword = {}
$scope.deleteAccount = {} $scope.deleteAccount = {}
# Initial form state.
$scope.sheet = false
$scope.delete = (form) -> $scope.delete = (form) ->
# If the password is correct, the account is deleted. # If the password is correct, the account is deleted.
# The extension is then removed from the page. # The extension is then removed from the page.
# Confirmation of success is given. # Confirmation of success is given.
return unless form.$valid return unless form.$valid
username = persona_filter $scope.session.userid username = persona_filter $scope.persona
packet = packet =
username: username username: username
pwd: form.pwd.$modelValue pwd: form.pwd.$modelValue
...@@ -52,7 +47,7 @@ class AccountManagement ...@@ -52,7 +47,7 @@ class AccountManagement
successHandler = angular.bind(null, onDelete, form) successHandler = angular.bind(null, onDelete, form)
errorHandler = angular.bind(null, onError, form) errorHandler = angular.bind(null, onError, form)
promise = profile.disable_user(packet) promise = session.disable_user(packet)
promise.$promise.then(successHandler, errorHandler) promise.$promise.then(successHandler, errorHandler)
$scope.submit = (form) -> $scope.submit = (form) ->
...@@ -60,7 +55,7 @@ class AccountManagement ...@@ -60,7 +55,7 @@ class AccountManagement
# forms. However, in the backend it is just one: edit_profile # forms. However, in the backend it is just one: edit_profile
return unless form.$valid return unless form.$valid
username = persona_filter $scope.session.userid username = persona_filter $scope.persona
packet = packet =
username: username username: username
pwd: form.pwd.$modelValue pwd: form.pwd.$modelValue
...@@ -70,14 +65,9 @@ class AccountManagement ...@@ -70,14 +65,9 @@ class AccountManagement
errorHandler = angular.bind(null, onError, form) errorHandler = angular.bind(null, onError, form)
$scope.$broadcast 'formState', form.$name, 'loading' # Update status btn $scope.$broadcast 'formState', form.$name, 'loading' # Update status btn
promise = profile.edit_profile(packet) promise = session.edit_profile(packet)
promise.$promise.then(successHandler, errorHandler) promise.$promise.then(successHandler, errorHandler)
$rootScope.$on 'nav:account', ->
$scope.$apply -> $scope.sheet = true
$rootScope.$on 'logout', ->
$scope.sheet = false
angular.module('h.controllers.AccountManagement', []) angular.module('h.controllers.AccountManagement', [])
.controller('AccountManagement', AccountManagement) .controller('AccountManagement', AccountManagement)
...@@ -107,6 +107,24 @@ privacy = -> ...@@ -107,6 +107,24 @@ privacy = ->
templateUrl: 'privacy.html' templateUrl: 'privacy.html'
# Extend the tabbable directive from angular-bootstrap with autofocus
tabbable = ['$timeout', ($timeout) ->
link: (scope, elem, attrs, ctrl) ->
return unless ctrl
render = ctrl.$render
ctrl.$render = ->
render.call(ctrl)
$timeout ->
elem
.find(':input')
.filter(':visible:first')
.focus()
, false
require: '?ngModel'
restrict: 'C'
]
tabReveal = ['$parse', ($parse) -> tabReveal = ['$parse', ($parse) ->
compile: (tElement, tAttrs, transclude) -> compile: (tElement, tAttrs, transclude) ->
panes = [] panes = []
...@@ -150,15 +168,6 @@ tabReveal = ['$parse', ($parse) -> ...@@ -150,15 +168,6 @@ tabReveal = ['$parse', ($parse) ->
] ]
# TODO: Move this behaviour to a route.
showAccount = ->
restrict: 'A'
link: (scope, elem, attr) ->
elem.on 'click', (event) ->
event.preventDefault()
scope.$emit('nav:account')
repeatAnim = -> repeatAnim = ->
restrict: 'A' restrict: 'A'
scope: scope:
...@@ -215,8 +224,8 @@ angular.module('h.directives', imports) ...@@ -215,8 +224,8 @@ angular.module('h.directives', imports)
.directive('formInput', formInput) .directive('formInput', formInput)
.directive('formValidate', formValidate) .directive('formValidate', formValidate)
.directive('privacy', privacy) .directive('privacy', privacy)
.directive('tabbable', tabbable)
.directive('tabReveal', tabReveal) .directive('tabReveal', tabReveal)
.directive('showAccount', showAccount)
.directive('repeatAnim', repeatAnim) .directive('repeatAnim', repeatAnim)
.directive('whenscrolled', whenscrolled) .directive('whenscrolled', whenscrolled)
.directive('match', match) .directive('match', match)
...@@ -2,17 +2,19 @@ assert = chai.assert ...@@ -2,17 +2,19 @@ assert = chai.assert
sinon.assert.expose assert, prefix: null sinon.assert.expose assert, prefix: null
sandbox = sinon.sandbox.create() sandbox = sinon.sandbox.create()
fakePromise = finally: sandbox.stub()
class MockSession class MockSession
$login: sandbox.stub().returns(finally: sandbox.stub()) login: sandbox.stub().returns($promise: fakePromise)
$register: (callback, errback) -> register: (data, callback, errback) ->
errback errback
data: data:
errors: errors:
username: 'taken' username: 'taken'
reason: 'registration error' reason: 'registration error'
finally: sandbox.stub() $promise: fakePromise
mockFlash = sandbox.spy()
mockFormHelpers = applyValidationErrors: sandbox.spy() mockFormHelpers = applyValidationErrors: sandbox.spy()
describe 'h.auth', -> describe 'h.auth', ->
...@@ -21,7 +23,7 @@ describe 'h.auth', -> ...@@ -21,7 +23,7 @@ describe 'h.auth', ->
beforeEach module ($provide) -> beforeEach module ($provide) ->
$provide.value '$timeout', sandbox.spy() $provide.value '$timeout', sandbox.spy()
$provide.value 'flash', sandbox.spy() $provide.value 'flash', mockFlash
$provide.value 'session', new MockSession() $provide.value 'session', new MockSession()
$provide.value 'formHelpers', mockFormHelpers $provide.value 'formHelpers', mockFormHelpers
return return
...@@ -40,7 +42,7 @@ describe 'h.auth', -> ...@@ -40,7 +42,7 @@ describe 'h.auth', ->
$timeout = _$timeout_ $timeout = _$timeout_
auth = $controller 'AuthController', {$scope} auth = $controller 'AuthController', {$scope}
session = _session_ session = _session_
session.$login.reset() session.login.reset()
describe '#submit()', -> describe '#submit()', ->
it 'should call session methods on submit', -> it 'should call session methods on submit', ->
...@@ -49,7 +51,7 @@ describe 'h.auth', -> ...@@ -49,7 +51,7 @@ describe 'h.auth', ->
$valid: true $valid: true
$setValidity: sandbox.stub() $setValidity: sandbox.stub()
assert.called session.$login assert.called session.login
it 'should do nothing when the form is invalid', -> it 'should do nothing when the form is invalid', ->
auth.submit auth.submit
...@@ -57,7 +59,7 @@ describe 'h.auth', -> ...@@ -57,7 +59,7 @@ describe 'h.auth', ->
$valid: false $valid: false
$setValidity: sandbox.stub() $setValidity: sandbox.stub()
assert.notCalled session.$login assert.notCalled session.login
it 'should apply validation errors on submit', -> it 'should apply validation errors on submit', ->
form = form =
...@@ -78,6 +80,7 @@ describe 'h.auth', -> ...@@ -78,6 +80,7 @@ describe 'h.auth', ->
describe 'timeout', -> describe 'timeout', ->
it 'should happen after a period of inactivity', -> it 'should happen after a period of inactivity', ->
sandbox.spy $scope, '$broadcast' sandbox.spy $scope, '$broadcast'
$scope.form = $setPristine: sandbox.stub()
$scope.model = $scope.model =
username: 'test' username: 'test'
email: 'test@example.com' email: 'test@example.com'
...@@ -88,9 +91,9 @@ describe 'h.auth', -> ...@@ -88,9 +91,9 @@ describe 'h.auth', ->
assert.called $timeout assert.called $timeout
$timeout.lastCall.args[0]() $timeout.lastCall.args[0]()
assert.called $scope.form.$setPristine, 'the form is pristine'
assert.isNull $scope.model, 'the model is erased' assert.isNull $scope.model, 'the model is erased'
assert.called mockFlash, 'a notification is flashed'
assert.calledWith $scope.$broadcast, 'timeout'
it 'should not happen if the model is empty', -> it 'should not happen if the model is empty', ->
$scope.model = undefined $scope.model = undefined
...@@ -100,33 +103,3 @@ describe 'h.auth', -> ...@@ -100,33 +103,3 @@ describe 'h.auth', ->
$scope.model = {} $scope.model = {}
$scope.$digest() $scope.$digest()
assert.notCalled $timeout assert.notCalled $timeout
describe 'authDirective', ->
elem = null
session = null
$rootScope = null
$scope = null
beforeEach inject ($compile, _$rootScope_, _session_) ->
elem = angular.element(
'''
<div class="auth" ng-form="form"
on-error="stub()" on-success="stub()" on-timeout="stub()">
</div>
'''
)
session = _session_
$rootScope = _$rootScope_
$compile(elem)($rootScope)
$rootScope.$digest()
$scope = elem.isolateScope()
it 'should invoke handlers set by attributes', ->
$rootScope.stub = sandbox.stub()
for event in ['error', 'success', 'timeout']
$rootScope.stub.reset()
$scope.$broadcast(event)
assert.called $rootScope.stub
...@@ -5,7 +5,7 @@ sandbox = sinon.sandbox.create() ...@@ -5,7 +5,7 @@ sandbox = sinon.sandbox.create()
describe 'h.controllers.AccountManagement', -> describe 'h.controllers.AccountManagement', ->
$scope = null $scope = null
fakeFlash = null fakeFlash = null
fakeProfile = null fakeSession = null
fakeIdentity = null fakeIdentity = null
fakeFormHelpers = null fakeFormHelpers = null
editProfilePromise = null editProfilePromise = null
...@@ -13,7 +13,7 @@ describe 'h.controllers.AccountManagement', -> ...@@ -13,7 +13,7 @@ describe 'h.controllers.AccountManagement', ->
createController = null createController = null
beforeEach module ($provide, $filterProvider) -> beforeEach module ($provide, $filterProvider) ->
fakeProfile = {} fakeSession = {}
fakeFlash = sandbox.spy() fakeFlash = sandbox.spy()
fakeIdentity = fakeIdentity =
logout: sandbox.spy() logout: sandbox.spy()
...@@ -23,7 +23,7 @@ describe 'h.controllers.AccountManagement', -> ...@@ -23,7 +23,7 @@ describe 'h.controllers.AccountManagement', ->
$filterProvider.register 'persona', -> $filterProvider.register 'persona', ->
sandbox.stub().returns('STUBBED_PERSONA_FILTER') sandbox.stub().returns('STUBBED_PERSONA_FILTER')
$provide.value 'profile', fakeProfile $provide.value 'session', fakeSession
$provide.value 'flash', fakeFlash $provide.value 'flash', fakeFlash
$provide.value 'identity', fakeIdentity $provide.value 'identity', fakeIdentity
$provide.value 'formHelpers', fakeFormHelpers $provide.value 'formHelpers', fakeFormHelpers
...@@ -33,31 +33,16 @@ describe 'h.controllers.AccountManagement', -> ...@@ -33,31 +33,16 @@ describe 'h.controllers.AccountManagement', ->
beforeEach inject ($rootScope, $q, $controller) -> beforeEach inject ($rootScope, $q, $controller) ->
$scope = $rootScope.$new() $scope = $rootScope.$new()
$scope.session = userid: 'egon@columbia.edu' $scope.persona = 'egon@columbia.edu'
disableUserPromise = {then: sandbox.stub()} disableUserPromise = {then: sandbox.stub()}
editProfilePromise = {then: sandbox.stub()} editProfilePromise = {then: sandbox.stub()}
fakeProfile.edit_profile = sandbox.stub().returns($promise: editProfilePromise) fakeSession.edit_profile = sandbox.stub().returns($promise: editProfilePromise)
fakeProfile.disable_user = sandbox.stub().returns($promise: disableUserPromise) fakeSession.disable_user = sandbox.stub().returns($promise: disableUserPromise)
createController = -> createController = ->
$controller('AccountManagement', {$scope: $scope}) $controller('AccountManagement', {$scope: $scope})
it 'hides the sheet by default', ->
controller = createController()
assert.isFalse($scope.sheet)
describe 'event subscriptions', ->
it 'should show the sheet on "nav:account" event', ->
controller = createController()
$scope.$emit('nav:account')
assert.isTrue($scope.sheet)
it 'should hide the sheet on "logout" event', ->
controller = createController()
$scope.$emit('logout')
assert.isFalse($scope.sheet)
describe '.submit', -> describe '.submit', ->
createFakeForm = (overrides={}) -> createFakeForm = (overrides={}) ->
defaults = defaults =
...@@ -73,7 +58,7 @@ describe 'h.controllers.AccountManagement', -> ...@@ -73,7 +58,7 @@ describe 'h.controllers.AccountManagement', ->
controller = createController() controller = createController()
$scope.submit(fakeForm) $scope.submit(fakeForm)
assert.calledWith(fakeProfile.edit_profile, { assert.calledWith(fakeSession.edit_profile, {
username: 'STUBBED_PERSONA_FILTER' username: 'STUBBED_PERSONA_FILTER'
pwd: 'gozer' pwd: 'gozer'
password: 'paranormal' password: 'paranormal'
...@@ -162,7 +147,7 @@ describe 'h.controllers.AccountManagement', -> ...@@ -162,7 +147,7 @@ describe 'h.controllers.AccountManagement', ->
controller = createController() controller = createController()
$scope.delete(fakeForm) $scope.delete(fakeForm)
assert.calledWith fakeProfile.disable_user, assert.calledWith fakeSession.disable_user,
username: 'STUBBED_PERSONA_FILTER' username: 'STUBBED_PERSONA_FILTER'
pwd: 'paranormal' pwd: 'paranormal'
......
...@@ -167,15 +167,3 @@ describe 'h.directives', -> ...@@ -167,15 +167,3 @@ describe 'h.directives', ->
controller = $element.controller('ngModel') controller = $element.controller('ngModel')
assert.isTrue(controller.$error.match) assert.isTrue(controller.$error.match)
describe '.showAccount', ->
$element = null
beforeEach ->
$element = $compile('<a show-account>Account</a>')($scope)
$scope.$digest()
it 'triggers the "nav:account" event when the Account item is clicked', (done) ->
$scope.$on 'nav:account', ->
done()
$element.click()
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment