Additional set of auth views with Rails/Devise - ruby-on-rails

I have a Ruby on Rails app that uses Devise (and Omniauth) for authentication.
I'm trying to integrate with an iOS app (out of my control) that wants to embed pages from my app. This app needs my pages to have a specific visual appearance, so I want to create an additional set of authentication views.
After digging around in the Devise docs, I've gathered that maybe I need to create a new devise_scope block in routes.rb:
devise_scope :user do
get "iosapp/users/sign_in" => "devise/sessions#iosapp_new"
post "iosapp/users/sign_in" => "devise/sessions#iosapp_create"
delete "iosapp/users/sign_out" => "devise/sessions#iosapp_destroy"
end
And I created a new set of views that correspond to those routes:
app/views/devise/sessions/iosapp_new.html.rb
app/views/devise/sessions/iosapp_create.html.rb
app/views/devise/sessions/iosapp_destroy.html.rb
But loading /iosapp/users/sign_in in the browser leads to a Rails error:
undefined method `errors' for nil:NilClass
That error stems from line 9 of devise_helper.rb (https://github.com/plataformatec/devise/blob/master/app/helpers/devise_helper.rb):
module DeviseHelper
# A simple way to show error messages for the current devise resource. If you need
# to customize this method, you can either overwrite it in your application helpers or
# copy the views to your application.
#
# This method is intended to stay simple and it is unlikely that we are going to change
# it to add more behavior or options.
def devise_error_messages!
return "" if resource.errors.empty?
messages = resource.errors.full_messages.map { |msg| content_tag(:li, msg) }.join
...
I'm obviously doing something wrong here, but can't figure out why resource is undefined when called from my "alternate" views. It seems as if I may need to create additional controller methods as well, but I can't find anything in the docs about this.
Am I way off track? Or is there a better way to accomplish my goal than this?

Your new views need form_for resource - do you have that in place? It can't find the errors on the resource if there is no resource, hence the error on nil.

Related

Accessing Doorkeeper Application Information before login redirect

Explanation
I am wanting to halt the authorization process of a client app (running OAuth2) coming to the parent app (running Doorkeeper) in order to see which client app is requesting a login. That way I can then look up the clientID and dynamically build a custom login screen for the client app. Right now, my client goes to parent, AuthorizationController is called, but before new is called and I can get the params[:client_id], authenticate_resource_owner! is called with a before_action. That then sends the user to the login page if they are not already logged in with the parent. So, before I can get the param, it is being redirected.
Question
The authenticate_resource_owner! is held in a Doorkeeper helper file. I thought that I set it up correctly to bypass the default helper and go to mine where I can try and grab the param and save in sessions before the redirect, but I guess my route is not set up correctly and I can't find any documentation on how to correctly call it. Can anyone help?
Code
Code for setting up the client:
def setup_client
#client = Application.find_by(uid: params[:client_id])
session[:client_name] = #client.name
authenticate_resource_owner!
end
I know that the first 2 lines work as I placed them in the CustomAuthorizationsController with a byebug and it triggered after the login and before redirect back to client and showed the client name stored in a session variable.
In my config/routes.rb
use_doorkeeper do
controllers :applications => 'doorkeeper/custom_applications'
controllers :authorizations => 'doorkeeper/custom_authorizations'
helpers :doorkeeper => 'doorkeeper/doorkeeper'
end
Helper file is located in app/helpers/doorkeeper/doorkeeper_helper.rb
Error
When I start up my server I get:
: from ~/ruby-2.5.0/gems/doorkeeper-5.0.2/lib/doorkeeper/rails/routes/mapper.rb:12:in `instance_eval'
~/settingsParentApp/config/routes.rb:65:in `block (2 levels) in <top (required)>': undefined method `helpers' for #<Doorkeeper::Rails::Routes::Mapper:0x00007ffd539b9c10> (NoMethodError)
Conclusion
Am I even doing this right? Is there a simpler way built into Doorkeeper that I am not seeing to get this information to customize the login screen? Or is there some error that I am not seeing in how I am calling the helper file?
After thinking through my problem in order to ask this question, a solution dawned on me. I tested it out and it worked. I forgot that in a controller, the before_action statements are called in the order they are presented. So, my solution was just to reorder my statements to call the setup_client first before the authenticate_resource_owner!. This set up the session variable before redirecting to the login screen and then allowed me to have the variable available for use.
Code
Within my config/routes.rb file:
use_doorkeeper do
controllers :applications => 'doorkeeper/custom_applications'
controllers :authorizations => 'doorkeeper/custom_authorizations'
end
This custom route bypasses the doorkeeper default authorization controller and goes to a custom one which inherits from the default controller. So, all I need within this custom one is this code:
Found: app/controllers/doorkeeper/custom_authorizations_controller.rb
module Doorkeeper
class CustomAuthorizationsController < Doorkeeper::AuthorizationsController
before_action :setup_client
before_action :authenticate_resource_owner!
def setup_client
#client = Application.find_by(uid: params[:client_id])
session[:client_name] = #client.name
end
end
end
This code is then run before it looks to the Doorkeeper's default AuthorizationsController and thus calls setup_client first. The session variable is then saved and in the login screen I can call it this way:
<%
if session[:client_name].nil?
#client_name = ''
else
#client_name = ' for ' + session[:client_name]
end
#page_name = "Login" + #client_name
%>
And then in header of the page I call this within the HTML:
<h1><%= #page_name %></h1>
I may do more fancy things later, like saving client icons/logos and color schemes to make branding specific on the login page, but for now, this basic issue has been resolved. Thank you all for acting as my sounding board and problem-solving ducks... if you know of that reference. :-) Happy Coding!

