generate unique username (omniauth + devise) - ruby-on-rails

I have an app with user authentication with devise + omniauth.
In my model that username in my app is unique. I dont want duplicate username in my app.
Some users in facebook has not a defined username in his profile.
I want generate an unique username if the user has not username defined in facebook.
For example for generate password I have this:
:password => Devise.friendly_token[0,20]
How can I generate a unique username for my app if the facebook user has not username in facebook?
Thank you

You can create a nice readable username (eg generated from the first part of the email) and then ensure it is unique by adding numbers until it is. eg
#in User
def get_unique_login
login_part = self.email.split("#").first
new_login = login_part.dup
num = 2
while(User.find_by_login(new_login).count > 0)
new_login = "#{login_part}#{num}"
num += 1
end
new_login
end
One problem here is that someone could potentially bag that login inbetween you getting it and you saving it. So, maybe it's best to combine it into a before_create filter:
#in User
before_create :ensure_login_uniqueness
def ensure_login_uniqueness
if self.login.blank? || User.find_by_login(self.login).count > 0
login_part = self.email.split("#").first
new_login = login_part.dup
num = 2
while(User.find_by_login(new_login).count > 0)
new_login = "#{login_part}#{num}"
num += 1
end
self.login = new_login
end
end

You can take a part of email before the # sign and add there smth like user_id, or just take the email itself. Or you can combine somehow the first and last names from the fb response.

Here is how i created Login with combination of first name and last name field.. Improvements on this code is welcome.
before_create :ensure_login_uniqueness
def ensure_login_uniqueness
if self.login.blank?
self.name = self.first_name + " " + self.last_name
firstnamePart = self.first_name.downcase.strip.gsub(' ', '').gsub(/[^\w-]/, '')
lastnamePart = self.last_name.downcase.strip.gsub(' ', '').gsub(/[^\w-]/, '')
login_part = firstnamePart+lastnamePart
new_login = login_part.dup
num = 1
while(User.where(:login => new_login).count > 0)
new_login = "#{login_part}#{num}"
num += 1
end
self.login = new_login
end
end

It did not work for me,but change:
while(User.find_by_login(new_login).count > 0)
to
while(User.where(login: new_login).count > 0)

Here's my methods that I use for Facebook
def ensure_username_uniqueness
self.username ||= self.email.split("#").first
num = 2
until(User.find_by(username: self.username).nil?)
self.username = "#{username_part}#{num}"
num += 1
end
end
def self.from_omniauth(auth)
where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
user.email = auth.info.email
user.password = Devise.friendly_token[0,20]
user.username = auth.info.name.downcase.gsub(" ", "")
user.username = user.username[0..29] if user.username.length > 30
user.ensure_username_uniqueness
end
end

Here is the way how you can generate "username" from "full name"
def self.generate_username(full_name)
generated = ActiveSupport::Inflector.transliterate(full_name) # change ñ => n
.downcase # only lower case
.strip # remove spaces around the string
.gsub(/[^a-z]/, '_') # any character that is not a letter or a number will be _
.gsub(/\A_+/, '') # remove underscores at the beginning
.gsub(/_+\Z/, '') # remove underscores at the end
.gsub(/_+/, '_')
taken_usernames = User
.where("username LIKE ?", "#{generated}%")
.pluck(:username)
# username if it's free
return generated unless taken_usernames.include?(generated)
count = 2
while true
# username_2, username_3...
new_username = "#{generated}_#{count}"
return new_username if ! taken_usernames.include?(new_username)
count += 1
end
end

Related

SCIM userName in PATCH operation

