rails 3 / heroku / devise: how specify different 'landing page' when users sign in? - ruby-on-rails

We'd like to have 3 different signin forms for our app:
the default signin form, takes them to their normal dashboard (as we do now)
a "foo" signin form that, if they use THAT form to sign in, takes them to a special purpose screen
a "bar" signin form that takes them to yet anogther special-purpose screen
I assume the right approach is to somehow
a) create a new route for /foo and /bar, probably directing both to the SAME signin method but in the route add a url parameter 'signin_type' telling us which "type" of signin form it is?
b) implement a custom RegistrationsController method(s) (what name?) to handle the signin form (we already have a custom new and create method for when they register, since our registration form needed a 'referral code' field added), and have the method look at the url parameter 'signin_type' to redirect the sign to either the normal, or foo, or bad
c) implement another method that handles the signin submit (is that a different method?) that looks at some special embedded form value to figur eout which signing form was used?
That's my best guess. If correct, it's how to do (b) and (c) that has me stumped. Any thoughts will be appreciated!

I think you may be trying to over engineer this. I would approach this with a single sign_in page, and just use conditional logic in overriding the after_sign_in_path_for(resource) method for the Devise controller. Not only will this be much easier to implement now, it will be a lot easier to maintain in the future. Simply add to your ApplicationController.rb:
protected
def stored_location_for(resource)
nil
end
def after_sign_in_path_for(resource)
if condition_foo
redirect_to foo_url
elsif condition bar
redirect_to bar_url
else
redirect_to dashboard_url
end
end
The first method overrides Devise's default location of root and sets it to nil, then the logic after that is pretty self explanatory. This should work for what you are wanting.

Related

How do you make sure that after sign in the user completes the profile form first before they can use the rest of the website functions.