What else to namespace when name spacing controllers?

I plan to make an app with different subdomains for different user types. I will namespace controllers for each of my user types. Now I am thinking what else I need to namespace to make it work right?
If I namespace controllers like this:
app/controllers/student/users_controller.erb
app/controllers/student/...
app/controllers/student/...
Then I guess I also need to namespace Views like this:
app/views/student/homeworks/index.html.erb
app/views/student/homeworks/...
app/views/student/homeworks/...
Should I also namespace helpers and my SessionController where I handle user logins? Also, I don't think I should namespace ApplicationController so how should I handle that problem?
Thanks!
Without seeing the rest of your code it is difficult to know exactly what you are trying to achieve but there is a great Railscasts episode, on sub-domains in Rails, that may help you.
Namespacing your code is not essential to get your code working, although it will help you keep your code organized. Below are some additional things to think about, when working with sub-domains:
Routes
...
match '/' => 'users#show', :contraints => { :subdomain => /.+/ }
...
This example uses a constraint to route all subdomains to the users#show action. You can use this technique to differentiate between subdomains, routing them to appropriate controller actions.
Controller
Once you have set up your routes file to route subdomains correctly, you can retrieve the subdomain in any controller via the request:
def show
#subdomain = request.subdomain
end
This will allow you to add sub-domain specific logic to your application.
View
Linking to views with other subdomains is straight forward, simply pass in the subdomain option to your url method:
root_url(subdomain: 'student')
The easiest way is to use genarators
rails g controller 'student/users' new create etc.
or
rails g controller student::users
When you want to add another controller:
rails g controller student::classes
This creates automatically the necessary structure for the controller and the views.
Then add in your routes:
namespace :student do
resources :users
resources :classes
end
You can use route helpers like new_student_user_path
With a non-namespaced controller you'd typically type form_for #user to create a user, with a namespaced:
<%= form_for [:student, #user] do |f| %>
...
Btw if the difference between the users is just their abilities it's better to use a gem like cancan or declarative_authorization to manage authorization.
If you store different information for each type you could make a single user model with a polymorphic relationship to different profiles.
More info:
How to model different users in Rails
Rails App with 3 different types of Users
......................................
Edit
You can set the layout from application_controller. I guess upon signing in you could store the layout in the cookies or the session hash: session[:layout] = 'student'
layout :set_layout
def set_layout
session[:layout] || 'application'
end
Or if you have a current_user method which gets the user you could check its class and choose the layout.
I strongly advise you to reconsider splitting the user class. Different user models will lead to repeated logic and complicated authentication.

understanding passing rails parameters between controllers

I am just starting to wrap my head around parameters in rails. I am currently working on a project that isn't accessible to the public, so keeping params secure isn't exactly a priority in this case.
I have a link_to to a different controller action that requires an object id to fulfil the controller action.
=link_to "Barcode", print_barcode_label_admin_items_path(:item_to_print => { :article_id => article.id })
Then in the relevant controller
def print_barcode_label
if params[:item_to_print][:article_id].present?
return if force_format :pdf
..........
private
def params_document
params.require(:document).permit!
end
As I was writing the code for this controller I am certain the parameters were being passed (I am using the better-errors gem to debug along the way so I could see them being passed in the request parameters hash). But now, not sure what I have done, but i get the error
undefined method `[]' for nil:NilClass
failing at line two in my above controller action. I am sure there is something really basic I am missing. What is it? Is there a more favourable way of doing this?
Update
So I started playing with other possible solutions, and one is naming a route that specifically carries the parameter
get 'print_barcode_label/:article_id', to: 'documents#print_barcode_label', as: 'print_barcode_label'
This seems a more robust and sensible approach. Howeever, despite passing the variable in the link, like this
=link_to "Barcode", print_barcode_label_admin_items_path(article.id)
Gives a no route matches error
No route matches {:action=>"print_barcode_label", :controller=>"admin/documents"} missing required keys: [:article_id]
It is hard to answer this question without seeing more code with some context. But if you want to do rails way you should propably create custom action on document resource.
In your routes.rb:
namespace :admin do
resources :documents
get :print_barcode_label, :on => :member
end
end
And then you can create link to this action:
= link_to 'Barcode', print_barcode_label_admin_document_path(article)

Is there a more elegant way? (accessing a rails controller from a static home page?)

OK, this is working but I feel there is a better way to do this in Rails... I have a home page which, if you have not signed in, is not currently pulling in anything from any model or controller. It exists at /pages/home.html.erb
On that page, I want to grab the next party from my Parties model and tell the website visitor about that party. Easy enough, right?:
/app/controllers/parties_controller.rb
def nextparty
#party = Party.find(:first, :order => "begins_on")
end
Now, in my home page, I used this and it works fine:
/app/views/pages/home.html.erb
<% #PartyCont = PartiesController.new() %>
<% #party = #PartyCont.nextparty() %>
<h3>The next party is <%= #party.name %></h3>
I tried helper methods, partials, ApplicationHelper, but this was the only code that actually worked. Most of the other things I tried seemed to fail because the #Party class was not instantiated (typically the error indicated the class with a temporary name and "undefined method").
Hey, I'm happy that it works, but I feel like there is a better way in Rails. I've seen a few posts that use code like the above example and then say "But you really shouldn't ever need to do this!".
Is this just fine, or is there a more Rails-like way?
UPDATE:
I think the problem is more than just elegance... I just realized that all RSPEC tests that hit the home page are failing with:
Failure/Error: get 'home'
ActionView::Template::Error:
undefined method `begins_on' for nil:NilClass
Thanks!
You want a controller behind every view and you don't want views crossing controller boundaries in order to present information. Consider having a welcome controller (or whatever you prefer to call it). It can have an index action:
def index
#party = Party.find(:first, :order => "begins_on")
end
In config/routes.rb, make it the root controller action:
root :to => "welcome#index"
Also, to DRY that up add a .nextparty class method to the Party model and call that from both of your controller actions instead of the find method.
Your view should only show data that already was made available by your controller. You want to display a party resource, so the request should go to the parties controller. If I understand your use case correctly, than more specifically to the index method on the PartiesController.
There you should have the following code:
def index
#party = Party.find(:first, :order => "begins_on")
end
That instance method will be available in your corresponding view app/views/parties/index.html.erb
<h3>The next party is <%= #party.name %></h3>
To make this available as your homepage you will have to adjust your route as well:
config/routes.rb
root :to => "parties#index"
Your view should contain as little logic as possible and mainly be concerned with how things look.
Your controller should get data for the view ready and make sure to call the right method on the model.
All the heavy business logic should be in your model.
I think you should work through a basic introductory Rails tutorial.

Devise. Registration and Login at the same page

I'm trying to integrate Devise into my application. I need implement login form at top of the page (I've implemented this form into layout page) and I've implemented registration which contains registration form.
But it shows validation errors for both form when I tried submit incorrect registration data.
Without more information, it's hard to guess what the problem is. I've found the Wiki pages to be really helpful (and increasingly so), though you may have already looked them over:
Devise Wiki Pages
Two pages that might be relevant to your needs:
Display a custom sign_in form anywhere in your app
Create custom layouts
Hope this helps!
-- ff
The problem of seeing the validation errors for both forms stems from the 2 things. First, devise forms use a generic 'resource' helper. This creates a User object, and that same user objet gets used for both the sign up and the sign in form. Second, devise errors are typically displayed using the 'devise_error_messages!' helper which uses that same shared resource.
To have sign in and sign up on the same page you need to create different user objects for each form, and a new way of displaying the error messages.
First off, you'll need to create your own registration controller (in app/controllers/users/)
class Users::RegistrationsController < Devise::RegistrationsController
include DevisePermittedParameters
protected
def build_resource(hash=nil)
super
# Create an instance var to use just for the sign up form
#sign_up_user = self.resource
end
end
And update your routes file accordingly
devise_for :users, controllers: {
registrations: 'users/registrations'
}
Next you'll need your own error messages and resource helpers. Create a new helper like devise_single_page_helper.rb and add the following:
module DeviseSinglePageHelper
def devise_error_messages_for_same_page(given_resource)
return "" if given_resource.errors.empty?
messages = given_resource.errors.full_messages.map { |msg| content_tag(:li, msg) }.join
sentence = I18n.t("errors.messages.not_saved",
count: given_resource.errors.count,
resource: given_resource.class.model_name.human.downcase)
html = <<-HTML
<div id="error_explanation">
<h2>#{sentence}</h2>
<ul>#{messages}</ul>
</div>
HTML
html.html_safe
end
def sign_up_user
#sign_up_user ||= User.new(username: 'su')
end
def sign_in_user
#sign_in_user ||= User.new(username: 'si')
end
end
Finally, in your views, update your forms like so:
-# The sign up form
= simple_form_for(sign_up_user, url: registration_path(resource_name)) do |f|
-#...
= devise_error_messages_for_same_page(sign_up_user)
-# The sign in form
= simple_form_for(sign_in_user, url: sessions_path(resource_name)) do |f|
#...
= devise_error_messages_for_same_page(sign_in_user)
All of this together gives you 2 different objects - 1 for sign up and 1 for sign in. This will prevent the error messages from one showing in the other. Please note that recommend putting both forms on your sign in page (and perhaps having the default sign up page redirect to the sign in page) because by default a failed sign in attempt will redirect to the sign in page.
You should have two forms on the page — one for signing up and one for registering. If you want a single form and multiple potential actions you are going to need a couple buttons that get handled client side and change the form's action & method to the appropriate route depending you want to create a user or a session.
If you think you did this already, the problem almost certainly lies in your code. If you were to share it with us we could perhaps point out something you may have missed.

Resources