Skip to content

Commit 0c64606

Browse files
authored
Fever: generate API key based on username/password (#947)
The fever spec requires this format, so this restores logic previously removed.
1 parent ca31b64 commit 0c64606

File tree

10 files changed

+95
-20
lines changed

10 files changed

+95
-20
lines changed

app/assets/stylesheets/application.css

+4
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ code {
2323
white-space: normal;
2424
}
2525

26+
.warning {
27+
background-color: #F2DEDE;
28+
}
29+
2630
.container {
2731
width: 100%;
2832
max-width: 720px;

app/controllers/profiles_controller.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,6 @@ def update
2222
private
2323

2424
def user_params
25-
params.require(:user).permit(:username)
25+
params.require(:user).permit(:username, :password_challenge)
2626
end
2727
end

app/models/user.rb

+17-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
class User < ApplicationRecord
66
has_secure_password
7-
has_secure_token :api_key
87

98
encrypts :api_key, deterministic: true
109

@@ -14,6 +13,8 @@ class User < ApplicationRecord
1413
validates :username, presence: true, uniqueness: { case_sensitive: false }
1514
validate :password_challenge_matches
1615

16+
before_save :update_api_key
17+
1718
attr_accessor :password_challenge
1819

1920
# `password_challenge` logic should be able to be removed in Rails 7.1
@@ -26,4 +27,19 @@ def password_challenge_matches
2627

2728
errors.add(:original_password, "does not match")
2829
end
30+
31+
def update_api_key
32+
return unless password_digest_changed? || username_changed?
33+
34+
if password_challenge.blank? && password.blank?
35+
message = "Cannot change username without providing a password"
36+
37+
raise(ActiveRecord::ActiveRecordError, message)
38+
end
39+
40+
password = password_challenge.presence || self.password.presence
41+
42+
# API key based on Fever spec: https://feedafever.com/api
43+
self.api_key = Digest::MD5.hexdigest("#{username}:#{password}")
44+
end
2945
end

app/views/profiles/edit.html.erb

+13-6
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,29 @@
11
<h1><%= t('.title') %></h1>
22

3+
<header class="warning">
4+
<p><%= t('.warning_html') %></p>
5+
</header>
6+
37
<%= form_with(model: user, url: profile_path) do |form| %>
48
<fieldset>
9+
<legend><%= t('.change_username') %></legend>
510
<%= form.label :username %>
6-
<%= form.text_field :username %>
11+
<%= form.text_field :username, required: true %>
12+
<%= form.label :password_challenge, "Existing password" %>
13+
<%= form.password_field :password_challenge, required: true %>
714
</fieldset>
8-
<%= form.submit("Save") %>
15+
<%= form.submit("Update username") %>
916
<% end %>
1017

11-
<h2><%= t('.change_password') %></h2>
1218
<%= form_with(model: user, url: password_path) do |form| %>
1319
<fieldset>
20+
<legend><%= t('.change_password') %></legend>
1421
<%= form.label :password_challenge, "Existing password" %>
15-
<%= form.password_field :password_challenge %>
22+
<%= form.password_field :password_challenge, required: true %>
1623
<%= form.label :password, "New password" %>
17-
<%= form.password_field :password %>
24+
<%= form.password_field :password, required: true %>
1825
<%= form.label :password_confirmation %>
19-
<%= form.password_field :password_confirmation %>
26+
<%= form.password_field :password_confirmation, required: true %>
2027
</fieldset>
2128
<%= form.submit("Update password") %>
2229
<% end %>

config/locales/en.yml

+4
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ en:
124124
profiles:
125125
edit:
126126
title: Edit profile
127+
warning_html:
128+
<strong>Warning!!!</strong> Editing your username or password will
129+
change your API key as well. You will need to update your credentials
130+
in any Fever clients you have connected to Stringer.
127131
update:
128132
success: Profile updated
129133
failure: 'Unable to update profile: %{errors}'

db/migrate/20230301024452_encrypt_api_key.rb

+1-4
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,7 @@ class EncryptAPIKey < ActiveRecord::Migration[7.0]
44
def change
55
ActiveRecord::Encryption.config.support_unencrypted_data = true
66

7-
User.find_each do |user|
8-
user.regenerate_api_key if user.api_key.blank?
9-
user.encrypt
10-
end
7+
User.find_each(&:encrypt)
118

129
ActiveRecord::Encryption.config.support_unencrypted_data = false
1310

spec/models/user_spec.rb

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe User do
4+
describe "#update_api_key" do
5+
it "updates the api key when the username changed" do
6+
user = create(:user, username: "stringer", password: "super-secret")
7+
user.update!(username: "booger")
8+
9+
expect(user.api_key).to eq(Digest::MD5.hexdigest("booger:super-secret"))
10+
end
11+
12+
it "updates the api key when the password changed" do
13+
user = create(:user, username: "stringer", password: "super-secret")
14+
user.update!(password: "new-password")
15+
16+
expect(user.api_key).to eq(Digest::MD5.hexdigest("stringer:new-password"))
17+
end
18+
19+
it "does nothing when the username and password have not changed" do
20+
user = create(:user, username: "stringer", password: "super-secret")
21+
user = described_class.find(user.id)
22+
23+
expect { user.save! }.to not_change(user, :api_key).and not_raise_error
24+
end
25+
26+
it "raises an error when password and password challenge are blank" do
27+
user = create(:user, username: "stringer", password: "super-secret")
28+
user = described_class.find(user.id)
29+
expected_message = "Cannot change username without providing a password"
30+
31+
expect { user.update!(username: "booger") }
32+
.to raise_error(ActiveRecord::ActiveRecordError, expected_message)
33+
end
34+
end
35+
end

spec/requests/profiles_controller_spec.rb

+5-3
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,19 @@
1616
context "when the user is valid" do
1717
it "updates the user's username" do
1818
login_as(default_user)
19-
20-
params = { user: { username: "new_username" } }
19+
password_challenge = default_user.password
20+
params = { user: { username: "new_username", password_challenge: } }
2121

2222
expect { patch(profile_path, params:) }
2323
.to change_record(default_user, :username).to("new_username")
2424
end
2525

2626
it "redirects to the news path" do
2727
login_as(default_user)
28+
password_challenge = default_user.password
29+
params = { user: { username: "new_username", password_challenge: } }
2830

29-
patch(profile_path, params: { user: { username: "new_username" } })
31+
patch(profile_path, params:)
3032

3133
expect(response).to redirect_to(news_path)
3234
end

spec/support/matchers.rb

+1
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,6 @@ def invoke(expected_method)
2323
RSpec::Matchers.define_negated_matcher(:not_change, :change)
2424
RSpec::Matchers.define_negated_matcher(:not_change_record, :change_record)
2525
RSpec::Matchers.define_negated_matcher(:not_delete_record, :delete_record)
26+
RSpec::Matchers.define_negated_matcher(:not_raise_error, :raise_error)
2627

2728
Dir[File.join(__dir__, "./matchers/*.rb")].each { |path| require path }

spec/system/profile_spec.rb

+14-5
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,19 @@
55
login_as(default_user)
66
visit(edit_profile_path)
77

8-
fill_in("Username", with: "new_username")
9-
click_on("Save")
8+
fill_in_username_fields(default_user.password)
9+
click_on("Update username")
1010

1111
expect(page).to have_text("Logged in as new_username")
1212
end
1313

14+
def fill_in_username_fields(existing_password)
15+
within_fieldset("Change Username") do
16+
fill_in("Username", with: "new_username")
17+
fill_in("Existing password", with: existing_password)
18+
end
19+
end
20+
1421
it "allows the user to edit their password" do
1522
login_as(default_user)
1623
visit(edit_profile_path)
@@ -22,8 +29,10 @@
2229
end
2330

2431
def fill_in_password_fields(existing_password, new_password)
25-
fill_in("Existing password", with: existing_password)
26-
fill_in("New password", with: new_password)
27-
fill_in("Password confirmation", with: new_password)
32+
within_fieldset("Change Password") do
33+
fill_in("Existing password", with: existing_password)
34+
fill_in("New password", with: new_password)
35+
fill_in("Password confirmation", with: new_password)
36+
end
2837
end
2938
end

0 commit comments

Comments
 (0)