Merge pull request #9424 from mfo/US/ETQ-instructeur-je-peux-redemander-un-lien-de-connexion-securise

amelioration(instructeur.connexion): ETQ instructeur, je peux redemander un lien de connexion securisé
This commit is contained in:
mfo 2023-08-30 11:49:46 +00:00 committed by GitHub
commit f5d9cf015c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 135 additions and 29 deletions

View file

@ -0,0 +1,6 @@
class Instructeurs::ActivateAccountFormComponent < ApplicationComponent
attr_reader :user
def initialize(user:)
@user = user
end
end

View file

@ -0,0 +1,6 @@
---
en:
title: Create your account for %{application_name}
activate: Activate your account %{email}
email_disabled: Instructor email address not changeable
submit: Choose password

View file

@ -0,0 +1,7 @@
---
fr:
title: Création de compte sur %{application_name}
activate: Se créer un compte pour %{email} en choissant un mot de passe
email_disabled: Adresse instructeur non modifiable
submit: Définir le mot de passe

View file

@ -0,0 +1,23 @@
= form_for user, url: { controller: 'users/activate', action: :create }, html: { class: "fr-py-5w" } do |f|
%h1.fr-h2.fr-mb-7w= t('.title', application_name: APPLICATION_NAME)
.fr-background-alt--grey.fr-px-12w.fr-py-7w
%fieldset.fr-mb-0.fr-fieldset{ aria: { labelledby: 'activate-account-legend' } }
%legend.fr-fieldset__legend#activate-account-legend
%h2.fr-h6.fr-mb-0= t('.activate', email: user.email)
.fr-fieldset__element
%p.fr-text--sm= t('utils.mandatory_champs')
.fr-fieldset__element= render Dsfr::InputComponent.new(form: f, attribute: :email, input_type: :email_field, opts: { disabled: :disabled, class: 'fr-input-group--disabled', value: t('.email_disabled') })
.fr-fieldset__element= render Dsfr::InputComponent.new(form: f, attribute: :password, input_type: :password_field, opts: { autocomplete: 'current-password' })
= f.hidden_field :reset_password_token, value: params[:token]
.fr-fieldset__element
.fr-btns-group--right.fr-btns-group.fr-btns-group--inline.fr-btns-group.fr-btns-group--inline
%ul
%li= f.submit t('.submit'), class: 'fr-mt-2v fr-btn fr-btn'

View file

@ -6,7 +6,7 @@ class Users::SessionsController < Devise::SessionsController
layout 'login', only: [:new, :create] layout 'login', only: [:new, :create]
before_action :restore_procedure_context, only: [:new, :create] before_action :restore_procedure_context, only: [:new, :create]
skip_before_action :redirect_if_untrusted, only: [:reset_link_sent]
# POST /resource/sign_in # POST /resource/sign_in
def create def create
user = User.find_by(email: params[:user][:email]) user = User.find_by(email: params[:user][:email])
@ -18,6 +18,13 @@ class Users::SessionsController < Devise::SessionsController
super super
end end
def reset_link_sent
if send_login_token_or_bufferize(current_instructeur)
flash[:notice] = "Nous venons de vous renvoyer un nouveau lien de connexion sécurisée à #{APPLICATION_NAME}"
end
redirect_to link_sent_path(email: current_instructeur.email)
end
def link_sent def link_sent
if Devise.email_regexp.match?(params[:email]) if Devise.email_regexp.match?(params[:email])
@email = params[:email] @email = params[:email]

View file

@ -19,8 +19,11 @@ module TrustedDeviceConcern
def send_login_token_or_bufferize(instructeur) def send_login_token_or_bufferize(instructeur)
if !instructeur.young_login_token? if !instructeur.young_login_token?
login_token = instructeur.create_trusted_device_token token = instructeur.create_trusted_device_token
InstructeurMailer.send_login_token(instructeur, login_token).deliver_later InstructeurMailer.send_login_token(instructeur, token).deliver_later
true
else
false
end end
end end

View file

@ -2,7 +2,7 @@
Bonjour, Bonjour,
%p %p
Veuillez cliquer sur le lien suivant pour vous connecter sur le site #{APPLICATION_NAME} :  Veuillez cliquer sur le lien sécurisé suivant pour vous connecter à #{APPLICATION_NAME} : 
= link_to(sign_in_by_link_url(@instructeur_id, jeton: @login_token), sign_in_by_link_url(@instructeur_id, jeton: @login_token)) = link_to(sign_in_by_link_url(@instructeur_id, jeton: @login_token), sign_in_by_link_url(@instructeur_id, jeton: @login_token))
%p %p

View file

