rodauth 1.9.0 → 1.10.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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +20 -0
  3. data/README.rdoc +14 -3
  4. data/Rakefile +2 -2
  5. data/doc/base.rdoc +1 -1
  6. data/doc/internals.rdoc +222 -0
  7. data/doc/release_notes/1.10.0.txt +80 -0
  8. data/doc/reset_password.rdoc +8 -2
  9. data/doc/verify_account.rdoc +7 -1
  10. data/doc/verify_change_login.rdoc +8 -6
  11. data/doc/verify_login_change.rdoc +52 -0
  12. data/lib/rodauth/features/account_expiration.rb +1 -1
  13. data/lib/rodauth/features/base.rb +1 -1
  14. data/lib/rodauth/features/change_login.rb +9 -2
  15. data/lib/rodauth/features/change_password.rb +1 -1
  16. data/lib/rodauth/features/close_account.rb +1 -1
  17. data/lib/rodauth/features/confirm_password.rb +1 -1
  18. data/lib/rodauth/features/create_account.rb +1 -1
  19. data/lib/rodauth/features/disallow_password_reuse.rb +1 -1
  20. data/lib/rodauth/features/email_base.rb +6 -2
  21. data/lib/rodauth/features/http_basic_auth.rb +1 -1
  22. data/lib/rodauth/features/jwt.rb +1 -1
  23. data/lib/rodauth/features/lockout.rb +1 -1
  24. data/lib/rodauth/features/login.rb +1 -1
  25. data/lib/rodauth/features/login_password_requirements_base.rb +1 -1
  26. data/lib/rodauth/features/logout.rb +1 -1
  27. data/lib/rodauth/features/otp.rb +1 -1
  28. data/lib/rodauth/features/password_complexity.rb +1 -1
  29. data/lib/rodauth/features/password_expiration.rb +1 -1
  30. data/lib/rodauth/features/password_grace_period.rb +1 -1
  31. data/lib/rodauth/features/recovery_codes.rb +1 -1
  32. data/lib/rodauth/features/remember.rb +1 -1
  33. data/lib/rodauth/features/reset_password.rb +22 -4
  34. data/lib/rodauth/features/session_expiration.rb +1 -1
  35. data/lib/rodauth/features/single_session.rb +1 -1
  36. data/lib/rodauth/features/sms_codes.rb +1 -1
  37. data/lib/rodauth/features/two_factor_base.rb +1 -1
  38. data/lib/rodauth/features/update_password_hash.rb +1 -1
  39. data/lib/rodauth/features/verify_account.rb +23 -5
  40. data/lib/rodauth/features/verify_account_grace_period.rb +1 -1
  41. data/lib/rodauth/features/verify_change_login.rb +1 -1
  42. data/lib/rodauth/features/verify_login_change.rb +189 -0
  43. data/lib/rodauth/version.rb +1 -1
  44. data/lib/rodauth.rb +16 -2
  45. data/spec/migrate/001_tables.rb +10 -0
  46. data/spec/migrate_travis/001_tables.rb +7 -0
  47. data/spec/reset_password_spec.rb +8 -1
  48. data/spec/rodauth_spec.rb +27 -0
  49. data/spec/spec_helper.rb +11 -7
  50. data/spec/verify_account_grace_period_spec.rb +36 -0
  51. data/spec/verify_account_spec.rb +6 -0
  52. data/spec/verify_login_change_spec.rb +179 -0
  53. data/templates/reset-password-request.str +3 -3
  54. data/templates/verify-account-resend.str +3 -3
  55. data/templates/verify-login-change-email.str +9 -0
  56. data/templates/verify-login-change.str +5 -0
  57. metadata +12 -2
@@ -18,11 +18,18 @@ describe 'Rodauth reset_password feature' do
18
18
  click_button 'Request Password Reset'
19
19
  page.find('#notice_flash').text.must_equal "An email has been sent to you with a link to reset the password for your account"
20
20
  page.current_path.must_equal '/'
21
-
22
21
  link = email_link(/(\/reset-password\?key=.+)$/)
22
+
23
23
  visit link[0...-1]
24
24
  page.find('#error_flash').text.must_equal "invalid password reset key"
25
25
 
