From 428ca8755fda8f8efc8de7f317655e5af8e132ea Mon Sep 17 00:00:00 2001 From: Pierre de La Morinerie Date: Tue, 31 Aug 2021 16:14:32 +0000 Subject: [PATCH] app: add a password_complexity component This component will replace the previous `password_field` component. --- .../stylesheets/password_complexity.scss | 40 +++++++++++++++++++ .../concerns/devise_populated_resource.rb | 13 ++++++ .../password_complexity_controller.rb | 15 +++++++ app/views/password_complexity/_bar.html.haml | 1 + .../password_complexity/_field.html.haml | 9 +++++ .../password_complexity/_label.html.haml | 16 ++++++++ app/views/password_complexity/show.js.erb | 3 ++ config/routes.rb | 2 + .../devise_populated_resource_spec.rb | 38 ++++++++++++++++++ .../password_complexity_controller_spec.rb | 35 ++++++++++++++++ 10 files changed, 172 insertions(+) create mode 100644 app/assets/stylesheets/password_complexity.scss create mode 100644 app/controllers/concerns/devise_populated_resource.rb create mode 100644 app/controllers/password_complexity_controller.rb create mode 100644 app/views/password_complexity/_bar.html.haml create mode 100644 app/views/password_complexity/_field.html.haml create mode 100644 app/views/password_complexity/_label.html.haml create mode 100644 app/views/password_complexity/show.js.erb create mode 100644 spec/controllers/concerns/devise_populated_resource_spec.rb create mode 100644 spec/controllers/password_complexity_controller_spec.rb diff --git a/app/assets/stylesheets/password_complexity.scss b/app/assets/stylesheets/password_complexity.scss new file mode 100644 index 000000000..24fc2ef77 --- /dev/null +++ b/app/assets/stylesheets/password_complexity.scss @@ -0,0 +1,40 @@ +@import "colors"; +@import "constants"; + +$complexity-bg: #EEEEEE; +$complexity-color-0: $lighter-red; +$complexity-color-1: #FF5000; +$complexity-color-2: $orange; +$complexity-color-3: #FFD000; +$complexity-color-4: $green; + +.password-complexity { + margin-top: -24px; + width: 100%; + height: 12px; + background: $complexity-bg; + display: block; + margin-bottom: $default-spacer; + text-align: center; + border-radius: 8px; + + &.complexity-0 { + background: linear-gradient(to right, $complexity-color-0 00%, $complexity-bg 20%); + } + + &.complexity-1 { + background: linear-gradient(to right, $complexity-color-1 20%, $complexity-bg 40%); + } + + &.complexity-2 { + background: linear-gradient(to right, $complexity-color-2 40%, $complexity-bg 60%); + } + + &.complexity-3 { + background: linear-gradient(to right, $complexity-color-3 60%, $complexity-bg 80%); + } + + &.complexity-4 { + background: $complexity-color-4; + } +} diff --git a/app/controllers/concerns/devise_populated_resource.rb b/app/controllers/concerns/devise_populated_resource.rb new file mode 100644 index 000000000..5d61113ae --- /dev/null +++ b/app/controllers/concerns/devise_populated_resource.rb @@ -0,0 +1,13 @@ +module DevisePopulatedResource + extend ActiveSupport::Concern + + # During a GET /password/edit, the resource is a brand new object. + # This method gives access to the actual resource record, complete with email, relationships, etc. + def populated_resource + resource_class.with_reset_password_token(resource.reset_password_token) + end + + included do + helper_method :populated_resource + end +end diff --git a/app/controllers/password_complexity_controller.rb b/app/controllers/password_complexity_controller.rb new file mode 100644 index 000000000..f1572b124 --- /dev/null +++ b/app/controllers/password_complexity_controller.rb @@ -0,0 +1,15 @@ +class PasswordComplexityController < ApplicationController + def show + @score, @words, @length = ZxcvbnService.new(password_param).complexity + @min_length = PASSWORD_MIN_LENGTH + @min_complexity = PASSWORD_COMPLEXITY_FOR_ADMIN + end + + private + + def password_param + params + .transform_keys! { |k| params[k].try(:has_key?, :password) ? 'resource' : k } + .dig(:resource, :password) + end +end diff --git a/app/views/password_complexity/_bar.html.haml b/app/views/password_complexity/_bar.html.haml new file mode 100644 index 000000000..a9b8c8262 --- /dev/null +++ b/app/views/password_complexity/_bar.html.haml @@ -0,0 +1 @@ +#complexity-bar.password-complexity{ class: "complexity-#{@length < @min_length ? @score/2 : @score}" } diff --git a/app/views/password_complexity/_field.html.haml b/app/views/password_complexity/_field.html.haml new file mode 100644 index 000000000..eaf29e700 --- /dev/null +++ b/app/views/password_complexity/_field.html.haml @@ -0,0 +1,9 @@ += form.password_field :password, autofocus: true, autocomplete: 'off', placeholder: 'Mot de passe', data: { remote: test_complexity, url: show_password_complexity_path } + +- if test_complexity + #complexity-bar.password-complexity + + .explication + #complexity-label{ style: 'font-weight: bold' } + Inscrivez un mot de passe. + Une courte phrase avec ponctuation peut être un mot de passe très sécurisé. diff --git a/app/views/password_complexity/_label.html.haml b/app/views/password_complexity/_label.html.haml new file mode 100644 index 000000000..2e9bda1d0 --- /dev/null +++ b/app/views/password_complexity/_label.html.haml @@ -0,0 +1,16 @@ +#complexity-label{ style: 'font-weight: bold' } + - if @length > 0 + - if @length < @min_length + Le mot de passe doit faire au moins #{@min_length} caractères. + - else + - case @score + - when 0..1 + Mot de passe très vulnérable. + - when 2...@min_complexity + Mot de passe vulnérable. + - when @min_complexity...4 + Mot de passe acceptable. Vous pouvez valider...
ou améliorer votre mot de passe. + - else + Félicitations ! Mot de passe suffisamment fort et sécurisé. + - else + Inscrivez un mot de passe. diff --git a/app/views/password_complexity/show.js.erb b/app/views/password_complexity/show.js.erb new file mode 100644 index 000000000..1d83ac45a --- /dev/null +++ b/app/views/password_complexity/show.js.erb @@ -0,0 +1,3 @@ +<%= render_to_element('#complexity-label', partial: 'label', outer: true) %> +<%= render_to_element('#complexity-bar', partial: 'bar', outer: true) %> +<%= raw("document.querySelector('#submit-password').disabled = #{@score < @min_complexity || @length < @min_length};") %> diff --git a/config/routes.rb b/config/routes.rb index 377c1c9a2..4b5de2c28 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -114,6 +114,8 @@ Rails.application.routes.draw do get '/administrateurs/password/test_strength' => 'administrateurs/passwords#test_strength' end + get 'password_complexity' => 'password_complexity#show', as: 'show_password_complexity' + # # Main routes # diff --git a/spec/controllers/concerns/devise_populated_resource_spec.rb b/spec/controllers/concerns/devise_populated_resource_spec.rb new file mode 100644 index 000000000..57f35b112 --- /dev/null +++ b/spec/controllers/concerns/devise_populated_resource_spec.rb @@ -0,0 +1,38 @@ +describe DevisePopulatedResource, type: :controller do + controller(Devise::PasswordsController) do + include DevisePopulatedResource + end + + let(:user) { create(:user) } + + before do + routes.draw do + get 'edit' => 'devise/passwords#edit' + put 'update' => 'devise/passwords#update' + end + + @request.env["devise.mapping"] = Devise.mappings[:user] + + @token = user.send_reset_password_instructions + end + + context 'when initiating a password reset' do + subject { get :edit, params: { reset_password_token: @token } } + + it 'returns the fully populated resource' do + subject + expect(controller.populated_resource.id).to eq(user.id) + expect(controller.populated_resource.email).to eq(user.email) + end + end + + context 'when submitting a password reset' do + subject { put :update, params: { user: { reset_password_token: @token } } } + + it 'returns the fully populated resource' do + subject + expect(controller.populated_resource.id).to eq(user.id) + expect(controller.populated_resource.email).to eq(user.email) + end + end +end diff --git a/spec/controllers/password_complexity_controller_spec.rb b/spec/controllers/password_complexity_controller_spec.rb new file mode 100644 index 000000000..7c60de3fc --- /dev/null +++ b/spec/controllers/password_complexity_controller_spec.rb @@ -0,0 +1,35 @@ +describe PasswordComplexityController, type: :controller do + describe '#show' do + let(:params) do + { user: { password: 'moderately complex password' } } + end + + subject { get :show, format: :js, params: params, xhr: true } + + it 'computes a password score' do + subject + expect(assigns(:score)).to eq(3) + end + + context 'with a different resource name' do + let(:params) do + { super_admin: { password: 'moderately complex password' } } + end + + it 'computes a password score' do + subject + expect(assigns(:score)).to eq(3) + end + end + + context 'when rendering the view' do + render_views + + it 'renders Javascript that updates the password complexity meter' do + subject + expect(response.body).to include('#complexity-label') + expect(response.body).to include('#complexity-bar') + end + end + end +end