@ -7,11 +7,18 @@
Vous venez dêtre nommé instructeur sur #{APPLICATION_NAME}. Vous venez dêtre nommé instructeur sur #{APPLICATION_NAME}.
%p %p
Votre compte a été créé pour l'adresse email #{@user.email}. Pour lactiver, nous vous invitons à cliquer sur le lien suivant :  Votre compte a été créé pour l'adresse email
%strong #{@user.email}.
%p
Pour lactiver, cliquez sur le lien suivant : 
= link_to(users_activate_url(token: @reset_password_token), users_activate_url(token: @reset_password_token)) = link_to(users_activate_url(token: @reset_password_token), users_activate_url(token: @reset_password_token))
%p %p
Par ailleurs, nous vous invitons à prendre quelques minutes pour consulter notre tutoriel à destination des nouveaux instructeurs : Lors de vos prochaines connexions sur #{APPLICATION_NAME} cliquez sur le bouton « Se connecter » positionné sur le haut de page ou bien sur ce lien : 
= link_to new_user_session_url, new_user_session_url
%p
Nous vous invitons aussi à consulter notre tutoriel à destination des nouveaux instructeurs :
= link_to(INSTRUCTEUR_TUTORIAL_URL, INSTRUCTEUR_TUTORIAL_URL) = link_to(INSTRUCTEUR_TUTORIAL_URL, INSTRUCTEUR_TUTORIAL_URL)
= render partial: "layouts/mailers/signature" = render partial: "layouts/mailers/signature"

View file

@ -1,7 +1,9 @@
.container = content_for(:page_id, 'activate_account')
= form_for @user, url: { controller: 'users/activate', action: :create }, html: { class: "form" } do |f| = content_for(:title, t('metas.users.activate.title', application_name: APPLICATION_NAME))
%br
%h1= @user.email .fr-container.fr-my-5w
= f.password_field :password, placeholder: 'Mot de passe' .fr-grid-row
= f.hidden_field :reset_password_token, value: params[:token] .fr-col-12.fr-col-offset-md-2.fr-col-md-8
= f.submit 'Définir le mot de passe', class: 'button large primary expand' = render Instructeurs::ActivateAccountFormComponent.new(user: @user)
= render partial: 'users/dossiers/index_footer'

View file

@ -3,18 +3,27 @@
- content_for :footer do - content_for :footer do
= render partial: 'root/footer' = render partial: 'root/footer'
#link-sent.container .fr-container.fr-my-5w
= image_tag('user/confirmation-email.svg', "aria-hidden": true) .fr-grid-row
%h1 Encore une petite étape :) .fr-col-12.fr-col-offset-md-1.fr-col-md-7
%h1.fr-mt-6w Encore une petite étape !
%section.link-sent-info %section
%p %p.fr-text--lead
Ouvrez votre boite email <strong>#{@email}</strong> puis cliquez sur le lien dactivation du message <strong>Connexion sécurisée à #{APPLICATION_NAME}</strong>. Nous venons de vous envoyer un courriel sur votre boite email <strong>#{@email}</strong>.
%p Veuillez louvrir et cliquer sur le lien de <strong>connexion sécurisée à #{APPLICATION_NAME}</strong>.
= t('views.users.shared.email_can_take_a_while_html')
%section.link-sent-help %p.fr-text--lead
%p Ce lien est <strong>valide une semaine</strong> et peut être réutilisé <strong>plusieurs fois</strong>.
Si vous voyez cette page trop souvent, consultez notre aide : #{link_to t("links.common.faq.confirmer_compte_chaque_connexion_url"), **external_link_attributes}
%p %p.fr-text--sm.fr-text-mention--grey
= t('views.users.shared.contact_us_if_any_trouble_html', href: contact_admin_url) Ce courriel peut mettre jusquà <strong>15 minutes</strong> pour arriver. Si vous navez pas reçu de courriel (nhésitez pas à vérifier dans les indésirables), cliquez sur le bouton ci-dessous.
= button_to instructeurs_reset_link_sent_path, class: 'fr-btn fr-btn--secondary fr-btn--icon-left fr-icon-mail-line', method: 'POST' do
Renvoyer le courriel
%section
%p.fr-mt-3w
Si vous voyez cette page trop souvent, consultez notre aide : #{link_to t("links.common.faq.confirmer_compte_chaque_connexion_url"), **external_link_attributes}
%p.fr-mt-3w
= t('views.users.shared.contact_us_if_any_trouble_html', href: contact_admin_url)

View file

@ -1,7 +1,6 @@
= content_for(:page_id, 'auth') = content_for(:page_id, 'auth')
= content_for(:title, t('metas.signin.title')) = content_for(:title, t('metas.signin.title'))
#session-new.auth-form.sign-in-form #session-new.auth-form.sign-in-form
= form_for resource, url: user_session_path, html: { class: "fr-py-5w" } do |f| = form_for resource, url: user_session_path, html: { class: "fr-py-5w" } do |f|

View file

