I'm working through Rails Tutorial and am stuck in Ch 8 -- the bit about login sessions. The error appears when I submit an email/pw combination that should create a new session.
Here's the error:
NoMethodError in SessionsController#create
undefined method `remember_token=' for #<User:0x0000010b454f50>
Extracted source (around line #435):
else
match = match_attribute_method?(method.to_s)
match ? attribute_missing(match, *args, &block) : super
end
end
From there I look under create in the sessions controller. And I try to see where I call the remember_token method.
Sessions controller:
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
log_in user
#if 1 then remember, else forget
params[:session][:remember_me] == '1' ? remember(user) : forget(user)
remember user
redirect_to user
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
def destroy
#call log_out helper method
log_out if logged_in?
redirect_to root_url
end
end
I call remember which looks important. Here's the session helper:
module SessionsHelper
#Logs in the given user.
def log_in(user)
session[:user_id] = user.id
end
def remember(user)
#generate a new token and provide the encrypted hash
user.remember
cookies.permanent.signed[:user_id]=user.id
cookies.permanent[:remember_token]=user.remember_token
end
def current_user
#if session user id exists, then set to user_id and then...
if (user_id = session[:user_id])
#set #current_user to the correct user account
#current_user||= User.find_by(id: user_id)
#else, if signed cookie user id exists, set to user_id and then...
elsif (user_id = cookies.signed[:user_id])
#set user to the correct user account and...
user = User.find_by(id: user_id)
#if user is authenticated, then log them in and set #current_user to user
if user && user.authenticated?(cookies[:remember_token])
log_in user
#current_user = user
end
end
end
def logged_in?
!current_user.nil?
end
#Forgets a persistent session
def forget(user)
user.forget
cookies.delete(:user_id)
cookies.delete(:remember_token)
end
def log_out
forget(current_user)
session.delete(:user_id)
#current_user = nil
end
end
That looks like the key -- cookies.permanent[:remember_token]=user.remember_token.
But I don't know how to define the remember_token method. When I go through the tutorial I don't see any methods that describe remember_token. The closest I get is in the user model (method called remember):
class User < ActiveRecord::Base
before_save { self.email = email.downcase }
belongs_to :courses
belongs_to :lessons
validates :first_name, presence: true, length: { maximum: 50 }
validates :last_name, presence: true, length: { maximum: 50 }
validates :email, presence: true, length: { maximum: 255 },
uniqueness: { case_sensitive: false }
has_secure_password
validates :password, length: { minimum: 6}
#Returns the hash digest of the given string
def User.digest(string)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
BCrypt::Password.create(string, cost: cost)
BCrypt::Password.create(string, cost: cost)
end
#Returns a random token
def User.new_token
SecureRandom.urlsafe_base64
end
#creates a new token (random string) then encypts it by returning a hash
def remember
self.remember_token = User.new_token
update_attribute(:remember_digest, User.digest(remember_token))
end
#Returns true if the given token matches the digest
def authenticated?(remember_token)
return false if remember_digest.nil?
BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
#forgets a user
def forget
update_attribute(:remember_digest, nil)
end
end
Am I understanding the interplay between the session controller, session helper and the user model? Is the interplay not setup right - and that's throwing the error? I don't understand why it doesn't work...
Background:
The tutorial intentionally avoids putting the remember_token in the database, as a security feature. So that isn't the problem.
The problem was in the sessions controller -- I didn't understand the ternary operator and had repeated a bit of its functionality. Here's the original Create method:
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
log_in user
#if 1 then remember, else forget
params[:session][:remember_me] == '1' ? remember(user) : forget(user)
remember user
redirect_to user
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
This bit remember user is redundant.
The ternary params[:session][:remember_me] == '1' ? remember(user) : forget(user) means if params[:session][:remember_me] is 1, then remember(user) else forget(user).
By then calling remember user (which is the same as remember(user)) I was executing the same things twice, causing the error.
Related
I am trying to seed my production database with a record for the table Users. Here is the model:
class User < ActiveRecord::Base
include Person
include Contact
has_many :coworkers, :class_name => 'Coworker'
has_many :customers, :class_name => 'Customer'
has_many :locations, :class_name => 'Location'
has_many :appointment_types, :class_name => 'AppointmentType'
before_save { self.email = email.downcase }
#before_create :create_remember_token
# password
has_secure_password
attr_accessor :remember_token, :activation_token, :reset_token
validates :password, length: { minimum: 6 }, on: :create
# rem_notice_hrs
validates :rem_notice_hrs, presence: true
validates :rem_notice_hrs, numericality: true
# rem_text
validates :rem_text, presence: true
# mandatory email:
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX }
after_initialize :init
#def create_remember_token
# self.remember_token = Account.digest(Account.new_remember_token)
#end
# Returns the hash digest of the given string.
def User.digest(string)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
BCrypt::Engine.cost
BCrypt::Password.create(string, cost: cost)
end
# Returns a random token.
def User.new_token
SecureRandom.urlsafe_base64
end
# Remembers a user in the database for use in persistent sessions.
def remember
self.remember_token = User.new_token
update_attribute(:remember_digest, User.digest(remember_token))
end
# Returns true if the given token matches the digest.
def authenticated?(attribute, token)
digest = send("#{attribute}_digest")
return false if digest.nil?
BCrypt::Password.new(digest).is_password?(token)
end
# Forgets a user.
def forget
update_attribute(:remember_digest, nil)
end
# Sets the password reset attributes.
def create_reset_digest
self.reset_token = User.new_token
update_attribute(:reset_digest, User.digest(reset_token))
update_attribute(:reset_sent_at, Time.zone.now)
end
# Sends password reset email.
def send_password_reset_email
UserMailer.password_reset(self).deliver_now
end
# Returns true if a password reset has expired.
def password_reset_expired?
reset_sent_at < 2.hours.ago
end
private
def init
if self.new_record?
if self.rem_notice_hrs.nil?
self.rem_notice_hrs = 24
end
if self.rem_text.nil?
if self.company.nil?
self.rem_text = "Dear [customer title: automatic] [customer family name: automatic], this is a reminder of your appointment with %{title} %{family_name} on [date/time]."
else
self.rem_text = "Dear [title] [customer family name], this is a reminder of your appointment with %{company} on [date/time]."
end
end
if self.start_day.nil?
self.start_day = Time.now
end
end
end
end
And here is the seed:
User.create!(first_name: "Nick",
email: "user#example.com",
password: "user12",
password_confirmation: "user12",
phone: 7933336337,
rem_notice_hrs: 24,
rem_text: "This is a reminder",
start_day: "2014-11-21 21:50:16",
)
I am running the seed task via capistrano and I am getting:
ActiveRecord::RecordInvalid: Validation failed: Password is too short (minimum is 6 characters)
/home/deploy/appmate/shared/bundle/ruby/2.1.0/gems/activerecord-4.2.0/lib/active_record/validations.rb:79:in `raise_record_invalid'
/home/deploy/appmate/shared/bundle/ruby/2.1.0/gems/activerecord-4.2.0/lib/active_record/validations.rb:43:in `save!'
/home/deploy/appmate/shared/bundle/ruby/2.1.0/gems/activerecord-4.2.0/lib/active_record/attribute_methods/dirty.rb:29:in `save!'
/home/deploy/appmate/shared/bundle/ruby/2.1.0/gems/activerecord-4.2.0/lib/active_record/transactions.rb:291:in `block in save!'
What is going wrong here? The password has exactly 6 characters. The validation error should not arise?!
I just had the same issue in Development and I haven't found a work around. For now I'm leaving password validation off.
validates_confirmation_of :password
validates_length_of :password, :within => 4..20
validates_presence_of :password, :if => :password_required?
From localhost:3000/users/new I input newuser#emailaddress.com / password and I received the error:
2 errors prohibited this user from being saved:
Password is too short (minimum is 4 characters) Password can't be
blank
I followed this devise wiki documentation on how to write a custom update action for the registration controller when you want to allow users to edit their account without providing their passwords except if changing their passwords themselves.
Devise Wiki - How to Allow Users to Edit Account Without Providing a Password.
However, I can't figure out what's missing in my Rspec test to make it pass. Here are the relevant code snippets:
app/controllers/registrations_controller.rb
def update
#user = User.find(current_user.id)
successfully_updated = if needs_password?(#user, params)
#user.update_with_password(devise_parameter_sanitizer.sanitize(:account_update))
else
# remove the virtual current_password attribute
# update_without_password doesn't know how to ignore it
params[:user].delete(:current_password)
#user.update_without_password(devise_parameter_sanitizer.sanitize(:account_update))
end
if successfully_updated
set_flash_message :notice, :updated
# Sign in the user bypassing validation in case their password changed
sign_in #user, :bypass => true
redirect_to users_path
else
render "edit"
end
end
spec/factories/users.rb
FactoryGirl.define do
factory :user do
email { Faker::Internet.email }
password 'XXXXXXXXX'
first_name { Faker::Name.first_name }
middle_name { Faker::Name.first_name }
last_name { Faker::Name.last_name }
end
end
spec/controllers/registrations_controller_spec.rb
describe "PUT #update" do
login_pcp
let(:user) { FactoryGirl.create(:user, first_name: 'Tom') }
it "changes user attributes" do
attrs = FactoryGirl.attributes_for(:user, first_name: 'Jerry')
attrs.delete(:password)
put :update, user: attrs
user.reload
assigns[:user].should_not be_new_record
expect(user.first_name).to eq 'Jerry'
expect(flash[:notice]).to eq 'You updated your account successfully.'
end
end
When I run the spec I get the following error:
Failures:
1) RegistrationsController PUT #update changes user attributes
Failure/Error: expect(user.first_name).to eq 'Jerry'
expected: "Jerry"
got: "Tom"
(compared using ==)
# ./spec/controllers/registrations_controller_spec.rb:55:in `block (3 levels) in <top (required)>'
For some reason, it's not saving the update. I'm not sure if a password should be entered in order for the update to take place? Any help would be appreciated. Thanks!
The test now looks like this and it passes:
describe "PUT #update" do
before :each do
#request.env['devise.mapping'] = Devise.mappings[:user]
user_tom = FactoryGirl.create(:user, email: 'tom#test.com')
sign_in user_tom
end
it "changes user attributes" do
put :update, user: { email: 'jerry#test.com' }
subject.current_user.reload
assigns[:user].should_not be_new_record
expect(subject.current_user.email).to eq 'jerry#test.com'
expect(flash[:notice]).to eq 'You updated your account successfully.'
end
end
I ran into this issue as well, but as I can see it's because when you fill the update form, you will be required to fill in a field called "Current password". Since the data won't be updated unless you fill in the filed. When you use factory girl to produce user data, there is no this value. I solved it as can be seen in following code.
describe "PATCH #UPDATE" do
before :each do
#user = create(:user)
#old_email = #user.email
sign_in #user
end
context 'valid attributes' do
it "updates user attributes" do
patch :update, id: #user,
user: attributes_for(:user, current_password: "password")
expect(#user.reload.email).not_to eq(#old_email)
end
end
end
So this is probably going to be a very dumb question but i've set up Oauth with twitter as the provider in a similar way to Ryan Bates's 'Simple Omniauth Railscast - my question is now that that is set up and working should i be setting strong parameters in my sessions controller or is this not necessary?
SessionsController.rb
class SessionsController < ApplicationController
def create
#user = User.find_by_uid(auth_hash[:uid]) || User.create_user(auth_hash)
session[:uid] = #user.id
if #user
redirect_to root_path
else
redirect_to root_path, flash: {signinerror: "Oops, something went wrong with your sign in. Please try again."}
end
end
def auth_hash
request.env['omniauth.auth']
end
def destroy
session[:uid] = nil
redirect_to root_path
end
end
User.rb
class User < ActiveRecord::Base
has_many :opinions
def self.create_user(auth_hash)
create do |user|
user.provider = auth_hash[:provider]
user.name = auth_hash[:info][:name]
user.uid = auth_hash[:uid]
user.username = auth_hash[:info][:nickname]
user.email = auth_hash[:info][:email]
user.image = auth_hash[:info][:image]
end
end
end
Thanks
Since you don't use mass assignment on object creation, strong parameters will not give you any additional security.
With this plugin Action Controller parameters are forbidden to be used in Active Model mass assignments until they have been whitelisted.
https://github.com/rails/strong_parameters
I'm following the rails-tutorials from Michael Hartl, it's really fun !
but i'm having troubles from upon listing 8.20 and so on.
Cause i guessed i had made a type-o i erased the app and started from scratch again..
The best way to learn is repeat right ?
Well.. also on the second build i got the same error.
> NoMethodError in SessionsController#create
undefined method `[]' for nil:NilClass
Extracted source (around line #7):
6 def create
7 user = User.find_by(email: params[:session][:email].downcase)
8 if user && user.authenticate(params[:session][:password])
9 sign_in user
10 else
after googling for a few hours i tried resetting the db ( drop / create / migrate test:prepare)
i have tried copying pieces of code out of the example (e.g. controllers/helpers/views)
but I cant seem to find the solution.
Here is the code for
session_controller:
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
sign_in user
else
flash.now[:error] = 'Invalid email/password combination'
render 'new'
end
end
def destroy
end
end
session helper:
module SessionsHelper
def sign_in(user)
remember_token = User.new_remember_token
cookies.permanent[:remember_token] = remember_token
user.update_attribute(:remember_token, User.encrypt(remember_token))
self.current_user = user
end
def signed_in?
!current_user.nil?
end
def current_user=(user)
#current_user = user
end
def current_user
remember_token = User.encrypt(cookies[:remember_token])
#current_user ||= User.find_by(remember_token: remember_token)
end
def current_user?(user)
user == current_user
end
def sign_out
current_user.update_attribute(:remember_token,
User.encrypt(User.new_remember_token))
cookies.delete(:remember_token)
self.current_user = nil
end
end
User model
class User < ActiveRecord::Base
before_save {self.email = email.downcase }
before_create :create_remember_token
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+#[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, format: { with: VALID_EMAIL_REGEX},
uniqueness: {case_sensetive: false}
has_secure_password
validates :password, length: { minimum: 3 }
def User.new_remember_token
SecureRandom.urlsafe_base64
# SecureRandom.urlsafe_base64
end
def User.encrypt(token)
Digest::SHA1.hexdigest(token.to_s)
end
private
def create_remember_token
self.remember_token = User.encrypt(User.new_remember_token)
end
end
user controller
class UsersController < ApplicationController
def new
#user = User.new
end
def show
#user = User.find(params[:id])
end
def create
#user = User.new(user_params)
if #user.save
flash.now[:succes] = "Welcome to the sample app"
redirect_to #user
else
render 'new'
end
end
private
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
end
and the user helper
module UsersHelper
# Returns the Gravatar (http://gravatar.com/) for the given user.
def gravatar_for(user, options = { size: 50 })
gravatar_id = Digest::MD5::hexdigest(user.email.downcase)
size = options[:size]
gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}?s=#{size}"
image_tag(gravatar_url, alt: user.name, class: "gravatar")
end
end
how can i properly copy-paste the error log ?
Based on the error you're getting, the problem is clearly somewhere in your Users controller:
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
sign_in user
else
flash.now[:error] = 'Invalid email/password combination'
render 'new'
end
end
The error is being generated because params[:session] is, in fact, nil. Now the question is, why?
My guess is that you have a typo in your sign_in form, it should be:
<%= form_for(:session, url: sessions_path) do |f| %>
<%= f.label :email %>
<%= f.text_field :email %>
<%= f.label :password %>
<%= f.password_field :password %>
<%= f.submit "Sign in", class: "btn btn-large btn-primary" %>
<% end %>
Please make sure :session is typed correctly.
Total Rails novice here, just coming from PHP.
I'm running a Rails4+Active Admin setup with (device/cancan (disregarding those now).
Implementing strong_parameters. Please note, the code is totally broken. I've tried it in a bunch of ways and this is as close as I've gotten. Hopefully it gives a view into what I'm trying to do.
Been following a tutorial on implementing an API key setup, and trying to modify it to be able to generate an APIKey for a user in the admin.
I've added a column to AdminUser to generate an APIKey
It is correctly linked to
#app/admin/admin_users.rb
ActiveAdmin.register AdminUser do
index do
column :email
column :current_sign_in_at
column :last_sign_in_at
column :sign_in_count
default_actions
column('API', :sortable => :id) { |resource| link_to "(Re)Generate Key", new_admin_admin_user_api_key_path(resource.id) }
end
end
Which gives me a (correct) link to
/admin/admin_users/:admin_user_id/api_keys/new(.:format)
# app/admin/api_key.rb
ActiveAdmin.register APIKey do
belongs_to :admin_user
controller do
def create_for admin_user
key = APIKey.new
key.assign_params_from_controller(admin_user)
APIKey.create(:admin_user_id => params[:admin_user_id]);
end
def permitted_params
params.permit create_for [:admin_user_id]
end
end
end
#app/models/api_key.rb
class APIKey < ActiveRecord::Base
before_create :generate_access_token
belongs_to :admin_user, :foreign_key => "id", :inverse_of => :api_key
attr_accessible :admin_user_id
def assign_params_from_controller(params)
#params = params
end
private
def generate_access_token
begin
self.admin_user_id = params.admin_user_id
self.access_token = SecureRandom.hex
end while self.class.exists?(admin_user_id: admin_user_id, acces_token: access_token)
end
end
This code gives me:
NameError in Admin::APIKeysController#new
undefined local variable or method `params' for #<APIKey:0x000000078d6470>
def generate_access_token
begin
self.admin_user_id = params.admin_user_id
self.access_token = SecureRandom.hex
end while self.class.exists?(admin_user_id: admin_user_id, acces_token: access_token)
end
UPDATE, ANSWER:
Problem solved.
Ended up being on the right track. But was straying off because of type conversion errors.
My admin_user_id ended up being a 'string' in the database. Nice copy-paste job there.
Wrote a migration first
class AlterTableAPIKeys < ActiveRecord::Migration
def up
execute "DELETE FROM `api_keys` WHERE 1"
change_column :api_keys, :access_token, :string, { null: false }
change_column :api_keys, :admin_user_id, :integer, { null: false }
add_column :api_keys, :active, :boolean, {null: false, default: true }
remove_column :api_keys, :role
add_index :api_keys, ["admin_user_id"], name: "index_api_keys_on_admin_user_id", unique: false
add_index :api_keys, ["access_token"], name: "index_api_keys_on_access_token", unique: true
end
end
I didn't pass the access_token to the create.
I ended up with this.
# app/admin/api_key.rb
ActiveAdmin.register APIKey do
belongs_to :admin_user
controller do
def new
key = APIKey.create(:admin_user_id => params[:admin_user_id])
{:access_token => key.access_token}
redirect_to admin_admin_users_path, :notice => "API Key #{key.access_token} created! "
end
def permitted_params
params.permit api_key: [:admin_user_id]
end
end
end
# app/models/api_key.rb
class APIKey < ActiveRecord::Base
attr_accessible :access_token, :expires_at, :admin_user_id, :active, :application
before_create :generate_access_token
before_create :set_expiration
belongs_to :admin_user
def expired?
DateTime.now >= self.expires_at
end
private
def generate_access_token
begin
self.access_token = SecureRandom.hex
end while self.class.exists?(access_token: access_token)
end
def set_expiration
self.expires_at = DateTime.now+30
end
end
Obviously this does not account for access, might solve that with roles (i.e. uberadmin can regenerate other admins API, admins only their own).