diff --git a/.env.example b/.env.example index 94596a0da..d30382d9e 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,4 @@ DB_USERNAME=postgres-user DB_PASSWORD=postgres-password +CORE_EMAIL_USERNAME=email@example.com +CORE_EMAIL_PASSWORD=password123 diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index 254cf36bc..c64e3d581 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -100,10 +100,14 @@ jobs: API_USER: ${{ secrets.API_USER }} API_KEY: ${{ secrets.API_KEY }} APP_NAME: dluhc-core + CORE_EMAIL_USERNAME: ${{ secrets.CORE_EMAIL_USERNAME }} + CORE_EMAIL_PASSWORD: ${{ secrets.CORE_EMAIL_PASSWORD }} run: | cf7 api $CF_API_ENDPOINT cf7 auth cf7 target -o $CF_ORG -s $CF_SPACE cf7 set-env $APP_NAME API_USER $API_USER cf7 set-env $APP_NAME API_KEY $API_KEY + cf7 set-env $APP_NAME CORE_EMAIL_USERNAME $CORE_EMAIL_USERNAME + cf7 set-env $APP_NAME CORE_EMAIL_PASSWORD $CORE_EMAIL_PASSWORD cf7 push --strategy rolling diff --git a/Gemfile b/Gemfile index cd22b2eb7..f2895387e 100644 --- a/Gemfile +++ b/Gemfile @@ -33,6 +33,8 @@ gem "chartkick" gem "roo" # Json Schema gem "json-schema" +# Authentication +gem "devise" gem "turbo-rails", "~> 0.8" gem "uk_postcode" diff --git a/Gemfile.lock b/Gemfile.lock index 04961facf..322fcc403 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -122,6 +122,7 @@ GEM activesupport (>= 3.0.0, < 6.2) ruby2_keywords (>= 0.0.2, < 1.0) ast (2.4.2) + bcrypt (3.1.16) bindex (0.8.1) bootsnap (1.9.1) msgpack (~> 1.0) @@ -146,6 +147,12 @@ GEM database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) deep_merge (1.2.1) + devise (4.8.0) + bcrypt (~> 3.0) + orm_adapter (~> 0.1) + railties (>= 4.1.0) + responders + warden (~> 1.2.3) diff-lcs (1.4.4) discard (1.2.0) activerecord (>= 4.2, < 7) @@ -227,6 +234,7 @@ GEM nio4r (2.5.8) nokogiri (1.12.5-x86_64-linux) racc (~> 1.4) + orm_adapter (0.5.0) overcommit (0.58.0) childprocess (>= 0.6.3, < 5) iniparse (~> 1.4) @@ -365,6 +373,8 @@ GEM view_component (2.39.0) activesupport (>= 5.0.0, < 8.0) method_source (~> 1.0) + warden (1.2.9) + rack (>= 2.0.9) web-console (4.2.0) actionview (>= 6.0.0) activemodel (>= 6.0.0) @@ -383,6 +393,7 @@ GEM zeitwerk (2.5.1) PLATFORMS + x86_64-darwin-20 x86_64-linux DEPENDENCIES @@ -392,6 +403,7 @@ DEPENDENCIES capybara chartkick database_cleaner-active_record + devise discard dotenv-rails factory_bot_rails diff --git a/app/controllers/case_logs_controller.rb b/app/controllers/case_logs_controller.rb index 4d4ed6db3..c2b4edc28 100644 --- a/app/controllers/case_logs_controller.rb +++ b/app/controllers/case_logs_controller.rb @@ -1,6 +1,7 @@ class CaseLogsController < ApplicationController skip_before_action :verify_authenticity_token, if: :json_api_request? before_action :authenticate, if: :json_api_request? + before_action :authenticate_user!, unless: :json_api_request? def index @completed_case_logs = CaseLog.completed diff --git a/app/controllers/users/passwords_controller.rb b/app/controllers/users/passwords_controller.rb new file mode 100644 index 000000000..da3b39158 --- /dev/null +++ b/app/controllers/users/passwords_controller.rb @@ -0,0 +1,20 @@ +class Users::PasswordsController < Devise::PasswordsController + def reset_confirmation + @email = params["email"] + flash[:notice] = "Reset password instructions have been sent to #{@email}" + render "devise/confirmations/reset" + end + + def create + self.resource = resource_class.send_reset_password_instructions(resource_params) + yield resource if block_given? + + respond_with({}, location: after_sending_reset_password_instructions_path_for(resource_name)) + end + +protected + + def after_sending_reset_password_instructions_path_for(_resource) + confirmations_reset_path(email: params.dig("user", "email")) + end +end diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 000000000..0ef982f25 --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,5 @@ +class User < ApplicationRecord + # Include default devise modules. Others available are: + # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable + devise :database_authenticatable, :recoverable, :rememberable, :validatable +end diff --git a/app/views/devise/confirmations/new.html.erb b/app/views/devise/confirmations/new.html.erb new file mode 100644 index 000000000..b12dd0cbe --- /dev/null +++ b/app/views/devise/confirmations/new.html.erb @@ -0,0 +1,16 @@ +

