accounts 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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: []