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 = [
class AuthController
this.$inject = ['$scope', '$timeout', 'session', 'formHelpers']
constructor: ( $scope, $timeout, session, formHelpers ) ->
this.$inject = ['$scope', '$timeout', 'flash', 'session', 'formHelpers']
constructor: ( $scope, $timeout, flash, session, formHelpers ) ->
timeout = null
success = ->
success = (data) ->
$scope.tab = if $scope.tab is 'forgot' then 'activate' else null
$scope.model = null
$scope.$broadcast 'success'
$scope.$emit 'session', data
failure = (form, response) ->
{errors, reason} = response.data
......@@ -23,18 +23,13 @@ class AuthController
return unless form.$valid
data = {}
method = '$' + form.$name
angular.copy $scope.model, session
session.$promise = session[method] success,
$scope.$broadcast 'formState', form.$name, 'loading'
session[form.$name] $scope.model, success,
angular.bind(this, failure, form)
session.$resolved = false
.$promise.finally -> $scope.$broadcast 'formState', form.$name, ''
# Update status btn
$scope.$broadcast 'formState', form.$name, 'loading'
session.$promise.finally ->
$scope.$broadcast 'formState', form.$name, ''
$scope.model = null
$scope.tab = 'login'
$scope.$on '$destroy', ->
if timeout
......@@ -48,57 +43,12 @@ class AuthController
# If the model is not empty, start the timeout
if value and not angular.equals(value, {})
timeout = $timeout ->
$scope.form?.$setPristine()
$scope.model = null
$scope.$broadcast 'timeout'
flash 'info',
'For your security, the forms have been reset due to inactivity.'
, 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)
.controller('AuthController', AuthController)
.directive('auth', authDirective)
......@@ -12,53 +12,47 @@ identityFactory = [
onlogout = null
onmatch = null
$rootScope.session = session
$rootScope.$watch 'session.$promise', (promise) ->
# Wait for any pending action to resolve.
promise.finally ->
# Get the userid and convert it to the persona format.
persona = session.userid?.replace(/^acct:/, '') or null
# Fire callbacks as appropriate.
# Consult the state matrix in the `navigator.id.watch` documentation.
# https://developer.mozilla.org/en-US/docs/Web/API/navigator.id.watch
if loggedInUser is null
if persona
loggedInUser = persona
onlogin?(session.csrf)
else
$rootScope.$on 'session', (event, session) ->
# Get the userid and convert it to the persona format.
persona = session.userid?.replace(/^acct:/, '') or null
# Fire callbacks as appropriate.
# Consult the state matrix in the `navigator.id.watch` documentation.
# https://developer.mozilla.org/en-US/docs/Web/API/navigator.id.watch
if loggedInUser is null
if persona
loggedInUser = persona
onlogin?(session.csrf)
else
onmatch?()
else if loggedInUser
if persona
if loggedInUser is persona
onmatch?()
else if loggedInUser
if persona
if loggedInUser is persona
onmatch?()
else
loggedInUser = persona
onlogin?(session.csrf)
else
loggedInUser = null
onlogout?()
else
if persona
loggedInUser = persona
onlogin?(session.csrf)
else
loggedInUser = null
onlogout?()
else
loggedInUser = null
onlogout?()
else
if persona
loggedInUser = persona
onlogin?(session.csrf)
else
loggedInUser = null
onlogout?()
logout: ->
# Clear the session but preserve its identity and give it a new promise.
$promise = session.$logout()
$resolved = false
angular.copy({$promise, $resolved}, session)
$rootScope.$broadcast 'logout'
session.logout({}).$promise.then ->
$rootScope.$emit 'session', {}
request: ->
$rootScope.$broadcast 'authorize'
watch: (options) ->
{loggedInUser, onlogin, onlogout, onmatch} = options
session.load().$promise.then (data) -> $rootScope.$emit 'session', data
]
......
......@@ -12,6 +12,7 @@ ACTION = [
'forgot'
'activate'
'edit_profile'
'disable_user'
]
ACTION_OPTION =
......@@ -90,37 +91,9 @@ class SessionProvider
actions[name].transformResponse = process
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) ->
defaults = $httpProvider.defaults
......@@ -143,4 +116,3 @@ configure = ['$httpProvider', ($httpProvider) ->
angular.module('h.session', imports, configure)
.provider('session', SessionProvider)
.factory('profile', profileProvider)
......@@ -246,6 +246,8 @@ class App
# Do not rely on the identity service to invoke callbacks within an
# angular digest cycle.
$scope.$evalAsync ->
$scope.dialog.visible = false
# Update any edits in progress.
for draft in drafts.all()
annotator.publish 'beforeAnnotationCreated', draft
......@@ -307,15 +309,12 @@ class App
$scope.updater.then (sock) ->
sock.send(JSON.stringify(sockmsg))
$scope.authTimeout = ->
flash 'info',
'For your security, the forms have been reset due to inactivity.'
$scope.clearSelection = ->
$scope.search.query = ''
$scope.selectedAnnotations = null
$scope.selectedAnnotationsCount = 0
$scope.dialog = visible: false
$scope.id = identity
$scope.model = persona: undefined
......
class AccountManagement
@inject = ['$scope', '$rootScope', '$filter', 'flash', 'profile',
'identity', 'formHelpers']
@inject = ['$scope', '$filter', 'flash', 'session', 'identity', 'formHelpers']
constructor: ($scope, $rootScope, $filter, flash, profile,
identity, formHelpers) ->
constructor: ($scope, $filter, flash, session, identity, formHelpers) ->
persona_filter = $filter('persona')
onSuccess = (form, response) ->
......@@ -36,15 +34,12 @@ class AccountManagement
$scope.changePassword = {}
$scope.deleteAccount = {}
# Initial form state.
$scope.sheet = false
$scope.delete = (form) ->
# If the password is correct, the account is deleted.
# The extension is then removed from the page.
# Confirmation of success is given.
return unless form.$valid
username = persona_filter $scope.session.userid
username = persona_filter $scope.persona
packet =
username: username
pwd: form.pwd.$modelValue
......@@ -52,7 +47,7 @@ class AccountManagement
successHandler = angular.bind(null, onDelete, form)
errorHandler = angular.bind(null, onError, form)
promise = profile.disable_user(packet)
promise = session.disable_user(packet)
promise.$promise.then(successHandler, errorHandler)
$scope.submit = (form) ->
......@@ -60,7 +55,7 @@ class AccountManagement
# forms. However, in the backend it is just one: edit_profile
return unless form.$valid
username = persona_filter $scope.session.userid
username = persona_filter $scope.persona
packet =
username: username
pwd: form.pwd.$modelValue
......@@ -70,14 +65,9 @@ class AccountManagement
errorHandler = angular.bind(null, onError, form)
$scope.$broadcast 'formState', form.$name, 'loading' # Update status btn
promise = profile.edit_profile(packet)
promise = session.edit_profile(packet)
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', [])
.controller('AccountManagement', AccountManagement)
......@@ -107,6 +107,24 @@ privacy = ->
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) ->
compile: (tElement, tAttrs, transclude) ->
panes = []
......@@ -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 = ->
restrict: 'A'
scope:
......@@ -215,8 +224,8 @@ angular.module('h.directives', imports)
.directive('formInput', formInput)
.directive('formValidate', formValidate)
.directive('privacy', privacy)
.directive('tabbable', tabbable)
.directive('tabReveal', tabReveal)
.directive('showAccount', showAccount)
.directive('repeatAnim', repeatAnim)
.directive('whenscrolled', whenscrolled)
.directive('match', match)
......@@ -2,17 +2,19 @@ assert = chai.assert
sinon.assert.expose assert, prefix: null
sandbox = sinon.sandbox.create()
fakePromise = finally: sandbox.stub()
class MockSession
$login: sandbox.stub().returns(finally: sandbox.stub())
$register: (callback, errback) ->
login: sandbox.stub().returns($promise: fakePromise)
register: (data, callback, errback) ->
errback
data:
errors:
username: 'taken'
reason: 'registration error'
finally: sandbox.stub()
$promise: fakePromise
mockFlash = sandbox.spy()
mockFormHelpers = applyValidationErrors: sandbox.spy()
describe 'h.auth', ->
......@@ -21,7 +23,7 @@ describe 'h.auth', ->
beforeEach module ($provide) ->
$provide.value '$timeout', sandbox.spy()
$provide.value 'flash', sandbox.spy()
$provide.value 'flash', mockFlash
$provide.value 'session', new MockSession()
$provide.value 'formHelpers', mockFormHelpers
return
......@@ -40,7 +42,7 @@ describe 'h.auth', ->
$timeout = _$timeout_
auth = $controller 'AuthController', {$scope}
session = _session_
session.$login.reset()
session.login.reset()
describe '#submit()', ->
it 'should call session methods on submit', ->
......@@ -49,7 +51,7 @@ describe 'h.auth', ->
$valid: true
$setValidity: sandbox.stub()
assert.called session.$login
assert.called session.login
it 'should do nothing when the form is invalid', ->
auth.submit
......@@ -57,7 +59,7 @@ describe 'h.auth', ->
$valid: false
$setValidity: sandbox.stub()
assert.notCalled session.$login
assert.notCalled session.login
it 'should apply validation errors on submit', ->
form =
......@@ -78,6 +80,7 @@ describe 'h.auth', ->
describe 'timeout', ->
it 'should happen after a period of inactivity', ->
sandbox.spy $scope, '$broadcast'
$scope.form = $setPristine: sandbox.stub()
$scope.model =
username: 'test'
email: 'test@example.com'
......@@ -88,9 +91,9 @@ describe 'h.auth', ->
assert.called $timeout
$timeout.lastCall.args[0]()
assert.called $scope.form.$setPristine, 'the form is pristine'
assert.isNull $scope.model, 'the model is erased'
assert.calledWith $scope.$broadcast, 'timeout'
assert.called mockFlash, 'a notification is flashed'
it 'should not happen if the model is empty', ->
$scope.model = undefined
......@@ -100,33 +103,3 @@ describe 'h.auth', ->
$scope.model = {}
$scope.$digest()
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()
describe 'h.controllers.AccountManagement', ->
$scope = null
fakeFlash = null
fakeProfile = null
fakeSession = null
fakeIdentity = null
fakeFormHelpers = null
editProfilePromise = null
......@@ -13,7 +13,7 @@ describe 'h.controllers.AccountManagement', ->
createController = null
beforeEach module ($provide, $filterProvider) ->
fakeProfile = {}
fakeSession = {}
fakeFlash = sandbox.spy()
fakeIdentity =
logout: sandbox.spy()
......@@ -23,7 +23,7 @@ describe 'h.controllers.AccountManagement', ->
$filterProvider.register 'persona', ->
sandbox.stub().returns('STUBBED_PERSONA_FILTER')
$provide.value 'profile', fakeProfile
$provide.value 'session', fakeSession
$provide.value 'flash', fakeFlash
$provide.value 'identity', fakeIdentity
$provide.value 'formHelpers', fakeFormHelpers
......@@ -33,31 +33,16 @@ describe 'h.controllers.AccountManagement', ->
beforeEach inject ($rootScope, $q, $controller) ->
$scope = $rootScope.$new()
$scope.session = userid: 'egon@columbia.edu'
$scope.persona = 'egon@columbia.edu'
disableUserPromise = {then: sandbox.stub()}
editProfilePromise = {then: sandbox.stub()}
fakeProfile.edit_profile = sandbox.stub().returns($promise: editProfilePromise)
fakeProfile.disable_user = sandbox.stub().returns($promise: disableUserPromise)
fakeSession.edit_profile = sandbox.stub().returns($promise: editProfilePromise)
fakeSession.disable_user = sandbox.stub().returns($promise: disableUserPromise)
createController = ->
$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', ->
createFakeForm = (overrides={}) ->
defaults =
......@@ -73,7 +58,7 @@ describe 'h.controllers.AccountManagement', ->
controller = createController()
$scope.submit(fakeForm)
assert.calledWith(fakeProfile.edit_profile, {
assert.calledWith(fakeSession.edit_profile, {
username: 'STUBBED_PERSONA_FILTER'
pwd: 'gozer'
password: 'paranormal'
......@@ -162,7 +147,7 @@ describe 'h.controllers.AccountManagement', ->
controller = createController()
$scope.delete(fakeForm)
assert.calledWith fakeProfile.disable_user,
assert.calledWith fakeSession.disable_user,
username: 'STUBBED_PERSONA_FILTER'
pwd: 'paranormal'
......
......@@ -167,15 +167,3 @@ describe 'h.directives', ->
controller = $element.controller('ngModel')
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