rodauth 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG +3 -0
  3. data/MIT-LICENSE +18 -0
  4. data/README.rdoc +484 -0
  5. data/Rakefile +91 -0
  6. data/lib/roda/plugins/rodauth.rb +265 -0
  7. data/lib/roda/plugins/rodauth/base.rb +428 -0
  8. data/lib/roda/plugins/rodauth/change_login.rb +48 -0
  9. data/lib/roda/plugins/rodauth/change_password.rb +42 -0
  10. data/lib/roda/plugins/rodauth/close_account.rb +42 -0
  11. data/lib/roda/plugins/rodauth/create_account.rb +92 -0
  12. data/lib/roda/plugins/rodauth/lockout.rb +292 -0
  13. data/lib/roda/plugins/rodauth/login.rb +77 -0
  14. data/lib/roda/plugins/rodauth/logout.rb +36 -0
  15. data/lib/roda/plugins/rodauth/remember.rb +226 -0
  16. data/lib/roda/plugins/rodauth/reset_password.rb +205 -0
  17. data/lib/roda/plugins/rodauth/verify_account.rb +228 -0
  18. data/spec/migrate/001_tables.rb +64 -0
  19. data/spec/migrate_password/001_tables.rb +38 -0
  20. data/spec/rodauth_spec.rb +1114 -0
  21. data/spec/views/layout.str +11 -0
  22. data/spec/views/login.str +21 -0
  23. data/templates/change-login.str +22 -0
  24. data/templates/change-password.str +21 -0
  25. data/templates/close-account.str +9 -0
  26. data/templates/confirm-password.str +16 -0
  27. data/templates/create-account.str +33 -0
  28. data/templates/login.str +25 -0
  29. data/templates/logout.str +9 -0
  30. data/templates/remember.str +28 -0
  31. data/templates/reset-password-email.str +5 -0
  32. data/templates/reset-password-request.str +7 -0
  33. data/templates/reset-password.str +23 -0
  34. data/templates/unlock-account-email.str +5 -0
  35. data/templates/unlock-account-request.str +11 -0
  36. data/templates/unlock-account.str +11 -0
  37. data/templates/verify-account-email.str +4 -0
  38. data/templates/verify-account-resend.str +7 -0
  39. data/templates/verify-account.str +11 -0
  40. metadata +227 -0
@@ -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