Skip to content

Commit 416a3ac

Browse files
committed
Add reset password commands
1 parent 17f8448 commit 416a3ac

File tree

6 files changed

+388
-22
lines changed

6 files changed

+388
-22
lines changed

Gemfile.lock

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -43,29 +43,31 @@ GEM
4343
ffi-compiler (1.3.2)
4444
ffi (>= 1.15.5)
4545
rake
46-
foob (0.0.12)
47-
foobara
48-
foobara-command-generator
49-
foobara-domain-generator
50-
foobara-domain-mapper-generator
51-
foobara-empty-ruby-project-generator
52-
foobara-empty-typescript-react-project-generator
53-
foobara-local-files-crud-driver-generator
54-
foobara-organization-generator
55-
foobara-rack-connector-generator
56-
foobara-redis-crud-driver-generator
57-
foobara-remote-imports-generator
58-
foobara-resque-connector-generator
59-
foobara-resque-scheduler-connector-generator
46+
foob (0.0.13)
47+
foobara (~> 0.0.99)
48+
foobara-command-generator (~> 0.0.1)
49+
foobara-domain-generator (~> 0.0.1)
50+
foobara-domain-mapper-generator (~> 0.0.1)
51+
foobara-empty-ruby-project-generator (~> 0.0.1)
52+
foobara-empty-typescript-react-project-generator (~> 0.0.1)
53+
foobara-local-files-crud-driver-generator (~> 0.0.1)
54+
foobara-mcp-connector-generator (~> 0.0.1)
55+
foobara-organization-generator (~> 0.0.1)
56+
foobara-rack-connector-generator (~> 0.0.1)
57+
foobara-redis-crud-driver-generator (~> 0.0.1)
58+
foobara-remote-imports-generator (~> 0.0.1)
59+
foobara-resque-connector-generator (~> 0.0.1)
60+
foobara-resque-scheduler-connector-generator (~> 0.0.1)
6061
foobara-sh-cli-connector (~> 0.0.10)
61-
foobara-sh-cli-connector-generator
62-
foobara-type-generator
63-
foobara-typescript-react-command-form-generator
64-
foobara-typescript-remote-command-generator
65-
foobara (0.0.96)
62+
foobara-sh-cli-connector-generator (~> 0.0.1)
63+
foobara-type-generator (~> 0.0.1)
64+
foobara-typescript-react-command-form-generator (~> 0.0.1)
65+
foobara-typescript-remote-command-generator (~> 0.0.1)
66+
foobara (0.0.102)
6667
bigdecimal
6768
foobara-lru-cache (~> 0.0.2)
6869
foobara-util (~> 0.0.11)
70+
inheritable-thread-vars (~> 0.0.1)
6971
foobara-command-generator (0.0.3)
7072
foobara
7173
foobara-files-generator
@@ -90,6 +92,8 @@ GEM
9092
foobara
9193
foobara-files-generator
9294
foobara-lru-cache (0.0.2)
95+
foobara-mcp-connector-generator (0.0.1)
96+
foobara (~> 0.0.99)
9397
foobara-organization-generator (0.0.2)
9498
foobara
9599
foobara-files-generator
@@ -110,7 +114,7 @@ GEM
110114
foobara-rubocop-rules (0.0.8)
111115
rubocop
112116
rubocop-rspec
113-
foobara-sh-cli-connector (0.0.13)
117+
foobara-sh-cli-connector (0.0.15)
114118
foobara (~> 0.0.88)
115119
foobara-sh-cli-connector-generator (0.0.4)
116120
foobara-files-generator (~> 0.0.1)
@@ -142,6 +146,7 @@ GEM
142146
guard-compat (~> 1.1)
143147
rspec (>= 2.99.0, < 4.0)
144148
hashdiff (1.1.2)
149+
inheritable-thread-vars (0.0.1)
145150
io-console (0.8.0)
146151
irb (1.15.2)
147152
pp (>= 0.6.0)
@@ -164,7 +169,7 @@ GEM
164169
shellany (~> 0.0)
165170
ostruct (0.6.1)
166171
parallel (1.26.3)
167-
parser (3.3.7.4)
172+
parser (3.3.8.0)
168173
ast (~> 2.4.1)
169174
racc
170175
pp (0.6.2)
@@ -220,7 +225,7 @@ GEM
220225
rubocop-ast (>= 1.44.0, < 2.0)
221226
ruby-progressbar (~> 1.7)
222227
unicode-display_width (>= 2.4.0, < 4.0)
223-
rubocop-ast (1.44.0)
228+
rubocop-ast (1.44.1)
224229
parser (>= 3.3.7.2)
225230
prism (~> 1.4)
226231
rubocop-rake (0.7.1)
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
RSpec.describe Foobara::Auth::CreateResetPasswordToken do
2+
after { Foobara.reset_alls }
3+
4+
before do
5+
Foobara::Persistence.default_crud_driver = Foobara::Persistence::CrudDrivers::InMemory.new
6+
end
7+
8+
let(:command) { described_class.new(inputs) }
9+
let(:outcome) { command.run }
10+
let(:result) { outcome.result }
11+
let(:errors) { outcome.errors }
12+
let(:errors_hash) { outcome.errors_hash }
13+
14+
let(:user) do
15+
Foobara::Auth::CreateUser.run!(username: "Basil", email: "[email protected]", plaintext_password:)
16+
end
17+
let(:plaintext_password) { "somepassword" }
18+
19+
let(:inputs) do
20+
{ user: user.id }
21+
end
22+
23+
it "sets the user's reset_password_token" do
24+
user_id = user.id
25+
26+
expect {
27+
expect(outcome).to be_success
28+
}.to change {
29+
Foobara::Auth::Types::User.transaction do
30+
Foobara::Auth::Types::User.load(user_id).reset_password_token&.id
31+
end
32+
}
33+
end
34+
end

