How do rails app have invite your friends custom url? - ruby-on-rails

I am currently using devise_invitable to submit an email that invites a new user. Each email you send out creates unique token which gets connects the two users if they accept the invitation. What I would like to do is for each current_user to have a unique invitation token that stays the same so they can tweet it, email it, etc. and anyone that accepts the invite by clicking on the link (www.mydomain.com/signup/'unique_current_user_token') will be taken to the signup form which will store the token in their 'account' so I can see who invited them. Does it make

You may want to do that manually by overriding the registration page of devise and allowing our new parameters to the model (attr_accessible rails 3 or params.require(:foo).permit)
First you should remove devise invitable as you will not use it for this approach i've searched and did not find a clean way to do it with devise_invitable.
Step 2:
add attribute to the users table called invitation_token (this one is different from devise invitable as this will be unique per user and will be used to send invitation not used when receiving invitation)
and add to the user model (according to rails casts)
before_create :generate_invitation_token
def generate_invitation_token
self.invitation_token = Digest::SHA1.hexdigest([Time.now, rand].join)
end
by this each user will have a unique invitation_token
Step 3: now you can have a url like /signup/:invitation_token for to invite users which is unique per user (you should add this to routes and map it to the Devise::RegistrationsController#create)
Step 4: accept users and recognize who invited them on registration
Now we have to override the Devise::RegistrationsController#create and new and view
in the new you should find the user who invited. and add a virtual attribute that should reference the that user ie: inviter.id
and add a hidden_field to the registration form to set such attribute
Now in the create check for the availability of that attribute if present you can do what ever you want (add relation between users in your case).
And that's it i think
If you need anything else comment and i will edit my answer to help you more

I'll add a more detailed steps, just if someone struggles like me to implement the solution from #khaled_gomaa, that worked fine.
1a. If you are overriding devise registration controller, add it to this controller:
before_filter :configure_permitted_parameters
protected
def configure_permitted_parameters
devise_parameter_sanitizer.for(:sign_up) << :invited_by
end
1b. If you are not using a custom controller, add the following to the application constroller:
before_action :configure_permitted_parameters, if: :devise_controller?
protected
def configure_permitted_parameters
devise_parameter_sanitizer.for(:sign_up) << :username
end
2. Run it in your console:
rails generate migration AddInvitationTokenToUsers invitation_token:string invited_by:integer
I'm not sure if it was in the original instructions but I think its really useful to set who invited a user.
Edit user model and add the following lines:
before_create :generate_invitation_token
def generate_invitation_token
self.invitation_token = Digest::SHA1.hexdigest([Time.now, rand].join)
end
3. I did not set the extra route in config/routes.rb
4. That's the part that was more confusing for me. Not sure if I'm not fully following the original instructions, but it worked fine in rails 4.
If you are not overriding registration constroller, you will need it, so my 1a step is stupid...
Add a new file under controllers folder named registrations_controller.rb with the following content:
class RegistrationsController < Devise::RegistrationsController
before_filter :configure_permitted_parameters
def new
if params.has_key?(:invitation_token)
inviter = User.where(:invitation_token => params[:invitation_token]).first
#invited_by = inviter.id
end
super
end
def create
super
if resource.save && params[:user].has_key?(:invited_by)
resource.invited_by = params[:user][:invited_by]
resource.save
end
end
protected
def configure_permitted_parameters
devise_parameter_sanitizer.for(:sign_up) << :invited_by
end
end
In new method, we are setting the extra field for the sign_up form. We are recovering the id of the inviter (who invites), using the token that comes from a param.
Creates method will save the user in the db. Before do it, we will get the hidden field from the form and add it to the user, in case that the user comes from an invitation.
In the view.html.erb (or whatever you are using) add the following line, inside the form:
<%= f.hidden_field :invited_by, :value => #invited_by %>
And that's it. Run the migration to add the new columns. Just go to http://example.org/users/sign_up?invitation_token=USER_TOKEN

Related

rails 5 dynamic routing redirections based on model attributes

I have the following resources
Places (:place_id, :name, :shortlink, :token)
Events (:event_id, :address)
I have a restricted user registration with a nested user resource (User has many Events).
When User register, (s)he create a new event with a referenced place column.
Users can only sign up when having theses parameters in the url
https://mysite/user/signup?q=[:place_id]&t=[:token] # place id must exist, place token must be equal to t parameter
before_action :check_has_access, only: [:new]
...
protected
def check_has_access
#place = Place.find(params[:q])
if params[:q].blank? && #place.blank? || params[:t].blank? || #place.token != params[:t]
redirect_to(root_path)
flash[:alert] = 'Restricted sign-up'
end
end
I want to generate a shortcut for each place for the user to register
https://mysite/[:shortlink] #place.shortlink
That will redirect to a given sign up form with corresponding filled form and restricted params so the user that has the direct url can sign up.
https://mysite/user/signup?q=[:place_id]&t=[:token]
How can I have routes to be generated ?
Subsidiary question, am I doing it right ?
Found a solution on this post here.
I need to restart the server so newly created route get loaded after a Place object creation. Not sure if I am doing it right though but it works..
#config/route.rb
Rails.application.routes.draw do
Place.all.each do |pl|
get "/#{pl.shortlink}" => redirect("/users/sign_up?q=#{pl.id}&t=#{pl.token}")
end
end
In order to load the newly created route when adding a Place, I added
#models/place.rb
after_create :add_shortlink
...
def add_shortlink
Rails.application.reload_routes!
end
This will not work on Heroku, issue addressed is here

Devise with Ruby on Rails - Force user to change password on first login

I have a RoR application (Rails 4.2, Ruby 2.2.0) running Devise. I have set it up so that admin users (identified the "is_admin" boolean I added to the user model) are able to create new user account, provide them with a generated password and confirmation email. This is all working properly. I have also added the datetime column pass_changed which should be updated when a user changes their password, and then checked against created_at to make sure that the password has changed since the account was created. If the two dates are equal then the user is redirected to the password change form.
I wrote a procedure for checking that the user has changed their password and placed it in my application_controller.rb:
def check_changed_pass
#user = current_user
if #user.pass_changed == #user.created_at #make sure user has changed their password before accessing internal pages
redirect_to edit_user_registration_path, alert: "You must change your password before logging in for the first time"
end
end
Then in my internal_controller.rb (used for all internal that require the user to be logged in:
before_action :check_changed_pass
So far so good, but the problem comes with attempting to update pass_changed at the right time. I have tried various configurations, but I can't find where to put this code so that it triggers when a password is updated, but not every time the user model is updated, which includes every login and logout, when I only want it to trigger if the password gets updated.
If I place "before_save :update_pass_change, only: [:edit, :update]" in my controller it doesn't trigger on a password update, and if I place it in the User model, I have to put the procedure in the model as well and then it won't pick up current_user as its not available in the model. The ideal thing would be if Devise had a hook for after_password_edit similar to the after_database_authentication hook. I had to override Devise's registrations_controller.rb to remove the line
prepend_before_filter :require_no_authentication, only: [ :cancel]
So that admin users would be able to add new users. I tried placing update_pass_change there, but it doesn't seem to trigger before_save on a password edit.
In application_controller.rb
def update_pass_change # records that the user has changed their password
unless current_user.pass_changed != current_user.created_at
current_user.pass_changed = Time.now
end
end
Similar unanswered question: Ruby on Rails: Devise - password change on first login
You could use a callback on your model and check, before save, if the changes includes your password attribute. Something like this:
class User < ActiveRecord::Base
before_save :check_password_changed
private
def check_password_changed
self.pass_changed = Time.now if changed.include? 'encrypted_password'
end
end
You can also try this
change datatype of pass_changed to boolean with default value false
Then in your application_controller.rb
before_action :check_pass_changed
private
def check_pass_changed
unless current_user.pass_changed
redirect_to custome_password_edit_path, alert: "You must change your password before logging in for the first time"
end
end
Then Create custom action in ur users_controller to display the password change form and to update.
eg.
To display form : custome_password_edit
To update :
update_user_password
also dont forget to write above in routes.rb
after successful change update the value of pass_changed to true

Displaying User Password With Devise Confirmation Page

I'm attempting to display a users password along in his confirmation page sent by the Devise mailer. The confirmation page is the default
Welcome test0#test.com!
You can confirm your account email through the link below:
Confirm my account
However, I wish to have
Welcome test0#test.com!
Your password is currently DASADSADS
You can confirm your account email through the link below:
Confirm my account
How do I access the user object in the view? Do I need to override the mailer controller with a custom one? If so, how do I tell what the methods of the current mailer do (tried looking at documentation but can't find any clues)?
I noticed that #email and #resource are used in the view. Can I use any of these to access the current password in its unhashed form?
Note that I am sending this email manually with user.find(1).send_confirmation_instructions
Although this can be done, I would caution very strongly against doing so. Hashed passwords are specifically used so that the password cannot be recreated easily. Passing the original password back to the user will cause it to be sent back in plain text which sort of defeats the whole purpose. Also, shouldn't the user already know their password (they did type it in twice after all)?!?
To do this, you would need to capture the original (unhashed) password in the registration create action and send the email at that point (passing along the password). You can do this by overriding the sign_up method - you can do this in an initializer:
class Devise::RegistrationsController < DeviseController
def sign_up(resource_name, resource)
sign_in(resource_name, resource)
resource.unhashed_password = resource_params[:password]
resource.send_confirmation_instructions
end
end
Alternatively, you can derive a new controller from Devise::RegistrationsController and put this override code there (the recommended approach - but then again, this whole operation isn't really recommended). You'll need to add the unhashed_password accessor for this to work:
class User < ActiveRecord::Base
attr_accessor :unhashed_password
end
And then you can update your confirmation view (at app/views/devise/mailer/confirmation_instructions.html.erb) to contain this:
<p>Your password is currently <%= #resource.unhashed_password %></p>
Devise save password in encrypted form: You can decrypt it using,
Generate new migration:
$ rails g migration AddLegacyPasswordToUser legacy_password:boolean
invoke active_record
create db/migrate/20120508083355_add_legacy_password_to_users.rb
$ rake db:migrate
Using legacy_password method in following code you can decrypt your password:
class User < ActiveRecord::Base
...
def valid_password?(password)
if self.legacy_password?
# Use Devise's secure_compare to avoid timing attacks
if Devise.secure_compare(self.encrypted_password, User.legacy_password(password))
self.password = password
self.password_confirmation = password
self.legacy_password = false
self.save!
else
return false
end
end
super(password)
end
# Put your legacy password hashing method here
def self.legacy_password(password)
return Digest::MD5.hexdigest("#{password}-salty-herring");
end
end
You can just use request.request_parameters[:user][:password] to get the plain text password on the create or update action.

Ruby on Rails security vulnerability with user enumeration via id

With Ruby on Rails, my models are being created with increasing unique ids. For example, the first user has a user id of 1, the second 2, the third 3.
This is not good from a security perspective because if someone can snoop on the user id of the last created user (perhaps by creating a new user), they can infer your growth rate. They can also easily guess user ids.
Is there a good way to use random ids instead?
What have people done about this? Google search doesn't reveal much of anything.
I do not consider exposing user IDs to public as a security flaw, there should be other mechanisms for security. Maybe it is a "marketing security flaw" when visitors find out you do not have that million users they promise ;-)
Anyway:
To avoid IDs in urls at all you can use the user's login in all places. Make sure the login does not contain some special characters (./\#? etc.), that cause problems in routes (use a whitelist regex). Also login names may not be changed later, that can cause trouble if you have hard links/search engine entries to your pages.
Example calls are /users/Jeff and /users/Jeff/edit instead of /users/522047 and /users/522047/edit.
In your user class you need to override the to_param to use the login for routes instead of the user's id. This way there is no need to replace anything in your routes file nor in helpers like link_to #user.
class User < ActiveRecord::Base
def to_param
self.login
end
end
Then in every controller replace User.find by User.find_by_login:
class UsersController < ApplicationController
def show
#user = User.find_by_login(params[:id])
end
end
Or use a before_filter to replace the params before. For other controllers with nested resources use params[:user_id]:
class UsersController < ApplicationController
before_filter :get_id_from_login
def show
#user = User.find(params[:id])
end
private
# As users are not called by +id+ but by +login+ here is a function
# that converts a params[:id] containing an alphanumeric login to a
# params[:id] with a numeric id
def get_id_from_login
user = User.find_by_login(params[:id])
params[:id] = user.id unless user.nil?
end
end
Even if you would generate random INTEGER id it also can be compromted very easy. You should generate a random token for each user like MD5 or SHA1 ("asd342gdfg4534dfgdf"), then it would help you. And you should link to user profile with this random hash.
Note, this is not actually the hash concept, it just a random string.
Another way is to link to user with their nick, for example.
However, my guess is knowing the users ID or users count or users growth rate is not a vulnerability itself!
Add a field called random_id or whatever you want to your User model. Then when creating a user, place this code in your UsersController:
def create
...
user.random_id = User.generate_random_id
user.save
end
And place this code in your User class:
# random_id will contain capital letters and numbers only
def self.generate_random_id(size = 8)
alphanumerics = ('0'..'9').to_a + ('A'..'Z').to_a
key = (0..size).map {alphanumerics[Kernel.rand(36)]}.join
# if random_id exists in database, regenerate key
key = generate_random_id(size) if User.find_by_random_id(key)
# output the key
return key
end
If you need lowercase letters too, add them to alphanumerics and make sure you get the correct random number from the kernel, i.e. Kernel.rand(62).
Also be sure to modify your routes and other controllers to utilize the random_id instead of the default id.
You need to add a proper authorization layer to prevent un-authorized access.
Let us say you you display the user information in show action of the Users controller and the code is as shown below:
class UsersController < ActionController::Base
before_filter :require_user
def show
#user = User.find(params[:id])
end
end
This implementation is vulnerable to id guessing. You can easily fix it by ensuring that show action always shows the information of the logged in user:
def show
#user = current_user
end
Now regardless of what id is given in the URL you will display the current users profile.
Let us say that we want to allow account admin and account owner to access the show action:
def show
#user = current_user.has_role?(:admin) ? User.find(params[:id]) : current_user
end
OTH authorization logic is better implemented using a gem like CanCan.

Customizing Devise gem (Rails)

I am trying to add a new field "registration code" along with the email/password in sign up form. (i have the registration code field in a separate database, only a valid registration code/email pair will have to work with the sign up)
I could not able to find any controller for actions done by devise gem.
How do i customize devise to achieve this?
Thanks in advance.
It seems like your question basically has nothing to do with Devise itself (besides the views). To validate your registration code/email pairs, you surely need to add this as validation.
The easy way to validate registration code could be:
class User
validate :validate_registration_code
private
def validate_registration_code
reg_code = RegistrationCode.find_by_code(registration_code)
unless reg_code.email == record.email
errors.add(:registration_code, "Invalid registration code for #{record.email}")
end
end
end
You also might want to write simple custom validator:
class RegistrationCodeValidator < ActiveModel::Validator
def validate(record)
# actual reg code validation
# might look like:
reg_code = RegistrationCode.find_by_code(record.registration_code)
unless reg_code.email == record.email
record.errors[:registration_code] << "Invalid registration code for #{record.email}"
end
end
end
# in your User model
class User
# include registration code validator
include RegistrationCodeValidator
validates_with MyValidator
end
Devise keeps it's controllers behind the scenes inside the gem. If you want to add an action or modify one, you have to subclass it and do a little work in routes to get the action to your controller.
However you shouldn't need to do that to add a field. See goshakkk's answer

Resources