billfold 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. data/.gitignore +1 -0
  2. data/.rvmrc +1 -0
  3. data/Gemfile +3 -0
  4. data/README.md +39 -0
  5. data/Rakefile +26 -0
  6. data/VERSION +1 -0
  7. data/app/controllers/billfold/identities_controller.rb +53 -0
  8. data/app/views/billfold/identities/_list.html.erb +8 -0
  9. data/app/views/billfold/identities/index.html.erb +2 -0
  10. data/billfold.gemspec +24 -0
  11. data/config/locales/en.yml +6 -0
  12. data/config/routes.rb +11 -0
  13. data/lib/billfold.rb +56 -0
  14. data/lib/billfold/active_record_identity.rb +47 -0
  15. data/lib/billfold/active_record_user.rb +46 -0
  16. data/lib/billfold/controller_support.rb +51 -0
  17. data/lib/billfold/engine.rb +25 -0
  18. data/lib/billfold/identity.rb +99 -0
  19. data/lib/billfold/user.rb +25 -0
  20. data/lib/rails/generators/billfold/migration_generator.rb +27 -0
  21. data/lib/rails/generators/billfold/models_generator.rb +14 -0
  22. data/lib/rails/generators/billfold/templates/identity.rb +5 -0
  23. data/lib/rails/generators/billfold/templates/migration.rb +23 -0
  24. data/lib/rails/generators/billfold/templates/user.rb +5 -0
  25. data/spec/billfold_spec.rb +23 -0
  26. data/spec/controllers/identities_controller_spec.rb +31 -0
  27. data/spec/dummy/.gitignore +5 -0
  28. data/spec/dummy/README +261 -0
  29. data/spec/dummy/Rakefile +7 -0
  30. data/spec/dummy/app/assets/images/rails.png +0 -0
  31. data/spec/dummy/app/assets/javascripts/application.js +9 -0
  32. data/spec/dummy/app/assets/stylesheets/application.css +7 -0
  33. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  34. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  35. data/spec/dummy/app/mailers/.gitkeep +0 -0
  36. data/spec/dummy/app/models/.gitkeep +0 -0
  37. data/spec/dummy/app/models/identity.rb +5 -0
  38. data/spec/dummy/app/models/user.rb +5 -0
  39. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  40. data/spec/dummy/config.ru +4 -0
  41. data/spec/dummy/config/application.rb +48 -0
  42. data/spec/dummy/config/boot.rb +6 -0
  43. data/spec/dummy/config/database.yml +25 -0
  44. data/spec/dummy/config/environment.rb +5 -0
  45. data/spec/dummy/config/environments/development.rb +30 -0
  46. data/spec/dummy/config/environments/production.rb +60 -0
  47. data/spec/dummy/config/environments/test.rb +42 -0
  48. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  49. data/spec/dummy/config/initializers/inflections.rb +10 -0
  50. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  51. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  52. data/spec/dummy/config/initializers/session_store.rb +8 -0
  53. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  54. data/spec/dummy/config/locales/en.yml +5 -0
  55. data/spec/dummy/config/routes.rb +58 -0
  56. data/spec/dummy/db/migrate/20110904204023_create_users_and_identities.rb +23 -0
  57. data/spec/dummy/db/schema.rb +31 -0
  58. data/spec/dummy/db/seeds.rb +7 -0
  59. data/spec/dummy/doc/README_FOR_APP +2 -0
  60. data/spec/dummy/lib/assets/.gitkeep +0 -0
  61. data/spec/dummy/lib/tasks/.gitkeep +0 -0
  62. data/spec/dummy/log/.gitkeep +0 -0
  63. data/spec/dummy/public/404.html +26 -0
  64. data/spec/dummy/public/422.html +26 -0
  65. data/spec/dummy/public/500.html +26 -0
  66. data/spec/dummy/public/favicon.ico +0 -0
  67. data/spec/dummy/public/index.html +241 -0
  68. data/spec/dummy/public/robots.txt +5 -0
  69. data/spec/dummy/script/rails +6 -0
  70. data/spec/dummy/test/fixtures/.gitkeep +0 -0
  71. data/spec/dummy/test/functional/.gitkeep +0 -0
  72. data/spec/dummy/test/integration/.gitkeep +0 -0
  73. data/spec/dummy/test/performance/browsing_test.rb +12 -0
  74. data/spec/dummy/test/test_helper.rb +13 -0
  75. data/spec/dummy/test/unit/.gitkeep +0 -0
  76. data/spec/dummy/vendor/assets/stylesheets/.gitkeep +0 -0
  77. data/spec/dummy/vendor/plugins/.gitkeep +0 -0
  78. data/spec/factories/identity_factories.rb +18 -0
  79. data/spec/factories/user_factories.rb +7 -0
  80. data/spec/models/identity_spec.rb +123 -0
  81. data/spec/spec_helper.rb +30 -0
  82. metadata +260 -0
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ Gemfile.lock
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm 1.9.2
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'http://rubygems.org'
2
+
3
+ gemspec
data/README.md ADDED
@@ -0,0 +1,39 @@
1
+ ## Billfold
2
+
3
+ Billfold provides backend Rails support for OmniAuth. Specifically, it
4
+ routes `/auth/:provider/callback` to
5
+ `Billfold::IdentitiesController#update_or_create!`, which handles
6
+ identity management.
7
+
8
+ ### Requirements
9
+
10
+ * Rails 3.x
11
+
12
+ ### Installation
13
+
14
+ 1. Add `gem 'billfold'` to your `Gemfile`
15
+ 1. Run `bundle` (or `bundle install`)
16
+ 1. Run `rails g billfold:migration`
17
+
18
+ ### Configuration
19
+
20
+ #### With ActiveRecord
21
+
22
+ If you don't have User and Identity model classes, run
23
+ `rails g billfold:models` to create them. Otherwise, include
24
+ `Billfold::ActiveRecordUser` and `Billfold::ActiveRecordIdentity` in
25
+ them respectively. You *may* wish to define
26
+ `User#perform_additional_merge_operations!` if you need to do additional
27
+ logic during a user merge.
28
+
29
+ #### Without ActiveRecord
30
+
31
+ Include `Billfold::User` and `Billfold::Identity` in the model classes.
32
+ You'll also have do define the following methods:
33
+
34
+ * `User.find_by_id(id)`
35
+ * `User#merge_into!(other_user)`
36
+ * `Identity.with_provider_and_value(provider, value)`
37
+ * `Identity#user`
38
+ * `Identity#update_attributes!`
39
+ * `Identity#save!`
data/Rakefile ADDED
@@ -0,0 +1,26 @@
1
+ # encoding: UTF-8
2
+ require 'rubygems'
3
+ begin
4
+ require 'bundler/setup'
5
+ rescue LoadError
6
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
7
+ end
8
+
9
+ require 'rake'
10
+ require 'rdoc/task'
11
+
12
+ require 'rspec/core'
13
+ require 'rspec/core/rake_task'
14
+ RSpec::Core::RakeTask.new(:spec)
15
+
16
+ desc "Default: run the unit tests."
17
+ task :default => [:spec]
18
+
19
+ Rake::RDocTask.new(:rdoc) do |rdoc|
20
+ rdoc.rdoc_dir = 'rdoc'
21
+ rdoc.title = "Billfold #{File.read './VERSION'}"
22
+ rdoc.options << '--line-numbers' << '--inline-source'
23
+ rdoc.rdoc_files.include('README.rdoc')
24
+ rdoc.rdoc_files.include('lib/**/*.rb')
25
+ rdoc.rdoc_files.include('app/**/*.rb')
26
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
@@ -0,0 +1,53 @@
1
+ module Billfold
2
+ class IdentitiesController < ApplicationController
3
+
4
+ before_filter :require_sign_in, :only => [ :index, :destroy ]
5
+
6
+ respond_to :html, :json, :xml
7
+ helper_method :identities, :identity
8
+
9
+ def index
10
+ respond_to do |format|
11
+ format.html {}
12
+ format.json { render :json => identities }
13
+ format.xml { render :xml => identities }
14
+ end
15
+ end
16
+
17
+ def update_or_create
18
+ omniauth_hash = request.env['omniauth.auth'] || {}
19
+ identity = Billfold.identity_class.update_or_create!({
20
+ :provider => params[:provider],
21
+ :value => omniauth_hash['uid'],
22
+ :data => omniauth_hash['user_info'],
23
+ :user => current_user
24
+ })
25
+ self.current_user ||= identity.user
26
+ respond_to do |format|
27
+ format.html { redirect_to '/' }
28
+ format.json { head 201 }
29
+ format.xml { head 201 }
30
+ end
31
+ end
32
+
33
+ def destroy
34
+ identity.destroy
35
+ respond_to do |format|
36
+ format.html { redirect_to :index }
37
+ format.json { head 200 }
38
+ format.xml { head 200 }
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def identities
45
+ @identities ||= current_user.identities
46
+ end
47
+
48
+ def identity
49
+ @identity ||= current_user.identities.find_by_id(params[:identity_id] || params[:id])
50
+ end
51
+
52
+ end
53
+ end
@@ -0,0 +1,8 @@
1
+ <ul class='identities'>
2
+ <% identities.each do |id| %>
3
+ <li class="identity <%= id.provider %>">
4
+ <%= id.value %>
5
+ <%= button_to t('billfold.delete'), identity_path(id), :method => :delete, :remote => true, :confirm => true %>
6
+ </li>
7
+ <% end %>
8
+ </ul>
@@ -0,0 +1,2 @@
1
+ <h2><%= t '.title' %></h2>
2
+ <%= render :partial => 'list', :identities => identities %>
data/billfold.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |gem|
4
+ gem.name = 'billfold'
5
+ gem.version = File.read('VERSION')
6
+ gem.description = %q{Identity Management with OmniAuth}
7
+ gem.summary = gem.description
8
+ gem.email = ['james.a.rosen@gmail.com']
9
+ gem.homepage = 'http://github.com/jamesarosen/billfold'
10
+ gem.authors = ['James A. Rosen']
11
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{|f| File.basename(f)}
12
+ gem.files = `git ls-files`.split("\n")
13
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
14
+ gem.require_paths = ['lib']
15
+ gem.required_rubygems_version = Gem::Requirement.new('>= 1.3.6') if gem.respond_to? :required_rubygems_version=
16
+
17
+ gem.add_development_dependency 'rails', '~> 3.1'
18
+ gem.add_development_dependency 'bundler'
19
+ gem.add_development_dependency 'mocha'
20
+ gem.add_development_dependency 'rake'
21
+ gem.add_development_dependency 'rspec-rails'
22
+ gem.add_development_dependency 'sqlite3'
23
+ gem.add_development_dependency 'factory_girl'
24
+ end
@@ -0,0 +1,6 @@
1
+ en:
2
+ billfold:
3
+ delete: delete
4
+ identities:
5
+ index:
6
+ title: Your Identities
data/config/routes.rb ADDED
@@ -0,0 +1,11 @@
1
+ Rails.application.routes.draw do
2
+
3
+ mount_at = Billfold::Engine.config.mount_at
4
+
5
+ post "#{mount_at}auth/:provider/callback" => 'billfold/identities#update_or_create'
6
+
7
+ resources :identities, :only => [ :index, :destroy ],
8
+ :module => 'billfold',
9
+ :path => "#{mount_at}/identities"
10
+
11
+ end
data/lib/billfold.rb ADDED
@@ -0,0 +1,56 @@
1
+ module Billfold
2
+
3
+ autoload :ControllerSupport, 'billfold/controller_support'
4
+ autoload :Identity, 'billfold/identity'
5
+ autoload :User, 'billfold/user'
6
+ autoload :ActiveRecordIdentity, 'billfold/active_record_identity'
7
+ autoload :ActiveRecordUser, 'billfold/active_record_user'
8
+
9
+ class <<self
10
+ # ## Billfold.user_class
11
+ #
12
+ # Used by `Billfold::Identity.update_or_create!` when building new users
13
+ # and `Billfold::ControllerSupport` when looking up the current user
14
+ # from the session. Calculated from `Billfold.user_class_name`.
15
+ def user_class
16
+ constantize user_class_name
17
+ end
18
+
19
+ # Used by `Billfold::ActiveRecordIdentity` for the `belongs_to :user`
20
+ # association and by `Billfold.user_class` for getting the actual
21
+ # class. By default, "User"
22
+ def user_class_name
23
+ @user_class ||= 'User'
24
+ end
25
+
26
+ attr_writer :user_class_name
27
+
28
+ # ## Billfold.identity_class
29
+ #
30
+ # Used by `Billfold::IdentitiesController.update_or_create`. Calculated
31
+ # from `Billfold.identity_class_name`.
32
+ def identity_class
33
+ constantize identity_class_name
34
+ end
35
+
36
+ def identity_class_name
37
+ @identity_class ||= 'Identity'
38
+ end
39
+
40
+ attr_writer :identity_class_name
41
+
42
+ private
43
+
44
+ def constantize(string)
45
+ return nil if string.blank?
46
+ return string.constantize if string.respond_to?(:constantize)
47
+ string.to_s.split('::').inject(Object) do |memo, name|
48
+ raise "#{memo}::#{name} does not exist" unless memo.const_defined?(name)
49
+ memo = memo.const_get(name)
50
+ end
51
+ end
52
+ end
53
+
54
+ require 'billfold/identity'
55
+ require 'billfold/engine' if defined?(Rails) && Rails::VERSION::MAJOR == 3
56
+ end
@@ -0,0 +1,47 @@
1
+ require 'active_support/concern'
2
+ require 'billfold/identity'
3
+
4
+ module Billfold
5
+
6
+ # ## Billfold::ActiveRecordIdentity
7
+ #
8
+ # Support for an ActiveRecord Identity class. (Builds on top of
9
+ # `Billfold::Identity`)
10
+ module ActiveRecordIdentity
11
+
12
+ extend ActiveSupport::Concern
13
+
14
+ included do
15
+ raise "Cannot define identity class until Billfold.user_class_name is set" unless Billfold.user_class_name.present?
16
+ belongs_to :user, :class_name => ::Billfold.user_class_name
17
+ serialize :data
18
+ validates_presence_of :user, :provider, :value
19
+
20
+ # There can only be one identity with a given provider and value
21
+ validates_uniqueness_of :value, :scope => [ :provider ]
22
+ end
23
+
24
+ module ClassMethods
25
+
26
+ include Billfold::Identity::ClassMethods
27
+
28
+ # ### Billfold::Identity.with_provider_and_value
29
+ #
30
+ # Return the identity with the given `provider` and `value`, or `nil`,
31
+ # if no such identity exists. Including classes *must* redefine this
32
+ # method.
33
+ def with_provider_and_value(provider, value)
34
+ where(:provider => provider, :value => value).first
35
+ end
36
+
37
+ end
38
+
39
+ module InstanceMethods
40
+
41
+ include Billfold::Identity::InstanceMethods
42
+
43
+ end
44
+
45
+ end
46
+
47
+ end
@@ -0,0 +1,46 @@
1
+ require 'active_support/concern'
2
+ require 'billfold/user'
3
+
4
+ module Billfold
5
+
6
+ # ## Billfold::ActiveRecordUser
7
+ #
8
+ # Support for an ActiveRecord User class. (Builds on top of
9
+ # `Billfold::User`)
10
+ module ActiveRecordUser
11
+
12
+ extend ActiveSupport::Concern
13
+
14
+ included do
15
+ raise "Cannot define user class until Billfold.identity_class_name is set" unless Billfold.identity_class_name.present?
16
+ validates_presence_of :name
17
+ has_many :identities, :class_name => ::Billfold.identity_class_name
18
+ end
19
+
20
+ module InstanceMethods
21
+
22
+ # Merge this user into another user, deleting this user and moving its
23
+ # identities to the other.
24
+ def merge_into!(other)
25
+ raise ArgumentError.new("#{other} is not a #{Billfold.user_class}") unless other.kind_of?(Billfold.user_class)
26
+ raise ArgumentError.new("#{other} is not saved") if other.new_record?
27
+ transaction do
28
+ identities.update_all({ :user_id => other.id })
29
+ perform_additional_merge_operations!(other)
30
+ destroy
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ # This method is called after all identities have been moved from
37
+ # `other` to `self`, but before `other` has been destroyed and before the
38
+ # end of the transaction. By default, it does nothing.
39
+ def perform_additional_merge_operations!(other)
40
+ end
41
+
42
+ end
43
+
44
+ end
45
+
46
+ end
@@ -0,0 +1,51 @@
1
+ require 'active_support/concern'
2
+ require 'billfold'
3
+
4
+ module Billfold
5
+
6
+ # ## Billfold::ControllerSupport
7
+ #
8
+ # Gets mixed in to `ApplicationController` automatically.
9
+ module ControllerSupport
10
+
11
+ extend ActiveSupport::Concern
12
+
13
+ included do
14
+ helper_method :current_user
15
+ end
16
+
17
+ module InstanceMethods
18
+
19
+ protected
20
+
21
+ # ### Billfold::ControllerSupport#current_user
22
+ #
23
+ # Return the current user, if signed in.
24
+ def current_user
25
+ @current_user ||= ::Billfold::user_class.find_by_id(session[:user_id])
26
+ end
27
+
28
+ # ### Billfold::ControllerSupport#current_user=
29
+ #
30
+ # Set the signed-in user.
31
+ def current_user=(user)
32
+ session[:user_id] = user ? user.id : nil
33
+ @current_user = user
34
+ end
35
+
36
+ # ### Billfold::ControllerSupport#require_sign_in
37
+ #
38
+ # A before filter that requires a user to be signed in. The default
39
+ # implementation adds a flash message and redirects to /
40
+ def require_sign_in
41
+ unless current_user
42
+ flash['info'] = 'Please sign in'
43
+ redirect_to root_path
44
+ end
45
+ end
46
+
47
+ end
48
+
49
+ end
50
+
51
+ end
@@ -0,0 +1,25 @@
1
+ require 'billfold'
2
+ require 'rails'
3
+
4
+ module Billfold
5
+ class Engine < Rails::Engine
6
+ engine_name :my_rails_engine
7
+
8
+ # Config defaults
9
+ config.mount_at = '/'
10
+
11
+ # Check the gem config
12
+ initializer "check config" do |app|
13
+ # make sure mount_at ends with trailing slash
14
+ config.mount_at += '/' unless config.mount_at.last == '/'
15
+ end
16
+
17
+ initializer "static assets" do |app|
18
+ app.middleware.use ::ActionDispatch::Static, "#{root}/public"
19
+ end
20
+
21
+ ActiveSupport.on_load(:action_controller) do
22
+ include Billfold::ControllerSupport
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,99 @@
1
+ require 'billfold'
2
+
3
+ module Billfold
4
+
5
+ # ## Billfold::Identity
6
+ #
7
+ # Support for an identity class. If you are using ActiveRecord, you
8
+ # probably want `Billfold::ActiveRecordIdentity`, which includes everything
9
+ # in this module.
10
+ #
11
+ # Requires the following attributes
12
+ # * `user` -- the owning `Billfold::User` instance; required
13
+ # * `provider` -- the OmniAuth provider name (e.g. "twitter"); required
14
+ # * `value` -- the unique identifier (within the scope of `provider`) of
15
+ # the identity (e.g. "my_twitter_handle"); required
16
+ # * `data` -- additional information passed in from OmniAuth under the
17
+ # `"user_info"` key
18
+ module Identity
19
+
20
+ def self.included(base)
21
+ base.extend ClassMethods
22
+ base.send(:include, InstanceMethods)
23
+ end
24
+
25
+ module ClassMethods
26
+
27
+ # ### Billfold::Identity.with_provider_and_value
28
+ #
29
+ # Return the identity with the given `provider` and `value`, or `nil`,
30
+ # if no such identity exists. Including classes *must* redefine this
31
+ # method.
32
+ def with_provider_and_value(provider, value)
33
+ raise NotImplementedError.new('classes including Billfold::Identity MUST redefine class method with_provider_and_value')
34
+ end
35
+
36
+ # ### Billfold::Identity.update_or_create!
37
+ #
38
+ # Updates or creates a `Billfold::Identity`.
39
+ #
40
+ # ### Parameters: a single `Hash` with the following keys:
41
+ # * `:provider` -- the name of an OmniAuth provider (e.g. "twitter")
42
+ # * `:user` -- if nil, creates a new `User` for the `Identity`
43
+ # * `:value` -- the unique identifier
44
+ # * `:data` -- extra data for the Identity
45
+ #
46
+ # ### Behavior
47
+ #
48
+ # If `:provider` or `:value` is `nil`, this method raises an
49
+ # `ArgumentError`.
50
+ #
51
+ # If `:user` is `nil`, this method creates a new `User` for the `Identity`.
52
+ #
53
+ # If `:user` exists and there is no other `Identity` with the
54
+ # same `:value`, this method adds a new `Identity` to the `User`.
55
+ #
56
+ # If there exists another `Identity` with the same `:value` and
57
+ # that `Identity` is owned by the given `User`, this method updates that
58
+ # `Identity`.
59
+ #
60
+ # If `:user` exists and there exists another `Identity` with
61
+ # the same `:value` and that `Identity` is owned by a *different* `User`,
62
+ # this method merges that User into `:user` and updates the `Identity`.
63
+ def update_or_create!(attributes = {})
64
+ raise ArgumentError.new("provider must not be blank") if attributes[:provider].blank?
65
+ raise ArgumentError.new("value must not be blank") if attributes[:value].blank?
66
+ identity = with_provider_and_value(attributes[:provider], attributes[:value])
67
+ if identity
68
+ old_owner, new_owner = identity.user, attributes[:user]
69
+ transaction do
70
+ identity.update_attributes!(attributes)
71
+ old_owner.merge_into!(new_owner) if old_owner != new_owner
72
+ end
73
+ else
74
+ identity = new(attributes)
75
+ identity.user = attributes[:user] || ::Billfold.user_class.new(:name => identity.name_for_user)
76
+ identity.save!
77
+ end
78
+ identity
79
+ end
80
+
81
+ end
82
+
83
+ module InstanceMethods
84
+
85
+ # ### Billfold::Identity#name_for_user
86
+ #
87
+ # When creating a new user from an identity, this method is used to
88
+ # generate a display name. By default, it tries to get the user's name
89
+ # from the OmniAuth data and falls back on using the identity's provider
90
+ # and value, but subclasses may have something better to do.
91
+ def name_for_user
92
+ (data && data['name']) || "#{provider} #{value}"
93
+ end
94
+
95
+ end
96
+
97
+ end
98
+
99
+ end