I have implemented user provisioning/deprovisioning with SCIM like so :
users_controller.rb
class Scim::UsersController < Scim::ScimController
before_action :set_scim_provider
def index
startIndex = params[:startIndex].to_i
startIndex = 1 if startIndex < 1# if the user send a startIndex < 1, it is bad data, we don't take it.
itemsPerPage = params[:count].to_i
if itemsPerPage < 1 || itemsPerPage > #scim_provider.max_results
itemsPerPage = #scim_provider.default_number_of_results
end
scim_users = #scim_provider.identity_provider.communaute_accesses.from_scim
if params["filter"]
parser = Scim::QueryFilter::Parser.new
rpn_array = parser.parse(params["filter"])
tree = parser.tree
if tree.length == 3 and tree[0]== 'eq' and tree[1] == 'userName'
userName = tree[2]
scim_users = scim_users.where(provider_identifier: userName.delete('"'))
else
fail 'e'
end
end
paginated_users = scim_users.order(:created_at).offset(startIndex - 1).limit(itemsPerPage)
r = {
"schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
"totalResults": scim_users.size,
"Resources": paginated_users.map { |ca| #scim_provider.representation_for_user(ca) },
"startIndex": startIndex,
"itemsPerPage": itemsPerPage
}
render_json_result(r, 200)
end
def create
if #scim_provider.identity_provider.communaute_accesses.from_scim.find_by(provider_identifier: #body_params['userName'])
render_409_conflict("uniqueness")
else
ca = #scim_provider.identity_provider.communaute_accesses.find_by(provider_identifier: #body_params['userName'], communaute_id: #scim_provider.identity_provider.communaute.id)
if ca.nil?
ca = #scim_provider.identity_provider.communaute_accesses.create(provider_identifier: #body_params['userName'], communaute_id: #scim_provider.identity_provider.communaute.id)
end
ca.update_last_raw_value("scim", #body_string)
ca.extract_values_from_scim
ca.queue_send
end
render_json_result(#scim_provider.representation_for_user(ca), 201)
end
def show
user = #scim_provider.identity_provider.communaute_accesses.from_scim.find_by(provider_identifier: #body_params['userName'])
if user
render_json_result(#scim_provider.representation_for_user(user), 200)
else
render_404_not_found(params[:id])
end
end
def update
ca = #scim_provider.identity_provider.communaute_accesses.from_scim.find_by(provider_identifier: #body_params['userName'])
uc = UserCommunaute.find_by(provider_identifier: #body_params['userName'])
ca.update_last_raw_value("scim", #body_string)
ca.extract_values_from_scim
unless ca.nil?
if ca.pending?
ca.update_last_raw_value("scim", #body_string)
ca.update(active: false)
if ca.active == false
fail "Unable to delete this user because of activeness" if ca.active == true
ca.destroy!
end
render_json_result(#scim_provider.representation_for_communaute_access_patch(ca), 200)
end
end
unless uc.nil?
uc.update(active: #body_params['active'])
if uc.active == false
uc.user.communaute_accesses.from_scim.destroy_all
uc.user.user_communautes.from_scim.destroy_all
render_json_result(#scim_provider.representation_for_user_communaute_patch(uc), 200)
end
end
end
end
Explanations:
When updating a user, SCIM sends a PATCH request like this:
{"schemas"=>["urn:ietf:params:scim:api:messages:2.0:PatchOp"], "Operations"=>[{"op"=>"Replace", "path"=>"active", "value"=>"False"}]} (#body_params in the code)
Which is what i am expecting. But, for a while, i was receiving the userName also in the body response during the PATCH operation.
This is how I fetch the correct user in my DB.
Actual result:
I don't receive the userName anymore when SCIM hits my update action.
Expected results:
Being able to receive information about the user during the PATCH operation to fetch the userName and find the right user in my database.
I have tried almost everything. When SCIM hits the index action, which it does everytime before going anywhere else, it does return me a userName et everything ends up as a 200 OK.
Then, when passing through update, it sends me nothing.
What I have tried last is to isolate the userName as an instance variable in the index action to fetch it after in the update like so:
# index
...
if params["filter"]
parser = Scim::QueryFilter::Parser.new
rpn_array = parser.parse(params["filter"])
tree = parser.tree
if tree.length == 3 and tree[0]== 'eq' and tree[1] == 'userName'
#user_name = tree[2]
scim_users = scim_users.where(provider_identifier: #user_name.delete('"'))
else
fail 'e'
end
end
...
# update
def update
ca = #scim_provider.identity_provider.communaute_accesses.from_scim.find_by(provider_identifier: #user_name)
uc = UserCommunaute.find_by(provider_identifier: #user_name)
ca.update_last_raw_value("scim", #body_string)
ca.extract_values_from_scim
...
But, #user_name in update seems to disappear as its value is nil.
I am deprovisioning from Azure Active Directory and Okta in a production environment.
Mapping is ok in both platforms.
Provisioning is working like a charm.
Please refer to https://developer.okta.com/docs/reference/scim/scim-20/#update-a-specific-user-patch for PATCH /Users/{userId}. Could you not make use of the userId in the url to identify the user ?

Add a random string while updating attribute

I am looking for a method that can generate a random string in the starting of the email field while updating the record.
def update
#user = User.find_by_id(4)
#user.email = #method to update email with random string
end
So if I have the email record abc#gmail.com and I want to update it like this:
dssaakj123_abc#gmail.com
How it can be done in rails?
You can use the SecureRandom library:
#user.email = "#{SecureRandom.hex(10)}_#{user.email}"
Why not use SecureRandom?
require 'securerandom'
random_string = SecureRandom.hex # provide argument to limit the no. of characters
# outputs: 5b5cd0da3121fc53b4bc84d0c8af2e81 (i.e. 32 chars of 0..9, a..f)
For appending before email, you can do something like
#user.email = "#{SecureRandom.hex(5))_#{#user.email}" # 5 is no. of characters
Hope it helps!
(1..8).map{|i| ('a'..'z').to_a[rand(26)]}.join
8 is the number of characters you want to generate randomly.
create an action in your application controller like this :
private
def generate_random_string
SecureRandom.urlsafe_base64(nil, false)
end
And use it like this in any controller you want:
def update
#user = User.find_by_id(4)
#user.email = generate_random_string + #user.email
end
I hope this will help you.
def update
#user = User.find_by_id(4)
#user.email = "#{generate_random_string(8)}_#{#user.email}"
## You can pass any length to generate_random_string method, in this I have passed 8.
end
private
def generate_random_string(length)
options = { :length => length.to_i, :chars => ('a'..'z').to_a + ('A'..'Z').to_a + ('0'..'9').to_a }
Array.new(options[:length]) { options[:chars].to_a[rand(options[:chars].to_a.size)] }.join
end

Cant found model with out an ID in rails 3.2.12

i ve this method. I m not at all able to understand the error which is
Couldn't find Company without an ID
in ActiveRecord::RecordNotFound in CustomersController#bulk_create
This method is written to create customers for a company in bulk by taking their name and numbers in format name:number.
The method is as follows:
def bulk_create
res = ""
comp_id = params[:customer][:selected_companies].delete_if{|a| a.blank?}.first
comp = Company.find(comp_id)
s = SentSmsMessage.new
s.set_defaults
s.data = tmpl("command_signup_ok", customer, comp) unless params[:customer][:email].length > 0
s.data = params[:customer][:email] if params[:customer][:email].length > 0
s.company = comp if !comp.nil?
s.save
unless comp_id.blank?
params[:customer][:name].lines.each do |line|
(name, phone) = line.split(/\t/) unless line.include?(":")
(name, phone) = line.split(":") if line.include?(":")
phone = phone.gsub("\"", "")
phone = phone.strip if phone.strip.to_i > 0
name = name.gsub("\"", "")
name = name.gsub("+", "")
phone = "47#{phone}" if params[:customer][:active].to_i == 1
customer = Customer.first(:conditions => ["phone_number = ?", phone])
if customer.nil?
customer = Customer.new
customer.name = name
# customer.email
# customer.login
# customer.password
customer.accepted_agreement = DateTime.now
customer.phone_number = phone
customer.active = true
customer.accepted_agreement = DateTime.now
customer.max_msg_week = params[:customer][:max_msg_week]
customer.max_msg_day = params[:customer][:max_msg_day]
customer.selected_companies = params[:customer][:selected_companies].delete_if{|a| a.blank?}
res += "#{name} - #{phone}: Create OK<br />" if customer.save
res += "#{name} - #{phone}: Create failed<br />" unless customer.save
else
params[:customer][:selected_companies].each do |cid|
new_company = Company.find(cid) unless cid.blank?
if !new_company.nil?
if !customer.companies.include?(new_company)
customer.companies << new_company
if customer.save
res += "#{name} - #{phone}: Customer exists and the customer was added to the firm #{new_company.name}<br />"
else
res += "#{name} - #{phone}: Customer exist, but something went wrong during storage. Check if the client is in the firm.<br />"
end
else
res += "#{name} - #{phone}: Customer exists and is already on firm #{new_company.name}<br />"
end
end
end
end
s.sms_recipients.create(:phone_number => customer.phone_number)
end
s.save
s.send_as_sms
#result = res
respond_to do |format|
format.html { render "bulk_create"}
end
else
#result = "You have not selected any firm to add these users. Press the back button and try again."
respond_to do |format|
format.html { render "bulk_create"}
end
end
end
I want to update one situation here. That when i submit the form blank then it gives this error. Also if i filled the form with the values then its show the situation which the method is returning in case of fail.
res += "#{name} - #{phone}: Create failed <br />"
The tmpl method
private
def tmpl(setting_name, customer, company = nil)
text = ""
if customer.companies.count > 0
sn = "#{setting_name}_#{#customer.companies.first.company_category.suffix}".downcase rescue setting_name
text = Setting.value_by(sn) rescue ""
end
textlenth = text.length rescue 0
if textlenth < 3
text = Setting.value_by(setting_name) rescue Setting.value_by("command_error")
end
return fill_template(text, customer, company)
end
From the model customer.rb
def selected_companies=(cmps)
cmps.delete("")
# Check the old ones. Make a note if they are not in the list. If the existing ones are not in the new list, just remove them
self.companies.each do |c|
self.offer_subscriptions.find(:first, ["customer_id = ?", c]).destroy unless cmps.include? c.id.to_s
cmps.delete c.id.to_s if cmps.include? c.id.to_s
end
# Then create the new ones
cmps.each do |c2|
cmp = Company.find(:first, ["id = ?", c2])
if cmp && !c2.blank?
offerSubs = offer_subscriptions.new
offerSubs.company_id = c2
offerSubs.save
end
end
end
def selected_companies
return self.companies.collect{|c| c.id}
end
The association of customer is as follows:
has_many :offer_subscriptions
has_many :companies, :through => :offer_subscriptions
This code is written by the some one else. I m trying to understand this method but so far not being able to understand this code.
Please help.
Thanks in advance.
You are getting 'Couldn't find Company without an ID' error because your Company table doesn't contain record with id = comp_id
Change comp = Company.find(comp_id) to comp = Company.find_by_id(comp_id).
This will return nil instead of an error.
Add comp is not nil condition is already handled in your code.
Your comp_id line is returning nil.
comp_id = params[:customer][:selected_companies].delete_if{|a| a.blank?}.first
Post the params that get passed to this function and we could hopefully find out why. In the meantime you could enclose the block in a begin - rescue block to catch these errors:
begin
<all your code>
rescue ActiveRecord::RecordNotFound
return 'Unable to find a matching record'
end
try this:
comp = ""
comp = Company.find(comp_id) unless comp_id.nil?
instead of comp = Company.find(comp_id)
further nil checking present in your code.
Reason being
params[:customer][:selected_companies].delete_if{|a| a.blank?} = []
so [].first = nil
therefor, params[:customer][:selected_companies].delete_if{|a| a.blank?}.first = nil
and comp_id is nil
So check the log file and check what is coming in the parameter "selected_companies"
when you will find the parameter, everything will be understood well....

Adding a bitwise virtual column to a model

I'm building this RoR site on an existing database. The user model on database has a column called "secret", which is a bitwise integer that holds information of the columns user has set as secret (first name, last name, etc).
Variables are to the power of two, for example: last name = 1<<1 = 2, first name = 1<<2 = 4, email == 1<<3 = 8, etc. So if user has set first name & email as secret, the column value becomes 4+8 = 12.
Now, I'm trying to find a generalized way to implement these virtual columns into a Rails model. So that, I could do (just a dummy example, the point being, i want to retrieve & store the status):
if user.secret_email?
user.secret_name_last = true
user.secret_name_first = false
end
How to implement these virtual columns neatly to a model (without modifying the existing database)? Current I've got following. It works, but it's not neat. As I've got 20 secret columns, the code looks very ugly.
SECRET_NAME_LAST = (1 << 1) # 2
attr_accessible :secret_name_last
def secret_name_last; secret & SECRET_NAME_LAST > 0 unless secret.nil?; end
def secret_name_last=(value); secret_set_value(SECRET_NAME_LAST, value); end
SECRET_NAME_FIRST = (1 << 2) # 4
attr_accessible :secret_name_first
def secret_name_first; secret & SECRET_NAME_FIRST > 0 unless secret.nil?; end
def secret_name_first=(value); secret_set_value(SECRET_NAME_FIRST, value); end
SECRET_EMAIL = (1 << 3) # 8
attr_accessible :secret_email
def secret_email; secret & SECRET_EMAIL > 0 unless secret.nil?; end
def secret_email=(value); secret_set_value(SECRET_EMAIL, value); end
***snip (17 more)***
private
def secret_set_value(item, value)
if self.secret.nil?
self.secret = 0
end
if value == "1" || value == true || value == 1
# Add item to secret column (if it doesn't exist)
if self.secret & item == 0
self.secret += item
end
else
# Remove item from secret column (if it exists)
if self.secret & item > 0
self.secret -= item
end
end
end
It would be great of I could just do something like:
as_bitwise :secret_name_first, :column=>'secret', :value=>4
as_bitwise :secret_name_last, :column=>'secret', :value=>2
Or even,
as_bitwise :secret, { :secret_name_last=>4, :secret_name_first=>2 }
EDIT
Based on Brandan's excellent answer, this is what I've got currently:
module BitwiseColumn
extend ActiveSupport::Concern
module ClassMethods
def bitwise_column(*args)
mapping = args.extract_options!
column_name = args.shift
real_column_name = args.shift
logger.debug "Initializing bitwisecolumn, column: " + column_name.to_s
mapping.each_pair do |attribute, offset|
logger.debug "\tSetting a pair: offset: " + offset.to_s + ", " + attribute.to_s
mask = 2 ** offset
class_eval %{
attr_accessible :#{column_name}_#{attribute}
def #{column_name}_#{attribute}?
#{real_column_name} & #{mask} > 0 unless #{real_column_name}.nil?
end
def #{column_name}_#{attribute}=(value)
if self.#{real_column_name}.nil?
self.#{real_column_name} = 0
end
if value == "1" || value == true || value == 1
if self.#{real_column_name} & #{mask} == 0
self.#{real_column_name} += #{mask}
end
else
if self.#{real_column_name} & #{mask} > 0
self.#{real_column_name} -= #{mask}
end
end
end
}
end
end
end
end
This allows me to use:
bitwise_column :secret, :realsecretcolumnatdatabase, :name_last=>1, :name_first=>2, :email=>3, :picture=>5, :dob=>6, :place=>12
After that, I can call User.first.secret_name_last? etc.
You can use class_eval to DRY up your code quite a bit. I'd also suggest factoring this behavior into some kind of a module separate from your User class so that you can test it thoroughly and separately from other User-specific behavior.
Like you, I tend to start these kinds of tasks with the desired API and work backwards. I started with this in my model:
class User < ActiveRecord::Base
include BitwiseColumn
bitwise_column :secret, :first_name => 1, :last_name => 2
end
The hash passed to bitwise_column maps the virtual attribute names to their mask value as an exponent. I felt like that was easier to manage than having to remember the powers of 2 myself :-)
Then I created the mixin:
module BitwiseColumn
extend ActiveSupport::Concern
module ClassMethods
def bitwise_column(*args)
mapping = args.extract_options!
column_name = args.shift
mapping.each_pair do |attribute, offset|
mask = 2 ** offset
class_eval %{
def secret_#{attribute}?
#{column_name} & #{mask} > 0 unless #{column_name}.nil?
end
def secret_#{attribute}=(value)
if self.#{column_name}.nil?
self.#{column_name} = 0
end
if value == "1" || value == true || value == 1
if self.#{column_name} & #{mask} == 0
self.#{column_name} += #{mask}
end
else
if self.#{column_name} & #{mask} > 0
self.#{column_name} -= #{mask}
end
end
end
}
end
end
end
end
This mixin creates two instance methods for each virtual attribute, one with a ? and one with a =, since that seems to be what you're after. I used your existing logic for the bitwise operations, which seems to work perfectly.

DRY function in a better way

could you tell me a way to write this code in a better way ?
Create an user with data attributes fetched from facebook .
User.create do |user|
user.email = data['email']
user.password = Devise.friendly_token
user.facebook_uid = data['facebook_uid']
user.first_name = data['first_name']
user.last_name = data['last_name']
user.gender = data['gender']
user.timezone = data['timezone']
user.birthday = data['birthday']
user.link = data['link']
user.locale = data['locale']
user.picture_url = 'https://graph.facebook.com/' + data['facebook_uid'] + '/picture?type=large'
user.fb_access_token = access_token
end
hash['picture_url'] = something
User.create(hash)
Solution 1
User.create do |user|
data.each do |k, v|
user.send("#{k}=", v) if User.columns_hash.has_key?(k)
end
user.fb_access_token = access_token
user.password = Devise.friendly_token
user.picture_url = "https://graph.facebook.com/%s/picture?type=large" %
data['facebook_uid']
end
Solution 2
hash = {}
data.each do |k, v|
hash[k]= v if User.columns_hash.has_key?(k)
end
hash['fb_access_token'] = access_token
hash['password'] = Devise.friendly_token
hash['picture_url'] = "https://graph.facebook.com/%s/picture?type=large" %
data['facebook_uid']
User.create(hash)
ActiveRecord's create method can take a hash of attributes. Since your hash keys match the accessor methods exactly, you can just pass in User.create(hash) after setting data['picture_url'] the same way you did before. Tass's answer is essentially correct, but to more precisely match your code:
data['picture_url'] = 'https://graph.facebook.com/' + data['facebook_uid'] + '/picture?type=large'
User.create(data)

Resources