app: add a password_complexity component

This component will replace the previous `password_field` component.
This commit is contained in:
Pierre de La Morinerie 2021-08-31 16:14:32 +00:00
parent 586f8ec543
commit 428ca8755f
10 changed files with 172 additions and 0 deletions

View file

@ -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;
}
}

View file

@ -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

View file

@ -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

View file

@ -0,0 +1 @@
#complexity-bar.password-complexity{ class: "complexity-#{@length < @min_length ? @score/2 : @score}" }

View file

@ -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é.

View file

@ -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...<br> ou améliorer votre mot de passe.
- else
Félicitations ! Mot de passe suffisamment fort et sécurisé.
- else
Inscrivez un mot de passe.

View file

@ -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};") %>

View file

@ -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
#

View file

@ -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

View file

@ -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