26
+ visit '/login'
27
+ click_link 'Forgot Password?'
28
+ fill_in 'Login', :with=>'foo@example.com'
29
+ click_button 'Request Password Reset'
30
+ email_link(/(\/reset-password\?key=.+)$/).must_equal link
31
+
32
+ visit '/login'
26
33
  login(:pass=>'01234567', :visit=>false)
27
34
  click_button 'Request Password Reset'
28
35
  email_link(/(\/reset-password\?key=.+)$/).must_equal link
data/spec/rodauth_spec.rb CHANGED
@@ -220,4 +220,31 @@ describe 'Rodauth' do
220
220
  app.instance_variable_get(:@middleware).length.must_equal 1
221
221
  app.ancestors.map(&:to_s).wont_include 'Roda::RodaPlugins::Flash::InstanceMethods'
222
222
  end
223
+
224
+ it "should inherit rodauth configuration in subclass" do
225
+ auth_class = nil
226
+ no_freeze!
227
+ rodauth{auth_class = auth}
228
+ roda(:csrf=>false, :flash=>false){}
229
+ Class.new(app).rodauth.must_equal auth_class
230
+ end
231
+
232
+ it "should use subclass of rodauth configuration if modifying rodauth configuration in subclass" do
233
+ auth_class = nil
234
+ no_freeze!
235
+ rodauth{auth_class = auth; auth_class_eval{def foo; 'foo' end}}
236
+ roda{|r| rodauth.foo}
237
+ visit '/'
238
+ page.html.must_equal 'foo'
239
+
240
+ a = Class.new(app)
241
+ a.plugin(:rodauth){auth_class_eval{def foo; "#{super}bar" end}}
242
+ a.rodauth.superclass.must_equal auth_class
243
+
244
+ visit '/'
245
+ page.html.must_equal 'foo'
246
+ self.app = a
247
+ visit '/'
248
+ page.html.must_equal 'foobar'
249
+ end
223
250
  end
data/spec/spec_helper.rb CHANGED
@@ -43,22 +43,26 @@ require 'tilt/string'
43
43
  db_url = ENV['RODAUTH_SPEC_DB'] || 'postgres:///?user=rodauth_test&password=rodauth_test'
44
44
  DB = Sequel.connect(db_url, :identifier_mangling=>false)
45
45
  DB.extension :freeze_datasets, :date_arithmetic
46
- DB.freeze
47
46
  puts "using #{DB.database_type}"
48
47
 
49
48
  #DB.loggers << Logger.new($stdout)
50
- if DB.adapter_scheme == :jdbc && DB.database_type == :postgres
51
- DB.add_named_conversion_proc(:citext){|s| s}
52
- end
53
- if DB.adapter_scheme == :jdbc && DB.database_type == :sqlite
54
- DB.timezone = :utc
55
- Sequel.application_timezone = :local
49
+ if DB.adapter_scheme == :jdbc
50
+ case DB.database_type
51
+ when :postgres
52
+ DB.add_named_conversion_proc(:citext){|s| s}
53
+ when :sqlite
54
+ DB.timezone = :utc
55
+ Sequel.application_timezone = :local
56
+ end
56
57
  end
58
+
57
59
  if ENV['RODAUTH_SPEC_MIGRATE']
58
60
  Sequel.extension :migration
59
61
  Sequel::Migrator.run(DB, 'spec/migrate_travis')
60
62
  end
61
63
 
64
+ DB.freeze
65
+
62
66
  ENV['RACK_ENV'] = 'test'
63
67
 
64
68
  ::Mail.defaults do
@@ -46,6 +46,42 @@ describe 'Rodauth verify_account_grace_period feature' do
46
46
  page.body.must_include('Logged Intrue')
47
47
  end
48
48
 
