theworkinggroup-wristband 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. data/README.rdoc +130 -0
  2. data/Rakefile +39 -0
  3. data/generators/wristband/templates/app/controllers/sessions_controller.rb +50 -0
  4. data/generators/wristband/templates/app/controllers/users_controller.rb +47 -0
  5. data/generators/wristband/templates/app/models/user.rb +30 -0
  6. data/generators/wristband/templates/app/models/user_notifier.rb +54 -0
  7. data/generators/wristband/templates/app/views/sessions/new.html.haml +17 -0
  8. data/generators/wristband/templates/app/views/user_notifier/email_verification.text.html.rhtml +7 -0
  9. data/generators/wristband/templates/app/views/user_notifier/email_verification.text.plain.rhtml +9 -0
  10. data/generators/wristband/templates/app/views/user_notifier/forgot_password.text.html.rhtml +10 -0
  11. data/generators/wristband/templates/app/views/user_notifier/forgot_password.text.plain.rhtml +10 -0
  12. data/generators/wristband/templates/app/views/users/forgot_password.html.haml +10 -0
  13. data/generators/wristband/templates/app/views/users/index.html.haml +6 -0
  14. data/generators/wristband/templates/db/migrate/create_wristband_tables.rb +28 -0
  15. data/generators/wristband/wristband_generator.rb +69 -0
  16. data/init.rb +1 -0
  17. data/lib/wristband.rb +121 -0
  18. data/lib/wristband/application_extensions.rb +78 -0
  19. data/lib/wristband/authority_check.rb +160 -0
  20. data/lib/wristband/support.rb +28 -0
  21. data/lib/wristband/user_extensions.rb +89 -0
  22. data/test/database.yml +3 -0
  23. data/test/fixtures/crypted_password_users.yml +12 -0
  24. data/test/fixtures/plain_text_password_users.yml +4 -0
  25. data/test/fixtures/users.yml +23 -0
  26. data/test/schema.rb +34 -0
  27. data/test/test_helper.rb +40 -0
  28. data/test/unit/crypted_password_users_test.rb +81 -0
  29. data/test/unit/has_authorities_test.rb +49 -0
  30. data/test/unit/plain_text_password_user_test.rb +92 -0
  31. data/wristband.gemspec +32 -0
  32. metadata +97 -0
