accounts 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,90 @@
1
+ # Copyright Westside Consulting LLC, Ann Arbor, MI, USA, 2012
2
+
3
+ module Accounts
4
+ module Helpers
5
+ class ::Accounts::AccountsError < Exception
6
+ attr_accessor :code
7
+ attr_accessor :message
8
+
9
+ def initialize(code, message)
10
+ @code = code
11
+ @message = message
12
+ end
13
+
14
+ def return_error_page
15
+ [ @code, @message ]
16
+ end
17
+ end
18
+
19
+ def site
20
+ scheme = request.env['rack.url_scheme']
21
+ host = request.env['HTTP_HOST'] # includes port number
22
+ "#{scheme}://#{host}"
23
+ end
24
+
25
+ def send_change_password_link(account)
26
+ tok = ::Accounts::ActionToken.create({ :account => account, :action => 'reset password' })
27
+ link = "#{site}/response-token/#{tok.id}"
28
+ Accounts.deliver_change_password_link[account.email, link]
29
+ end
30
+
31
+ def send_change_email_confirmation(account, new_email)
32
+ tok = ::Accounts::ActionToken.create({
33
+ :account => account,
34
+ :action => 'change email',
35
+ :params => {:new_email => new_email}
36
+ })
37
+ link = "#{site}/response-token/#{tok.id}"
38
+ Accounts.deliver_change_email_confirmation[account.email, new_email, link]
39
+ end
40
+
41
+ def on_email_confirmed(account)
42
+ account.status << :email_confirmed
43
+ account.taint! :status # taint!() defined in model.rb
44
+ account.save
45
+ Accounts.deliver_new_account_admin_notification[account.email]
46
+ end
47
+
48
+ def respond_to_token(id)
49
+ token = ::Accounts::ActionToken.get(id)
50
+
51
+ raise ::Accounts::AccountsError.new 404, %Q{Page not found. Go to <a href="/">home page</a>.} \
52
+ unless token
53
+
54
+ begin
55
+ on_email_confirmed token.account \
56
+ unless token.account.status.include? :email_confirmed
57
+
58
+ case token.action
59
+ when 'change email' then
60
+ token.account.email = token.params[:new_email]
61
+ token.account.save or return "We are unable to change your e-mail right now. Try again later."
62
+ session[:account_id] = token.account.id # this visitor is authenticated
63
+ redirect to("/logon?email=#{token.params[:new_email]}")
64
+ when 'reset password' then
65
+ session[:account_id] = token.account.id # this visitor is authenticated
66
+ redirect '/change-password'
67
+ else
68
+ nil
69
+ end
70
+ ensure
71
+ token.destroy or raise "Failed to destroy token #{token.id}"
72
+ end
73
+ end
74
+
75
+ def authenticate!(email, password)
76
+ account = ::Accounts::Account.first(:email => email) \
77
+ or return false
78
+ account.confirm_password(password) or return false
79
+ session[:account_id] = account.id
80
+ end
81
+
82
+ def register_new_account(email)
83
+ account = ::Accounts::Account.create ({ :email => email })
84
+ account.saved? or return "We are unable to register you at this time. Please try again later."
85
+ tok = ::Accounts::ActionToken.create({ :account => account, :action => 'reset password' })
86
+ link = "#{site}/response-token/#{tok.id}"
87
+ Accounts.deliver_registration_confirmation[account.email, link]
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,126 @@
1
+ # Copyright Westside Consulting LLC, Ann Arbor, MI, USA, 2012
2
+
3
+ require 'data_mapper'
4
+ require 'dm-types/enum'
5
+ require 'dm-types/flag'
6
+ require 'digest/sha2'
7
+
8
+ DataMapper.setup(:default, ENV['DATABASE_URL'] || 'postgres:accounts')
9
+ DataMapper::Property::String.length(255)
10
+ DataMapper::Property::Boolean.allow_nil(false)
11
+ DataMapper::Property::Boolean.default(false)
12
+
13
+ #DataMapper::Model.raise_on_save_failure = true
14
+
15
+ #=begin
16
+ # Override definition in dm-core / lib / dm-core / resource.rb
17
+ # http://github.com/datamapper/dm-core/blob/master/lib/dm-core/resource.rb
18
+ # Get it to log messages when there is a problem saving.
19
+ module DataMapper
20
+ module Resource
21
+
22
+ #@@logger = Logger.new(STDOUT)
23
+
24
+ # override .save
25
+ # @see http://blog.jayfields.com/2006/12/ruby-alias-method-alternative.html
26
+ base_save = self.instance_method(:save)
27
+
28
+ define_method(:save) do |*a|
29
+ result = base_save.bind(self).call(*a)
30
+
31
+ if !result then
32
+ STDERR.puts "Cannot save #{self.class.to_s}"
33
+ STDERR.puts self.pretty_inspect
34
+ self.errors.each {|e| STDERR.puts e.to_s}
35
+ end
36
+ result
37
+ end
38
+ end
39
+ end
40
+ #=end
41
+
42
+ # modified from https://gist.github.com/763374
43
+ module DataMapper
44
+ module Resource
45
+ def taint! property
46
+ self.persistence_state = PersistenceState::Dirty.new(self) \
47
+ unless self.persistence_state.kind_of?(PersistenceState::Dirty)
48
+ self.persistence_state.original_attributes[properties[property]] = Object.new
49
+ end
50
+ end
51
+ end
52
+
53
+ # Example:
54
+ # my_resource.array_prop << 123
55
+ # my_resource.taint! :array_prop
56
+ # my_resource.save
57
+
58
+ module Accounts
59
+
60
+ class Account
61
+ include DataMapper::Resource
62
+ property :id, Serial
63
+ property :email, String, :unique => true
64
+ property :password, String, :required => false # a hash
65
+ property :status, Flag[:suspended, :email_confirmed]
66
+ property :last_logon, DateTime, :required => false
67
+ timestamps :created_at
68
+ timestamps :updated_at
69
+
70
+ def set_password(arg)
71
+ self.update :password => self.class.hashpasswd(arg)
72
+ end
73
+
74
+ def confirm_password(arg)
75
+ self.class.hashpasswd(arg) == self.password
76
+ end
77
+
78
+ private
79
+
80
+ @@digest = Digest::SHA2.new
81
+ SALT = 'b593b7bf01cd29d9cc15d11f9d81f586e255fd252fab264129e6046934876c23' # random
82
+
83
+ # Can't name this "hash". Name already taken.
84
+ def self.hashpasswd(arg)
85
+ @@digest.hexdigest(SALT + arg)
86
+ end
87
+ end
88
+
89
+ # An action-token represents permission for the agent to perform an action on behalf of a user.
90
+ class ActionToken
91
+ include DataMapper::Resource
92
+
93
+ @@digest = Digest::SHA2.new
94
+
95
+ property :id, String, :key => true, :default => lambda { |res, tok| @@digest.hexdigest(rand.to_s) }
96
+ property :action, String, :unique => :account
97
+ property :params, Object, :allow_nil => true # a hash of key => value
98
+ belongs_to :account
99
+ property :expires, DateTime, :default => Time.new + 24 * 3600 # one day
100
+
101
+ # override .save
102
+ # @see http://blog.jayfields.com/2006/12/ruby-alias-method-alternative.html
103
+ base_save = self.instance_method(:save)
104
+
105
+ define_method(:save) do |*args|
106
+ # If there is a previous instance, delete it
107
+ if self.class.count(:action => self.action, :account => self.account) then
108
+ self.class.destroy
109
+ end
110
+ result = base_save.bind(self).call(*args)
111
+ #STDERR.puts "#{self.inspect}.save returned #{result}"
112
+ end
113
+
114
+ base_destroy = self.instance_method(:destroy)
115
+
116
+ define_method(:destroy) do |*args|
117
+ # If there is a previous instance, delete it
118
+ result = base_destroy.bind(self).call(*args)
119
+ #STDERR.puts "#{self.inspect}.destroy returned #{result}"
120
+ end
121
+ end
122
+
123
+ end
124
+
125
+ DataMapper.finalize
126
+ DataMapper.auto_upgrade! # preserve existing database
@@ -0,0 +1,5 @@
1
+ module Accounts
2
+ module Gem
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,105 @@
1
+ $: << '../lib'
2
+
3
+ require 'rubygems'
4
+ require 'rspec'
5
+
6
+ # suppress excessive warnings from DataMapper libraries
7
+ $VERBOSE=nil
8
+ require 'accounts/model'
9
+ $VERBOSE=false
10
+
11
+ # Make sure we're still seeing warnings about our own code
12
+ FOO=:bar
13
+ FOO=:baz
14
+
15
+ begin
16
+ DataMapper.auto_migrate! # empty database
17
+ STDERR.puts "called DataMapper.auto_migrate!"
18
+ end
19
+
20
+ describe Accounts::Account do
21
+
22
+ it 'contains e-mails and passwords and flags' do
23
+ account = Accounts::Account.create :email => "stewie@family.guy"
24
+ account.should be_saved
25
+ account.password.should be_nil
26
+ account.status.should have(0).flags
27
+ account.last_logon.should be_nil
28
+ account.created_at.should_not be_nil
29
+ account.updated_at.should_not be_nil
30
+ end
31
+
32
+ it 'cannot create account with duplicate e-mail' do
33
+ account = Accounts::Account.create :email => "stewie@family.guy"
34
+ account.should_not be_saved
35
+ end
36
+
37
+ it 'new user e-mail is initially invalid' do
38
+ account = Accounts::Account.create :email => "lois@family.guy"
39
+ #puts account.status.inspect
40
+ account.status.include?(:email_confirmed).should be_false
41
+ account.status.include?(:suspended).should be_false
42
+ end
43
+
44
+ it 'responds to .set_password' do
45
+ account = Accounts::Account.create :email => "brian@family.guy"
46
+ account.should respond_to :set_password
47
+ account.set_password('hotforlois').should be_true
48
+ end
49
+
50
+ it 'can confirm password' do
51
+ account = Accounts::Account.create :email => "peter@family.guy"
52
+ account.set_password('notsosmart').should be_true
53
+ account.confirm_password('notsosmart').should be_true
54
+ account.confirm_password('notsobright').should be_false
55
+ end
56
+
57
+ it 'stores passwords as hashes' do
58
+ Accounts::Account.all.each do |acct|
59
+ if !acct.password.nil?
60
+ acct.password.should be_instance_of String
61
+ acct.password.length.should be == 64
62
+ acct.password.should_not match(/[^0-9a-f]/)
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ describe Accounts::ActionToken do
69
+ before :all do
70
+ @meg = Accounts::Account.create :email => 'meg@familyguy.com'
71
+ @chris = Accounts::Account.create :email => 'chris@familyguy.com'
72
+ end
73
+
74
+ it 'can create a token for a user to perform a specific action' do
75
+ tok = Accounts::ActionToken.create :account => @meg, :action => 'party'
76
+ tok.should be_saved
77
+ tok.id.should be_instance_of String
78
+ tok.id.length.should be == 64
79
+ tok.id.should_not match(/[^0-9a-f]/)
80
+ end
81
+
82
+ it 'creating a new TokenAction for an action replaces any previous token' do
83
+ tas = Accounts::ActionToken.all(:account => @meg, :action => 'party')
84
+ tas.should_not be_nil
85
+ tas.should have(1).item
86
+
87
+ tok = Accounts::ActionToken.create :account => @meg, :action => 'party'
88
+ tok.should be_saved
89
+ tok.id.should_not be == tas[0].id
90
+ end
91
+
92
+ it 'can return account and action given token' do
93
+ tok = Accounts::ActionToken.create :account => @chris, :action => 'act stupid'
94
+ ta_found = Accounts::ActionToken.get(tok.id)
95
+ ta_found.account.should be == @chris
96
+ ta_found.action.should be == 'act stupid'
97
+ end
98
+
99
+ it 'has an expire date' do
100
+ Accounts::ActionToken.all.each do |tok|
101
+ tok.expires.should be_kind_of DateTime
102
+ tok.expires.should > DateTime.new
103
+ end
104
+ end
105
+ end
metadata ADDED
@@ -0,0 +1,263 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: accounts
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Larry Siden, Westside Consulting LLC
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-01-02 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: capybara
16
+ requirement: &83116690 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *83116690
25
+ - !ruby/object:Gem::Dependency
26
+ name: cucumber
27
+ requirement: &83116290 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *83116290
36
+ - !ruby/object:Gem::Dependency
37
+ name: rspec
38
+ requirement: &83115900 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *83115900
47
+ - !ruby/object:Gem::Dependency
48
+ name: rdoc
49
+ requirement: &83115290 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *83115290
58
+ - !ruby/object:Gem::Dependency
59
+ name: sinatra-contrib
60
+ requirement: &83048450 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *83048450
69
+ - !ruby/object:Gem::Dependency
70
+ name: mail-store-agent
71
+ requirement: &83047670 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: *83047670
80
+ - !ruby/object:Gem::Dependency
81
+ name: mail-single_file_delivery
82
+ requirement: &83043620 !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ! '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ type: :development
89
+ prerelease: false
90
+ version_requirements: *83043620
91
+ - !ruby/object:Gem::Dependency
92
+ name: haml
93
+ requirement: &83042990 !ruby/object:Gem::Requirement
94
+ none: false
95
+ requirements:
96
+ - - ! '>='
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ type: :development
100
+ prerelease: false
101
+ version_requirements: *83042990
102
+ - !ruby/object:Gem::Dependency
103
+ name: rack
104
+ requirement: &83041620 !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ~>
108
+ - !ruby/object:Gem::Version
109
+ version: 1.3.6
110
+ type: :runtime
111
+ prerelease: false
112
+ version_requirements: *83041620
113
+ - !ruby/object:Gem::Dependency
114
+ name: sinatra
115
+ requirement: &83041110 !ruby/object:Gem::Requirement
116
+ none: false
117
+ requirements:
118
+ - - ! '>='
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ type: :runtime
122
+ prerelease: false
123
+ version_requirements: *83041110
124
+ - !ruby/object:Gem::Dependency
125
+ name: thin
126
+ requirement: &83028320 !ruby/object:Gem::Requirement
127
+ none: false
128
+ requirements:
129
+ - - ! '>='
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: *83028320
135
+ - !ruby/object:Gem::Dependency
136
+ name: data_mapper
137
+ requirement: &83027720 !ruby/object:Gem::Requirement
138
+ none: false
139
+ requirements:
140
+ - - ! '>='
141
+ - !ruby/object:Gem::Version
142
+ version: '0'
143
+ type: :runtime
144
+ prerelease: false
145
+ version_requirements: *83027720
146
+ - !ruby/object:Gem::Dependency
147
+ name: dm-types
148
+ requirement: &83027240 !ruby/object:Gem::Requirement
149
+ none: false
150
+ requirements:
151
+ - - ! '>='
152
+ - !ruby/object:Gem::Version
153
+ version: '0'
154
+ type: :runtime
155
+ prerelease: false
156
+ version_requirements: *83027240
157
+ - !ruby/object:Gem::Dependency
158
+ name: dm-timestamps
159
+ requirement: &83026620 !ruby/object:Gem::Requirement
160
+ none: false
161
+ requirements:
162
+ - - ! '>='
163
+ - !ruby/object:Gem::Version
164
+ version: '0'
165
+ type: :runtime
166
+ prerelease: false
167
+ version_requirements: *83026620
168
+ - !ruby/object:Gem::Dependency
169
+ name: dm-postgres-adapter
170
+ requirement: &83026060 !ruby/object:Gem::Requirement
171
+ none: false
172
+ requirements:
173
+ - - ! '>='
174
+ - !ruby/object:Gem::Version
175
+ version: '0'
176
+ type: :runtime
177
+ prerelease: false
178
+ version_requirements: *83026060
179
+ - !ruby/object:Gem::Dependency
180
+ name: mail
181
+ requirement: &83025360 !ruby/object:Gem::Requirement
182
+ none: false
183
+ requirements:
184
+ - - ! '>='
185
+ - !ruby/object:Gem::Version
186
+ version: '0'
187
+ type: :runtime
188
+ prerelease: false
189
+ version_requirements: *83025360
190
+ - !ruby/object:Gem::Dependency
191
+ name: logger
192
+ requirement: &83024820 !ruby/object:Gem::Requirement
193
+ none: false
194
+ requirements:
195
+ - - ! '>='
196
+ - !ruby/object:Gem::Version
197
+ version: '0'
198
+ type: :runtime
199
+ prerelease: false
200
+ version_requirements: *83024820
201
+ description: ! "\nAccounts::Server defines the following paths for your web-app:\n\n*
202
+ POST '/logon'\n* POST '/register'\n* POST '/forgot-password'\n* POST '/change-password'\n*
203
+ POST '/change-email'\n\nYour app must provide the pages and forms that will post
204
+ to these paths.\n "
205
+ email:
206
+ - lsiden@westside-consulting.com
207
+ executables: []
208
+ extensions: []
209
+ extra_rdoc_files: []
210
+ files:
211
+ - .gitignore
212
+ - .rvmrc
213
+ - Gemfile
214
+ - Gemfile.lock
215
+ - README.mkd
216
+ - Rakefile
217
+ - accounts.gemspec
218
+ - demo/views/change_email.haml
219
+ - demo/views/change_password.haml
220
+ - demo/views/forgot_password.haml
221
+ - demo/views/logon.haml
222
+ - demo/views/register.haml
223
+ - demo/web_app.rb
224
+ - features/change-email.feature
225
+ - features/change-password.feature
226
+ - features/register.feature
227
+ - features/step_definitions/steps.rb
228
+ - features/step_definitions/web_steps.rb
229
+ - features/support/env.rb
230
+ - features/support/paths.rb
231
+ - features/support/unused.rb
232
+ - lib/accounts.rb
233
+ - lib/accounts/configure.rb
234
+ - lib/accounts/helpers.rb
235
+ - lib/accounts/model.rb
236
+ - lib/accounts/version.rb
237
+ - spec/user_model_spec.rb
238
+ homepage: https://github.com/lsiden/accounts
239
+ licenses: []
240
+ post_install_message:
241
+ rdoc_options: []
242
+ require_paths:
243
+ - lib
244
+ required_ruby_version: !ruby/object:Gem::Requirement
245
+ none: false
246
+ requirements:
247
+ - - ! '>='
248
+ - !ruby/object:Gem::Version
249
+ version: '0'
250
+ required_rubygems_version: !ruby/object:Gem::Requirement
251
+ none: false
252
+ requirements:
253
+ - - ! '>='
254
+ - !ruby/object:Gem::Version
255
+ version: '0'
256
+ requirements: []
257
+ rubyforge_project: accounts
258
+ rubygems_version: 1.8.10
259
+ signing_key:
260
+ specification_version: 3
261
+ summary: ! '*accounts* is a website plug-in that offers than the basic user-account
262
+ management and authentication features that many sites need.'
263
+ test_files: []