49
+ it "should resend verify account email if attempting to create new account with same login" do
50
+ rodauth do
51
+ enable :login, :logout, :change_password, :create_account, :verify_account_grace_period
52
+ change_password_requires_password? false
53
+ end
54
+ roda do |r|
55
+ r.rodauth
56
+ r.root{view :content=>rodauth.logged_in? ? "Logged In#{rodauth.verified_account?}" : "Not Logged"}
57
+ end
58
+
59
+ visit '/create-account'
60
+ fill_in 'Login', :with=>'foo@example2.com'
61
+ fill_in 'Confirm Login', :with=>'foo@example2.com'
62
+ fill_in 'Password', :with=>'0123456789'
63
+ fill_in 'Confirm Password', :with=>'0123456789'
64
+ click_button 'Create Account'
65
+ page.find('#notice_flash').text.must_equal "An email has been sent to you with a link to verify your account"
66
+ link = email_link(/(\/verify-account\?key=.+)$/, 'foo@example2.com')
67
+ page.body.must_include('Logged Infalse')
68
+ page.current_path.must_equal '/'
69
+
70
+ logout
71
+ visit '/create-account'
72
+ fill_in 'Login', :with=>'foo@example2.com'
73
+ click_button 'Create Account'
74
+ click_button 'Send Verification Email Again'
75
+ page.find('#notice_flash').text.must_equal "An email has been sent to you with a link to verify your account"
76
+ page.current_path.must_equal '/login'
77
+ email_link(/(\/verify-account\?key=.+)$/, 'foo@example2.com').must_equal link
78
+
79
+ visit link
80
+ click_button 'Verify Account'
81
+ page.find('#notice_flash').text.must_equal "Your account has been verified"
82
+ page.body.must_include('Logged Intrue')
83
+ end
84
+
49
85
  it "should not allow changing logins for unverified accounts" do
50
86
  rodauth do
51
87
  enable :login, :logout, :change_login, :verify_account_grace_period
@@ -26,8 +26,14 @@ describe 'Rodauth verify_account feature' do
26
26
  page.html.must_include("If you no longer have the email to verify the account, you can request that it be resent to you")
27
27
  click_button 'Send Verification Email Again'
28
28
  page.current_path.must_equal '/login'
29
+ email_link(/(\/verify-account\?key=.+)$/, 'foo@example2.com').must_equal link
29
30
 
31
+ click_link 'Resend Verify Account Information'
32
+ fill_in 'Login', :with=>'foo@example2.com'
33
+ click_button 'Send Verification Email Again'
34
+ page.current_path.must_equal '/login'
30
35
  email_link(/(\/verify-account\?key=.+)$/, 'foo@example2.com').must_equal link
36
+
31
37
  visit '/create-account'
32
38
  fill_in 'Login', :with=>'foo@example2.com'
33
39
  click_button 'Create Account'
