#! causing routing issues using Angular/Rails with multiple layouts - ruby-on-rails

I'm developing an AngularJS application with a RoR backend and ran into an issue when using multiple layouts. The application uses one layout when rendering pages to an unauthenticated user, and changes to another layout once the user is authenticated.
The layout is server on the initial pageload, and is managed by Rails. Some sample code illustrating how we're loading the different layouts based on the route:
class SampleController < ApplicationController
layout :current_layout
def current_layout
"layout" unless request.xhr?
end
end
Sample controller for different section:
class SampleController2 < ApplicationController
layout :current_layout
def current_layout
"anotherLayout" unless request.xhr?
end
end
This is defined separately for the controllers managing the authenticated/unauthenticated users, and basically serves up the proper layout. We're using a XHR check to prevent routing loops when Angular comes into picture.
So this works fine in most browser, but breaks when using IE9. Angular falls back to using #! URLs in IE9, so Rails has no idea which controller to load since the hash doesn't get sent to the backend. In this case, Rails loads the root and it's associated layout. If the authenticated section is set as the default, then it loads this layout even for unauthenticated users, and vice-versa.
So basically, I need to find a way to make this multiple layout application work properly even in browsers which don't support HTML5 pushState. I've checked all over the place for a proper solution for this and couldn't come up with anything yet.

Alright, so after testing lots of tweaks on both the client and server side, I ended up using the following solution. Basically, I just stopped exposing Angular to multiple layouts and dealt with that in Rails. Of course, this does put a limitation on users.
The app is divided into two main sections: dashboard and authentication. So in the backend, we restrict the users from accessing any authentication pages if they're logged in, and obviously dashboard pages can only be accessed if the user is authenticated.
The root of the problem was that Rails had no way of knowing which layout to serve, if a layout should be served at all. We used the cookies to differentiate the authenticated and unauthenticated users. If the cookie is set, and it passes the authenticity testing, then the dashboard layout should be loaded. Otherwise, any user trying to access the dashboard will be redirected to the authentication section:
class DashboardController < ApplicationController
layout :current_layout
before_filter :authenticate_user
def authenticate_user
if request.xhr? && !cookies[:access_token]
redirect_to "/login"
end
end
def current_layout
if cookies[:access_token]
"dashboard" unless request.xhr?
else
"application" unless request.xhr?
end
end
end
Similarly for the authentication section:
class AuthenticationController < ApplicationController
layout :current_layout
before_filter :redirect_if_authenticated
def redirect_if_authenticated
if request.xhr? && cookies[:access_token]
redirect_to "/dashboard"
end
end
def current_layout
if cookies[:access_token]
"dashboard" unless request.xhr?
else
"application" unless request.xhr?
end
end
end
So main points to note here:
If the request is an XHR, don't serve the layout again. Serving the layout again will cause infinite loading loops in IE9, possibly in other IE versions as well.
Since we're not getting the URL Fragment at the server side, we have no way of knowing which layout should be loaded. So we're using the cookies as the source of truth, and access is controlled solely based on this.
This introduces some asymmetry on the clientside: In some cases, the url and layout will remain if the user types in the URL for another section. Since we can't do anything based on the URL from the server, and since cookies remain unchanged in this case, this has to be handled by Angular.
For the last point, the authentication section in my case only had a few possible urls, so I just redirected if I got a regex match as follows:
if(Modernizr.history) {
if(!/(login|sign|pass)/.test(location.pathname)) {
$location.path('/');
}
} else {
if(location.hash.length > 3 && !/(login|sign|pass)/.test(location.hash)) {
$window.location.href = '/';
}
}
Checking for the history API here, because in older IE browsers(and I guess in other older ones as well?), $location.path('/') wasn't reloading the entire page consistently. When using this, the $routeProvider configuration should also conditionally load the layouts, for cases where we redirect to root /:
$routeProvider
// Code for routes
.otherwise({
// Of course, add something to check access_token authenticity here as well :P
redirectTo: $.cookie('access_token') ? "/dashboard" : "/login"
});
So, with these couple tweaks, the app is working properly in all browsers and functioning with multiple layouts. Of course, I know nothing about RoR, so I'm hoping someone is going to have a few improvements to suggest, or a better answer. ;)

Related

Devise Password Controller edit method renders nothing