@ -515,7 +515,7 @@ en:
check_france_connect_html: Have you once logged in with France Connect? If yes, <a href=\"%{href}\">try again with France Connect</a>. check_france_connect_html: Have you once logged in with France Connect? If yes, <a href=\"%{href}\">try again with France Connect</a>.
check_gpdr: "The account may have been deleted in the event of prolonged inactivity and no current file. In this case you will have to recreate an account from a procedure." check_gpdr: "The account may have been deleted in the event of prolonged inactivity and no current file. In this case you will have to recreate an account from a procedure."
shared: shared:
email_can_take_a_while_html: <strong>Please note</strong> that this message can take up to 15 minutes to arrive. email_can_take_a_while_html: <strong>Please note</strong> that this email can take up to 15 minutes to arrive.
contact_us_if_any_trouble_html: 'You can contact us <a href="%{href}">through this form</a> if a problem still exists.' contact_us_if_any_trouble_html: 'You can contact us <a href="%{href}">through this form</a> if a problem still exists.'
modal: modal:
publish: publish:

View file

@ -519,7 +519,7 @@ fr:
open_your_mailbox: "Maintenant ouvrez votre boite email." open_your_mailbox: "Maintenant ouvrez votre boite email."
title: "Lien de réinitialisation du mot de passe envoyé" title: "Lien de réinitialisation du mot de passe envoyé"
shared: shared:
email_can_take_a_while_html: "<strong>Attention</strong>, ce message peut mettre jusquà <strong>15 minutes</strong> pour arriver." email_can_take_a_while_html: "<strong>Attention</strong>, ce courriel peut mettre jusquà <strong>15 minutes</strong> pour arriver."
contact_us_if_any_trouble_html: 'En cas de difficultés, nous restons joignables <a href="%{href}">via ce formulaire</a>.' contact_us_if_any_trouble_html: 'En cas de difficultés, nous restons joignables <a href="%{href}">via ce formulaire</a>.'
modal: modal:
publish: publish:

View file

@ -17,3 +17,5 @@ en:
title: "Modification of draft nº %{number} (%{procedure_label})" title: "Modification of draft nº %{number} (%{procedure_label})"
merci: merci:
title: "File submitted (%{procedure_label})" title: "File submitted (%{procedure_label})"
activate:
title: Activate my account on %{application_name}

View file

@ -17,3 +17,5 @@ fr:
title: "Modification du brouillon nº %{number} (%{procedure_label})" title: "Modification du brouillon nº %{number} (%{procedure_label})"
merci: merci:
title: "Dossier envoyé (%{procedure_label})" title: "Dossier envoyé (%{procedure_label})"
activate:
title: Activer mon compte sur %{application_name}

View file

@ -125,6 +125,7 @@ Rails.application.routes.draw do
get '/users/no_procedure' => 'users/sessions#no_procedure' get '/users/no_procedure' => 'users/sessions#no_procedure'
get 'connexion-par-jeton/:id' => 'users/sessions#sign_in_by_link', as: 'sign_in_by_link' get 'connexion-par-jeton/:id' => 'users/sessions#sign_in_by_link', as: 'sign_in_by_link'
get 'lien-envoye' => 'users/sessions#link_sent', as: 'link_sent' get 'lien-envoye' => 'users/sessions#link_sent', as: 'link_sent'
post '/instructeurs/reset-link-sent' => 'users/sessions#reset_link_sent'
get '/users/password/reset-link-sent' => 'users/passwords#reset_link_sent' get '/users/password/reset-link-sent' => 'users/passwords#reset_link_sent'
end end

View file

@ -244,4 +244,36 @@ describe Users::SessionsController, type: :controller do
end end
end end
end end
describe '#reset_link_sent' do
let(:instructeur) { create(:instructeur, user: user) }
before { sign_in(user) }
subject { post :reset_link_sent }
context 'when the instructeur is signed without trust_device_token' do
it 'send InstructeurMailer.send_login_token' do
expect(InstructeurMailer).to receive(:send_login_token).with(instructeur, anything).and_return(double(deliver_later: true))
expect { subject }.to change { instructeur.trusted_device_tokens.count }.by(1)
end
end
context 'when the instructeur is signed with an young trust_device_token' do
before { instructeur.create_trusted_device_token }
it 'doesnot send InstructeurMailer.send_login_token' do
expect(InstructeurMailer).not_to receive(:send_login_token)
expect { subject }.to change { instructeur.trusted_device_tokens.count }.by(0)
end
end
context 'when the instructeur is signed with an old trust_device_token' do
let(:token) { instructeur.create_trusted_device_token }
before do
travel_to 15.minutes.from_now
end
it 'send InstructeurMailer.send_login_token' do
expect(InstructeurMailer).to receive(:send_login_token).with(instructeur, anything).and_return(double(deliver_later: true))
expect { subject }.to change { instructeur.trusted_device_tokens.count }.by(1)
end
end
end
end end