@@ -0,0 +1,179 @@
1
+ require File.expand_path("spec_helper", File.dirname(__FILE__))
2
+
3
+ describe 'Rodauth verify_login_change feature' do
4
+ it "should support verifying login changes" do
5
+ rodauth do
6
+ enable :login, :logout, :verify_login_change
7
+ change_login_requires_password? false
8
+ end
9
+ roda do |r|
10
+ r.rodauth
11
+ r.root{view :content=>rodauth.logged_in? ? "Logged In" : "Not Logged"}
12
+ end
13
+
14
+ login
15
+
16
+ visit '/change-login'
17
+ fill_in 'Login', :with=>'foo@example2.com'
18
+ fill_in 'Confirm Login', :with=>'foo@example2.com'
19
+ click_button 'Change Login'
20
+ link = email_link(/(\/verify-login-change\?key=.+)$/, 'foo@example2.com')
21
+ page.find('#notice_flash').text.must_equal "An email has been sent to you with a link to verify your login change"
22
+
23
+ visit '/change-login'
24
+ fill_in 'Login', :with=>'foo@example2.com'
25
+ fill_in 'Confirm Login', :with=>'foo@example2.com'
26
+ click_button 'Change Login'
27
+ email_link(/(\/verify-login-change\?key=.+)$/, 'foo@example2.com').must_equal link
28
+
29
+ visit '/change-login'
30
+ fill_in 'Login', :with=>'foo@example3.com'
31
+ fill_in 'Confirm Login', :with=>'foo@example3.com'
32
+ click_button 'Change Login'
33
+ new_link = email_link(/(\/verify-login-change\?key=.+)$/, 'foo@example3.com')
34
+ new_link.wont_equal link
35
+
36
+ logout
37
+
38
+ visit link
39
+ page.find('#error_flash').text.must_equal "invalid verify login change key"
40
+
41
+ visit new_link
42
+ page.title.must_equal 'Verify Login Change'
43
+ click_button 'Verify Login Change'
44
+ page.find('#notice_flash').text.must_equal "Your login change has been verified"
45
+ page.body.must_include('Not Logged')
46
+
47
+ login
48
+ page.find('#error_flash').text.must_equal "There was an error logging in"
49
+
50
+ login(:login=>'foo@example3.com')
51
+ page.body.must_include('Logged In')
52
+ end
53
+
54
+ it "should support verifying login changes with autologin" do
55
+ rodauth do
56
+ enable :login, :logout, :verify_login_change
57
+ verify_login_change_autologin? true
58
+ change_login_requires_password? false
59
+ end
60
+ roda do |r|
61
+ r.rodauth
62
+ r.root{view :content=>rodauth.logged_in? ? "Logged In" : "Not Logged"}
63
+ end
64
+
65
+ login
66
+
67
+ visit '/change-login'
68
+ fill_in 'Login', :with=>'foo@example2.com'
69
+ fill_in 'Confirm Login', :with=>'foo@example2.com'
70
+ click_button 'Change Login'
71
+ link = email_link(/(\/verify-login-change\?key=.+)$/, 'foo@example2.com')
72
+
73
+ logout
74
+
75
+ visit link
76
+ click_button 'Verify Login Change'
77
+ page.find('#notice_flash').text.must_equal "Your login change has been verified"
78
+ page.body.must_include('Logged In')
79
+ end
80
+
81
+ it "should handle uniqueness errors raised when inserting password reset token" do
82
+ unique = false
83
+ rodauth do
84
+ enable :login, :logout, :verify_login_change
85
+ change_login_requires_password? false
86
+
87
+ auth_class_eval do
88
+ define_method(:raised_uniqueness_violation) do |*a, &block|
89
+ unique.call if unique
90
+ super(*a, &block)
91
+ end
92
+ end
93
+ end
94
+ roda do |r|
95
+ r.rodauth
96
+ r.root{view :content=>rodauth.logged_in? ? "Logged In" : "Not Logged"}
97
+ end
98
+
99
+ login
100
+
101
+ visit '/change-login'
102
+ fill_in 'Login', :with=>'foo@example2.com'
103
+ fill_in 'Confirm Login', :with=>'foo@example2.com'
104
+ click_button 'Change Login'
105
+ link = email_link(/(\/verify-login-change\?key=.+)$/, 'foo@example2.com')
106
+ page.find('#notice_flash').text.must_equal "An email has been sent to you with a link to verify your login change"
107
+
108
+ unique = lambda{DB[:account_login_change_keys].update(:login=>'foo@example3.com'); true}
109
+ visit '/change-login'
110
+ fill_in 'Login', :with=>'foo@example2.com'
111
+ fill_in 'Confirm Login', :with=>'foo@example2.com'
112
+ proc{click_button 'Change Login'}.must_raise Sequel::ConstraintViolation
113
+ end
114
+
115
+ it "should clear verify login change token when closing account" do
116
+ rodauth do
117
+ enable :login, :verify_login_change, :close_account
118
+ change_login_requires_password? false
119
+ end
120
+ roda do |r|
121
+ r.rodauth
122
+ r.root{view :content=>rodauth.logged_in? ? "Logged In" : "Not Logged"}
123
+ end
124
+
125
+ login
126
+
127
+ visit '/change-login'
128
+ fill_in 'Login', :with=>'foo@example2.com'
129
+ fill_in 'Confirm Login', :with=>'foo@example2.com'
130
+ click_button 'Change Login'
131
+ email_link(/key=.+$/, 'foo@example2.com').wont_be_nil
132
+
133
+ DB[:account_login_change_keys].count.must_equal 1
134
+ visit '/close-account'
135
+ fill_in 'Password', :with=>'0123456789'
136
+ click_button 'Close Account'
137
+ DB[:account_login_change_keys].count.must_equal 0
138
+ end
139
+
140
+ it "should support verifying login changes for accounts via jwt" do
141
+ rodauth do
142
+ enable :login, :verify_login_change
143
+ change_login_requires_password? false
144
+ verify_login_change_email_body{verify_login_change_email_link}
145
+ end
146
+ roda(:jwt) do |r|
147
+ r.rodauth
148
+ end
149
+
150
+ json_login
151
+
152
+ res = json_request('/change-login', :login=>'foo2@example.com', "login-confirm"=>'foo2@example.com')
153
+ res.must_equal [200, {'success'=>"An email has been sent to you with a link to verify your login change"}]
154
+ link = email_link(/key=.+$/, 'foo2@example.com')
155
+
156
+ res = json_request('/change-login', :login=>'foo2@example.com', "login-confirm"=>'foo2@example.com')
157
+ res.must_equal [200, {'success'=>"An email has been sent to you with a link to verify your login change"}]
158
+ email_link(/key=.+$/, 'foo2@example.com').must_equal link
159
+
160
+ res = json_request('/change-login', :login=>'foo3@example.com', "login-confirm"=>'foo3@example.com')
161
+ res.must_equal [200, {'success'=>"An email has been sent to you with a link to verify your login change"}]
162
+ new_link = email_link(/key=.+$/, 'foo3@example.com')
163
+ new_link.wont_equal link
164
+
165
+ res = json_request('/verify-login-change')
166
+ res.must_equal [401, {"error"=>"Unable to verify login change"}]
167
+
168
+ res = json_request('/verify-login-change', :key=>link[4..-1])
169
+ res.must_equal [401, {"error"=>"Unable to verify login change"}]
170
+
171
+ res = json_request('/verify-login-change', :key=>new_link[4..-1])
172
+ res.must_equal [200, {"success"=>"Your login change has been verified"}]
173
+
174
+ res = json_request("/login", :login=>'foo@example.com', :password=>'0123456789')
175
+ res.must_equal [401, {'error'=>"There was an error logging in", "field-error"=>["login", "no matching login"]}]
176
+
177
+ json_login(:login=>'foo3@example.com')
178
+ end
179
+ end
@@ -1,7 +1,7 @@
1
1
  <form action="#{rodauth.prefix}/#{rodauth.reset_password_request_route}" method="post" class="rodauth form-horizontal" role="form" id="reset-password-request-form">
