diff --git a/app/controllers/email_verifications_controller.rb b/app/controllers/email_verifications_controller.rb new file mode 100644 index 00000000..c6d13680 --- /dev/null +++ b/app/controllers/email_verifications_controller.rb @@ -0,0 +1,34 @@ +class EmailVerificationsController < ApplicationController + skip_before_action :ensure_user_signed_in, only: [:show] + + def show + @verification = API::EmailVerification.verify_email(tokened: params[:token]) + + @message = if @verification + 'You’ve successfully verified your email. You can now continue to your account.' + else + 'Verification link is invalid or has expired.' + end + rescue StandardError => e + Rails.logger.warn("Email verification failed: \\#{e.class} - \\#{e.message}") + @message = 'Verification link is invalid or has expired.' + end + + def resend_email + if API::User.update_email(email: params[:email]).any? + redirect_to user_detail_path, notice: 'A new verification email has been sent to your new email address.' + end + rescue JSONAPI::Consumer::Errors::ConnectionError + redirect_to user_detail_path, alert: 'There was a problem connecting to the email service. Please try again later.' + end + + def cancel_pending_email_change + if API::EmailVerification.delete_pending_email_change + redirect_to user_detail_path, notice: 'Your pending email change has been cancelled.' + else + redirect_to user_detail_path, alert: 'There was a problem cancelling your pending email change. Please try again.' + end + rescue JSONAPI::Consumer::Errors::ConnectionError + redirect_to user_detail_path, alert: 'There was a problem connecting to the email service. Please try again later.' + end +end diff --git a/app/controllers/user_details_controller.rb b/app/controllers/user_details_controller.rb index 149276ab..dc7ee1fe 100644 --- a/app/controllers/user_details_controller.rb +++ b/app/controllers/user_details_controller.rb @@ -1,6 +1,8 @@ class UserDetailsController < ApplicationController def show @suppliers = API::Supplier.all + @email_verification = API::EmailVerification.email_verification_pending? + @user_logs = API::User.user_auth_logs end def edit; end @@ -20,4 +22,40 @@ def update flash.now[:alert] = 'There was a problem connecting to the user service. Please try again later.' render :edit, status: :service_unavailable end + + def edit_email; end + + def update_email + new_email = params[:email] + email_confirmation = params[:email_confirmation] + + error_message = email_update_validation(new_email, email_confirmation) + if error_message + flash.now[:alert] = error_message + return render :edit_email, status: :unprocessable_entity + end + + api_response = API::User.update_email(email: new_email) + + if api_response.any? + redirect_to user_detail_path, + notice: 'You must verify your new email address. Please check your inbox for a verification email.' + else + flash.now[:alert] = 'There was a problem updating your email address. Please try again.' + render :edit_email, status: :unprocessable_entity + end + rescue JSONAPI::Consumer::Errors::ConnectionError + flash.now[:alert] = 'There was a problem connecting to the user service. Please try again later.' + render :edit_email, status: :service_unavailable + end + + private + + def email_update_validation(email, confirmation) + return 'Please enter and confirm your email address.' if email.blank? || confirmation.blank? + return 'Email addresses do not match. Please try again.' unless email == confirmation + return 'Please enter a valid email address.' unless email =~ URI::MailTo::EMAIL_REGEXP + + nil + end end diff --git a/app/models/api/email_verification.rb b/app/models/api/email_verification.rb new file mode 100644 index 00000000..d17926ac --- /dev/null +++ b/app/models/api/email_verification.rb @@ -0,0 +1,29 @@ +# app/models/api/email_verification.rb +module API + class EmailVerification < Base + custom_endpoint :verify_token, on: :collection, request_method: :post + custom_endpoint :active_verification, on: :collection, request_method: :get + custom_endpoint :cancel_pending_email_change, on: :collection, request_method: :delete + + def self.verify_email(tokened:) + response = verify_token({ token: tokened }) + response&.any? + rescue JSONAPI::Consumer::Errors::NotFound, JSONAPI::Consumer::Errors::ConnectionError + false + end + + def self.email_verification_pending? + response = active_verification + response&.first + rescue JSONAPI::Consumer::Errors::NotFound, JSONAPI::Consumer::Errors::ConnectionError + nil + end + + def self.delete_pending_email_change + cancel_pending_email_change + true + rescue JSONAPI::Consumer::Errors::NotFound, JSONAPI::Consumer::Errors::ConnectionError + false + end + end +end diff --git a/app/models/api/user.rb b/app/models/api/user.rb index 86c4bdc3..984def89 100644 --- a/app/models/api/user.rb +++ b/app/models/api/user.rb @@ -1,5 +1,7 @@ module API class User < Base custom_endpoint :update_name, on: :collection, request_method: :patch + custom_endpoint :update_email, on: :collection, request_method: :patch + custom_endpoint :user_auth_logs, on: :collection, request_method: :get end end diff --git a/app/views/email_verifications/show.html.haml b/app/views/email_verifications/show.html.haml new file mode 100644 index 00000000..69079d3b --- /dev/null +++ b/app/views/email_verifications/show.html.haml @@ -0,0 +1,6 @@ +- page_title 'Email Verification' +%div{ class: "govuk-panel govuk-panel--confirmation govuk-!-margin-top-6 govuk-!-margin-bottom-6" } + %h1.govuk-panel__title + Email Verification + %div.govuk-panel__body + = @message diff --git a/app/views/errors/internal_server_error.json.jbuilder b/app/views/errors/internal_server_error.json.jbuilder new file mode 100644 index 00000000..d7b531fa --- /dev/null +++ b/app/views/errors/internal_server_error.json.jbuilder @@ -0,0 +1,2 @@ +json.error 'Internal Server Error' +json.message 'Sorry, something went wrong. Please try again later.' diff --git a/app/views/sessions/verify_email.html.erb b/app/views/sessions/verify_email.html.erb new file mode 100644 index 00000000..a818a3c5 --- /dev/null +++ b/app/views/sessions/verify_email.html.erb @@ -0,0 +1,2 @@ +
Your email address is not verified. Please check your inbox for a verification email and follow the instructions to verify your account before continuing.
\ No newline at end of file diff --git a/app/views/user_details/edit_email.html.haml b/app/views/user_details/edit_email.html.haml new file mode 100644 index 00000000..a1ebfc1b --- /dev/null +++ b/app/views/user_details/edit_email.html.haml @@ -0,0 +1,26 @@ +- page_title 'Change your email address' + +.govuk-grid-row + .govuk-grid-column-two-thirds + %h1.govuk-heading-xl Change your email address + + - if flash[:alert].present? + .govuk-error-summary{role: "alert", tabindex: "-1"} + .govuk-error-summary__title + %h2.govuk-error-summary__title There is a problem + .govuk-error-summary__body + %ul.govuk-list.govuk-error-summary__list + %li= flash[:alert] + + = form_with url: update_email_user_detail_path, method: :patch, local: true do |form| + .govuk-form-group + = form.label :email, 'Email address', class: 'govuk-label' + = form.email_field :email, class: 'govuk-input' + + .govuk-form-group + = form.label :email_confirmation, 'Confirm email address', class: 'govuk-label' + = form.email_field :email_confirmation, class: 'govuk-input' + + %p{class: 'govuk-!-margin-top-7'} + = form.submit 'Update email', class: 'govuk-button' + = link_to 'Cancel', user_detail_path, class: 'govuk-link govuk-!-margin-left-3 govuk-!-font-size-19' diff --git a/app/views/user_details/show.html.haml b/app/views/user_details/show.html.haml index 5cd743cb..081cd35e 100644 --- a/app/views/user_details/show.html.haml +++ b/app/views/user_details/show.html.haml @@ -1,5 +1,19 @@ - page_title 'User profile' +- if @email_verification&.token.present? + .govuk-notification-banner{"aria-labelledby" => "govuk-notification-banner-title", "data-module" => "govuk-notification-banner", :role => "region"} + .govuk-notification-banner__header + %h2#govuk-notification-banner-title.govuk-notification-banner__title + Important + .govuk-notification-banner__content + %p.govuk-notification-banner__heading + Verify your new email address + = @email_verification.new_email + = succeed "." do + = link_to 'Resend email', resend_email_verification_path(email: @email_verification.new_email), method: :post, class: 'govuk-notification-banner__link' + .govuk-button-group + = button_to 'Cancel', cancel_pending_email_change_path, method: :post, data: { confirm: 'Are you sure you want to cancel your pending email change?' }, class: 'govuk-button govuk-button--secondary' + .govuk-grid-row .govuk-grid-column-full %h1.govuk-heading-xl @@ -19,10 +33,33 @@ %td.govuk-table__cell Email %td.govuk-table__cell= current_user.email %td.govuk-table__cell + = link_to 'Change email', edit_email_user_detail_path, class: 'govuk-link' %tr.govuk-table__row %td.govuk-table__cell User since %td.govuk-table__cell= user_created_date(current_user).to_fs(:default) %td.govuk-table__cell + - if @user_logs.present? + %tr.govuk-table__row + %td.govuk-table__cell Previous logins + %td.govuk-table__cell + - @user_logs.each_with_index do |log, i| + %ul.govuk-list.govuk-list + %li + %strong Date: + = Time.parse(log.date).utc.strftime("%d %B %Y %H:%M UTC") + %li + %strong IP: + = log.ip + %li + %strong User Agent: + = log.user_agent + %li + %strong Device: + = log.isMobile ? "Mobile" : "Desktop" + - unless i == @user_logs.size - 1 + %hr.govuk-section-break.govuk-section-break--visible + + %td.govuk-table__cell %tr.govuk-table__row %td.govuk-table__cell Supplier(s) %td.govuk-table__cell diff --git a/config/routes.rb b/config/routes.rb index c405b709..cbedcae9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -41,11 +41,15 @@ resources :release_notes, only: %i[index] - resource :user_detail, only: %i[show edit update] + resource :user_detail, only: %i[show edit update] do + get :edit_email + patch :update_email + end match '/auth/:provider/callback', to: 'sessions#create', via: %i[get post] match '/auth/failure', to: 'errors#auth_failure', via: %i[get post] get '/sign_out', to: 'sessions#destroy', as: :sign_out + get '/verify_email', to: 'sessions#verify_email', as: :verify_email get '/style_guide', to: 'styleguide#index' get '/support', to: 'support#index' get '/support/frameworks', to: 'support#frameworks' @@ -53,4 +57,8 @@ get '/cookie-policy', to: 'home#cookie_policy' get '/accessibility', to: 'accessibility#index' get '/check', to: 'check#index' + get '/email/verification/:token', to: 'email_verifications#show', as: :email_verification + post '/email/verification/resend', to: 'email_verifications#resend_email', as: :resend_email_verification + post '/email/verification/cancel_pending_email_change', to: 'email_verifications#cancel_pending_email_change', + as: :cancel_pending_email_change end diff --git a/spec/features/user_can_view_profile_spec.rb b/spec/features/user_can_view_profile_spec.rb index 227ca154..50b7d0b7 100644 --- a/spec/features/user_can_view_profile_spec.rb +++ b/spec/features/user_can_view_profile_spec.rb @@ -6,6 +6,8 @@ mock_user_with_multiple_suppliers_endpoint! mock_notifications_endpoint! mock_suppliers_endpoint! + mock_email_verification_pending_endpoint! + mock_user_auth_logs_endpoint! visit '/' click_button 'sign-in' diff --git a/spec/features/users_can_sign_in_and_out_spec.rb b/spec/features/users_can_sign_in_and_out_spec.rb index 5def0bcc..9e669906 100644 --- a/spec/features/users_can_sign_in_and_out_spec.rb +++ b/spec/features/users_can_sign_in_and_out_spec.rb @@ -7,6 +7,8 @@ mock_incomplete_tasks_endpoint! mock_notifications_endpoint! mock_user_endpoint! + mock_email_verification_pending_endpoint! + mock_user_auth_logs_endpoint! visit '/tasks' @@ -38,6 +40,8 @@ mock_unstarted_tasks_endpoint! mock_incomplete_tasks_endpoint! mock_user_endpoint! + mock_email_verification_pending_endpoint! + mock_user_auth_logs_endpoint! click_on 'sign-in' @@ -51,6 +55,8 @@ mock_incomplete_tasks_endpoint! mock_notifications_endpoint! mock_user_endpoint! + mock_email_verification_pending_endpoint! + mock_user_auth_logs_endpoint! visit '/' click_on 'sign-in' diff --git a/spec/requests/user_details_spec.rb b/spec/requests/user_details_spec.rb index 280a44bc..d2997b4a 100644 --- a/spec/requests/user_details_spec.rb +++ b/spec/requests/user_details_spec.rb @@ -5,6 +5,8 @@ mock_notifications_endpoint! mock_user_with_multiple_suppliers_endpoint! mock_suppliers_endpoint! + mock_email_verification_pending_endpoint! + mock_user_auth_logs_endpoint! end describe 'visiting the user profile page' do diff --git a/spec/support/api_helpers.rb b/spec/support/api_helpers.rb index 2af830c9..3213d532 100644 --- a/spec/support/api_helpers.rb +++ b/spec/support/api_helpers.rb @@ -486,6 +486,14 @@ def mock_update_user_name_endpoint_connection_error! .to_raise(Faraday::ConnectionFailed.new('Connection failed')) end + def mock_email_verification_pending_endpoint!(result = nil) + allow(API::EmailVerification).to receive(:email_verification_pending?).and_return(result) + end + + def mock_user_auth_logs_endpoint!(logs = []) + allow(API::User).to receive(:user_auth_logs).and_return(logs) + end + private def json_headers