heartsentwined / ember-auth

Authentication framework for ember.js.
http://ember-auth.herokuapp.com/
397 stars 43 forks source link

App.Auth.user not available immediately after redirect #49

Closed kevinansfield closed 11 years ago

kevinansfield commented 11 years ago

Steps to reproduce the issue I'm seeing:

  1. Access /signup/step_2
  2. Redirected to /home/sign_in
  3. Sign in successfully
  4. Redirected to /signup/step_2

Now at this stage, the console.log calls both output null yet App.Auth.get('signedIn') is true. Obviously this then causes the model not to be loaded so step_2 my form is empty.

However, immediately after the last redirect when I am on the page with the empty form, I can run App.Auth.get('user) in the console and the user object is available.

Am I doing something wrong, or is there some way that the model can be lazily loaded?

Thanks.

Relevant parts of router:

App.Router.reopen
  location: 'history'

App.Router.map ->
  @resource 'home', ->
    @route 'sign_in'
  @resource 'signup', ->
    @route 'step_1'
    @route 'step_2'
    @route 'step_3'
    @route 'confirmation'

App.SecretRoute = Ember.Route.extend App.Auth.AuthRedirectable

App.MarketingRoute = Ember.Route.extend
  setupController: (controller, model) ->
    @controllerFor('application').set('isMarketing', true)
  deactivate: ->
    @controllerFor('application').set('isMarketing', false)

App.SecretMarketingRoute = App.MarketingRoute.extend App.Auth.AuthRedirectable

App.HomeRoute = App.MarketingRoute.extend()

App.SignupStep2Route = App.SecretMarketingRoute.extend
  model: ->
    console.log App.Auth.get('user')
    if App.Auth.get('signedIn')
      console.log App.Auth.get('user')
      App.Auth.get('user.profile')

App.SignInRoute = App.MarketingRoute.extend()
kevinansfield commented 11 years ago

Sorry, just hit the Post button accidentally. Still working on this issue.

kevinansfield commented 11 years ago

Updated original with full description.

heartsentwined commented 11 years ago

I'll try to create a minimal test case for it, and I'll update you with results.

kevinansfield commented 11 years ago

This could be something else.

I've just tested again now that I've got the rememberable module working it seems that App.Auth.get('user') returns null in all model and setupController hooks, regardless of it being a redirect or not.

Running App.Auth.get('user') in the console once the route has loaded works fine.

kevinansfield commented 11 years ago

App.Auth.get('userId') also returns null

I believe my server response is correct for the sign-in call:

{"user_id":26,"auth_token":"fvDeU9Ly8AMs63TUqiw8"}
heartsentwined commented 11 years ago

Observation confirmed.

heartsentwined commented 11 years ago

What is your expected behavior though? I can make signedIn wait until post-sign in triggers have finished loading before becoming true, but -

If we make it synchronous, such that it refuses to load your route until it has finished loading, then this would incur at least a double-roundtrip delay (sign in + fetch user), somewhat defeating the purpose of using an async-style ember app.

Or, if we make it async, then the model route would have been called before user has finished loading anyway - 99% of time, that is, except for someone with lightning-fast network speed.

heartsentwined commented 11 years ago

Yet a third option is to use ember's native observation pattern. Your controller would start with an empty user model - this becoming part of your app's expected behavior. You then define an observer for the path App.Auth.user, and then populate the fields afterwards. You might also display a loading gif while this is happening (that's what I did for my own ember apps).

heartsentwined commented 11 years ago

Your observation that

Running App.Auth.get('user') in the console once the route has loaded works fine.

is actually non-deterministic in the ember sense. It is just a coincidence that the model has finished loading when the route does - these two happen asynchronously. Try making your server delay, say, 10 secs before returning the user model, and you will see that the user is still not there when you test for it after the route has finished loading.

kevinansfield commented 11 years ago

I was expecting it to work similarly to if I had App.User.find(x) in the model, i.e. I'd receive an empty User model in the loading state that is asynchronously populated when the data is available.

kevinansfield commented 11 years ago

I have managed to get this working using your observation pattern by removing everything from the model section of the route and adding this to the controller:

App.SignupStep2Controller = App.ObjectController.extend

  content: null

  loadProfile: (->
    if App.Auth.get('user.profile')
      @set('content', App.Auth.get('user.profile'))
  ).observes('App.Auth.user.profile')

Is there any way to use App.Auth.get('user') in a route when it's possible that it hasn't been loaded yet?

heartsentwined commented 11 years ago

No - this is actually not an ember-auth specific issue; try replacing user.profile with another "vanilla / innocent" model, e.g. comment.user, product.stock, etc, and (I think) you would get the same results.

The way I tackle this in my ember apps, is to simply set the model as user, and then use

Name: {{profile.name}}
Email: {{profile.email}}

{{! OR }}

{{#with profile}}
  Name: {{name}}
  Email: {{email}}
{{/with}}

etc, and let ember's view layer bindings do its magic.

kevinansfield commented 11 years ago

The way I tackle this in my ember apps, is to simply set the model as user, and then use

Sorry for all the questions, but where are you setting the model?

I'm just trying out the observation pattern in a few other places and I'm getting inconsistent behaviour. Sometimes the content gets set, other times it doesn't.

heartsentwined commented 11 years ago

No problem, we're all exploring ember :smile:

App.Foo = DS.Model.extend
  fooValue: DS.attr 'string'
  bar: DS.belongsTo 'App.Bar'

App.Bar = DS.Model.extend 
  barValue: DS.attr 'string'
  baz: DS.belongsTo 'App.Baz'

App.Baz = DS.Model.extend
  bazValue: DS.attr 'string'

App.FoosShowRoute = Em.Route.extend
  model: (param) ->
    App.Foo.find(param.foo_id) # model for a standard 'show' route
This is a show page for foo.
{{fooValue}} {{! let's say this would display a Foo's name? }}
{{bar.baz.bazValue}} {{! we can access nested belongsTo values directly}}
{{#with bar.baz}}
  {{bazValue}} {{! or like this}}
{{/with}}
heartsentwined commented 11 years ago

The way my suggested observation pattern works, is that it relies on an (implicit to userland code) App.User.find() call in ember-auth.

Normally, when you observe something, you will, of course, call find() somewhere in your code, in order to populate the data. You normally care about displaying the data, only that you don't mind its being blank before it's loaded. So you let ember take all the time it wants to load the model, and let ember automatically display it when it's ready.

A classic example is a hasMany relationship.

App.Parent = DS.Model.extend
  parentName: DS.attr 'string'
  children: DS.hasMany 'App.Child'

App.Child = DS.Model.extend
  childName: DS.attr 'string'
  parent: DS.belongsTo 'App.Parent'

App.ParentsShowRoute = Em.Route.extend
  model: (param) ->
    App.Parent.find(param.parent_id)
{{! the parents.show template}}
<h1>{{parentName}}</h1>
<ul>
{{#each children}}
  <li>{{childName}}</li>
{{/each}}
</ul>

Here, at first, parent would be loaded into the model, but the list of children would be empty. Ember would then load the parent's children asynchronously, and populate the list when it's done.

heartsentwined commented 11 years ago

Updated docs to reflect this caveat.

flynfish commented 11 years ago

I am having a similar issue. I have the user in a dropdown in the nav, so there isn't a route for this since it is show on every route.

I have this in my template: {{App.Auth.user.email}}, but there is a timing issue so it isn't always populated. How can I set the user so I have access to it in the template and it has embers view layer bindings if there isn't a specific route on which to set the model?

heartsentwined commented 11 years ago

Update: ember has got a new router, which is heavily relevant to authenticated-backed apps. I'm working with it to update ember-auth with the new APIs. For now, this issue is put on hold - I'll reproduce the scenario after I finish with the new router, and see if the issue persists.

Ref:

heartsentwined commented 11 years ago

7.x branch now released, with rc6 (new router!) support. changelog, upgrade guide, docs and demo all properly updated. Main notes:


@kevinansfield can you try upgrading and see if the issue persists?

Note however that, as stated above,

the new router now encourages you to use authRedirectable as "the ember way" of solving auth-only routes (with auth-only models)

This may prompt a rewrite of your original use case, as we pretty much have to embrace the promise pattern to solve auth-only use cases. (ref: detailed description in gist)

ember-auth has already embraced the promise pattern, and all its redirects, server requests, etc, use promises and return promises. The new router will pick this up, and wait for these to resolve before progressing. On the flip side, redirections are also implemented in the same way, in that a redirection will abort the current route transition, and begin a new one (to the redirected destinatioin).

remkoboschker commented 11 years ago

Hi,

Thanks for the nice Ember extension? plugin?

I'm a bit confused by the behaviour of the autoloading of the current user.

If I do App.User.find('123') I get the user alright. If I do App.Auth.get('userId') I get '123' But if I do App.Auth.get('user'), I get 'null'

What is the best way of accessing the details of the currently logged in user?

heartsentwined commented 11 years ago

Straight from the docs:

App.Auth = Ember.Auth.create
  # ...
  userModel: 'App.Member' # default null
  # pass the string, not a class App.Member

# access the current user model
# as with all things ember, this is loaded asynchronously
App.Auth.get('user')

If you didn't set the userModel config, ember-auth will not auto-load the model for you. That's still fine though, as you can still use App.Auth.get('userId') to get the user ID, and call App.User.find(id) when you need it.

remkoboschker commented 11 years ago

Hi, I had read the documentation and set the model. That's why I expected the behaviour. It's working fine now. I got confused by (Note that it is not a string 'App.User'.) on http://www.verious.com/code/heartsentwined/ember-auth/ I missed the # pass the string in the docs. Thank you for your swift reply.

heartsentwined commented 11 years ago

Closing due to inactivity (of the original issue).