<%= f.govuk_error_summary(presenter: ErrorSummaryFullMessagesPresenter) %>
diff --git a/app/views/devise/passwords/reset_password.html.erb b/app/views/devise/passwords/reset_password.html.erb
new file mode 100644
index 000000000..bd96a9c5d
--- /dev/null
+++ b/app/views/devise/passwords/reset_password.html.erb
@@ -0,0 +1,33 @@
+<% content_for :title, "Reset your password" %>
+
+<% content_for :before_content do %>
+ <%= govuk_back_link(
+ text: 'Back',
+ href: :back,
+ ) %>
+<% end %>
+
+<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| %>
+ <%= f.hidden_field :reset_password_token %>
+
+
+ <%= f.govuk_error_summary %>
+
+
+ <%= content_for(:title) %>
+
+
+ <%= f.govuk_password_field :password,
+ label: { text: "New password" },
+ hint: @minimum_password_length ? { text: "Your password must be at least #{@minimum_password_length} characters and hard to guess." } : nil,
+ autocomplete: "new-password"
+ %>
+
+ <%= f.govuk_password_field :password_confirmation,
+ label: { text: "Confirm new password" }
+ %>
+
+ <%= f.govuk_submit "Update" %>
+
+
+<% end %>
diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb
index fab47b4a1..9daa5fab6 100644
--- a/app/views/devise/sessions/new.html.erb
+++ b/app/views/devise/sessions/new.html.erb
@@ -1,4 +1,10 @@
-<% content_for :title, "Sign in to your account to submit CORE data" %>
+<% if resource_name == :admin_user %>
+ <% title = "Sign in to your CORE administration account" %>
+<% else %>
+ <% title = "Sign in to your account to submit CORE data" %>
+<% end %>
+
+<% content_for :title, title %>
<%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml
index d84886a71..2d90c1cd0 100644
--- a/config/locales/devise.en.yml
+++ b/config/locales/devise.en.yml
@@ -35,6 +35,7 @@ en:
send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes."
send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes."
updated: "Your password has been changed successfully. You are now signed in."
+ updated_2FA: "Your password has been changed successfully. Your security code has been sent."
updated_not_active: "Your password has been changed successfully."
registrations:
destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon."
diff --git a/config/routes.rb b/config/routes.rb
index 02b3a1e40..c9738f1e3 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -2,14 +2,14 @@ Rails.application.routes.draw do
devise_for :admin_users, {
path: :admin,
controllers: {
- sessions: "active_admin/devise/sessions",
- passwords: "active_admin/devise/passwords",
+ sessions: "auth/sessions",
+ passwords: "auth/passwords",
unlocks: "active_admin/devise/unlocks",
registrations: "active_admin/devise/registrations",
confirmations: "active_admin/devise/confirmations",
two_factor_authentication: "auth/two_factor_authentication",
},
- path_names: { sign_in: "login", sign_out: "logout", two_factor_authentication: "two-factor-authentication" },
+ path_names: { sign_in: "sign-in", sign_out: "sign-out", two_factor_authentication: "two-factor-authentication" },
sign_out_via: %i[delete get],
}
diff --git a/db/migrate/20220216163601_add_trackable_to_admin_user.rb b/db/migrate/20220216163601_add_trackable_to_admin_user.rb
new file mode 100644
index 000000000..f1fe8277e
--- /dev/null
+++ b/db/migrate/20220216163601_add_trackable_to_admin_user.rb
@@ -0,0 +1,13 @@
+class AddTrackableToAdminUser < ActiveRecord::Migration[7.0]
+ def change
+ change_table :admin_users, bulk: true do |t|
+ t.string :name
+ ## Trackable
+ t.integer :sign_in_count, default: 0, null: false
+ t.datetime :current_sign_in_at
+ t.datetime :last_sign_in_at
+ t.string :current_sign_in_ip
+ t.string :last_sign_in_ip
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index e0fee6a27..75b05b615 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -30,6 +30,12 @@ ActiveRecord::Schema[7.0].define(version: 202202071123100) do
t.datetime "direct_otp_sent_at", precision: nil
t.datetime "totp_timestamp", precision: nil
t.string "phone"
+ t.string "name"
+ t.integer "sign_in_count", default: 0, null: false
+ t.datetime "current_sign_in_at", precision: nil
+ t.datetime "last_sign_in_at", precision: nil
+ t.string "current_sign_in_ip"
+ t.string "last_sign_in_ip"
t.index ["encrypted_otp_secret_key"], name: "index_admin_users_on_encrypted_otp_secret_key", unique: true
end
diff --git a/spec/features/admin_panel_spec.rb b/spec/features/admin_panel_spec.rb
index 93a0e0a1f..3536e1c08 100644
--- a/spec/features/admin_panel_spec.rb
+++ b/spec/features/admin_panel_spec.rb
@@ -11,6 +11,12 @@ RSpec.describe "Admin Panel" do
allow(notify_client).to receive(:send_sms).and_return(true)
end
+ it "shows the admin sign in page" do
+ visit("/admin")
+ expect(page).to have_current_path("/admin/sign-in")
+ expect(page).to have_content("Sign in to your CORE administration account")
+ end
+
context "with a valid 2FA code" do
before do
allow(SecureRandom).to receive(:random_number).and_return(otp)
@@ -23,7 +29,7 @@ RSpec.describe "Admin Panel" do
expect(notify_client).to receive(:send_sms).with(
hash_including(phone_number: admin.phone, template_id: mfa_template_id),
)
- click_button("Login")
+ click_button("Sign in")
fill_in("code", with: otp)
click_button("Submit")
expect(page).to have_content("Dashboard")
@@ -32,7 +38,7 @@ RSpec.describe "Admin Panel" do
context "but it is more than 15 minutes old" do
it "does not authenticate successfully" do
- click_button("Login")
+ click_button("Sign in")
admin.update!(direct_otp_sent_at: 16.minutes.ago)
fill_in("code", with: otp)
click_button("Submit")
@@ -49,7 +55,7 @@ RSpec.describe "Admin Panel" do
visit("/admin")
fill_in("admin_user[email]", with: admin.email)
fill_in("admin_user[password]", with: admin.password)
- click_button("Login")
+ click_button("Sign in")
fill_in("code", with: otp)
click_button("Submit")
expect(page).to have_content("Check your phone")
@@ -64,7 +70,7 @@ RSpec.describe "Admin Panel" do
visit("/admin")
fill_in("admin_user[email]", with: admin.email)
fill_in("admin_user[password]", with: admin.password)
- click_button("Login")
+ click_button("Sign in")
end
it "displays the resend view" do
@@ -88,14 +94,68 @@ RSpec.describe "Admin Panel" do
visit("/admin")
fill_in("admin_user[email]", with: admin.email)
fill_in("admin_user[password]", with: admin.password)
- click_button("Login")
+ click_button("Sign in")
fill_in("code", with: otp)
click_button("Submit")
click_link("Logout")
+ visit("/admin")
fill_in("admin_user[email]", with: admin.email)
fill_in("admin_user[password]", with: admin.password)
- click_button("Login")
+ click_button("Sign in")
expect(page).to have_content("Check your phone")
end
end
+
+ context "when the admin has forgotten their password" do
+ let!(:admin_user) { FactoryBot.create(:admin_user, last_sign_in_at: Time.zone.now) }
+ let(:notify_client) { instance_double(Notifications::Client) }
+ let(:reset_password_token) { "MCDH5y6Km-U7CFPgAMVS" }
+ let(:devise_notify_mailer) { DeviseNotifyMailer.new }
+
+ before do
+ allow(DeviseNotifyMailer).to receive(:new).and_return(devise_notify_mailer)
+ allow(devise_notify_mailer).to receive(:notify_client).and_return(notify_client)
+ allow(notify_client).to receive(:send_email).and_return(true)
+ allow(Devise.token_generator).to receive(:generate).and_return(reset_password_token)
+ end
+
+ it " is redirected to the reset password page when they click the reset password link" do
+ visit("/admin")
+ click_link("reset your password")
+ expect(page).to have_current_path("/admin/password/new")
+ end
+
+ it " is shown an error message if they submit without entering an email address" do
+ visit("/admin/password/new")
+ click_button("Send email")
+ expect(page).to have_selector("#error-summary-title")
+ expect(page).to have_selector("#user-email-field-error")
+ expect(page).to have_title("Error")
+ end
+
+ it " is redirected to admin login page after reset email is sent" do
+ visit("/admin/password/new")
+ fill_in("admin_user[email]", with: admin_user.email)
+ click_button("Send email")
+ expect(page).to have_content("Check your email")
+ end
+
+ it " is sent a reset password email via Notify" do
+ expect(notify_client).to receive(:send_email).with(
+ {
+ email_address: admin_user.email,
+ template_id: admin_user.reset_password_notify_template,
+ personalisation: {
+ name: admin_user.email,
+ email: admin_user.email,
+ organisation: "",
+ link: "http://localhost:3000/admin/password/edit?reset_password_token=#{reset_password_token}",
+ },
+ },
+ )
+ visit("/admin/password/new")
+ fill_in("admin_user[email]", with: admin_user.email)
+ click_button("Send email")
+ end
+ end
end
diff --git a/spec/features/organisation_spec.rb b/spec/features/organisation_spec.rb
index 1eb906212..04c602c9a 100644
--- a/spec/features/organisation_spec.rb
+++ b/spec/features/organisation_spec.rb
@@ -5,7 +5,7 @@ RSpec.describe "User Features" do
include Helpers
let(:organisation) { user.organisation }
let(:org_id) { organisation.id }
- let(:set_password_template_id) { DeviseNotifyMailer::SET_PASSWORD_TEMPLATE_ID }
+ let(:set_password_template_id) { User::SET_PASSWORD_TEMPLATE_ID }
let(:notify_client) { instance_double(Notifications::Client) }
let(:reset_password_token) { "MCDH5y6Km-U7CFPgAMVS" }
let(:devise_notify_mailer) { DeviseNotifyMailer.new }
@@ -13,7 +13,6 @@ RSpec.describe "User Features" do
before do
allow(DeviseNotifyMailer).to receive(:new).and_return(devise_notify_mailer)
allow(devise_notify_mailer).to receive(:notify_client).and_return(notify_client)
- allow(devise_notify_mailer).to receive(:host).and_return("test.com")
allow(Devise.token_generator).to receive(:generate).and_return(reset_password_token)
allow(notify_client).to receive(:send_email).and_return(true)
sign_in user
@@ -56,7 +55,7 @@ RSpec.describe "User Features" do
name: "New User",
email: "new_user@example.com",
organisation: organisation.name,
- link: "https://test.com/users/password/edit?reset_password_token=#{reset_password_token}",
+ link: "http://localhost:3000/users/password/edit?reset_password_token=#{reset_password_token}",
},
},
)
diff --git a/app/views/users/reset_password.html.erb b/spec/features/reset_password.html.erb
similarity index 100%
rename from app/views/users/reset_password.html.erb
rename to spec/features/reset_password.html.erb
diff --git a/spec/features/user_spec.rb b/spec/features/user_spec.rb
index e6b0fafd2..bb08854db 100644
--- a/spec/features/user_spec.rb
+++ b/spec/features/user_spec.rb
@@ -2,7 +2,7 @@ require "rails_helper"
RSpec.describe "User Features" do
let!(:user) { FactoryBot.create(:user, last_sign_in_at: Time.zone.now) }
- let(:reset_password_template_id) { DeviseNotifyMailer::RESET_PASSWORD_TEMPLATE_ID }
+ let(:reset_password_template_id) { User::RESET_PASSWORD_TEMPLATE_ID }
let(:notify_client) { instance_double(Notifications::Client) }
let(:reset_password_token) { "MCDH5y6Km-U7CFPgAMVS" }
let(:devise_notify_mailer) { DeviseNotifyMailer.new }
@@ -10,7 +10,6 @@ RSpec.describe "User Features" do
before do
allow(DeviseNotifyMailer).to receive(:new).and_return(devise_notify_mailer)
allow(devise_notify_mailer).to receive(:notify_client).and_return(notify_client)
- allow(devise_notify_mailer).to receive(:host).and_return("test.com")
allow(notify_client).to receive(:send_email).and_return(true)
allow(Devise.token_generator).to receive(:generate).and_return(reset_password_token)
end
@@ -19,6 +18,7 @@ RSpec.describe "User Features" do
it " is required to log in" do
visit("/logs")
expect(page).to have_current_path("/users/sign-in")
+ expect(page).to have_content("Sign in to your account to submit CORE data")
end
it "does not see the default devise error message" do
@@ -109,7 +109,7 @@ RSpec.describe "User Features" do
name: user.name,
email: user.email,
organisation: user.organisation.name,
- link: "https://test.com/users/password/edit?reset_password_token=#{reset_password_token}",
+ link: "http://localhost:3000/users/password/edit?reset_password_token=#{reset_password_token}",
},
},
)
diff --git a/spec/request_helper.rb b/spec/request_helper.rb
index ce00bc48e..d2bef6d5c 100644
--- a/spec/request_helper.rb
+++ b/spec/request_helper.rb
@@ -7,6 +7,8 @@ module RequestHelper
.to_return(status: 200, body: "{\"status\":404,\"error\":\"Postcode not found\"}", headers: {})
WebMock.stub_request(:post, /api.notifications.service.gov.uk\/v2\/notifications\/email/)
.to_return(status: 200, body: "", headers: {})
+ WebMock.stub_request(:post, /api.notifications.service.gov.uk\/v2\/notifications\/sms/)
+ .to_return(status: 200, body: "", headers: {})
end
def self.real_http_requests
diff --git a/spec/requests/auth/passwords_controller_spec.rb b/spec/requests/auth/passwords_controller_spec.rb
index b39b3a1f8..c84cc850e 100644
--- a/spec/requests/auth/passwords_controller_spec.rb
+++ b/spec/requests/auth/passwords_controller_spec.rb
@@ -2,7 +2,6 @@ require "rails_helper"
require_relative "../../support/devise"
RSpec.describe Auth::PasswordsController, type: :request do
- let(:params) { { user: { email: } } }
let(:page) { Capybara::Node::Simple.new(response.body) }
let(:notify_client) { instance_double(Notifications::Client) }
let(:devise_notify_mailer) { DeviseNotifyMailer.new }
@@ -13,60 +12,134 @@ RSpec.describe Auth::PasswordsController, type: :request do
allow(notify_client).to receive(:send_email).and_return(true)
end
- context "when a password reset is requested for a valid email" do
- let(:user) { FactoryBot.create(:user) }
- let(:email) { user.email }
+ context "when a regular user" do
+ let(:params) { { user: { email: } } }
- it "redirects to the email sent page" do
- post "/users/password", params: params
- expect(response).to have_http_status(:redirect)
- follow_redirect!
- expect(response.body).to match(/Check your email/)
+ context "when a password reset is requested for a valid email" do
+ let(:user) { FactoryBot.create(:user) }
+ let(:email) { user.email }
+
+ it "redirects to the email sent page" do
+ post "/users/password", params: params
+ expect(response).to have_http_status(:redirect)
+ follow_redirect!
+ expect(response.body).to match(/Check your email/)
+ end
end
- end
- context "when a password reset is requested with an email that doesn't exist in the system" do
- before do
- allow(Devise.navigational_formats).to receive(:include?).and_return(false)
+ context "when a password reset is requested with an email that doesn't exist in the system" do
+ before do
+ allow(Devise.navigational_formats).to receive(:include?).and_return(false)
+ end
+
+ let(:email) { "madeup_email@test.com" }
+
+ it "redirects to the email sent page anyway" do
+ post "/users/password", params: params
+ expect(response).to have_http_status(:redirect)
+ follow_redirect!
+ expect(response.body).to match(/Check your email/)
+ end
end
- let(:email) { "madeup_email@test.com" }
+ describe "#Update - reset password" do
+ let(:user) { FactoryBot.create(:user) }
+ let(:token) { user.send(:set_reset_password_token) }
+ let(:updated_password) { "updated_password_280" }
+ let(:update_password_params) do
+ {
+ user:
+ {
+ reset_password_token: token,
+ password: updated_password,
+ password_confirmation: updated_password,
+ },
+ }
+ end
+ let(:message) { "Your password has been changed successfully. You are now signed in" }
- it "redirects to the email sent page anyway" do
- post "/users/password", params: params
- expect(response).to have_http_status(:redirect)
- follow_redirect!
- expect(response.body).to match(/Check your email/)
+ it "changes the password" do
+ expect { put "/users/password", params: update_password_params }
+ .to(change { user.reload.encrypted_password })
+ end
+
+ it "after password change, the user is signed in" do
+ put "/users/password", params: update_password_params
+ # Devise redirects once after re-sign in with new password and then root redirects as well.
+ follow_redirect!
+ follow_redirect!
+ expect(page).to have_css("div", class: "govuk-notification-banner__heading", text: message)
+ end
end
end
- describe "#Update - reset password" do
- let(:user) { FactoryBot.create(:user) }
- let(:token) { user.send(:set_reset_password_token) }
- let(:updated_password) { "updated_password_280" }
- let(:update_password_params) do
- {
- user:
+ context "when an admin user" do
+ let(:admin_user) { FactoryBot.create(:admin_user) }
+
+ describe "reset password" do
+ let(:new_value) { "new-password" }
+
+ before do
+ allow(Sms).to receive(:notify_client).and_return(notify_client)
+ allow(notify_client).to receive(:send_sms).and_return(true)
+ end
+
+ it "renders the user edit password view" do
+ _raw, enc = Devise.token_generator.generate(AdminUser, :reset_password_token)
+ get "/admin/password/edit?reset_password_token=#{enc}"
+ expect(page).to have_css("h1", text: "Reset your password")
+ end
+
+ context "when passwords entered don't match" do
+ let(:raw) { admin_user.send_reset_password_instructions }
+ let(:params) do
{
- reset_password_token: token,
- password: updated_password,
- password_confirmation: updated_password,
- },
- }
- end
- let(:message) { "Your password has been changed successfully. You are now signed in" }
+ id: admin_user.id,
+ admin_user: {
+ password: new_value,
+ password_confirmation: "something_else",
+ reset_password_token: raw,
+ },
+ }
+ end
- it "changes the password" do
- expect { put "/users/password", params: update_password_params }
- .to(change { user.reload.encrypted_password })
- end
+ it "shows an error" do
+ put "/admin/password", headers: headers, params: params
+ expect(response).to have_http_status(:unprocessable_entity)
+ expect(page).to have_content("doesn't match Password")
+ end
+ end
+
+ context "when passwords is reset" do
+ let(:raw) { admin_user.send_reset_password_instructions }
+ let(:params) do
+ {
+ id: admin_user.id,
+ admin_user: {
+ password: new_value,
+ password_confirmation: new_value,
+ reset_password_token: raw,
+ },
+ }
+ end
+
+ it "updates the password" do
+ expect {
+ put "/admin/password", headers: headers, params: params
+ admin_user.reload
+ }.to change(admin_user, :encrypted_password)
+ end
+
+ it "sends you to the 2FA page" do
+ put "/admin/password", headers: headers, params: params
+ expect(response).to redirect_to("/admin/two-factor-authentication")
+ end
- it "after password change, the user is signed in" do
- put "/users/password", params: update_password_params
- # Devise redirects once after re-sign in with new password and then root redirects as well.
- follow_redirect!
- follow_redirect!
- expect(page).to have_css("div", class: "govuk-notification-banner__heading", text: message)
+ it "triggers an SMS" do
+ expect(notify_client).to receive(:send_sms)
+ put "/admin/password", headers: headers, params: params
+ end
+ end
end
end
end