2
2
  #{rodauth.reset_password_request_additional_form_tags}
3
- <input type="hidden" name="#{rodauth.login_param}" value="#{h request[rodauth.login_param]}"/>
4
3
  #{rodauth.csrf_tag}
5
- If you have forgotten your password, you can request a password reset:
6
- <input type="submit" class="btn btn-primary inline" value="#{rodauth.reset_password_request_button}"/>
4
+ <p>If you have forgotten your password, you can request a password reset: </p>
5
+ #{(login = request[rodauth.login_param]) ? "<input type=\"hidden\" name=\"#{rodauth.login_param}\" value=\"#{h login}\"/>" : rodauth.render('login-field')}
6
+ #{rodauth.button(rodauth.reset_password_request_button)}
7
7
  </form>
@@ -1,7 +1,7 @@
1
1
  <form action="#{rodauth.prefix}/#{rodauth.verify_account_resend_route}" method="post" class="rodauth form-horizontal" role="form" id="verify-account-resend-form">
2
2
  #{rodauth.verify_account_resend_additional_form_tags}
3
- <input type="hidden" name="#{rodauth.login_param}" value="#{h request[rodauth.login_param]}"/>
4
3
  #{rodauth.csrf_tag}
5
- If you no longer have the email to verify the account, you can request that it be resent to you:
6
- <input type="submit" class="btn btn-primary inline" value="#{rodauth.verify_account_resend_button}"/>
4
+ <p>If you no longer have the email to verify the account, you can request that it be resent to you:</p>
5
+ #{(login = request[rodauth.login_param]) ? "<input type=\"hidden\" name=\"#{rodauth.login_param}\" value=\"#{h login}\"/>" : rodauth.render('login-field')}
6
+ #{rodauth.button(rodauth.verify_account_resend_button)}
7
7
  </form>