@@ -0,0 +1,69 @@
1
+ class WristbandGenerator < Rails::Generator::Base
2
+ def manifest
3
+ record do |m|
4
+ # User files
5
+ m.directory 'app/models'
6
+ m.template 'app/models/user.rb', 'app/models/user.rb'
7
+ m.directory 'app/controllers'
8
+ m.template 'app/controllers/users_controller.rb', 'app/controllers/users_controller.rb'
9
+ m.directory 'app/views/users'
10
+ m.template 'app/views/users/index.html.haml', 'app/views/users/index.html.haml'
11
+
12
+ # User notifier files
13
+ m.directory 'app/models'
14
+ m.template 'app/models/user_notifier.rb', 'app/models/user_notifier.rb'
15
+ m.directory 'app/views/user_notifier'
16
+ m.file 'app/views/user_notifier/forgot_password.text.html.rhtml', 'app/views/user_notifier/forgot_password.text.html.rhtml'
17
+ m.file 'app/views/user_notifier/forgot_password.text.plain.rhtml', 'app/views/user_notifier/forgot_password.text.plain.rhtml'
18
+ m.file 'app/views/user_notifier/email_verification.text.html.rhtml', 'app/views/user_notifier/email_verification.text.html.rhtml'
19
+ m.file 'app/views/user_notifier/email_verification.text.plain.rhtml', 'app/views/user_notifier/email_verification.text.plain.rhtml'
20
+
21
+
22
+ # Session files
23
+ m.directory 'app/controllers'
24
+ m.template 'app/controllers/sessions_controller.rb', 'app/controllers/sessions_controller.rb'
25
+ m.directory 'app/views/sessions'
26
+ m.template 'app/views/sessions/new.html.haml', 'app/views/sessions/new.html.haml'
27
+
28
+ # Migrations
29
+ m.directory 'db/migrate'
30
+ m.migration_template 'db/migrate/create_wristband_tables.rb', 'db/migrate', :migration_file_name => "create_wristband_tables"
31
+
32
+ # Routes
33
+ m.add_route %{
34
+ # Edit these as needed ...
35
+ map.with_options :controller => 'sessions' do |session|
36
+ session.login '/login', :action => 'new', :conditions => { :method => :get }
37
+ session.login '/login', :action => 'create', :conditions => { :method => :post }
38
+ session.logout '/logout', :action => 'destroy'
39
+ end
40
+
41
+ map.with_options :controller => 'users' do |user|
42
+ user.register '/register', :action => 'new', :conditions => { :method => :get }
43
+ user.register '/register', :action => 'create', :conditions => { :method => :post }
44
+ user.verify_email '/users/verify_email/:email_validation_key', :action => 'verify_email', :conditions => { :method => :get }
45
+ user.forgot_password '/forgot_password', :action => 'forgot_password'
46
+ end
47
+
48
+ map.home '/', :controller => 'users', :action => 'index'
49
+ }
50
+ m.route_resources :sessions, :users
51
+ end
52
+ end
53
+
54
+ def file_name
55
+ 'wristband'
56
+ end
57
+
58
+ class Rails::Generator::Commands::Create
59
+ def add_route(text)
60
+ sentinel = 'ActionController::Routing::Routes.draw do |map|'
61
+ logger.route "specific routes"
62
+ unless options[:pretend]
63
+ gsub_file 'config/routes.rb', /(#{Regexp.escape(sentinel)})/mi do |match|
64
+ "#{match}\n #{text}\n"
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require File.join(File.dirname(__FILE__), "lib", "wristband")
data/lib/wristband.rb ADDED
@@ -0,0 +1,121 @@
1
+ require 'wristband/user_extensions'
2
+ require 'wristband/support'
3
+ require 'wristband/application_extensions'
4
+ require 'wristband/authority_check'
5
+
6
+ module Wristband
7
+
8
+ VERSION = "1.0.0"
9
+
10
+ class << self
11
+ def included base #:nodoc:
12
+ base.extend ClassMethods
13
+ end
14
+ end
15
+
16
+ module ClassMethods
17
+ def wristband(options={})
18
+ options[:login_with] ||= [:username]
19
+ options[:before_authentication] ||= []
20
+ options[:after_authentication] ||= []
21
+ options[:plain_text_password] ||= false
22
+ options[:has_authorities] ||= false
23
+ options[:roles] ||= []
24
+
25
+ class_eval do
26
+ include Wristband::UserExtensions
27
+
28
+ if options[:plain_text_password]
29
+ options[:password_column] ||= :password
30
+ else
31
+ options[:password_column] ||= :password_crypt
32
+
33
+ # These two are used on the login form
34
+ attr_accessor :password
35
+ attr_accessor :password_confirmation
36
+
37
+ before_save :encrypt_password
38
+ end
39
+
40
+ # Add roles
41
+ unless options[:roles].blank?
42
+ options[:roles].each do |role|
43
+ define_method "is_#{role}?" do
44
+ self.role == role.to_s
45
+ end
46
+ end
47
+ end
48
+
49
+ class << self
50
+ attr_accessor :wristband
51
+ end
52
+ end
53
+
54
+ self.wristband = {
55
+ :login_with_fields => [options[:login_with]].flatten,
56
+ :before_authentication_chain => [options[:before_authentication]].flatten,
57
+ :after_authentication_chain => [options[:after_authentication]].flatten,
58
+ :password_column => options[:password_column],
59
+ :plain_text_password => options[:plain_text_password],
60
+ :roles => options[:roles]
61
+ }
62
+
63
+ if options[:has_authorities]
64
+ self.wristband[:authority_class] = UtilityMethods.interpret_class_specification(self, options[:has_authorities])
65
+ end
66
+ end
67
+ end
68
+
69
+ module MigrationExtensions
70
+ def create_users_table(options = { }, &block)
71
+ case (options)
72
+ when String:
73
+ options = { :options => options }
74
+ end
75
+
76
+ create_table :users, options do |t|
77
+ t.default_user_columns
78
+ yield(t) if (block)
79
+ end
80
+ end
81
+ end
82
+
83
+ module TableDefinitionMethods
84
+ def default_user_columns
85
+ column :username, :string
86
+ column :password_crypt, :string, :limit => 40
87
+ column :password_salt, :string, :limit => 40
88
+ column :remember_token, :string
89
+ column :email_validation_key, :string
90
+ column :created_at, :datetime
91
+ column :updated_at, :datetime
92
+ end
93
+ end
94
+
95
+ module UtilityMethods
96
+ def self.interpret_class_specification(model_class, with_class)
97
+ case (with_class)
98
+ when Symbol:
99
+ "#{model_class.class_name}#{with_class.to_s.camelcase}".constantize
100
+ when String:
101
+ with_class.constantize
102
+ when true:
103
+ "#{model_class.class_name}AuthorityCheck".constantize
104
+ else
105
+ with_class
106
+ end
107
+ end
108
+ end
109
+
110
+ end
111
+
112
+ # Set it all up.
113
+ if Object.const_defined?("ActiveRecord")
114
+ ActiveRecord::Base.send(:extend, Wristband::ClassMethods)
115
+ end
116
+
117
+ if Object.const_defined?("ActionController")
118
+ ActionController::Base.send(:include, Wristband::ApplicationExtensions)
119
+ end
120
+
121
+
@@ -0,0 +1,78 @@
1
+ module Wristband
2
+ module ApplicationExtensions
3
+ def self.included(base)
4
+ base.send(:extend, Wristband::ApplicationExtensions::ClassMethods)
5
+ base.send(:include, Wristband::ApplicationExtensions::InstanceMethods)
6
+ base.send(:helper_method, :logged_in?, :session_user) if base.respond_to?(:helper_method)
7
+ end
8
+
9
+ module ClassMethods
10
+ # ...
11
+ end
12
+
13
+ module InstanceMethods
14
+ def authenticate_and_login(username, password, remember_me=false)
15
+ if user = ::User.authenticate(username, password)
16
+ login_as_user(user, remember_me)
17
+ user
18
+ end
19
+ end
20
+
21
+ def login_as_user(user, remember_me=false)
22
+ self.session_user = user
23
+ if remember_me
24
+ token = Support.encrypt_with_salt(user.id.to_s, Time.now.to_f.to_s)
25
+ cookies[:login_token] = { :value => token, :expires => 2.weeks.from_now.utc }
26
+ user.update_attribute(:remember_token, token)
27
+ end
28
+ end
29
+
30
+ # Logs a user out and deletes the remember_token.
31
+ def logout
32
+ session_user.update_attribute(:remember_token, nil) if session_user
33
+ cookies.delete(:login_token)
34
+ reset_session
35
+ end
36
+
37
+ # Returns true if a user is logged in
38
+ def logged_in?
39
+ !!session_user
40
+ end
41
+
42
+ # Returns the current user in session. Use this on your views and controllers.
43
+ def session_user
44
+ @session_user ||= (session[:user_id] and ::User.find_by_id(session[:user_id]))
45
+ end
46
+
47
+ # Sets the current user in session
48
+ def session_user=(user)
49
+ @session_user = user
50
+ session[:user_id] = (user and user.id)
51
+ end
52
+
53
+ # Logs a user automatically from his cookie
54
+ #
55
+ # You can use this function as a before filter on your controllers.
56
+ def login_from_cookie
57
+ return if (logged_in? or !cookies[:login_token])
58
+ self.session_user = ::User.find_by_remember_token(cookies[:login_token])
59
+ end
60
+
61
+ # You can use this function as a before filter on your controllers that require autentication.
62
+ #
63
+ # If the user is not logged in +respond_not_logged_in+ will be called.
64
+ def login_required
65
+ unless logged_in?
66
+ respond_not_logged_in
67
+ return false
68
+ end
69
+ end
70
+
71
+ # Override this on your controllers to specify what to do when the user is not logged in.
72
+ def respond_not_logged_in
73
+ false
74
+ end
75
+
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,160 @@
1
+ # = AuthorityCheck
2
+ # The different user authorities are defined in a separate class so as to reduce
3
+ # clutter in the User model itself.
4
+ #
5
+ # class User < ActiveRecord::Base
6
+ # wristband :has_authorities => true
7
+ # end
8
+ #
9
+ # This will refer to the class UserAuthorityCheck for all authority tests, but the
10
+ # name of this module can be defined as required:
11
+ #
12
+ # class User < ActiveRecord::Base
13
+ # has_authorities => :permissions
14
+ # end
15
+ #
16
+ # That would reference the class UserPermissions instead for all tests.
17
+ #
18
+ # A sample authority checking class is defined as:
19
+ #
20
+ # class UserAuthorityCheck < AuthorityCheck
21
+ # def wear_shoes?
22
+ # unless (@user.name.match(/^a/i))
23
+ # fail!("Only people with names that start with 'A' can wear shoes.")
24
+ # end
25
+ # end
26
+ # end
27
+ #
28
+ # <b>Note the syntax: </b>All authority checks are defined as ending with a trailing question mark
29
+ # character.
30
+ #
31
+ # A check is considered to have passed if
32
+ # * a call to <tt>allow!</tt> has been made, or
33
+ # * no calls to <tt>fail!</tt> have been made.
34
+ #
35
+ # Once defined, the user authorities are checked via a call to a User instance:
36
+ #
37
+ # user.has_authority_to?(:wear_shoes)
38
+ #
39
+ # While the <tt>has_authority_to?</tt> method returns only true or false, a call to
40
+ # <tt>has_objections_to?</tt> will return nil on success or any error messages if there
41
+ # is a failure.
42
+ #
43
+ #
44
+ # ==== Passing parameters to the authority methods
45
+ #
46
+ # Any call to these tests may include options in the form of a Hash:
47
+ #
48
+ # user.has_authority_to?(:send_message, :text => "Foo bar")
49
+ #
50
+ # These options can be acted upon within the authority check:
51
+ #
52
+ # def send_message?
53
+ # if (options[:text].match(/foo/i))
54
+ # fail!("Messages may not contain forbidden words.")
55
+ # end
56
+ # end
57
+ #
58
+ # ==== Before chains
59
+ #
60
+ # In addition to defining straight tests, a chain can be defined to run before
61
+ # any of the tests themselves. This allows certain calls to be over-ruled. For
62
+ # example:
63
+ #
64
+ # before_check :allow_if_admin!
65
+ #
66
+ # def allow_if_admin!
67
+ # if (@user.is_admin?)
68
+ # allow!
69
+ # end
70
+ # end
71
+ #
72
+ # In this case, the <tt>allow_if_admin!</tt> method will be called before any checks are performed. If
73
+ # the <tt>allow!</tt> method is executed, all subsequent tests are halted and the check
74
+ # is considered to have passed.
75
+
76
+ class AuthorityCheck
77
+ attr_accessor :user
78
+ attr_accessor :options
79
+
80
+ def initialize(user, test_method, options = { })
81
+ self.user = user
82
+ self.options = options
83
+
84
+ @test_method = "#{test_method}?".to_sym
85
+
86
+ @result = nil
87
+ end
88
+
89
+ # Checkes if the user is allowed to do something.
90
+ # Returns <tt>true</tt> or <tt>false</tt>
91
+ def allowed_to?
92
+ execute_tests!
93
+
94
+ # Either explicitly allowed (true) or not given any reasons as to why
95
+ # not (nil, empty)
96
+ (@result === true or (@result === nil and @reasons.blank?)) ? true : false
97
+ end
98
+
99
+ def denied_for_reasons
100
+ @reasons = [ ]
101
+
102
+ allowed_to? ? nil : @reasons
103
+ end
104
+
105
+
106
+ class << self
107
+ def check_chain
108
+ @check_chain ||= [ ]
109
+ end
110
+
111
+ def check_chain=(value)
112
+ @check_chain = value
113
+ end
114
+
115
+ def before_check(method, options = { })
116
+ self.check_chain += remap_chain_methods([ method ])
117
+ end
118
+
119
+ def check_alias_as(original, *aliases)
120
+ aliases.flatten.each do |alias_name|
121
+ alias_method alias_name, original
122
+ end
123
+ end
124
+ end
125
+
126
+
127
+
128
+ protected
129
+ def allow!
130
+ @result = true
131
+ end
132
+
133
+ def fail!(message)
134
+ if (@reasons and message)
135
+ @reasons << message
136
+ end
137
+
138
+ @result = false
139
+ end
140
+
141
+ def execute_tests!
142
+ self.class.check_chain.each do |method|
143
+ case (method)
144
+ when Symbol, String:
145
+ send(method)
146
+ else
147
+ method.call
148
+ end
149
+
150
+ # Stop evaluating if this test called allow!
151
+ return if (@result === true)
152
+ end
153
+
154
+ self.send(@test_method)
155
+ end
156
+
157
+ def self.remap_chain_methods(methods)
158
+ methods
159
+ end
160
+ end
@@ -0,0 +1,28 @@
1
+ module Wristband
2
+ module Support
3
+ CONSONANTS = %w( b c d f g h j k l m n p qu r s t v w x z ch cr fr nd ng nk nt ph pr rd sh sl sp st th tr )
4
+ VOWELS = %w( a e i o u y )
5
+
6
+ def random_string(length = 8)
7
+ (1 .. length).collect { |n|
8
+ (n % 2 != 0) ? CONSONANTS[rand(CONSONANTS.size)] : VOWELS[rand(VOWELS.size)]
9
+ }.to_s[0, length]
10
+ end
11
+ module_function :random_string
12
+
13
+ def encrypt_with_salt(password, salt)
14
+ return password unless (salt and !salt.empty?)
15
+
16
+ Digest::SHA1.hexdigest([ password, salt ].to_s)
17
+ end
18
+ module_function :encrypt_with_salt
19
+
20
+ def random_salt(length = nil)
21
+ salt = Digest::SHA1.hexdigest([ rand, rand, random_string(64), rand, rand ].to_s)
22
+
23
+ length ? salt[0, length] : salt
24
+ end
25
+ module_function :random_salt
26
+
27
+ end
28
+ end