Resend confirmation instructions

+ +<%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + +
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email", value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email) %> +
+ +
+ <%= f.submit "Resend confirmation instructions" %> +
+<% end %> + +<%= render "devise/shared/links" %> diff --git a/app/views/devise/confirmations/reset.html.erb b/app/views/devise/confirmations/reset.html.erb new file mode 100644 index 000000000..2932d21bd --- /dev/null +++ b/app/views/devise/confirmations/reset.html.erb @@ -0,0 +1,8 @@ +
+
+

Check your email

+

We’ve sent a link to reset your password to <%= @email %>.

+

You’ll only this receive this link if your email address already exists in our system.

+

If you don’t receive the email within 5 minutes, check your spam or junk folders. Try again if you still haven’t received the email.

+
+
\ No newline at end of file diff --git a/app/views/devise/mailer/confirmation_instructions.html.erb b/app/views/devise/mailer/confirmation_instructions.html.erb new file mode 100644 index 000000000..dc55f64f6 --- /dev/null +++ b/app/views/devise/mailer/confirmation_instructions.html.erb @@ -0,0 +1,5 @@ +

Welcome <%= @email %>!

+ +

You can confirm your account email through the link below:

+ +

<%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %>

diff --git a/app/views/devise/mailer/email_changed.html.erb b/app/views/devise/mailer/email_changed.html.erb new file mode 100644 index 000000000..32f4ba803 --- /dev/null +++ b/app/views/devise/mailer/email_changed.html.erb @@ -0,0 +1,7 @@ +

Hello <%= @email %>!

+ +<% if @resource.try(:unconfirmed_email?) %> +

We're contacting you to notify you that your email is being changed to <%= @resource.unconfirmed_email %>.

+<% else %> +

We're contacting you to notify you that your email has been changed to <%= @resource.email %>.

+<% end %> diff --git a/app/views/devise/mailer/password_change.html.erb b/app/views/devise/mailer/password_change.html.erb new file mode 100644 index 000000000..b41daf476 --- /dev/null +++ b/app/views/devise/mailer/password_change.html.erb @@ -0,0 +1,3 @@ +

Hello <%= @resource.email %>!

+ +

We're contacting you to notify you that your password has been changed.

diff --git a/app/views/devise/mailer/reset_password_instructions.html.erb b/app/views/devise/mailer/reset_password_instructions.html.erb new file mode 100644 index 000000000..f667dc12f --- /dev/null +++ b/app/views/devise/mailer/reset_password_instructions.html.erb @@ -0,0 +1,8 @@ +

Hello <%= @resource.email %>!

+ +

Someone has requested a link to change your password. You can do this through the link below.

+ +

<%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %>

+ +

If you didn't request this, please ignore this email.

+

Your password won't change until you access the link above and create a new one.

diff --git a/app/views/devise/mailer/unlock_instructions.html.erb b/app/views/devise/mailer/unlock_instructions.html.erb new file mode 100644 index 000000000..41e148bf2 --- /dev/null +++ b/app/views/devise/mailer/unlock_instructions.html.erb @@ -0,0 +1,7 @@ +

Hello <%= @resource.email %>!

+ +

Your account has been locked due to an excessive number of unsuccessful sign in attempts.

+ +

Click the link below to unlock your account:

+ +

<%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @token) %>

diff --git a/app/views/devise/passwords/edit.html.erb b/app/views/devise/passwords/edit.html.erb new file mode 100644 index 000000000..0b2bc2067 --- /dev/null +++ b/app/views/devise/passwords/edit.html.erb @@ -0,0 +1,19 @@ +<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| %> +
+
+

Reset your password

+ <%= render "devise/shared/error_messages", resource: resource %> + <%= f.hidden_field :reset_password_token %> + +
+ <%= f.label :password, "New password", class: "govuk-label" %> + <% if @minimum_password_length %> +
Your password must be at least 8 characters and hard to guess.
+ <% end %> + <%= f.password_field :password, autofocus: true, autocomplete: "new-password", class: "govuk-input" %> +
+ + <%= f.submit "Reset password", class: "govuk-button" %> +
+
+<% end %> diff --git a/app/views/devise/passwords/new.html.erb b/app/views/devise/passwords/new.html.erb new file mode 100644 index 000000000..332b2fe89 --- /dev/null +++ b/app/views/devise/passwords/new.html.erb @@ -0,0 +1,24 @@ +<% content_for :before_content do %> + <%= link_to 'Back', :back, class: "govuk-back-link" %> +<% end %> + +<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %> +
+
+

Reset password

+ <%= render "devise/shared/error_messages", resource: resource %> + +

Enter the email address you used to create your account.

+

We’ll email you a link to reset your password. This link will expire in 3 hours.