I've been having a lot of trouble getting devise's reset password page to actually render. I've overrided
the edit function and have been trying to force it to render the view but it ends up as an empty string. Any ideas as to how to make it work?
Side note is that I'm using rails in API mode, is there anything I need to configure to get the page to render?
class PasswordsController < Devise::PasswordsController
def edit
self.resource = resource_class.new
set_minimum_password_length
resource.reset_password_token = params[:reset_password_token]
render template: 'devise/users/passwords/edit'
end
end
well, еhough strictly speaking, need to add view, but if your app is kind of API with endpoints only, it's not a way for sure, so you shouldn't redefine device methods or whatever.
In the case of API mode, you need to implement route on the FE part, and then use the Device's endpoints to reset-password logic.
Btw, this is a common feature and Device's wiki is presented at least 3 solutions to implement this, please check this link:
https://github.com/heartcombo/devise/wiki/How-To:-Allow-users-to-edit-their-password
I hope it's help

Unable to log out and redirect to new page from yielded layout

In a 3.2.16 Rails app using Devise, we allow users to stay logged in for a number of days. This means, of course, that if they click a link to our page (say in their bookmarks) they come right back into the app (assuming their session is still active).
For our main screen, we have a yielding layout
...
<body>
...
<%= yield %>
...
</body>
The layout surrounding the yield includes a display of the username among other things.
And now I have a new controller:
class AccountSelectionsController < ApplicationController
def new
if user_signed_in?
sign_out current_user
current_user = nil
end
...
render :layout => "external"
end
...
end
When the new action is invoked, I want the user signed out completely, the session cleared, and the user taken to a completely different layout. The use-case assumes the user is reaching this controller from a link in, say, an email or a page outside my app (IOW, not from a spot inside my app).
I first thought I merely had do a sign_out current_user(as above), but that didn't do anything obvious: the user seems to stay signed in.
The above was just my starting point. I've tried just sign_out (without a resource, implying all scopes), reset session, and redirect_to destroy_user_session_path (which is what our standard logout button does, a button positioned on the surrounding layout).
What I got though was my new external view (or the normal new session sign in screen, depending on the permutation of what I tried) trying to render inside the old layout (as if it was part of the yield).
I could try the Devise after_sign_out_path_for to help with redirect, but then I'd only want it if it was tied to this particular controller and action and I'm not quite sure how to safely accomplish that. And now I'm not convinced it wouldn't just keep me wrapped in the surrounding layout anyway.
So, (1) is there a reason the main layout stays intact even upon a full redirect_to (even using :status => 301) that I should be able to defeat (for instance, is the yield interfering?), or (2) am I on the right track with Devise after_sign_out_path_for and what do I need to do to limit that behavior to just respond to this one controller action?
Thank you!
Richard
UPDATE: the served page (via view source) shows the intended screen body is wrapped within the layout of the origin screen
UPDATE 2: I've also tried returning a head :reset_content from a before filter along with various other things in a before_filter. Still the old layout keeps rendering before it attempts to render the new page. This is although I'm using different Chrome tabs in the test (i.e., the session stays in memory); I've tried it in Firefox too. Same result. The output of rails s shows the redirects and gives no indication that it's attempting to go through another controller first, something is triggering the layout. Is there away to force a layout in a redirect?
Try this instead,
sign_out current_user, :bypass => true
So this is my penance for posting the question.
I just figured out that a before_filter was intercepting the call to the controller and redirecting it to the wrong layout before a sign-in was ever checked for. Normally this is desired for this particular application, but I didn't realize that the filter actually was catching the redirect ahead of my controller (the logs suggested it happened at a later point). Once I set that filter to be skipped in my controller, all was well.
Moral of the story, I need to better consider the side effects of before_filters in the ApplicationController.
Thank you to RSB and Jasdeep Singh and everyone else who spent time considering an answer for this.

rails nested devise redirect

i have an app with nested resources. my routes are:
resources :teams do
resources :blogs
end
in my blogs controller, im using a different layout, by adding
layout "teamlayout"
to the controller.
Both layouts, the application.html.erb and the teamlayout.html.erb have included a login form itself. which i made working by this: https://github.com/plataformatec/devise/wiki/How-To:-Display-a-custom-sign_in-form-anywhere-in-your-app
now my question. when a user logs in, i want him redirected to the page from where he logs in.
You have a couple options:
Include a hidden field value in the login form that describes the source of the login (e.g. hidden_field_tag(:login_source, "team")) and define your own logic for SessionsController#create that uses the hidden field value to determine the location for response_with.
Or, you could keep track of the user's location by using a before_filter in the controllers with login forms by doing something like
def store_location
session['saved_location'] = request.request_uri
end
Then, you can override the after_sign_in_path_for(resource) method in your application controller to use the session saved_location value to determine where to redirect.
The second option seems a little less invasive to the Devise infrastructure to me, but is a little less flexible.

Rails 3 Layout + Querystring Problem

