rodauth 0.9.0
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.
- checksums.yaml +7 -0
- data/CHANGELOG +3 -0
- data/MIT-LICENSE +18 -0
- data/README.rdoc +484 -0
- data/Rakefile +91 -0
- data/lib/roda/plugins/rodauth.rb +265 -0
- data/lib/roda/plugins/rodauth/base.rb +428 -0
- data/lib/roda/plugins/rodauth/change_login.rb +48 -0
- data/lib/roda/plugins/rodauth/change_password.rb +42 -0
- data/lib/roda/plugins/rodauth/close_account.rb +42 -0
- data/lib/roda/plugins/rodauth/create_account.rb +92 -0
- data/lib/roda/plugins/rodauth/lockout.rb +292 -0
- data/lib/roda/plugins/rodauth/login.rb +77 -0
- data/lib/roda/plugins/rodauth/logout.rb +36 -0
- data/lib/roda/plugins/rodauth/remember.rb +226 -0
- data/lib/roda/plugins/rodauth/reset_password.rb +205 -0
- data/lib/roda/plugins/rodauth/verify_account.rb +228 -0
- data/spec/migrate/001_tables.rb +64 -0
- data/spec/migrate_password/001_tables.rb +38 -0
- data/spec/rodauth_spec.rb +1114 -0
- data/spec/views/layout.str +11 -0
- data/spec/views/login.str +21 -0
- data/templates/change-login.str +22 -0
- data/templates/change-password.str +21 -0
- data/templates/close-account.str +9 -0
- data/templates/confirm-password.str +16 -0
- data/templates/create-account.str +33 -0
- data/templates/login.str +25 -0
- data/templates/logout.str +9 -0
- data/templates/remember.str +28 -0
- data/templates/reset-password-email.str +5 -0
- data/templates/reset-password-request.str +7 -0
- data/templates/reset-password.str +23 -0
- data/templates/unlock-account-email.str +5 -0
- data/templates/unlock-account-request.str +11 -0
- data/templates/unlock-account.str +11 -0
- data/templates/verify-account-email.str +4 -0
- data/templates/verify-account-resend.str +7 -0
- data/templates/verify-account.str +11 -0
- metadata +227 -0
data/Rakefile
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
require "rake"
|
2
|
+
require "rake/clean"
|
3
|
+
|
4
|
+
CLEAN.include ["rodauth-*.gem", "rdoc", "coverage"]
|
5
|
+
|
6
|
+
# Packaging
|
7
|
+
|
8
|
+
desc "Build rodauth gem"
|
9
|
+
task :package=>[:clean] do |p|
|
10
|
+
sh %{#{FileUtils::RUBY} -S gem build rodauth.gemspec}
|
11
|
+
end
|
12
|
+
|
13
|
+
### RDoc
|
14
|
+
|
15
|
+
RDOC_DEFAULT_OPTS = ["--line-numbers", "--inline-source", '--title', 'Rodauth: Authentication framework using Roda, Sequel, and PostgreSQL']
|
16
|
+
|
17
|
+
begin
|
18
|
+
gem 'hanna-nouveau'
|
19
|
+
RDOC_DEFAULT_OPTS.concat(['-f', 'hanna'])
|
20
|
+
rescue Gem::LoadError
|
21
|
+
end
|
22
|
+
|
23
|
+
rdoc_task_class = begin
|
24
|
+
require "rdoc/task"
|
25
|
+
RDoc::Task
|
26
|
+
rescue LoadError
|
27
|
+
require "rake/rdoctask"
|
28
|
+
Rake::RDocTask
|
29
|
+
end
|
30
|
+
|
31
|
+
RDOC_OPTS = RDOC_DEFAULT_OPTS + ['--main', 'README.rdoc']
|
32
|
+
RDOC_FILES = %w"README.rdoc CHANGELOG MIT-LICENSE lib/**/*.rb" + Dir["doc/*.rdoc"] + Dir['doc/release_notes/*.txt']
|
33
|
+
|
34
|
+
rdoc_task_class.new do |rdoc|
|
35
|
+
rdoc.rdoc_dir = "rdoc"
|
36
|
+
rdoc.options += RDOC_OPTS
|
37
|
+
rdoc.rdoc_files.add RDOC_FILES
|
38
|
+
end
|
39
|
+
|
40
|
+
# Specs
|
41
|
+
|
42
|
+
desc "Run specs"
|
43
|
+
task :default=>:spec
|
44
|
+
|
45
|
+
spec = proc do |env|
|
46
|
+
env.each{|k,v| ENV[k] = v}
|
47
|
+
sh "#{FileUtils::RUBY} spec/rodauth_spec.rb"
|
48
|
+
env.each{|k,v| ENV.delete(k)}
|
49
|
+
end
|
50
|
+
|
51
|
+
desc "Run specs"
|
52
|
+
task "spec" do
|
53
|
+
spec.call({})
|
54
|
+
end
|
55
|
+
|
56
|
+
desc "Run specs with coverage"
|
57
|
+
task "spec_cov" do
|
58
|
+
ENV['COVERAGE'] = '1'
|
59
|
+
spec.call('COVERAGE'=>'1')
|
60
|
+
end
|
61
|
+
|
62
|
+
desc "Run specs with -w, some warnings filtered"
|
63
|
+
task "spec_w" do
|
64
|
+
ENV['RUBYOPT'] ? (ENV['RUBYOPT'] += " -w") : (ENV['RUBYOPT'] = '-w')
|
65
|
+
rake = ENV['RAKE'] || "#{FileUtils::RUBY} -S rake"
|
66
|
+
sh %{#{rake} 2>&1 | egrep -v \": warning: instance variable @.* not initialized|: warning: method redefined; discarding old|: warning: previous definition of|: warning: statement not reached"}
|
67
|
+
end
|
68
|
+
|
69
|
+
desc "Setup database used for testing"
|
70
|
+
task :db_setup do
|
71
|
+
sh 'echo "CREATE USER rodauth_test PASSWORD \'rodauth_test\'" | psql -U postgres'
|
72
|
+
sh 'echo "CREATE USER rodauth_test_password PASSWORD \'rodauth_test\'" | psql -U postgres'
|
73
|
+
sh 'createdb -U postgres -O rodauth_test rodauth_test'
|
74
|
+
sh 'echo "CREATE EXTENSION pgcrypto" | psql -U postgres rodauth_test'
|
75
|
+
sh 'echo "CREATE EXTENSION citext" | psql -U postgres rodauth_test'
|
76
|
+
require 'sequel'
|
77
|
+
Sequel.extension :migration
|
78
|
+
Sequel.postgres(:user=>'rodauth_test', :password=>'rodauth_test') do |db|
|
79
|
+
Sequel::Migrator.run(db, 'spec/migrate')
|
80
|
+
end
|
81
|
+
Sequel.postgres('rodauth_test', :user=>'rodauth_test_password', :password=>'rodauth_test') do |db|
|
82
|
+
Sequel::Migrator.run(db, 'spec/migrate_password', :table=>'schema_info_password')
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
desc "Teardown database used for testing"
|
87
|
+
task :db_teardown do
|
88
|
+
sh 'dropdb -U postgres rodauth_test'
|
89
|
+
sh 'dropuser -U postgres rodauth_test_password'
|
90
|
+
sh 'dropuser -U postgres rodauth_test'
|
91
|
+
end
|
@@ -0,0 +1,265 @@
|
|
1
|
+
require 'tilt/string'
|
2
|
+
|
3
|
+
class Roda
|
4
|
+
module RodaPlugins
|
5
|
+
module Rodauth
|
6
|
+
def self.load_dependencies(app, opts={})
|
7
|
+
app.plugin :render
|
8
|
+
app.plugin :flash
|
9
|
+
app.plugin :h
|
10
|
+
app.plugin :csrf
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.configure(app, opts={}, &block)
|
14
|
+
((app.opts[:rodauths] ||= {})[opts[:name]] ||= Class.new(Auth)).configure(&block)
|
15
|
+
end
|
16
|
+
|
17
|
+
DSL_META_TYPES = [:auth, :auth_value].freeze
|
18
|
+
FEATURES = {}
|
19
|
+
|
20
|
+
class Feature < Module
|
21
|
+
DSL_META_TYPES.each do |meth|
|
22
|
+
name = :"#{meth}_methods"
|
23
|
+
define_method(name) do |*v|
|
24
|
+
iv = :"@#{name}"
|
25
|
+
existing = instance_variable_get(iv) || []
|
26
|
+
if v.empty?
|
27
|
+
existing
|
28
|
+
else
|
29
|
+
instance_variable_set(iv, existing + v)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
attr_accessor :feature_name
|
35
|
+
attr_accessor :dependencies
|
36
|
+
|
37
|
+
def self.define(name, &block)
|
38
|
+
feature = new
|
39
|
+
feature.dependencies = []
|
40
|
+
feature.feature_name = name
|
41
|
+
feature.module_eval(&block)
|
42
|
+
FEATURES[name] = feature
|
43
|
+
end
|
44
|
+
|
45
|
+
DEFAULT_REDIRECT_BLOCK = proc{default_redirect}
|
46
|
+
def redirect(&block)
|
47
|
+
meth = :"#{feature_name}_redirect"
|
48
|
+
block ||= DEFAULT_REDIRECT_BLOCK
|
49
|
+
define_method(meth, &block)
|
50
|
+
auth_value_methods meth
|
51
|
+
end
|
52
|
+
|
53
|
+
def view(page, title)
|
54
|
+
meth = :"#{feature_name}_view"
|
55
|
+
define_method(meth) do
|
56
|
+
view(page, title)
|
57
|
+
end
|
58
|
+
auth_methods meth
|
59
|
+
end
|
60
|
+
|
61
|
+
def depends(*deps)
|
62
|
+
dependencies.concat(deps)
|
63
|
+
end
|
64
|
+
|
65
|
+
def after
|
66
|
+
meth = :"after_#{feature_name}"
|
67
|
+
define_method(meth) do
|
68
|
+
nil
|
69
|
+
end
|
70
|
+
auth_methods meth
|
71
|
+
end
|
72
|
+
|
73
|
+
def additional_form_tags
|
74
|
+
meth = :"#{feature_name}_additional_form_tags"
|
75
|
+
define_method(meth) do
|
76
|
+
nil
|
77
|
+
end
|
78
|
+
auth_value_methods meth
|
79
|
+
end
|
80
|
+
|
81
|
+
def require_account
|
82
|
+
@account_required = true
|
83
|
+
end
|
84
|
+
|
85
|
+
def account_required?
|
86
|
+
@account_required
|
87
|
+
end
|
88
|
+
|
89
|
+
[:route, :notice_flash, :error_flash, :button].each do |meth|
|
90
|
+
define_method(meth) do |v|
|
91
|
+
inst_meth = :"#{feature_name}_#{meth}"
|
92
|
+
define_method(inst_meth){v}
|
93
|
+
auth_value_methods inst_meth
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
[:get, :post, :route].each do |meth|
|
98
|
+
define_method("#{meth}_block") do |&block|
|
99
|
+
if block
|
100
|
+
instance_variable_set("@#{meth}_block", block)
|
101
|
+
else
|
102
|
+
instance_variable_get("@#{meth}_block")
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
class Auth
|
109
|
+
class << self
|
110
|
+
attr_reader :features
|
111
|
+
attr_reader :route_block_methods
|
112
|
+
end
|
113
|
+
|
114
|
+
def self.inherited(subclass)
|
115
|
+
super
|
116
|
+
subclass.instance_exec do
|
117
|
+
@features = []
|
118
|
+
@route_block_methods = []
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def self.configure(&block)
|
123
|
+
DSL.new(self, &block)
|
124
|
+
end
|
125
|
+
|
126
|
+
def self.freeze
|
127
|
+
@features.freeze
|
128
|
+
@route_block_methods.freeze
|
129
|
+
super
|
130
|
+
end
|
131
|
+
|
132
|
+
def route_block_methods
|
133
|
+
self.class.route_block_methods
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
class DSL
|
138
|
+
def def_auth_method(meth)
|
139
|
+
define_sclass_method(meth) do |&block|
|
140
|
+
_def_auth_method(meth, &block)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def def_auth_value_method(meth)
|
145
|
+
define_sclass_method(meth) do |*v, &block|
|
146
|
+
v = v.first
|
147
|
+
block ||= proc{v}
|
148
|
+
_def_auth_method(meth, &block)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def def_auth_block_method(meth)
|
153
|
+
define_sclass_method(meth) do |&block|
|
154
|
+
_def_auth_method(meth){block}
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def initialize(auth, &block)
|
159
|
+
@auth = auth
|
160
|
+
load_feature(:base)
|
161
|
+
instance_exec(&block)
|
162
|
+
end
|
163
|
+
|
164
|
+
def enable(*features)
|
165
|
+
new_features = features - @auth.features
|
166
|
+
new_features.each{|f| load_feature(f)}
|
167
|
+
@auth.features.concat(new_features)
|
168
|
+
end
|
169
|
+
|
170
|
+
private
|
171
|
+
|
172
|
+
def _def_auth_method(meth, &block)
|
173
|
+
@auth.send(:define_method, meth, &block)
|
174
|
+
end
|
175
|
+
|
176
|
+
def define_sclass_method(meth, &block)
|
177
|
+
(class << self; self end).send(:define_method, meth, &block)
|
178
|
+
end
|
179
|
+
|
180
|
+
def load_feature(feature_name)
|
181
|
+
require "roda/plugins/rodauth/#{feature_name}"
|
182
|
+
feature = FEATURES[feature_name]
|
183
|
+
enable(*feature.dependencies)
|
184
|
+
|
185
|
+
DSL_META_TYPES.each do |type|
|
186
|
+
feature.send(:"#{type}_methods").each{|m| send(:"def_#{type}_method", m)}
|
187
|
+
end
|
188
|
+
|
189
|
+
if get_block = feature.get_block
|
190
|
+
def_auth_block_method :"#{feature_name}_get_block"
|
191
|
+
_def_auth_method(:"#{feature_name}_get_block"){get_block}
|
192
|
+
end
|
193
|
+
|
194
|
+
if post_block = feature.post_block
|
195
|
+
def_auth_block_method :"#{feature_name}_post_block"
|
196
|
+
_def_auth_method(:"#{feature_name}_post_block"){post_block}
|
197
|
+
end
|
198
|
+
|
199
|
+
route_block = feature.route_block
|
200
|
+
if route_block || (get_block && post_block)
|
201
|
+
check_before_meth = :"check_before_#{feature_name}"
|
202
|
+
before_meth = :"before_#{feature_name}"
|
203
|
+
def_auth_block_method :"#{feature_name}_route_block"
|
204
|
+
route_block ||= proc do |r, auth|
|
205
|
+
r.is auth.send(:"#{feature_name}_route") do
|
206
|
+
auth.check_before(feature)
|
207
|
+
auth.send(before_meth)
|
208
|
+
|
209
|
+
r.get do
|
210
|
+
instance_exec(r, auth, &auth.send(:"#{feature_name}_get_block"))
|
211
|
+
end
|
212
|
+
|
213
|
+
r.post do
|
214
|
+
instance_exec(r, auth, &auth.send(:"#{feature_name}_post_block"))
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
_def_auth_method(:"#{feature_name}_route_block"){route_block}
|
219
|
+
_def_auth_method(before_meth){nil}
|
220
|
+
def_auth_method(before_meth)
|
221
|
+
@auth.route_block_methods << :"#{feature_name}_route_block"
|
222
|
+
end
|
223
|
+
|
224
|
+
@auth.send(:include, feature)
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
module InstanceMethods
|
229
|
+
def rodauth(name=nil)
|
230
|
+
if name
|
231
|
+
(@_rodauths ||= {})[name] ||= self.class.rodauth(name).new(self)
|
232
|
+
else
|
233
|
+
@_rodauth ||= self.class.rodauth.new(self)
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
module ClassMethods
|
239
|
+
def rodauth(name=nil)
|
240
|
+
opts[:rodauths][name]
|
241
|
+
end
|
242
|
+
|
243
|
+
def freeze
|
244
|
+
if opts[:rodauths]
|
245
|
+
opts[:rodauths].each_value(&:freeze)
|
246
|
+
opts[:rodauths].freeze
|
247
|
+
end
|
248
|
+
super
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
module RequestMethods
|
253
|
+
def rodauth(name=nil)
|
254
|
+
auth = scope.rodauth(name)
|
255
|
+
auth.route_block_methods.each do |meth|
|
256
|
+
scope.instance_exec(self, auth, &auth.send(meth))
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
register_plugin(:rodauth, Rodauth)
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
@@ -0,0 +1,428 @@
|
|
1
|
+
class Roda
|
2
|
+
module RodaPlugins
|
3
|
+
module Rodauth
|
4
|
+
Base = Feature.define(:base) do
|
5
|
+
auth_value_methods(
|
6
|
+
:account_id,
|
7
|
+
:account_model,
|
8
|
+
:account_open_status_value,
|
9
|
+
:account_password_hash_column,
|
10
|
+
:account_status_id,
|
11
|
+
:account_unverified_status_value,
|
12
|
+
:default_redirect,
|
13
|
+
:email_from,
|
14
|
+
:email_subject_prefix,
|
15
|
+
:login_column,
|
16
|
+
:login_confirm_label,
|
17
|
+
:login_confirm_param,
|
18
|
+
:login_label,
|
19
|
+
:login_param,
|
20
|
+
:logins_do_not_match_message,
|
21
|
+
:no_matching_login_message,
|
22
|
+
:password_confirm_label,
|
23
|
+
:password_confirm_param,
|
24
|
+
:password_does_not_meet_requirements_message,
|
25
|
+
:password_hash_column,
|
26
|
+
:password_hash_cost,
|
27
|
+
:password_hash_table,
|
28
|
+
:password_label,
|
29
|
+
:password_minimum_length,
|
30
|
+
:password_param,
|
31
|
+
:passwords_do_not_match_message,
|
32
|
+
:prefix,
|
33
|
+
:require_login_notice_message,
|
34
|
+
:require_login_redirect,
|
35
|
+
:session_key,
|
36
|
+
:skip_status_checks?,
|
37
|
+
:title_instance_variable
|
38
|
+
)
|
39
|
+
|
40
|
+
auth_methods(
|
41
|
+
:account_from_login,
|
42
|
+
:account_from_session,
|
43
|
+
:account_id_value,
|
44
|
+
:account_session_value,
|
45
|
+
:after_close_account,
|
46
|
+
:already_logged_in,
|
47
|
+
:clear_session,
|
48
|
+
:create_email,
|
49
|
+
:email_to,
|
50
|
+
:logged_in?,
|
51
|
+
:login_errors_message,
|
52
|
+
:login_required,
|
53
|
+
:open_account?,
|
54
|
+
:password_hash,
|
55
|
+
:password_meets_requirements?,
|
56
|
+
:random_key,
|
57
|
+
:session_value,
|
58
|
+
:set_error_flash,
|
59
|
+
:set_notice_flash,
|
60
|
+
:set_password,
|
61
|
+
:set_redirect_error_flash,
|
62
|
+
:set_title,
|
63
|
+
:unverified_account_message,
|
64
|
+
:update_session
|
65
|
+
)
|
66
|
+
|
67
|
+
attr_reader :scope
|
68
|
+
attr_reader :account
|
69
|
+
|
70
|
+
def initialize(scope)
|
71
|
+
@scope = scope
|
72
|
+
end
|
73
|
+
|
74
|
+
def features
|
75
|
+
self.class.features
|
76
|
+
end
|
77
|
+
|
78
|
+
def request
|
79
|
+
scope.request
|
80
|
+
end
|
81
|
+
|
82
|
+
def response
|
83
|
+
scope.response
|
84
|
+
end
|
85
|
+
|
86
|
+
def session
|
87
|
+
scope.session
|
88
|
+
end
|
89
|
+
|
90
|
+
def flash
|
91
|
+
scope.flash
|
92
|
+
end
|
93
|
+
|
94
|
+
# Overridable methods
|
95
|
+
|
96
|
+
def account_id_value
|
97
|
+
account.send(account_id)
|
98
|
+
end
|
99
|
+
alias account_session_value account_id_value
|
100
|
+
|
101
|
+
def session_value
|
102
|
+
session[session_key]
|
103
|
+
end
|
104
|
+
|
105
|
+
def account_status_id_value
|
106
|
+
account.send(account_status_id)
|
107
|
+
end
|
108
|
+
|
109
|
+
def _account_from_login(login)
|
110
|
+
@account = account_from_login(login)
|
111
|
+
end
|
112
|
+
|
113
|
+
def account_from_login(login)
|
114
|
+
ds = account_model.where(login_column=>login)
|
115
|
+
ds = ds.where(account_status_id=>[account_unverified_status_value, account_open_status_value]) unless skip_status_checks?
|
116
|
+
ds.first
|
117
|
+
end
|
118
|
+
|
119
|
+
def open_account?
|
120
|
+
skip_status_checks? || account_status_id_value == account_open_status_value
|
121
|
+
end
|
122
|
+
|
123
|
+
def unverified_account_message
|
124
|
+
"unverified account, please verify account before logging in"
|
125
|
+
end
|
126
|
+
|
127
|
+
def update_session
|
128
|
+
clear_session
|
129
|
+
session[session_key] = account_session_value
|
130
|
+
end
|
131
|
+
|
132
|
+
def check_before(feature)
|
133
|
+
meth = :"check_before_#{feature.feature_name}"
|
134
|
+
if respond_to?(meth)
|
135
|
+
send(meth)
|
136
|
+
elsif feature.account_required?
|
137
|
+
require_account
|
138
|
+
elsif logged_in?
|
139
|
+
already_logged_in
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def account_model
|
144
|
+
::Account
|
145
|
+
end
|
146
|
+
|
147
|
+
def db
|
148
|
+
account_model.db
|
149
|
+
end
|
150
|
+
|
151
|
+
# If the account_password_hash_column is set, the password hash is verified in
|
152
|
+
# ruby, it will not use a database function to do so, it will check the password
|
153
|
+
# hash using bcrypt.
|
154
|
+
def account_password_hash_column
|
155
|
+
nil
|
156
|
+
end
|
157
|
+
|
158
|
+
def already_logged_in
|
159
|
+
nil
|
160
|
+
end
|
161
|
+
|
162
|
+
def clear_session
|
163
|
+
session.clear
|
164
|
+
end
|
165
|
+
|
166
|
+
def default_redirect
|
167
|
+
'/'
|
168
|
+
end
|
169
|
+
|
170
|
+
def require_login_redirect
|
171
|
+
"#{prefix}/login"
|
172
|
+
end
|
173
|
+
|
174
|
+
def require_login_notice_message
|
175
|
+
"Please login to continue"
|
176
|
+
end
|
177
|
+
|
178
|
+
def prefix
|
179
|
+
''
|
180
|
+
end
|
181
|
+
|
182
|
+
def login_required
|
183
|
+
set_notice_flash require_login_notice_message
|
184
|
+
request.redirect require_login_redirect
|
185
|
+
end
|
186
|
+
|
187
|
+
def random_key
|
188
|
+
require 'securerandom'
|
189
|
+
if RUBY_VERSION >= '1.9'
|
190
|
+
SecureRandom.urlsafe_base64(32)
|
191
|
+
else
|
192
|
+
# :nocov:
|
193
|
+
SecureRandom.hex(32)
|
194
|
+
# :nocov:
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def title_instance_variable
|
199
|
+
nil
|
200
|
+
end
|
201
|
+
|
202
|
+
def set_title(title)
|
203
|
+
if title_instance_variable
|
204
|
+
scope.instance_variable_set(title_instance_variable, title)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def set_error_flash(message)
|
209
|
+
flash.now[:error] = message
|
210
|
+
end
|
211
|
+
|
212
|
+
def set_redirect_error_flash(message)
|
213
|
+
flash[:error] = message
|
214
|
+
end
|
215
|
+
|
216
|
+
def set_notice_flash(message)
|
217
|
+
flash[:notice] = message
|
218
|
+
end
|
219
|
+
|
220
|
+
def login_column
|
221
|
+
:email
|
222
|
+
end
|
223
|
+
|
224
|
+
def password_hash_column
|
225
|
+
:password_hash
|
226
|
+
end
|
227
|
+
|
228
|
+
def password_hash_table
|
229
|
+
:account_password_hashes
|
230
|
+
end
|
231
|
+
|
232
|
+
def no_matching_login_message
|
233
|
+
"no matching login"
|
234
|
+
end
|
235
|
+
|
236
|
+
def logged_in?
|
237
|
+
session[session_key]
|
238
|
+
end
|
239
|
+
|
240
|
+
def require_login
|
241
|
+
login_required unless logged_in?
|
242
|
+
end
|
243
|
+
|
244
|
+
def require_account
|
245
|
+
require_login
|
246
|
+
unless _account_from_session
|
247
|
+
clear_session
|
248
|
+
login_required
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
def login_param
|
253
|
+
'login'
|
254
|
+
end
|
255
|
+
|
256
|
+
def login_confirm_param
|
257
|
+
'login-confirm'
|
258
|
+
end
|
259
|
+
|
260
|
+
def login_label
|
261
|
+
'Login'
|
262
|
+
end
|
263
|
+
|
264
|
+
def login_confirm_label
|
265
|
+
"Confirm #{login_label}"
|
266
|
+
end
|
267
|
+
|
268
|
+
def password_label
|
269
|
+
'Password'
|
270
|
+
end
|
271
|
+
|
272
|
+
def password_confirm_label
|
273
|
+
"Confirm #{password_label}"
|
274
|
+
end
|
275
|
+
|
276
|
+
def login_errors_message
|
277
|
+
if errors = account.errors.on(login_column)
|
278
|
+
errors.join(', ')
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
def logins_do_not_match_message
|
283
|
+
'logins do not match'
|
284
|
+
end
|
285
|
+
|
286
|
+
def password_param
|
287
|
+
'password'
|
288
|
+
end
|
289
|
+
|
290
|
+
def password_confirm_param
|
291
|
+
'password-confirm'
|
292
|
+
end
|
293
|
+
|
294
|
+
def session_key
|
295
|
+
:account_id
|
296
|
+
end
|
297
|
+
|
298
|
+
def account_id
|
299
|
+
:id
|
300
|
+
end
|
301
|
+
|
302
|
+
def account_status_id
|
303
|
+
:status_id
|
304
|
+
end
|
305
|
+
|
306
|
+
def passwords_do_not_match_message
|
307
|
+
'passwords do not match'
|
308
|
+
end
|
309
|
+
|
310
|
+
def password_does_not_meet_requirements_message
|
311
|
+
"invalid password, does not meet requirements (minimum #{password_minimum_length} characters)"
|
312
|
+
end
|
313
|
+
|
314
|
+
def password_minimum_length
|
315
|
+
6
|
316
|
+
end
|
317
|
+
|
318
|
+
def password_meets_requirements?(password)
|
319
|
+
password_minimum_length <= password.length
|
320
|
+
end
|
321
|
+
|
322
|
+
def account_unverified_status_value
|
323
|
+
1
|
324
|
+
end
|
325
|
+
|
326
|
+
def account_open_status_value
|
327
|
+
2
|
328
|
+
end
|
329
|
+
|
330
|
+
def account_initial_status_value
|
331
|
+
account_open_status_value
|
332
|
+
end
|
333
|
+
|
334
|
+
def _account_from_session
|
335
|
+
@account = account_from_session
|
336
|
+
end
|
337
|
+
|
338
|
+
def account_from_session
|
339
|
+
ds = account_model.where(account_id=>scope.session[session_key])
|
340
|
+
ds = ds.where(account_status_id=>account_open_status_value) unless skip_status_checks?
|
341
|
+
ds.first
|
342
|
+
end
|
343
|
+
|
344
|
+
def password_hash_cost
|
345
|
+
require 'bcrypt'
|
346
|
+
if ENV['RACK_ENV'] == 'test'
|
347
|
+
BCrypt::Engine::MIN_COST
|
348
|
+
else
|
349
|
+
# :nocov:
|
350
|
+
BCrypt::Engine::DEFAULT_COST
|
351
|
+
# :nocov:
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
def password_hash(password)
|
356
|
+
require 'bcrypt'
|
357
|
+
BCrypt::Password.create(password, :cost=>password_hash_cost)
|
358
|
+
end
|
359
|
+
|
360
|
+
def set_password(password)
|
361
|
+
hash = password_hash(password)
|
362
|
+
if account_password_hash_column
|
363
|
+
account.set(account_password_hash_column=>hash).save_changes(:raise_on_save_failure=>true)
|
364
|
+
else
|
365
|
+
if db[password_hash_table].where(account_id=>account_id_value).update(password_hash_column=>hash) == 0
|
366
|
+
db[password_hash_table].insert(account_id=>account_id_value, password_hash_column=>hash)
|
367
|
+
end
|
368
|
+
end
|
369
|
+
end
|
370
|
+
|
371
|
+
def transaction(&block)
|
372
|
+
db.transaction(&block)
|
373
|
+
end
|
374
|
+
|
375
|
+
def email_from
|
376
|
+
"webmaster@#{request.host}"
|
377
|
+
end
|
378
|
+
|
379
|
+
def email_to
|
380
|
+
account.email
|
381
|
+
end
|
382
|
+
|
383
|
+
def create_email(subject, body)
|
384
|
+
require 'mail'
|
385
|
+
m = Mail.new
|
386
|
+
m.from = email_from
|
387
|
+
m.to = email_to
|
388
|
+
m.subject = "#{email_subject_prefix}#{subject}"
|
389
|
+
m.body = body
|
390
|
+
m
|
391
|
+
end
|
392
|
+
|
393
|
+
def email_subject_prefix
|
394
|
+
nil
|
395
|
+
end
|
396
|
+
|
397
|
+
def view(page, title)
|
398
|
+
set_title(title)
|
399
|
+
_view(:view, page)
|
400
|
+
end
|
401
|
+
|
402
|
+
def render(page)
|
403
|
+
_view(:render, page)
|
404
|
+
end
|
405
|
+
|
406
|
+
def skip_status_checks?
|
407
|
+
false
|
408
|
+
end
|
409
|
+
|
410
|
+
def after_close_account
|
411
|
+
end
|
412
|
+
|
413
|
+
private
|
414
|
+
|
415
|
+
def _view(meth, page)
|
416
|
+
auth = self
|
417
|
+
scope.instance_exec do
|
418
|
+
template_opts = find_template(parse_template_opts(page, :locals=>{:rodauth=>auth}))
|
419
|
+
unless File.file?(template_path(template_opts))
|
420
|
+
template_opts[:path] = File.join(File.dirname(__FILE__), '../../../../templates', "#{page}.str")
|
421
|
+
end
|
422
|
+
send(meth, template_opts)
|
423
|
+
end
|
424
|
+
end
|
425
|
+
end
|
426
|
+
end
|
427
|
+
end
|
428
|
+
end
|