@@ -0,0 +1,9 @@
1
+ Someone with an account has requested their login be changed to this email address:
2
+
3
+ Old Login: #{rodauth.verify_login_change_old_login}
4
+ New Login: #{rodauth.verify_login_change_new_login}
5
+
6
+ If you did not request this login change, please ignore this message. If you
7
+ requested this login change, please go to
8
+ #{rodauth.verify_login_change_email_link}
9
+ to verify the login change.
@@ -0,0 +1,5 @@
1
+ <form method="post" class="rodauth form-horizontal" role="form" id="verify-login-change-form">
2
+ #{rodauth.verify_login_change_additional_form_tags}
3
+ #{rodauth.csrf_tag}
4
+ #{rodauth.button(rodauth.verify_login_change_button)}
5
+ </form>
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rodauth
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.0
4
+ version: 1.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Evans
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-02-23 00:00:00.000000000 Z
11
+ date: 2017-03-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sequel
@@ -226,6 +226,8 @@ extra_rdoc_files:
226
226
  - doc/verify_change_login.rdoc
227
227
  - doc/update_password_hash.rdoc
228
228
  - doc/http_basic_auth.rdoc
229
+ - doc/verify_login_change.rdoc
230
+ - doc/internals.rdoc
229
231
  - doc/release_notes/1.0.0.txt
230
232
  - doc/release_notes/1.1.0.txt
231
233
  - doc/release_notes/1.2.0.txt
@@ -236,6 +238,7 @@ extra_rdoc_files:
236
238
  - doc/release_notes/1.7.0.txt
237
239
  - doc/release_notes/1.8.0.txt
238
240
  - doc/release_notes/1.9.0.txt
241
+ - doc/release_notes/1.10.0.txt
239
242
  files:
240
243
  - CHANGELOG
241
244
  - MIT-LICENSE
@@ -251,6 +254,7 @@ files:
251
254
  - doc/disallow_password_reuse.rdoc
252
255
  - doc/email_base.rdoc
253
256
  - doc/http_basic_auth.rdoc
257
+ - doc/internals.rdoc
254
258
  - doc/jwt.rdoc
255
259
  - doc/lockout.rdoc
256
260
  - doc/login.rdoc
@@ -263,6 +267,7 @@ files:
263
267
  - doc/recovery_codes.rdoc
264
268
  - doc/release_notes/1.0.0.txt
265
269
  - doc/release_notes/1.1.0.txt
270
+ - doc/release_notes/1.10.0.txt
266
271
  - doc/release_notes/1.2.0.txt
267
272
  - doc/release_notes/1.3.0.txt
268
273
  - doc/release_notes/1.4.0.txt
@@ -281,6 +286,7 @@ files:
281
286
  - doc/verify_account.rdoc
282
287
  - doc/verify_account_grace_period.rdoc
283
288
  - doc/verify_change_login.rdoc
289
+ - doc/verify_login_change.rdoc
284
290
  - lib/roda/plugins/rodauth.rb
285
291
  - lib/rodauth.rb
286
292
  - lib/rodauth/features/account_expiration.rb
@@ -313,6 +319,7 @@ files:
313
319
  - lib/rodauth/features/verify_account.rb
314
320
  - lib/rodauth/features/verify_account_grace_period.rb
315
321
  - lib/rodauth/features/verify_change_login.rb
322
+ - lib/rodauth/features/verify_login_change.rb
316
323
  - lib/rodauth/migrations.rb
317
324
  - lib/rodauth/version.rb
318
325
  - spec/account_expiration_spec.rb
@@ -345,6 +352,7 @@ files:
345
352
  - spec/verify_account_grace_period_spec.rb
346
353
  - spec/verify_account_spec.rb
347
354
  - spec/verify_change_login_spec.rb
355
+ - spec/verify_login_change_spec.rb
348
356
  - spec/views/layout-other.str
349
357
  - spec/views/layout.str
350
358
  - spec/views/login.str
@@ -383,6 +391,8 @@ files:
383
391
  - templates/verify-account-email.str
384
392
  - templates/verify-account-resend.str
385
393
  - templates/verify-account.str
394
+ - templates/verify-login-change-email.str
395
+ - templates/verify-login-change.str
386
396
  homepage: https://github.com/jeremyevans/rodauth
387
397
  licenses:
388
398
  - MIT