rodauth 0.10.0 → 1.0.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 (137) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +146 -0
  3. data/README.rdoc +644 -220
  4. data/Rakefile +99 -11
  5. data/doc/account_expiration.rdoc +55 -0
  6. data/doc/base.rdoc +104 -0
  7. data/doc/change_login.rdoc +29 -0
  8. data/doc/change_password.rdoc +26 -0
  9. data/doc/close_account.rdoc +31 -0
  10. data/doc/confirm_password.rdoc +22 -0
  11. data/doc/create_account.rdoc +34 -0
  12. data/doc/disallow_password_reuse.rdoc +37 -0
  13. data/doc/email_base.rdoc +19 -0
  14. data/doc/jwt.rdoc +35 -0
  15. data/doc/lockout.rdoc +83 -0
  16. data/doc/login.rdoc +27 -0
  17. data/doc/login_password_requirements_base.rdoc +50 -0
  18. data/doc/logout.rdoc +21 -0
  19. data/doc/otp.rdoc +100 -0
  20. data/doc/password_complexity.rdoc +50 -0
  21. data/doc/password_expiration.rdoc +52 -0
  22. data/doc/password_grace_period.rdoc +10 -0
  23. data/doc/recovery_codes.rdoc +60 -0
  24. data/doc/release_notes/1.0.0.txt +443 -0
  25. data/doc/remember.rdoc +82 -0
  26. data/doc/reset_password.rdoc +70 -0
  27. data/doc/session_expiration.rdoc +27 -0
  28. data/doc/single_session.rdoc +43 -0
  29. data/doc/sms_codes.rdoc +119 -0
  30. data/doc/two_factor_base.rdoc +27 -0
  31. data/doc/verify_account.rdoc +70 -0
  32. data/doc/verify_account_grace_period.rdoc +15 -0
  33. data/doc/verify_change_login.rdoc +9 -0
  34. data/lib/roda/plugins/rodauth.rb +3 -262
  35. data/lib/rodauth.rb +260 -0
  36. data/lib/rodauth/features/account_expiration.rb +108 -0
  37. data/lib/rodauth/features/base.rb +479 -0
  38. data/lib/rodauth/features/change_login.rb +77 -0
  39. data/lib/rodauth/features/change_password.rb +66 -0
  40. data/lib/rodauth/features/close_account.rb +82 -0
  41. data/lib/rodauth/features/confirm_password.rb +51 -0
  42. data/lib/rodauth/features/create_account.rb +128 -0
  43. data/lib/rodauth/features/disallow_password_reuse.rb +82 -0
  44. data/lib/rodauth/features/email_base.rb +63 -0
  45. data/lib/rodauth/features/jwt.rb +151 -0
  46. data/lib/rodauth/features/lockout.rb +262 -0
  47. data/lib/rodauth/features/login.rb +61 -0
  48. data/lib/rodauth/features/login_password_requirements_base.rb +123 -0
  49. data/lib/rodauth/features/logout.rb +37 -0
  50. data/lib/rodauth/features/otp.rb +338 -0
  51. data/lib/rodauth/features/password_complexity.rb +89 -0
  52. data/lib/rodauth/features/password_expiration.rb +111 -0
  53. data/lib/rodauth/features/password_grace_period.rb +46 -0
  54. data/lib/rodauth/features/recovery_codes.rb +240 -0
  55. data/lib/rodauth/features/remember.rb +200 -0
  56. data/lib/rodauth/features/reset_password.rb +207 -0
  57. data/lib/rodauth/features/session_expiration.rb +55 -0
  58. data/lib/rodauth/features/single_session.rb +87 -0
  59. data/lib/rodauth/features/sms_codes.rb +498 -0
  60. data/lib/rodauth/features/two_factor_base.rb +135 -0
  61. data/lib/rodauth/features/verify_account.rb +232 -0
  62. data/lib/rodauth/features/verify_account_grace_period.rb +76 -0
  63. data/lib/rodauth/features/verify_change_login.rb +20 -0
  64. data/lib/rodauth/migrations.rb +130 -0
  65. data/lib/rodauth/version.rb +9 -0
  66. data/spec/account_expiration_spec.rb +90 -0
  67. data/spec/all.rb +1 -0
  68. data/spec/change_login_spec.rb +149 -0
  69. data/spec/change_password_spec.rb +177 -0
  70. data/spec/close_account_spec.rb +162 -0
  71. data/spec/confirm_password_spec.rb +70 -0
  72. data/spec/create_account_spec.rb +127 -0
  73. data/spec/disallow_password_reuse_spec.rb +84 -0
  74. data/spec/lockout_spec.rb +228 -0
  75. data/spec/login_spec.rb +188 -0
  76. data/spec/migrate/001_tables.rb +103 -16
  77. data/spec/migrate/002_account_password_hash_column.rb +11 -0
  78. data/spec/migrate_password/001_tables.rb +60 -42
  79. data/spec/migrate_travis/001_tables.rb +116 -0
  80. data/spec/password_complexity_spec.rb +108 -0
  81. data/spec/password_expiration_spec.rb +243 -0
  82. data/spec/password_grace_period_spec.rb +93 -0
  83. data/spec/remember_spec.rb +424 -0
  84. data/spec/reset_password_spec.rb +185 -0
  85. data/spec/rodauth_spec.rb +57 -980
  86. data/spec/session_expiration_spec.rb +58 -0
  87. data/spec/single_session_spec.rb +107 -0
  88. data/spec/spec_helper.rb +202 -0
  89. data/spec/two_factor_spec.rb +1310 -0
  90. data/spec/verify_account_grace_period_spec.rb +135 -0
  91. data/spec/verify_account_spec.rb +142 -0
  92. data/spec/verify_change_login_spec.rb +46 -0
  93. data/spec/views/login.str +2 -2
  94. data/templates/add-recovery-codes.str +2 -0
  95. data/templates/button.str +5 -0
  96. data/templates/change-login.str +5 -18
  97. data/templates/change-password.str +6 -14
  98. data/templates/close-account.str +3 -6
  99. data/templates/confirm-password.str +4 -14
  100. data/templates/create-account.str +6 -30
  101. data/templates/login-confirm-field.str +6 -0
  102. data/templates/login-field.str +6 -0
  103. data/templates/login.str +5 -19
  104. data/templates/logout.str +2 -6
  105. data/templates/otp-auth-code-field.str +6 -0
  106. data/templates/otp-auth.str +8 -0
  107. data/templates/otp-disable.str +6 -0
  108. data/templates/otp-setup.str +21 -0
  109. data/templates/password-confirm-field.str +6 -0
  110. data/templates/password-field.str +6 -0
  111. data/templates/recovery-auth.str +12 -0
  112. data/templates/recovery-codes.str +6 -0
  113. data/templates/remember.str +8 -12
  114. data/templates/reset-password-request.str +2 -2
  115. data/templates/reset-password.str +4 -18
  116. data/templates/sms-auth.str +6 -0
  117. data/templates/sms-code-field.str +6 -0
  118. data/templates/sms-confirm.str +7 -0
  119. data/templates/sms-disable.str +7 -0
  120. data/templates/sms-request.str +5 -0
  121. data/templates/sms-setup.str +12 -0
  122. data/templates/unlock-account-request.str +3 -7
  123. data/templates/unlock-account.str +4 -7
  124. data/templates/verify-account-resend.str +2 -2
  125. data/templates/verify-account.str +2 -6
  126. metadata +191 -29
  127. data/lib/roda/plugins/rodauth/base.rb +0 -428
  128. data/lib/roda/plugins/rodauth/change_login.rb +0 -48
  129. data/lib/roda/plugins/rodauth/change_password.rb +0 -42
  130. data/lib/roda/plugins/rodauth/close_account.rb +0 -42
  131. data/lib/roda/plugins/rodauth/create_account.rb +0 -92
  132. data/lib/roda/plugins/rodauth/lockout.rb +0 -292
  133. data/lib/roda/plugins/rodauth/login.rb +0 -81
  134. data/lib/roda/plugins/rodauth/logout.rb +0 -36
  135. data/lib/roda/plugins/rodauth/remember.rb +0 -226
  136. data/lib/roda/plugins/rodauth/reset_password.rb +0 -205
  137. data/lib/roda/plugins/rodauth/verify_account.rb +0 -228
