I'm working through the Railscast on implementing Devise and OmniAuth (along with the Devise documentation) -- currently, I've got a site going where visitors can sign up using their facebook accounts or by filling out a form.
I'm running into trouble when users that sign up via OmniAuth try to edit their profiles, though. Devise looks for the user's current password when they submit changes to their profiles, but those that logged in with facebook don't know their passwords (they're set automatically in the user model):
def self.find_for_facebook_oauth(auth, signed_in_resource=nil)
user = User.where(:provider => auth.provider, :uid => auth.uid).first
unless user
user = User.create(first_name:auth.extra.raw_info.first_name,
last_name:auth.extra.raw_info.last_name,
provider:auth.provider,
uid:auth.uid,
email:auth.info.email,
password:Devise.friendly_token[0,20]
)
end
user
end
When a user edits his information, the app should not require password confirmation if he set up his account through OmniAuth. The tutorial suggests that the handy password_required? method will help me achieve this outcome. Specifically, adding this method to the user model means that it should only return true if the user didn't sign up through OmniAuth (the provider attribute would be nil in that case):
def password_required?
super && provider.blank?
end
Thus, a piece of code like:
<%= form_for(resource, :as => resource_name, :url => registration_path(resource_name), :html => { :method => :put }) do |f| %>
<%= devise_error_messages! %>
<%= render :partial => "essential_user_info_inputs", :locals => { :f => f } %>
<%= render :partial => "inessential_user_info_inputs", :locals => { :f => f } %>
<% if f.object.password_required? %>
<%= render :partial => "password_inputs", :locals => { :f => f } %>
<%= f.label :current_password %> <i>(we need your current password to confirm your changes)</i><br />
<%= f.password_field :current_password %>
<% end %>
<%= f.submit "Update" %>
<% end %>
would theoretically only display password inputs when needed. It also suggests that Devise has built in logic saying that OmniAuth users don't need to use passwords to edit their accounts. I have no idea if this is true, but the tutorial kind of makes it look like that. But when an OmniAuth user tries to edit his account, I get "Current password can't be blank." Same thing with non-OmniAuth users (this makes sense, since the password fields don't show up on those users' edit pages either).
Some poking around confirms that the password_required? method is returning false, both when the user signed up through OmniAuth and through the site's regular user signup. Even when I change it to simply run the superclass method, it returns false.
Any ideas of what's going on with the password_required method? I can't find anything about it anywhere, but I feel like that's what's tripping things up right now.
Update:
This is now working, but not using the method outlined in the Railscast, which relies on requires_password? method, a topic that I still know nothing about. Instead, I implemented the solution outlined here, as suggested here. So I am now only requiring passwords to update non-OmniAuth accounts with the code:
class Users::RegistrationsController < Devise::RegistrationsController
def update
#user = User.find(current_user.id)
email_changed = #user.email != params[:user][:email]
is_facebook_account = !#user.provider.blank?
successfully_updated = if !is_facebook_account
#user.update_with_password(params[:user])
else
#user.update_without_password(params[:user])
end
if successfully_updated
# Sign in the user bypassing validation in case his password changed
sign_in #user, :bypass => true
redirect_to root_path
else
render "edit"
end
end
end
The easiest way is to overwrite the update_resource method in your RegistrationsController. This is advised by devise in their own implementation of the controller:
# By default we want to require a password checks on update.
# You can overwrite this method in your own RegistrationsController.
def update_resource(resource, params)
resource.update_with_password(params)
end
So the solution is to overwrite this method in your own controller like this:
class Users::RegistrationsController < Devise::RegistrationsController
# Overwrite update_resource to let users to update their user without giving their password
def update_resource(resource, params)
if current_user.provider == "facebook"
params.delete("current_password")
resource.update_without_password(params)
else
resource.update_with_password(params)
end
end
end
I've added an update to the link below that includes my solution to the Devise/ OmniAuth change user profile/password issue and collected some helpful links:
stackoverflow - Allowing users to edit accounts without saving passwords in devise
I saw this used somewhere.
def update
params[:user].delete(:current_password)
params[:user].delete(:password)
params[:user].delete(:password_confirmation)
if current_user.update_without_password(params[:user])
redirect_to somewhere_wicked_path, notice => "You rock"
else
render 'edit', :alert => 'you roll'
end
end
use something like this in your update method in your controller. Pretty sure that method is in Devise too.
Related
I am building a digital library, and I have completed a lot of the functionalities needed. I am currently having an issue with integrating the digital library with a Learning Management System (LMS).
I already have an admin authentication system for the digital library using the Devise gem. My goal is to allow users who want to access the digital library to login to the digital library using their Learning Management System (LMS) credentials (username and password).
I have been provided with the Login API endpoint and other needed parameters of the Learning Management System (LMS), and I have created the User Model, the Sessions Controller and the Sessions View Templates.
I am currently using the RestClient Gem for the API call, but I having an error undefined local variable or method `username' for # Did you mean? user_path. I can't figure out where things went wrong.
Sessions Controller
require 'rest-client'
class SessionsController < ApplicationController
def new
end
def create
response = RestClient::Request.execute(
method: :post,
url: 'https://newapi.example.com/token',
payload: { 'username': "#{username}",
'password': "#{password}",
'grant_type':'password' },
headers: { apiCode: '93de0db8-333b-4f478-aa92-2b43cdb7aa9f' }
)
case response.code
when 400
flash.now[:alert] = 'Email or password is invalid'
render 'new'
when 200
session[:user_id] = user.id
redirect_to root_url, notice: 'Logged in!'
else
raise "Invalid response #{response.to_str} received."
end
end
def destroy
session[:user_id] = nil
redirect_to root_url, notice: 'Logged out!'
end
end
Sessions New View
<p id=”alert”><%= alert %></p>
<h1>Login</h1>
<%= form_tag sessions_path do %>
<div class="field">
<%= label_tag :username %>
<%= text_field_tag :username %>
</div>
<div class="field">
<%= label_tag :password %>
<%= password_field_tag :password %>
</div>
<div class="actions">
<%= submit_tag 'Login' %>
</div>
<% end %>
User Model
class User < ApplicationRecord
has_secure_password
validates :username, presence: true, uniqueness: true
end
Any form of help with code samples will be greatly appreciated. I am also open to providing more information about this integration if required. Thank you in advance.
I think that the problem is in fact that inside your SessionsController in create action, you are interpolating username and password. There's no definition for these methods in your code so you get undefined local variable or method.
You could probably pick those from params like this:
def username
params[:username]
end
def password
params[:password]
end
Or interpolate them directly in payload replacing current method calls with params[:username] and params[:password].
In such situations, it is good to use byebug or pry to debug your code and see what's happening inside your controller.
You could also think of closing some parts of your logic in Service objects - you shouldn't have more 10-15 lines in your controller action (unless the situation requires it)
Maybe you should use params[:username] rather than only username ?
username and password in payload are undefined variables. Please set their values. Possible values could be params[:username] and params[:password]
I am trying to make an app with Rails 4. I use, Devise, Rolify and Simple Form.
My current problem is in trying to assign rolify roles to users.
I have the following code:
User.rb
def self.find_for_oauth(auth, signed_in_resource = nil)
# Get the identity and user if they exist
identity = Identity.find_for_oauth(auth)
# If a signed_in_resource is provided it always overrides the existing user
# to prevent the identity being locked with accidentally created accounts.
# Note that this may leave zombie accounts (with no associated identity) which
# can be cleaned up at a later date.
user = signed_in_resource ? signed_in_resource : identity.user
# p '11111'
# Create the user if needed
if user.nil?
# p 22222
# Get the existing user by email if the provider gives us a verified email.
# If no verified email was provided we assign a temporary email and ask the
# user to verify it on the next step via UsersController.finish_signup
email_is_verified = auth.info.email && (auth.info.verified || auth.info.verified_email)
email = auth.info.email
user = User.where(:email => email).first if email
# Create the user if it's a new registration
if user.nil?
# p 33333
user = User.new(
# at least one problem with this is that each provider uses different terms to desribe first name/last name/email. See notes on linkedin above
first_name: auth.info.first_name,
last_name: auth.info.last_name,
email: email,
#username: auth.info.nickname || auth.uid,
password: Devise.friendly_token[0,20])
#
# debugger
# if email_is_verified
# user.skip_confirmation!
# end
user.skip_confirmation!
user.save!
end
end
# Associate the identity with the user if needed
if identity.user != user
identity.user = user
identity.save!
end
user
end
def email_verified?
self.email && TEMP_EMAIL_REGEX =~ self.email
end
def full_name
[*first_name.capitalize, last_name.capitalize].join(" ")
end
after_create :add_default_role
def add_default_role
add_role(:pending) if self.roles.blank?
end
Role.rb
class Role < ActiveRecord::Base
has_and_belongs_to_many :users, :join_table => :users_roles
belongs_to :resource, :polymorphic => true
validates :resource_type,
:inclusion => { :in => Rolify.resource_types },
:allow_nil => true
scopify
end
Users/omniauth_callbacks_controller
def self.provides_callback_for(provider)
class_eval %Q{
def #{provider}
#user = User.find_for_oauth(env["omniauth.auth"])
if #user.persisted?
sign_in_and_redirect #user, event: :authentication
set_flash_message(:notice, :success, kind: "#{provider}".capitalize) if is_navigational_format?
else
session["devise.#{provider}_data"] = env["omniauth.auth"]
redirect_to new_user_registration_url
end
end
}
end
# sign_in_and_redirect_user(:user, event: :authentication)
[:twitter, :facebook, :linkedin, :google_oauth2].each do |provider|
provides_callback_for provider
end
def after_sign_in_path_for(resource)
if resource.email_verified?
super resource
else
finish_signup_path(resource)
end
end
Users controller
private
def set_user
#user = User.find(params[:id])
end
def user_params
accessible = [ :first_name, :last_name, :email, {role_ids: []}] # extend with your own params
accessible << [ :password, :password_confirmation ] unless params[:user][:password].blank?
accessible << [:approved] if user.admin
params.require(:user).permit(accessible)
end
Users#index
<% Users.each do |user| %>
<div class="row">
<div class="col-xs-5">
<%= link_to "#{user.full_name}", user %>
</div>
<div class="col-xs-5">
<%= link_to "#{user.email}", user %>
</div>
<div class="col-xs-2">
<%= link_to "edit", edit_user_path(user) %>
</div>
</div>
<% end %>
Users#form
<%= simple_form_for(#user) do |f| %>
<%= f.error_notification %>
<% Role.all.each do |role| %>
<div class="form-inputs">
<%= f.input "user[role_ids][]", role.id, collection: #user.role_ids.include?(role.id) %>
<%= role.name %>
</div>
<div class="form-actions">
<%= f.button :submit, "Add role", :class => 'formsubmit' %>
</div>
I have also tried:
<%= f.association :roles %>
<%= role.name %>
in the user#form
Migration to add roles to role table:
class AddRolesToRolifyTable < ActiveRecord::Migration
def change
['admin', # internal admin
'manager', # internal manager
'editor', # internal web content editor
'support', # internal user support
'educator', # lecturers
'faculty_manager', #manage the business side
'project_manager',
'pending', # new people that are not yet confirmed in a role - default role assignment
].each do |role_name|
Role.create! name: role_name
end
end
end
When I save this and try to run the local host and go to users#index, I get an error that says:
Couldn't find User with 'id'=
This method is highlighted:
private
def set_user
#user = User.find(params[:id])
end
I can't say I've properly understood how rolify works with devise. My console shows that I have two test users in the db, each of which has an id (so im not sure how to explore this error further). Does anyone see where I've gone wrong?
I have adapted this setup using advice in this post:
Defining Roles with Rolify
Rolify and Devise
I can't say I've properly understood how rolify works with devise.
Rolify and Devise do very different jobs. There is no actual integration between the two.
Devise is an authentication solution, authentication is only about determining if there is a signed in user and if that user is who he/she claims to be.
Authorization on the other hand is about who is allowed to do what in a system. Rolify is a generic roles library which is meant to be used as a smaller part of an authentication solution. Just defining your authorization with .has_role? is going to get very messy quick.
It is most often combined with something like CanCanCan or Pundit which provide facilities to define authentication rules.
https://github.com/CanCanCommunity/cancancan/wiki/Role-Based-Authorization
Collection helpers
Rails has built in helpers for creating selects and checkboxes for associations and simple_form takes it a bit further.
So lets say you have a users/:user_id/edit route where admins edit a users profile and add roles:
<%= simple_form_for(#user) do |f| %>
<fieldset>
<legend>Roles</legend>
<%= f.association :roles, as: :check_boxes, collection: Role.all %>
</fieldset>
<% f.submit %>
<% end %>
Note that you don't need a special form for editing the roles in this case - this will work both when creating and editing users.
Your params sanitation is bit off:
def user_params
accessible = [ :first_name, :last_name, :email, role_ids: []]
accessible << [ :password, :password_confirmation ] unless params[:user][:password].blank?
accessible << [:approved] if user.admin? # don't do this. There are better ways.
params.require(:user).permit(accessible)
end
Also:
Migrations are for altering the database schema, not for seeding the system with default info. Use rake db:seeds or create your own rake task instead.
I would also question if you really need a pending role and the approved param. I would simply check if the user has no roles - which defacto means that he is pending. When a admin or peer verifies a user you would add the approved role.
Currently I have ActionMailer send an email when a user registers, and I generate a random :sign_in_token with the user.
How can a user then click on the link sent to his email and update the users :registration_complete boolean value to TRUE?
Currently, I am able to send the link and generates a random token, but I don't know how to update the boolean value through the email.
MODELS
class User < ActiveRecord::Base
attr_accessible :name, :email, :password, :password_confirmation, :sign_in_token,
:registration_complete
###This generates my sign_in_token
def generate_sign_in_token
self.sign_in_token = Digest::SHA1.hexdigest([Time.now, rand].join)
end
end
CONTROLLER
def create
#user = RegularUser.new(params[:regular_user])
if #user.save
###Sends the User an email with sign_in_token
UserMailer.registration_confirmation(#user, login_url+"/#{#user.sign_in_token}").deliver
flash[:success] = "Please Check Your Email to Verify your Registration!"
redirect_to (verifyemail_path)
else
render 'new'
end
end
USER_MAILER
def registration_confirmation(user, login_url)
#login_url = login_url
#user = user
mail(:to => "#{user.name} <#{user.email}>", :subject => "Welcome to APP")
end
VIEWS
###Redirects User to Login Page, But how do i connect it to my activate method?
<%= link_to "Complete Registration", #login_url %>
ROUTES
match '/login/:sign_in_token', :to => 'sessions#new'
When they click a link, it takes them to a controller with an action of set_complete using a GET request, which sets the boolean value.
Something like:
def set_complete
user = User.find(params[:user])
user.update_attribute(registration_complete => true)
redirect_to login_path # or whatever your login url is...
end
For the controller action and something like this for the link:
<a href="example.com/registrations/set_complete?user=1" />
Here is a sample of what might go in the routes file:
get "/users/set_complete", :to => "users#set_complete"
You'd probably need to set the user id to whatever you want using erb, andmake a few other app-specific customizations, but this is the general idea.
Hope this helps!
Say I have a site like this (generic Q&A site) in Rails and I wanted this "ask" page w/ a text box to be the first page a user sees, even if he's not logged in. He enters a question, and on the 'new' method I check that he's not logged in, and bounced him to /session/new, where he can either log in or create a new account. Question is, how do I (and what is the best way to) preserve that question that he initially asked all through this process?
I'm understanding the flow of action described in the question to be
user is presented with a form
user is redirected to log in page on submit
user is redirected back to form on successful log in
repopulate form on load (Question asks how to do this step)
user finally submits their form.
With steps 2-4 omitted if the user is logged in.
I'm sorry, but I see your question more as a symptom of an underlying UI issue than a rails question.
If only logged in users can post questions, then why display the text box?
If a user is going to have log in any way, why not get that out of the way first. An even better solution is to integrate the log in and form.
Something like this in the view:
<% form_for :question do |form| %>
<% unless logged_in? %>
<% fields_for :session do |session_form|%>
<%= session_form.label :login %>
<%= session_form.text_field :login %>
<%= session_form.label :password %>
<%= session_form.password_field :password %>
<%end%>
<%end%>
<%= form.text_area :question %>
<%end%>
And in the controller
def new
...
unless params[:session].nil?
self.current_user = User.authenticate(params[:session][:login], params[:session][:password])
end
if logged_in?
flash[:notice] = "Logged in successfully"
else
flash[:error] = "Incorrect username and or password."
end
if logged_in? && #question.save
.... process successful entry
else
... process unsuccessful entry
end
end
Edit: Mohamad's raises the question of reusing this pattern across multiple controllers and forms. So the answer was updated to address reuse of this pattern.
To simplify this for reuse, you could put this block in a helper function that is referenced in the before_filter for actions that require it.
def login
unless params[:session].nil?
self.current_user = User.authenticate(params[:session][:login], params[:session][:password])
if logged_in?
flash[:notice] = "Logged in successfully"
else
flash[:error] = "Incorrect username and or password."
end
end
end
as in:
before_filter :login => :only [:new , :edit, :update, :delete]
On the view side, it shouldn't be too hard to construct a new variant of form_for that embeds the session parameters. Maybe form_for_with_session?
As for handling an unsuccessful response, I would suggest helper function that takes a block of code. Sorry I don't have time to write out or test one for you.
You keep it in the session. So after logging in, when the user goes back to asking his question, you see there's already something in session.
And you can directly display it.
def create
if current_user # Implement this method in your auth framework
#question = Question.new(params[:question] || session.delete[:question])
# (the usual stuff you'd do to save)
else
session[:question] = params[:question]
redirect_to :controller => :sessions, :action => "new"
end
end
Then, after your user creation and authentication stuff is all done in your login action, just make sure you POST back to this create action if session[:question] is defined.
One thing you can do on my rap lyric explanation site is "like" explanations (once you're logged in):
http://dl.getdropbox.com/u/2792776/screenshots/2010-01-17_1645.png
I'd like to show the "Like" links to users who aren't logged in, and then, when a non-logged in user clicks "Like", show him a lightbox with a "Login or Register" form (like Digg / Reddit)
http://dl.getdropbox.com/u/2792776/screenshots/2010-01-17_1650.png
What's the best way to accomplish this?
Currently I'm using this approach:
Clicking "Like" POSTs to /annotations/:id/vote (the POST body indicates whether the user is liking or "unliking").
The vote Annotation controller action has a require_user before_filter that looks like this:
def require_user
unless current_user
store_desired_location
flash[:notice] = "You'll need to login or register to do that"
redirect_to login_path # map.login '/login', :controller => 'user_sessions', :action => 'new'
return false
end
end
user_sessions#new looks like this:
def new
#user_session = UserSession.new
respond_to do |format|
format.html {}
format.js {
render :layout => false
}
end
end
The problem is that the redirect doesn't seem to work correctly over javascript:
http://dl.getdropbox.com/u/2792776/screenshots/2010-01-17_1700.png
How do I get this to redirect correctly?
Also, is this the right general approach? Another thought I had was to attach a different handler to the "Like" links in javascript when there was no logged in user (but I don't think this method scales well to other actions that I'd like to handle the same way)
There's a few problems to overcome here.
Browsers in general do not allow redirecting to a POST request.
redirect_to doesn't preserve format without additional input.
Store location does not preserve form data.
All these problems can be solved by eliminating redirects.
Here is how I've handed it in the past:
Instead of redirecting in required_user, render. If a before filter redirects or renders the pending action is cancelled. (No need to return false either). Unfortunately going this route blurs controller boundaries. But allows for simple html fallback, and lends its self to DRYness.
The high level view of the new work flow will be:
Request to annotations#vote (POST)
required_user filter fails
render new session
submit login information and original POST data back to annotations#vote (POST)
new filter in vote captures session information and logs in. vote proceeds as expected. If login fails return to 3.
annotations#vote redirects/renders as it should
Start by reworking the require_user to render the user_sessions#new template.
def require_user
unless current_user
flash[:notice] = "You'll need to login or register to do that"
#user_session ||= UserSession.new
respond_to do |format|
format.html {render :template => 'user_sessions/new'}
format.js {
render :template => 'user_sessions/new', :layout => false
}
end
end
end
The #user_session ||= UserSession.new ensures we can return validation errors to the form.
Now we've got to beef up your user_session#new template so that it can remember the action. Also if you plan on using lightboxes, this should be a partial rendered rendered by relevant RJS or the new.html.erb.
First we create a partial to create hidden fields preserving the POST data that would have been lost in a redirect:
<% if params[:controller] == "annotations" %>
<% content_for :old_form do %>
<%= hidden_field_tag "annotation[song_id]", params[:annotation][:song_id] %>
<%= hidden_field_tag "annotation[vote]", params[:annotation][:vote] %>
<% end %>
<% end %>
Then render that partial in the login partial that will occupy your lightbox:
<%= render :partial => vote_form_replica %>
<% url = params[:controller] == "user_sessions ? user_sessions_url : {} %>
<% form_tag #user_session, :url => url do |f| %>
<%= yield :old_form %>
<%= f.label :user_name %>
<%= f.text_field :user_name %>
<%= f.label :password %>
<%= f.password_field :password %>
<%= submit_tag %>
<%end%>
The empty hash for url in the form_tag looks like an error, but isn't. It ensures that the form data is posted to the url that rendered the form. Which at this point should be annotations/:id/vote
Now for the new filter to login. Essentially it will be doing what ever UserSessionsController#create does without the render/redirect. The following is copied from the RESTful authentication plugin.
def authenticate
self.current_user = User.authenticate(params[:login], params[:password])
if logged_in?
if params[:remember_me] == "1"
current_user.remember_me unless current_user.remember_token?
cookies[:auth_token] = { :value => self.current_user.remember_token,
:expires => self.current_user.remember_token_expires_at }
end
end
end
All that's left is to make sure the filter order is right.
before_filter :authenticate, :require_user, :only => :vote
N.B.: You're probably not going to use this version of require_user without this version of authenticate so it makes sense to combine them into a single filter.
And that's it. The way this has been set up allows for robust DRY easily reuseable code. By placing the new filters into ApplicationController they're available in any controller. From this point, adding this functionality to any other controllers/actions takes only 3 simple steps:
Create a new partial modelled after the vote_form_replica partial.
Add the corresponding render statement to the new session template.
Apply the filters to your actions.
I would approach this in the way you describe at the bottom of your question. Before displaying the page initially, check if the user is logged in. If they are, the "Like" links should use their normal behavior. If not, bind a click event to show the register/login panel. There's nothing about this that can't be reused. In fact, we use this exact method at my job. Any user action that requires authentication either follows its normal behavior or pops up a generic login panel depending on login state at the time the page loads.