spec/reset_password_spec.rb

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
RSpec.describe Foobara::Auth::ResetPassword do
2+
after { Foobara.reset_alls }
3+
4+
before do
5+
Foobara::Persistence.default_crud_driver = Foobara::Persistence::CrudDrivers::InMemory.new
6+
end
7+
8+
let(:command) { described_class.new(inputs) }
9+
let(:outcome) { command.run }
10+
let(:result) { outcome.result }
11+
let(:errors) { outcome.errors }
12+
let(:errors_hash) { outcome.errors_hash }
13+
14+
let(:user) do
15+
Foobara::Auth::CreateUser.run!(username: "Basil", email: "[email protected]", plaintext_password:)
16+
end
17+
let(:plaintext_password) { "somepassword" }
18+
let(:old_password) { plaintext_password }
19+
let(:new_password) { "somenewpassword" }
20+
let(:reset_password_token_secret) do
21+
Foobara::Auth::CreateResetPasswordToken.run!(user: user.id)
22+
end
23+
24+
context "when using a reset password token" do
25+
let(:inputs) do
26+
{ reset_password_token_secret:, new_password: }
27+
end
28+
29+
it "is successful and we can verify with the new password after resetting" do
30+
expect(
31+
Foobara::Auth::VerifyPassword.run!(user: user.id, plaintext_password: old_password)
32+
).to be true
33+
expect(
34+
Foobara::Auth::VerifyPassword.run!(user: user.id, plaintext_password: new_password)
35+
).to be false
36+
37+
expect(outcome).to be_success
38+
expect(result).to be_a(Foobara::Auth::Types::User)
39+
40+
expect(
41+
Foobara::Auth::VerifyPassword.run!(user: user.id, plaintext_password: old_password)
42+
).to be false
43+
expect(
44+
Foobara::Auth::VerifyPassword.run!(user: user.id, plaintext_password: new_password)
45+
).to be true
46+
end
47+
48+
it "marks the original refresh token as used" do
49+
reset_password_token_secret
50+
51+
reloaded_user = Foobara::Auth::FindUser.run!(id: user.id)
52+
reset_token_id = reloaded_user.reset_password_token.id
53+
54+
expect {
55+
expect(outcome).to be_success
56+
}.to change {
57+
Foobara::Auth::Types::User.transaction do
58+
Foobara::Auth::Types::Token.load(reset_token_id).inactive?
59+
end
60+
}.from(false).to(true)
61+
end
62+
63+
context "with an invalid refresh token" do
64+
let(:inputs) do
65+
{ reset_password_token_secret: invalid_token, new_password: }
66+
end
67+
let(:invalid_token) do
68+
text = reset_password_token_secret.dup
69+
text[-5] = text[-5] == "x" ? "y" : "x"
70+
text
71+
end
72+
73+
it "fails with appropriate error" do
74+
expect(outcome).to_not be_success
75+
expect(outcome.errors_hash).to include("runtime.invalid_reset_password_token")
76+
end
77+
end
78+
79+
context "when the reset password token has been replaced" do
80+
it "fails with the expected error" do
81+
reset_password_token_secret
82+
Foobara::Auth::CreateResetPasswordToken.run!(user: user.id)
83+
84+
expect(outcome).to_not be_success
85+
expect(outcome.errors_hash.keys).to include("runtime.user_not_found_for_reset_token")
86+
end
87+
end
88+
end
89+
90+
context "when using the old password to reset" do
91+
let(:inputs) do
92+
{ user: user.id, old_password:, new_password: }
93+
end
94+
95+
it "is successful and we can verify with the new password after resetting" do
96+
expect(
97+
Foobara::Auth::VerifyPassword.run!(user: user.id, plaintext_password: old_password)
98+
).to be true
99+
expect(
100+
Foobara::Auth::VerifyPassword.run!(user: user.id, plaintext_password: new_password)
101+
).to be false
102+
103+
expect(outcome).to be_success
104+
expect(result).to be_a(Foobara::Auth::Types::User)
105+
106+
expect(
107+
Foobara::Auth::VerifyPassword.run!(user: user.id, plaintext_password: old_password)
108+
).to be false
109+
expect(
110+
Foobara::Auth::VerifyPassword.run!(user: user.id, plaintext_password: new_password)
111+
).to be true
112+
end
113+
114+
context "when the old password is bad" do
115+
let(:inputs) do
116+
{ user: user.id, old_password: "badpassword", new_password: }
117+
end
118+
119+
it "gives the expected error" do
120+
expect(outcome).to_not be_success
121+
expect(outcome.errors_hash.keys).to include("runtime.incorrect_old_password")
122+
end
123+
end
124+
125+
context "when omitting the user" do
126+
let(:inputs) do
127+
{ old_password:, new_password: }
128+
end
129+
130+
it "gives the expected error" do
131+
expect(outcome).to_not be_success
132+
expect(outcome.errors_hash.keys).to include("runtime.user_not_given")
133+
end
134+
end
135+
end
136+
137+
context "when giving everything" do
138+
let(:inputs) do
139+
{ user: user.id, old_password:, new_password:, reset_password_token_secret: }
140+
end
141+
142+
it "gives the expected error" do
143+
expect(outcome).to_not be_success
144+
expect(outcome.errors_hash.keys).to include("runtime.both_password_reset_token_and_old_password_given")
145+
end
146+
end
147+
148+
context "when only giving the user and new_password" do
149+
let(:inputs) do
150+
{ user: user.id, new_password: }
151+
end
152+
153+
it "gives the expected error" do
154+
expect(outcome).to_not be_success
155+
expect(outcome.errors_hash.keys).to include("runtime.no_password_reset_token_or_old_password_given")
156+
end
157+
end
158+
end

src/create_reset_password_token.rb

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
require "securerandom"
2+
3+
require_relative "create_token"
4+
5+
module Foobara
6+
module Auth
7+
class CreateResetPasswordToken < Foobara::Command
8+
depends_on CreateToken
9+
10+
inputs do
11+
user Types::User, :required
12+
token_ttl :integer, default: 5 * 60
13+
end
14+
15+
result :string, :sensitive_exposed
16+
17+
def execute
18+
determine_timestamps
19+
20+
generate_new_reset_password_token
21+
save_new_reset_password_token_on_user
22+
23+
reset_password_token_secret
24+
end
25+
26+
attr_accessor :expires_at, :reset_password_token_record, :reset_password_token_secret
27+
28+
def determine_timestamps
29+
now = Time.now
30+
self.expires_at = now + token_ttl
31+
end
32+
33+
def generate_new_reset_password_token
34+
result = run_subcommand!(CreateToken, expires_at:)
35+
36+
self.reset_password_token_record = result[:token_record]
37+
self.reset_password_token_secret = result[:token_string]
38+
end
39+
40+
def save_new_reset_password_token_on_user
41+
user.reset_password_token = reset_password_token_record
42+
end
43+
end
44+
end
45+
end

0 commit comments

Comments
 (0)