+ +
+ <%= f.label :email, "Email address", class: "govuk-label" %> + <%= f.email_field :email, autofocus: true, autocomplete: "email", class: "govuk-input" %> +
+ + <%= f.submit "Send email", class: "govuk-button" %> +
+
+<% end %> + +<%= render "devise/shared/links" %> diff --git a/app/views/devise/registrations/edit.html.erb b/app/views/devise/registrations/edit.html.erb new file mode 100644 index 000000000..38d95b85a --- /dev/null +++ b/app/views/devise/registrations/edit.html.erb @@ -0,0 +1,43 @@ +

Edit <%= resource_name.to_s.humanize %>

+ +<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + +
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email" %> +
+ + <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %> +
Currently waiting confirmation for: <%= resource.unconfirmed_email %>
+ <% end %> + +
+ <%= f.label :password %> (leave blank if you don't want to change it)
+ <%= f.password_field :password, autocomplete: "new-password" %> + <% if @minimum_password_length %> +
+ <%= @minimum_password_length %> characters minimum + <% end %> +
+ +
+ <%= f.label :password_confirmation %>
+ <%= f.password_field :password_confirmation, autocomplete: "new-password" %> +
+ +
+ <%= f.label :current_password %> (we need your current password to confirm your changes)
+ <%= f.password_field :current_password, autocomplete: "current-password" %> +
+ +
+ <%= f.submit "Update" %> +
+<% end %> + +

Cancel my account

+ +

Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete %>

+ +<%= link_to "Back", :back %> diff --git a/app/views/devise/registrations/new.html.erb b/app/views/devise/registrations/new.html.erb new file mode 100644 index 000000000..d655b66f6 --- /dev/null +++ b/app/views/devise/registrations/new.html.erb @@ -0,0 +1,29 @@ +

Sign up

+ +<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + +
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email" %> +
+ +
+ <%= f.label :password %> + <% if @minimum_password_length %> + (<%= @minimum_password_length %> characters minimum) + <% end %>
+ <%= f.password_field :password, autocomplete: "new-password" %> +
+ +
+ <%= f.label :password_confirmation %>
+ <%= f.password_field :password_confirmation, autocomplete: "new-password" %> +
+ +
+ <%= f.submit "Sign up" %> +
+<% end %> + +<%= render "devise/shared/links" %> diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb new file mode 100644 index 000000000..940ccf9d1 --- /dev/null +++ b/app/views/devise/sessions/new.html.erb @@ -0,0 +1,21 @@ +<%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %> +
+
+

Sign in to your account to submit CORE data

+ +
+ <%= f.label :email, "Email address", class: "govuk-label" %> + <%= f.email_field :email, autofocus: true, autocomplete: "email", class: "govuk-input" %> +
+ +
+ <%= f.label :password, class: "govuk-label" %> + <%= f.password_field :password, autocomplete: "current-password", class: "govuk-input" %> +
+ + <%= f.submit "Sign in", class: "govuk-button" %> +
+
+<% end %> + +<%= render "devise/shared/links" %> diff --git a/app/views/devise/shared/_error_messages.html.erb b/app/views/devise/shared/_error_messages.html.erb new file mode 100644 index 000000000..ba7ab8870 --- /dev/null +++ b/app/views/devise/shared/_error_messages.html.erb @@ -0,0 +1,15 @@ +<% if resource.errors.any? %> +
+

+ <%= I18n.t("errors.messages.not_saved", + count: resource.errors.count, + resource: resource.class.model_name.human.downcase) + %> +

+ +
+<% end %> diff --git a/app/views/devise/shared/_links.html.erb b/app/views/devise/shared/_links.html.erb new file mode 100644 index 000000000..d8395536a --- /dev/null +++ b/app/views/devise/shared/_links.html.erb @@ -0,0 +1,25 @@ +<%- if controller_name != 'sessions' %> +

Already have an account? <%= link_to "Sign in", new_session_path(resource_name) %>.


+<% end %> + +<%- if devise_mapping.registerable? && controller_name != 'registrations' %> + <%= link_to "Sign up", new_registration_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %> +

You can <%= link_to "reset your password", new_password_path(resource_name) %> if you've forgotten it.


+<% end %> + +<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %> + <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %> + <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.omniauthable? %> + <%- resource_class.omniauth_providers.each do |provider| %> + <%= link_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider), method: :post %>
+ <% end %> +<% end %> diff --git a/app/views/devise/unlocks/new.html.erb b/app/views/devise/unlocks/new.html.erb new file mode 100644 index 000000000..ffc34de8d --- /dev/null +++ b/app/views/devise/unlocks/new.html.erb @@ -0,0 +1,16 @@ +

Resend unlock instructions

+ +<%= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + +
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email" %> +
+ +
+ <%= f.submit "Resend unlock instructions" %> +
+<% end %> + +<%= render "devise/shared/links" %> diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index ad52856b9..34e97e8b9 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -16,6 +16,8 @@ <%= stylesheet_pack_tag 'application', media: 'all' %> <%= javascript_pack_tag 'application', defer: true %> +

<%= notice %>

+

<%= alert %>

<% if Rails.env.development? %>