I'm trying to setup an ajax-based website that loads a fragment of a webpage when a specific combination of GET variables and HTTP Headers are given.
GET /normal/html/page?ajax=true
X-ajax: true
What I've setup in my controller is:
before_filter do |controller|
if request_by_ajax?
ApplicationController.layout false
end
end
This works, but only in development mode. You see when I attempt to access the given page it only gives the fragmented (layout free) page.
When the normal page is accessed without ajax:
GET /normal/html/page
This returns only the view for that page and NOT the layout around it. But if I access that page when the webserver is reloaded then it returns the layout and when its accessed afterwards via AJAX it returns the layout + the view. Clearly there is a caching issue here.
I would really like to keep the same route for the page. If there is an ajax call then I would prefer to figure out the response based on the querystring and request header values. But rails prefers to classify querystring parameters and request headers as meaningless when serving a file (i.e. /normal/html/page and /normal/html/page?ajax=true) return the same actual template file (this what I assume).
Any idea how to get around this?
I figured out the issue.
It turns out that the layout call must be defined early on in the application controller:
---- application_controller.rb
layout :choose_layout
def choose_layout
if request_by_ajax?
false
else
'application'
end
end
Works like a charm.

Is it possible to specify two root pages in Rails (one for anonymous user another for logged in user)

I am building a product that has static pages and dynamic pages(product related). Both category of pages have different release life cycle. The marketing team working with the designer, release the static pages and the product pages are released by the engineering team.
The static pages reside in public/home and they are self contained. They don't need access to the Rails infrastructure other than providing links.
In this setup, I am trying to implement the following behavior:
When an un-authenticated visitor launches http://www.xyz.com, the user should be taken to the static landing page.
When an authenticated visitor launches http://www.xyz.com, the user should be taken to the product landing page (LandingsController, index action).
In my current implementation, I check if the user is authenticated in the Rails world and render the static page OR the product page.
I want to know the following:
1) How do you handle such scenarios?
2) Is there a way to avoid entering the Rails stack for static home page.
3) Is there a customization for the root_path method to return different root based on the context
1) How do you handle such scenarios?
The common answer would look like this:
class LandingsController < ApplicationController
before_filter :login_required
def index
...
end
...
private
def login_required
if not_logged_in? # This methods depends on your authentication strategy
send_file "/your/static/path/#{params[:action]}", :type => "application/html charset=utf8;"
return false # Halt chain
end
end
send_file documentation
And, depending on the correspondence between each of your actions and your templates, you can further abstract the login_required method into the ApplicationController, and validate if the file exists.
2) Is there a way to avoid entering the Rails stack for static pages
Yes. You have to take my word for it, because I haven't done it myself, but you can use a Rack middleware to do that. Here is an example of how to do something similar, with the exception that instead of a redirect, you would serve the file statically (just set the headers and the results of File.read as content) This depends on the authentication library you're working with, though.
3) Is there a customization for the root_path method to return different
root based on the context
You cannot define a conditional route (that is, defining multiple routes in the routes.rb file), but you can override the root_url method in ApplicationController, assuming you are using a named path root in your route definitions. Something like
class ApplicationController
def root_url(*options)
if logged_in?
"/return/something/custom"
else
super(*options)
end
end
end
This, however, sound really a bad idea, since 1) You should point to the same url, and let the controller handle the request (your links should be blind of where to take you), and 2) It may potentially break other stuff that rely on the root_url and root_path methods.
Unfortunately, Rails' routing can only route requests to different controllers based on something in the request, making the per-request session data just out of reach. Your current implementation is certainly the most common strategy. I am guessing something like this:
def index
if logged_in?
# any logged in logic you need.
else
render :file => 'public/home', :layout => false
end
end
The only way to refactor this to make it feel less "icky" is to move that render call to a before_filter. Since the filter will have rendered?, your action won't get invoked at all. Of course, you could also choose to redirect_to another location for authenticated (or non-authenticated) requests in a before filter, which would solve the problem entirely.
The only thing you could do would be based on the non-existence of the session cookie.
Write a middleware component or Rack application (etc.) that explicitly handles the request if no session cookie is present. Similarly, you could use middleware to re-write the request, and then pass it onto the application layer.
Use a similar strategy as #1, but do it via web server configuration (Apache or nginx), avoiding the Rails app entirely.
But, it's definitely possible for someone to have a session and yet not be logged in (e.g. if they went to another page which you didn't handle this way), or even have invalid session data, so you wouldn't be able to actually eliminate the code you have now. These changes would only serve to increase the performance of the session-less requests, but unless those pages are causing a significant problem (which I doubt), so I would not recommend doing so.

Resources