@@ -0,0 +1,116 @@
1
+ require 'rodauth/migrations'
2
+
3
+ Sequel.migration do
4
+ up do
5
+ extension :date_arithmetic
6
+
7
+ create_table(:account_statuses) do
8
+ Integer :id, :primary_key=>true
9
+ String :name, :null=>false, :unique=>true
10
+ end
11
+ from(:account_statuses).import([:id, :name], [[1, 'Unverified'], [2, 'Verified'], [3, 'Closed']])
12
+
13
+ db = self
14
+ create_table(:accounts) do
15
+ primary_key :id, :type=>Bignum
16
+ foreign_key :status_id, :account_statuses, :null=>false, :default=>1
17
+ if db.database_type == :postgres
18
+ citext :email, :null=>false
19
+ constraint :valid_email, :email=>/^[^,;@ \r\n]+@[^,@; \r\n]+\.[^,@; \r\n]+$/
20
+ index :email, :unique=>true, :where=>{:status_id=>[1, 2]}
21
+ else
22
+ String :email, :null=>false
23
+ index :email, :unique=>true
24
+ end
25
+
26
+ String :ph
27
+ end
28
+
29
+ create_table(:account_password_hashes) do
30
+ foreign_key :id, :accounts, :primary_key=>true, :type=>Bignum
31
+ String :password_hash, :null=>false
32
+ end
33
+ Rodauth.create_database_authentication_functions(self)
34
+
35
+ deadline_opts = proc do |days|
36
+ if database_type == :mysql
37
+ {:null=>false}
38
+ else
39
+ {:null=>false, :default=>Sequel.date_add(Sequel::CURRENT_TIMESTAMP, :days=>days)}
40
+ end
41
+ end
42
+
43
+ create_table(:account_password_reset_keys) do
44
+ foreign_key :id, :accounts, :primary_key=>true, :type=>Bignum
45
+ String :key, :null=>false
46
+ DateTime :deadline, deadline_opts[1]
47
+ end
48
+
49
+ create_table(:account_verification_keys) do
50
+ foreign_key :id, :accounts, :primary_key=>true, :type=>Bignum
51
+ String :key, :null=>false
52
+ DateTime :requested_at, :null=>false, :default=>Sequel::CURRENT_TIMESTAMP
53
+ end
54
+
55
+ create_table(:account_remember_keys) do
56
+ foreign_key :id, :accounts, :primary_key=>true, :type=>Bignum
57
+ String :key, :null=>false
58
+ DateTime :deadline, deadline_opts[14]
59
+ end
60
+
61
+ create_table(:account_login_failures) do
62
+ foreign_key :id, :accounts, :primary_key=>true, :type=>Bignum
63
+ Integer :number, :null=>false, :default=>1
64
+ end
65
+ create_table(:account_lockouts) do
66
+ foreign_key :id, :accounts, :primary_key=>true, :type=>Bignum
67
+ String :key, :null=>false
68
+ DateTime :deadline, deadline_opts[1]
69
+ end
70
+
71
+ create_table(:account_password_change_times) do
72
+ foreign_key :id, :accounts, :primary_key=>true, :type=>Bignum
73
+ DateTime :changed_at, :null=>false, :default=>Sequel::CURRENT_TIMESTAMP
74
+ end
75
+
76
+ create_table(:account_activity_times) do
77
+ foreign_key :id, :accounts, :primary_key=>true, :type=>Bignum
78
+ DateTime :last_activity_at, :null=>false
79
+ DateTime :last_login_at, :null=>false
80
+ DateTime :expired_at
81
+ end
82
+
83
+ create_table(:account_session_keys) do
84
+ foreign_key :id, :accounts, :primary_key=>true, :type=>Bignum
85
+ String :key, :null=>false
86
+ end
87
+
88
+ create_table(:account_otp_keys) do
89
+ foreign_key :id, :accounts, :primary_key=>true, :type=>Bignum
90
+ String :key, :null=>false
91
+ Integer :num_failures, :null=>false, :default=>0
92
+ Time :last_use, :null=>false, :default=>Sequel::CURRENT_TIMESTAMP
93
+ end
94
+
95
+ create_table(:account_recovery_codes) do
96
+ foreign_key :id, :accounts, :type=>Bignum
97
+ String :code
98
+ primary_key [:id, :code]
99
+ end
100
+
101
+ create_table(:account_sms_codes) do
102
+ foreign_key :id, :accounts, :primary_key=>true, :type=>Bignum
103
+ String :phone_number, :null=>false
104
+ Integer :num_failures
105
+ String :code
106
+ DateTime :code_issued_at, :null=>false, :default=>Sequel::CURRENT_TIMESTAMP
107
+ end
108
+
109
+ create_table(:account_previous_password_hashes) do
110
+ primary_key :id, :type=>Bignum
111
+ foreign_key :account_id, :accounts, :type=>Bignum
112
+ String :password_hash, :null=>false
113
+ end
114
+ Rodauth.create_database_previous_password_check_functions(self)
115
+ end
116
+ end
@@ -0,0 +1,108 @@
1
+ require File.expand_path("spec_helper", File.dirname(__FILE__))
2
+
3
+ describe 'Rodauth password complexity feature' do
4
+ it "should do additional password complexity checks" do
5
+ rodauth do
6
+ enable :login, :change_password, :password_complexity
7
+ change_password_requires_password? false
8
+ password_dictionary_file 'spec/words'
9
+ end
10
+ roda do |r|
11
+ r.rodauth
12
+ r.root{view :content=>""}
13
+ end
14
+
15
+ login
16
+ page.current_path.must_equal '/'
17
+
18
+ visit '/change-password'
19
+
20
+ bad_passwords = [
21
+ ["minimum 6 characters", %w"a1OX"],
22
+ ["does not include uppercase letters, lowercase letters, and numbers",
23
+ %w'sdflksdfl sdflks!fl Sdflksdfl dfl1sdfl DFL1SDFL DFL!SDFL'],
24
+ ["includes common character sequence",
25
+ %w"Aqwerty12 Aazerty12 HA123ha HA234ha HA345ha HA456ha HA567ha HA678ha HA789ha HA890ha"],
26
+ ["contains 3 or more of the same character in a row", %w"Helll0 Hellllll0"],
27
+ ["is a word in a dictionary",
28
+ %w"Password1 1Password1 1PaSSword1 1P@$5w0Rd1 2398|3@$+7809 2|!7+1e l4$7$124 N!88|e56"]
29
+ ]
30
+
31
+
32
+ bad_passwords.each do |message, passwords|
33
+ passwords.each do |pass|
34
+ fill_in 'New Password', :with=>pass
35
+ fill_in 'Confirm Password', :with=>pass
36
+ click_button 'Change Password'
37
+ page.html.must_include("invalid password, does not meet requirements (#{message})")
38
+ page.find('#error_flash').text.must_equal "There was an error changing your password"
39
+ end
40
+ end
41
+
42
+ fill_in 'New Password', :with=>'footpassword'
43
+ fill_in 'Confirm Password', :with=>'footpassword'
44
+ click_button 'Change Password'
45
+ page.find('#notice_flash').text.must_equal "Your password has been changed"
46
+ end
47
+
48
+ it "should support default dictionary" do
49
+ default_dictionary = '/usr/share/dict/words'
50
+ skip("#{default_dictionary} not present") unless File.file?(default_dictionary)
51
+ password = File.read(default_dictionary).split.sort_by{|w| w.length}.last
52
+
53
+ rodauth do
54
+ enable :login, :change_password, :password_complexity
55
+ change_password_requires_password? false
56
+ end
57
+ roda do |r|
58
+ r.rodauth
59
+ r.root{view :content=>""}
60
+ end
61
+
62
+ login
63
+ page.current_path.must_equal '/'
64
+
65
+ visit '/change-password'
66
+ fill_in 'New Password', :with=>"135#{pass}135"
67
+ fill_in 'Confirm Password', :with=>"135#{pass}135"
68
+ click_button 'Change Password'
69
+ page.html.must_include("invalid password")
70
+ page.find('#error_flash').text.must_equal "There was an error changing your password"
71
+
72
+ fill_in 'New Password', :with=>'footpassword'
73
+ fill_in 'Confirm Password', :with=>'footpassword'
74
+ click_button 'Change Password'
75
+ page.find('#notice_flash').text.must_equal "Your password has been changed"
76
+ end
77
+
78
+ it "should support no dictionary" do
79
+ default_dictionary = '/usr/share/dict/words'
80
+ skip("#{default_dictionary} not present") unless File.file?(default_dictionary)
81
+ password = File.read(default_dictionary).split.sort_by{|w| w.length}.last
82
+
83
+ rodauth do
84
+ enable :login, :change_password, :password_complexity
85
+ change_password_requires_password? false
86
+ password_dictionary_file false
87
+ end
88
+ roda do |r|
89
+ r.rodauth
90
+ r.root{view :content=>""}
91
+ end
92
+
93
+ login
94
+ page.current_path.must_equal '/'
95
+
96
+ visit '/change-password'
97
+ fill_in 'New Password', :with=>"password123"
98
+ fill_in 'Confirm Password', :with=>"password123"
99
+ click_button 'Change Password'
100
+ page.html.must_include("invalid password")
101
+ page.find('#error_flash').text.must_equal "There was an error changing your password"
102
+
103
+ fill_in 'New Password', :with=>'Password1'
104
+ fill_in 'Confirm Password', :with=>'Password1'
105
+ click_button 'Change Password'
106
+ page.find('#notice_flash').text.must_equal "Your password has been changed"
107
+ end
108
+ end
@@ -0,0 +1,243 @@
1
+ require File.expand_path("spec_helper", File.dirname(__FILE__))
2
+
3
+ describe 'Rodauth password expiration feature' do
4
+ it "should force password changes after x number of days" do
5
+ rodauth do
6
+ enable :login, :logout, :change_password, :reset_password, :password_expiration
7
+ allow_password_change_after 1000
8
+ change_password_requires_password? false
9
+ end
10
+ roda do |r|
11
+ r.rodauth
12
+ rodauth.require_current_password if rodauth.logged_in?
13
+ r.root{view :content=>""}
14
+ end
15
+
16
+ login(:pass=>'01234567')
17
+ click_button 'Request Password Reset'
18
+ link = email_link(/(\/reset-password\?key=.+)$/)
19
+
20
+ visit link
21
+ page.current_path.must_equal '/reset-password'
22
+
23
+ login
24
+ page.current_path.must_equal '/'
25
+
26
+ visit '/change-password'
27
+ fill_in 'New Password', :with=>'banana'
28
+ fill_in 'Confirm Password', :with=>'banana'
29
+ click_button 'Change Password'
30
+ page.current_path.must_equal '/'
31
+
32
+ visit '/change-password'
33
+ page.current_path.must_equal '/'
34
+ page.find('#error_flash').text.must_equal "Your password cannot be changed yet"
35
+
36
+ logout
37
+
38
+ visit link
39
+ page.current_path.must_equal '/'
40
+ page.find('#error_flash').text.must_equal "Your password cannot be changed yet"
41
+
42
+ DB[:account_password_change_times].update(:changed_at=>Time.now - 1100)
43
+
44
+ visit link
45
+ page.current_path.must_equal '/reset-password'
46
+
47
+ login(:pass=>'banana')
48
+ page.current_path.must_equal '/'
49
+
50
+ visit '/change-password'
51
+ page.current_path.must_equal '/change-password'
52
+
53
+ logout
54
+
55
+ DB[:account_password_change_times].update(:changed_at=>Time.now - 91*86400)
56
+
57
+ login(:pass=>'banana')
58
+ page.current_path.must_equal '/change-password'
59
+ page.find('#error_flash').text.must_equal "Your password has expired and needs to be changed"
60
+
61
+ visit '/foo'
62
+ page.current_path.must_equal '/change-password'
63
+ page.find('#error_flash').text.must_equal "Your password has expired and needs to be changed"
64
+
65
+ fill_in 'New Password', :with=>'banana2'
66
+ fill_in 'Confirm Password', :with=>'banana2'
67
+ click_button 'Change Password'
68
+ page.current_path.must_equal '/'
69
+
70
+ visit '/change-password'
71
+ page.current_path.must_equal '/'
72
+ page.find('#error_flash').text.must_equal "Your password cannot be changed yet"
73
+
74
+ logout
75
+
76
+ visit link
77
+ page.current_path.must_equal '/'
78
+ page.find('#error_flash').text.must_equal "Your password cannot be changed yet"
79
+ end
80
+
81
+ it "should update password changed at when creating accounts" do
82
+ rodauth do
83
+ enable :login, :change_password, :password_expiration
84
+ password_expiration_default true
85
+ change_password_requires_password? false
86
+ end
87
+ roda do |r|
88
+ r.rodauth
89
+ rodauth.require_current_password
90
+ r.root{view :content=>""}
91
+ end
92
+
93
+ login
94
+ page.current_path.must_equal '/change-password'
95
+
96
+ visit '/'
97
+ page.current_path.must_equal '/change-password'
98
+ fill_in 'New Password', :with=>'banana'
99
+ fill_in 'Confirm Password', :with=>'banana'
100
+ click_button 'Change Password'
101
+ page.current_path.must_equal '/'
102
+ end
103
+
104
+ it "should update password changed at when creating accounts" do
105
+ rodauth do
106
+ enable :login, :create_account, :password_expiration
107
+ allow_password_change_after 1000
108
+ account_password_hash_column :ph
109
+ end
110
+ roda do |r|
111
+ r.rodauth
112
+ r.root{view :content=>""}
113
+ end
114
+
115
+ visit '/create-account'
116
+ fill_in 'Login', :with=>'foo2@example.com'
117
+ fill_in 'Confirm Login', :with=>'foo2@example.com'
118
+ fill_in 'Password', :with=>'apple2'
119
+ fill_in 'Confirm Password', :with=>'apple2'
120
+ click_button 'Create Account'
121
+
122
+ visit '/change-password'
123
+ page.current_path.must_equal '/'
124
+ page.find('#error_flash').text.must_equal "Your password cannot be changed yet"
125
+ end
126
+
127
+ it "should remove password expiration data when closing accounts" do
128
+ rodauth do
129
+ enable :create_account, :close_account, :password_expiration
130
+ close_account_requires_password? false
131
+ create_account_autologin? true
132
+ end
133
+ roda do |r|
134
+ r.rodauth
135
+ r.root{view :content=>""}
136
+ end
137
+
138
+ visit '/create-account'
139
+ fill_in 'Login', :with=>'foo2@example.com'
140
+ fill_in 'Confirm Login', :with=>'foo2@example.com'
141
+ fill_in 'Password', :with=>'apple2'
142
+ fill_in 'Confirm Password', :with=>'apple2'
143
+ click_button 'Create Account'
144
+
145
+ DB[:account_password_change_times].count.must_equal 1
146
+ visit '/close-account'
147
+ click_button 'Close Account'
148
+ DB[:account_password_change_times].count.must_equal 0
149
+ end
150
+
151
+ it "should handle the case where the password is expired while the user has logged in" do
152
+ rodauth do
153
+ enable :login, :change_password, :password_expiration
154
+ password_expiration_default true
155
+ change_password_requires_password? false
156
+ require_password_change_after 100
157
+ end
158
+ roda do |r|
159
+ r.rodauth
160
+ rodauth.require_current_password
161
+ r.get("expire/:d"){|d| session[:password_changed_at] = Time.now.to_i - d.to_i; r.redirect '/'}
162
+ r.root{view :content=>""}
163
+ end
164
+
165
+ login
166
+ page.current_path.must_equal '/change-password'
167
+
168
+ visit '/'
169
+ page.current_path.must_equal '/change-password'
170
+ fill_in 'New Password', :with=>'banana'
171
+ fill_in 'Confirm Password', :with=>'banana'
172
+ click_button 'Change Password'
173
+ page.current_path.must_equal '/'
174
+
175
+ visit "/expire/90"
176
+ page.current_path.must_equal '/'
177
+
178
+ visit "/expire/110"
179
+ page.current_path.must_equal '/change-password'
180
+ end
181
+
182
+ it "should force password changes via jwt" do
183
+ rodauth do
184
+ enable :login, :logout, :change_password, :reset_password, :password_expiration
185
+ allow_password_change_after 1000
186
+ change_password_requires_password? false
187
+ reset_password_email_body{reset_password_email_link}
188
+ end
189
+ roda(:jwt) do |r|
190
+ r.rodauth
191
+ rodauth.require_current_password
192
+ if rodauth.authenticated?
193
+ [1]
194
+ else
195
+ [2]
196
+ end
197
+ end
198
+
199
+ json_request.must_equal [200, [2]]
200
+
201
+ res = json_request('/reset-password-request', :login=>'foo@example.com')
202
+ res.must_equal [200, {"success"=>"An email has been sent to you with a link to reset the password for your account"}]
203
+ link = email_link(/key=.+$/)
204
+
205
+ json_login
206
+
207
+ res = json_request('/change-password', :password=>'0123456789', "new-password"=>'0123456', "password-confirm"=>'0123456')
208
+ res.must_equal [200, {'success'=>"Your password has been changed"}]
209
+
210
+ json_request.must_equal [200, [1]]
211
+
212
+ res = json_request('/change-password', :password=>'0123456', "new-password"=>'01234567', "password-confirm"=>'01234567')
213
+ res.must_equal [400, {'error'=>"Your password cannot be changed yet"}]
214
+
215
+ json_logout
216
+
217
+ res = json_request('/reset-password', :key=>link[4..-1], :password=>'01234567', "password-confirm"=>'01234567')
218
+ res.must_equal [400, {'error'=>"Your password cannot be changed yet"}]
219
+
220
+ DB[:account_password_change_times].update(:changed_at=>Time.now - 1100)
221
+ res = json_request('/reset-password', :key=>link[4..-1], :password=>'01234567', "password-confirm"=>'01234567')
222
+ res.must_equal [200, {"success"=>"Your password has been reset"}]
223
+
224
+ DB[:account_password_change_times].update(:changed_at=>Time.now - 1100)
225
+ json_login(:pass=>'01234567')
226
+ res = json_request('/change-password', :password=>'01234567', "new-password"=>'012345678', "password-confirm"=>'012345678')
227
+ res.must_equal [200, {'success'=>"Your password has been changed"}]
228
+
229
+ DB[:account_password_change_times].update(:changed_at=>Time.now - 91*86400)
230
+
231
+ json_logout
232
+ json_request.must_equal [200, [2]]
233
+
234
+ res = json_login(:pass=>'012345678', :no_check=>true)
235
+ res.must_equal [400, {'error'=>"Your password has expired and needs to be changed"}]
236
+ json_request.must_equal [400, {'error'=>"Your password has expired and needs to be changed"}]
237
+
238
+ res = json_request('/change-password', :password=>'012345678', "new-password"=>'012345678a', "password-confirm"=>'012345678a')
239
+ res.must_equal [200, {'success'=>"Your password has been changed"}]
240
+
241
+ json_request.must_equal [200, [1]]
242
+ end
243
+ end