How do you make sure that after sign in the user completes the profile form first before they can use the rest of the website functions. I am trying to make sure that after the member has completed the sign up form and then completes there email confirmation with devise that when they sign in that when they are redirected to the new_member_profile_path(current_member) form that they stay on this page and that if they decide to go to a link and click that that they will automatically be redirected back to the complete your profile page with the notice before please complete your profile first. I have it set already once they have completed the form they will be redirected to their member's page. I have looked in multi-forms with wicked - I really feel that because I am still am a Novice rails developer that this would be unnecessary. I am thinking about putting an if clause in the application.html.erb where the site nav template is based and putting a clause with <% if current_member_profile.blank ? %> then redirect back to new_member_profile_path(current_member) with a flash notice tag written in the html file. I have tried this if clause but does not work - comes up as undefined method. Please could someone point me in the right direction or give me the simple solution of getting this idea to work. Thanks in advance ;)
There are various approaches to achieve what you're trying to do. Perhaps the cleanest with the least amount of code needed would be to first authenticate the user with Devise's own authenticate_user! filter and then check for a field that can only be there when the profile has been filled in.
# in user.rb
def has_completed_profile?
first_name.present?
end
# in application_controller.rb
before_action :authenticate_user!
before_action :require_user_profile
private
def require_user_profile
# nothing needs to be done if the profile was already filled in
return if user_signed_in? && current_user.has_completed_profile?
redirect_to edit_profile_url, alert: "Please complete your profile first!"
return false
end
Notice how I've extracted has_completed_profile? into the User model instead of putting the name check directly into the controller. This way, if you need to change the logic of what makes a profile "complete", you don't need to touch the controller at all ("complete profile" is a business concept, not a routing/HTTP concept and thus doesn't belong in the controller).
In controllers where you don't want the additional profile check – e.g. the controller where the user actually completes their profile, where they presumably need to be logged in but can't have a profile yet – you just skip the additional filter:
# in profiles_controller.rb
skip_before_action :require_user_profile
Side note: Over the years I've learned that it's best to keep things like profile data, address data, phone numbers and what not in a separate model and don't extend Devise's User model. This prevents various issues and keeps the already huge User model (Devise includes dozens of methods into it and turns it into a God Object as it is) a bit slimmer. And if you think about it, it also makes sense in terms of business logic: A user has_one profile, has_one address (or has_many addresses) etc.
Hope that's clear.
You can add a new method in the application_controller.rb
For eg
def current_member_profile
current_user.name.blank?
end
Check the params which should not be blank when creating a member profile. I have added 'name' here for example. if the params is blank, then it will redirect as you have specified in your application.html.erb
Instead for putting an if condition on the application.html.erb, You can use a before_action in your application_controller.
Something like this.
application_controller.rb:
before_action :check_for_profile_completion
def check_for_profile_completion
// your code of redirection to the page if the profile is incomplete
end
Also you can skip this action on controller which you don't wanna restrict user to go. Like
skip_before_action :check_for_profile_completion, only: [://actions you wanna skip seperated by comma]

In rails 4.2, how to display a form for preview but ensure it cannot be submitted

I'd like to have a a form view that can, depending on circumstances, have submit functionality disabled in a bullet-proof way so that even a clever user could not edit the HTML source (via a browser extension) to re-add the submit button.
It seems one way to do that might be to somehow inject an invalid authenticity token that replaces the (valid) rails-generated one, so that even if a user somehow re-adds the submit button (by editing the HTML via a browser extension) it would still be an invalid submission.
My thought is to have some logic in the view:
- if #form_disabled # set by controller
- somehow_invalidate_the_authenticity_token?
How might one 'break' Rails form submission?
The purpose of doing this, instead of rendering the preview in a :show action, is to have the exact same view displaying both the live-form and the dead-form.
If I were you, I would use pundit.
It's pretty simple, and has few lines of code if you need to know how it works.
I'd start to write the code here, but I realize that the example at the readme fit your needs.
At the application controller add this
At the folder app/policies put the class PostPolicy, of course, you must replace "Post" with the name of your controller in singular (even if you have not a model with that name). The update? (and create?) actions should return true/false to indicate if user is allowed or not.
A few lines down on the readme, you will find the PostsController#update action, which call to authorize with the record before the update. I think you want do the same with create (then you need a create? method at the policy class).
Pundit needs current_user controller method, if you don't have it. Just follow the user customization instructions.
Of course, new and edit actions don't call authorize because they are allowed to everybody. Only the POST & the PUT/PATCH actions are forbidden.
Yes, it's more than a surgery of one line of code. But it's simple and the right way of give access to users.
After reading my other answer, I start thinking that you can do the same that Pundit does at the controller:
def update
if <unauthorized user>
flash[:alert] = "You are not authorized to perform this action."
redirect_to(request.referrer || root_path)
else
# all the update stuff
# ...
end
end

Devise stored_location_for(resource) returns nil

I read the few posts about troubleshooting stored_location_for here, but can't seem to figure it out and not sure how to troubleshoot.
I tried deleting my custom after_sign_in_path_for, but that didn't work either. My location is never getting saved, although as I understand it after each session/page update it should store the location. Do I need to through that in as a filter manually?
def after_sign_in_path_for(resource)
stored_location_for(resource) ||
if resource.is_a?(Account)
add_quote_to_account(resource)
if resource.applications.any?
edit_application_path(resource.applications(true).last)
else
root_path
end
else
super
end
end
May be you are not storing the location where you want to redirect_to after signing in with devise. Devise provides two methods - store_location_for and stored_location_for
https://github.com/plataformatec/devise/blob/master/lib/devise/controllers/store_location.rb
Suppose your user model is named "user" then
A call to store_location_for(:user, my_desired_path) in your controller will store the url "my_desired_path" in session with key "user_return_to". Basically this method will simply do this - session["user_return_to"] = my_desired_path. Probably you are missing this. I have a booking controller and a "login" action which stores the checkout location for booking in booking controller and then displays a login form for users in rendered view -
def login
my_desired_path = url_for(controller: 'bookings', action: 'checkout')
store_location_for(:user, my_desired_path)
end
Now you can use stored_location_for(:user) to retrieve my_desired_path from your session. So to say, a call to stored_location_for(:user) will return "my_desired_path.
Now if you use stored_location_for in your custom after_sign_in_path_for(:user) then it shall return "my_desired_path".
Additional Point -
A call to stored_location_for(:user) returns session[:user_return_to] but also clears this session after returning the value if your redirect format is navigational format. So a second call to stored_location_for(:user) will not return my_desired_path. And sometimes this is how we want our application to behave. On contrary, if your redirect format is not navigational format then session wont be cleared and a second sign-in will again redirect to same "my_desired_path".
Sometimes you want to redirect to different locations in signing-in from different pages in your application. Suppose, you want to redirect to "\X" on page A and to "\Y" on page B. Now follow these steps -
User in on page A - store_location_for(:user, "\X") is saved in session.
Application provides a sign-in form but User does not sign-in and just browse here and there in your application.
User is on page B and perform a sign-in expecting to land on "\Y" but unexpectedly lands to "\X".
So take care of it in your application. This scenario is more likely to occur in applications which uses AJAX for signing-in.

Best practice with custom restful routes

In my web-app I have a typical User-Activation process by e-Mail.
At the moment I have a custom controller action for each step in the activation process, i.e. I've got an action "activate" that more or less simply renders the "register"-page, then I have another action "activation" which is the target action of the actual form on the register page.
I wonder if this is best practice? - right now, I'm on the verge of implementing admin-initiated password resets (admin clicks a link for a password reset on a specific user, the user gets an email with a link to a page where he can set a new password). I would go further, and add 3 more controller actions (one for sending the reset-email, one for the user accessing the reset-page, and one for the action of the actual reset-form).
This seems to clutter up my controller quite a bit and I wonder if this was the "right" way to do it?
thx for any advice
What you are doing is fine - it is not necessary to conform your controller actions to the RESTful actions. REST should only be used if it fits your model, but you shouldn't try to make your model conform to a RESTful architecture unless it makes the interaction easier and intuitive.
It might be best to place these custom actions into their own controllers though, rather than the UsersController. Once you feel that your controller is getting too crowded, it's probably a good idea to move some actions into separate modules or controllers of their own.
class RegistrationController
def activate # perform the activation
..
end
def activation # show the activation page
...
end
end
class PasswordController
def send # send the email
end
def resetter # show the page to reset the password
end
def reset # actually reset the password
end
end
These controller actions could be accessed via custom routes instead of resources.
match 'register/activation' => 'registration#activation'
post 'register/activate' => 'registration#activate'
post 'password/send' => 'password#send'
match 'password/resetter' => 'password#resetter'
post 'password/reset' => 'password#reset'

Preventing discoverability in a RESTfully routed model

I have a model in my database whose 'show' action is open to viewing at URLs like:
mysite.com/project/12
mysite.com/project/14
The way my system is set up, there are a couple of defined methods through which these should be accessible:
A custom route I've set up is accessible to any visitor (registered or unregistered) who has this route. As an example, this custom route might be mysite.com/companyname/projectid, which the company might pass out itself to certain people it wants to have access. Note that this custom route runs a separate controller action, which sets some internal analytics then redirects to the show action.
Direct access when linked to by a registered user's home page.
I want to restrict the ability to start with mysite.com/project/14 then simply change the IDs, thereby seeing any project. How can I do this?
Clarification
My goal with this question is not just to obfuscate record IDs to make discovering certain records harder. Instead, I would like there to be only two allowable means of accessing project/12:
A user clicks on a link we provide on their home page (how can I ensure this link alone reaches project 12?)
A user or simple visitor is redirected here by another (specific) controller action.
Typing in project/12 directly should not be possible. At the moment, I imagine the best way to do this would be for the two methods above to pass a code that gets picked up by the project#show action. I just don't know how to implement this and if there are potential drawbacks.
Whatever you come up with - it is going to end up being security through obscurity due to this simple requirement:
A user clicks on a link we provide on
their home page (how can I ensure this
link alone reaches project 12?)
What you can do, however, is make it difficult to just straight-up guess the correct URL for the project.
My thought would be to give every Project a unique 'token' - If you are not logged in as the owner of the project, then you must use the token to access it.
For instance, in your project model you could have this:
class Project
before_create :set_public_token
protected
def set_public_token
# Randomizes a 20-digit long hex code
self.token = ActiveSupport::SecureRandom.hex(20)
end
end
Then, in your project's show action you would need to have this:
class ProjectsController < ApplicationController
def show
#project = Project.find(params[:id])
# Obviously you would changed signed_in? to whatever method
# you have that verifies someone is logged in
if !signed_in? || #project.owner_id != current_user.id
raise "Unauthorized Access" if #project.token != params[:token]
end
end
end
Then the owner of the project can share the 'public' link of their project to people they want to have access to it, which would look something like this:
www.example.com/projects/14?token=3jks83kasdkt84h6cd86
Again, anyone with that url could access the project, and I don't think you will be able to sanely get away from that - but it makes it a lot more difficult to do so.
This is the same concept many password reset functions work. Anyone with access to the password reset token could reset your password after you've requested a password. But knowing what token to use will take you ages (Make the token longer to make it harder to bruteforce).
That personally is how I would handle it, and how I've seen this sort of thing handled in the past (photobucket, private gists on github, etc)
The easiest way is to associate a project with a user or account, then require authentication when browsing your non public routes. If you setup an association, you can then do:
#user = current_user
#project = #user.projects.find(params[:id])
This will ensure that a given user can only find projects they 'own'.
If you don't want authorization, and just want obfuscation, you won't be able to use the 'id' alone in the route (as it is sequential). You could either pair the 'id' with a random key stored in the model (/projects/1?key=1234) or use a GUID instead of an id.
OK so another attempt now that I sort of understand.
First in your public controller action you want to do something like this:
def public_redirect
session[:authorized_for] = params[:id]
redirect_to resource_show_path(params[:id])
end
Now in your private controller:
def show
#resource = current_user.resources.find params[:id]
if #resource # authorized
respond_with #resource # ok
elsif session[:authorized_for] == params[:id] #redirected from public route
#resource = Resource.find params[:id]
respond_with #resource # ok
else
raise NotAuthorizedException # not ok, do something
end
end
This relies on sessions. This is certainly hackable, but it would be much harder then figuring out the public route. See http://guides.rubyonrails.org/security.html#sessions.
You can reuse the session technique for other similar needs (like for links from home pages where you can't verify the user from the controller, etc.
I have a project that has a similar requirement. Now first I feel the need to say that this is security by obscurity - and thus not much security at all. But for some apps that can be OK.
I have a on create callback on my model that generates a random string (or number) that I use as my ID - thus it is impossible hard to guess another resource's path.

Resources