diff --git a/app/models/badge_type.rb b/app/models/badge_type.rb index abebf9495dc..74551256780 100644 --- a/app/models/badge_type.rb +++ b/app/models/badge_type.rb @@ -8,10 +8,10 @@ end # # Table name: badge_types # -# id :integer not null, primary key -# name :string(255) not null -# created_at :datetime -# updated_at :datetime +# id :integer not null, primary key +# name :string(255) not null +# created_at :datetime +# updated_at :datetime # # Indexes # diff --git a/app/models/discourse_single_sign_on.rb b/app/models/discourse_single_sign_on.rb index a630fa7aa19..26c53065806 100644 --- a/app/models/discourse_single_sign_on.rb +++ b/app/models/discourse_single_sign_on.rb @@ -60,6 +60,10 @@ class DiscourseSingleSignOn < SingleSignOn user.enqueue_welcome_message('welcome_user') end + custom_fields.each do |k,v| + user.custom_fields[k] = v + end + # optionally save the user and sso_record if they have changed user.save! sso_record.save! diff --git a/app/models/user.rb b/app/models/user.rb index 80d3c3bdf55..8bda6da5520 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -32,6 +32,7 @@ class User < ActiveRecord::Base has_many :invites, dependent: :destroy has_many :topic_links, dependent: :destroy has_many :uploads + has_many :user_custom_fields, dependent: :destroy has_one :facebook_user_info, dependent: :destroy has_one :twitter_user_info, dependent: :destroy @@ -68,6 +69,7 @@ class User < ActiveRecord::Base after_save :update_tracked_topics after_save :clear_global_notice_if_needed + after_save :save_custom_fields after_create :create_email_token after_create :create_user_stat @@ -587,8 +589,35 @@ class User < ActiveRecord::Base nil end + def custom_fields + @custom_fields ||= begin + @custom_fields_orig = Hash[*user_custom_fields.pluck(:name,:value).flatten] + @custom_fields_orig.dup + end + end + protected + def save_custom_fields + if @custom_fields && @custom_fields_orig != @custom_fields + dup = @custom_fields.dup + + user_custom_fields.each do |f| + if dup[f.name] != f.value + f.destroy + else + dup.remove[f.name] + end + end + + dup.each do |k,v| + user_custom_fields.create(name: k, value: v) + end + + @custom_fields_orig = @custom_fields + end + end + def cook if bio_raw.present? self.bio_cooked = PrettyText.cook(bio_raw, omit_nofollow: self.has_trust_level?(:leader)) if bio_raw_changed? diff --git a/app/models/user_custom_field.rb b/app/models/user_custom_field.rb new file mode 100644 index 00000000000..887abd6bd19 --- /dev/null +++ b/app/models/user_custom_field.rb @@ -0,0 +1,19 @@ +class UserCustomField < ActiveRecord::Base + belongs_to :user +end + +# == Schema Information +# +# Table name: user_custom_fields +# +# id :integer not null, primary key +# user_id :integer not null +# name :string(256) not null +# value :text +# created_at :datetime +# updated_at :datetime +# +# Indexes +# +# index_user_custom_fields_on_user_id_and_name (user_id,name) +# diff --git a/db/migrate/20140421235646_add_user_custom_fields.rb b/db/migrate/20140421235646_add_user_custom_fields.rb new file mode 100644 index 00000000000..59c36589467 --- /dev/null +++ b/db/migrate/20140421235646_add_user_custom_fields.rb @@ -0,0 +1,12 @@ +class AddUserCustomFields < ActiveRecord::Migration + def change + create_table :user_custom_fields do |t| + t.integer :user_id, null: false + t.string :name, limit: 256, null: false + t.text :value + t.timestamps + end + + add_index :user_custom_fields, [:user_id, :name] + end +end diff --git a/lib/single_sign_on.rb b/lib/single_sign_on.rb index 977b89ee3a5..ae89265f9ee 100644 --- a/lib/single_sign_on.rb +++ b/lib/single_sign_on.rb @@ -1,5 +1,7 @@ class SingleSignOn - ACCESSORS = [:nonce, :name, :username, :email, :about_me, :external_email, :external_username, :external_name, :external_id] + ACCESSORS = [:nonce, :name, :username, :email, + :about_me, :external_email, :external_username, + :external_name, :external_id] FIXNUMS = [] NONCE_EXPIRY_TIME = 10.minutes @@ -14,14 +16,6 @@ class SingleSignOn raise RuntimeError, "sso_url not implemented on class, be sure to set it on instance" end - def sso_secret - @sso_secret || self.class.sso_secret - end - - def sso_url - @sso_url || self.class.sso_url - end - def self.parse(payload, sso_secret = nil) sso = new sso.sso_secret = sso_secret if sso_secret @@ -39,9 +33,33 @@ class SingleSignOn val = val.to_i if FIXNUMS.include? k sso.send("#{k}=", val) end + + decoded_hash.each do |k,v| + # 1234567 + # custom. + # + if k[0..6] == "custom." + field = k[7..-1] + sso.custom_fields[field] = v + end + end + sso end + def sso_secret + @sso_secret || self.class.sso_secret + end + + def sso_url + @sso_url || self.class.sso_url + end + + def custom_fields + @custom_fields ||= {} + end + + def sign(payload) OpenSSL::HMAC.hexdigest("sha256", sso_secret, payload) end @@ -65,6 +83,12 @@ class SingleSignOn payload[k] = val end + if @custom_fields + @custom_fields.each do |k,v| + payload["custom.#{k}"] = v.to_s + end + end + Rack::Utils.build_query(payload) end diff --git a/spec/controllers/session_controller_spec.rb b/spec/controllers/session_controller_spec.rb index 8813417909c..2bc5e7426a3 100644 --- a/spec/controllers/session_controller_spec.rb +++ b/spec/controllers/session_controller_spec.rb @@ -43,7 +43,6 @@ describe SessionController do response.should redirect_to('/') logged_on_user = Discourse.current_user_provider.new(request.env).current_user logged_on_user.email.should == user.email - logged_on_user.single_sign_on_record.external_id.should == "abc" logged_on_user.single_sign_on_record.external_username.should == 'sam' end @@ -54,12 +53,17 @@ describe SessionController do sso.email = 'bob@bob.com' sso.name = 'Sam Saffron' sso.username = 'sam' + sso.custom_fields["shop_url"] = "http://my_shop.com" + sso.custom_fields["shop_name"] = "Sam" get :sso_login, Rack::Utils.parse_query(sso.payload) response.should redirect_to('/a/') logged_on_user = Discourse.current_user_provider.new(request.env).current_user + # ensure nothing is transient + logged_on_user = User.find(logged_on_user.id) + logged_on_user.email.should == 'bob@bob.com' logged_on_user.name.should == 'Sam Saffron' logged_on_user.username.should == 'sam' @@ -67,8 +71,11 @@ describe SessionController do logged_on_user.single_sign_on_record.external_id.should == "666" logged_on_user.single_sign_on_record.external_username.should == 'sam' logged_on_user.active.should == true + logged_on_user.custom_fields["shop_url"].should == "http://my_shop.com" + logged_on_user.custom_fields["shop_name"].should == "Sam" + logged_on_user.custom_fields["bla"].should == nil end - + it 'allows login to existing account with valid nonce' do sso = get_sso('/hello/world') sso.external_id = '997' diff --git a/spec/models/discourse_single_sign_on_spec.rb b/spec/models/discourse_single_sign_on_spec.rb index eb242218529..c65007bec0e 100644 --- a/spec/models/discourse_single_sign_on_spec.rb +++ b/spec/models/discourse_single_sign_on_spec.rb @@ -19,6 +19,8 @@ describe DiscourseSingleSignOn do sso.username = "sam" sso.name = "sam saffron" sso.external_id = "100" + sso.custom_fields["a"] = "Aa" + sso.custom_fields["b.b"] = "B.b" sso end @@ -28,6 +30,8 @@ describe DiscourseSingleSignOn do parsed.username.should == sso.username parsed.name.should == sso.name parsed.external_id.should == sso.external_id + parsed.custom_fields["a"].should == "Aa" + parsed.custom_fields["b.b"].should == "B.b" end it "can fill in data on way back" do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index fcd8e635f88..ae13a2523eb 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1149,4 +1149,29 @@ describe User do end + describe "custom fields" do + it "allows modification of custom fields" do + user = Fabricate(:user) + + user.custom_fields["a"].should == nil + + user.custom_fields["bob"] = "marley" + user.custom_fields["jack"] = "black" + user.save + + user = User.find(user.id) + + user.custom_fields["bob"].should == "marley" + user.custom_fields["jack"].should == "black" + + user.custom_fields.delete("bob") + user.custom_fields["jack"] = "jill" + + user.save + user = User.find(user.id) + + user.custom_fields.should == {"jack" => "jill"} + end + end + end