commit
cc64ab64ec
45 changed files with 666 additions and 99 deletions
1
app/assets/images/user/confirmation-email.svg
Normal file
1
app/assets/images/user/confirmation-email.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="81" height="86" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><g stroke-linecap="round" stroke-linejoin="round" stroke="#0069CC" stroke-width="2"><path d="M9.203 31.348L1.083 37h-.045L1 77.334C1 81.606 4.538 85 8.865 85h62.92c4.325 0 8.296-3.168 8.296-7.44V37m-.068.125L71 31"/><path d="M2.459 37.212l38.014 25.92 38.014-26.211M3.384 82.25L29.967 56m47.595 26.25L50.979 56"/></g><path d="M40.04 63.132L9.08 41.505V5.307a4 4 0 0 1 4-4H67a4 4 0 0 1 4 4v36.96L40.04 63.132z" stroke="#0069CC" stroke-width="2" fill="#EFF6FF" stroke-linecap="round" stroke-linejoin="round"/><path d="M40.87 45.53c-3.103 0-5.81-.63-8.12-1.89-2.31-1.26-4.09-3.022-5.337-5.285-1.249-2.263-1.873-4.877-1.873-7.84 0-3.197.665-5.973 1.995-8.33s3.156-4.165 5.477-5.425c2.322-1.26 4.953-1.89 7.893-1.89 3.057 0 5.682.59 7.875 1.767 2.193 1.179 3.856 2.76 4.987 4.743 1.132 1.983 1.698 4.165 1.698 6.545 0 2.147-.327 3.955-.98 5.425-.653 1.47-1.528 2.572-2.625 3.307a6.398 6.398 0 0 1-3.64 1.103c-1.26 0-2.263-.268-3.01-.805-.747-.537-1.19-1.283-1.33-2.24h-.07c-1.33 2.03-3.068 3.045-5.215 3.045-1.68 0-3.004-.566-3.973-1.698-.968-1.131-1.452-2.665-1.452-4.602 0-1.657.315-3.18.945-4.568.63-1.388 1.51-2.49 2.642-3.307 1.132-.817 2.421-1.225 3.868-1.225 1.003 0 1.89.227 2.66.682.77.456 1.295 1.068 1.575 1.838h.07l.385-2.17h3.255l-1.575 8.785c-.093.49-.14.992-.14 1.505 0 .747.157 1.29.472 1.627.316.339.811.508 1.488.508.537 0 1.08-.24 1.627-.718.549-.478 1.01-1.254 1.383-2.327s.56-2.462.56-4.165c0-1.937-.432-3.675-1.295-5.215s-2.158-2.765-3.885-3.675c-1.727-.91-3.827-1.365-6.3-1.365-2.31 0-4.38.507-6.213 1.522-1.831 1.016-3.272 2.491-4.322 4.428-1.05 1.937-1.575 4.235-1.575 6.895 0 2.45.484 4.59 1.452 6.422a10.232 10.232 0 0 0 4.165 4.253c1.809 1.003 3.961 1.505 6.458 1.505 1.843 0 3.424-.24 4.742-.718C46.931 41.5 47.917 40.84 48.57 40h4.235c-.957 1.68-2.467 3.022-4.533 4.025-2.065 1.003-4.532 1.505-7.402 1.505zm-1.575-10.395c1.027 0 1.913-.315 2.66-.945.747-.63 1.312-1.447 1.698-2.45a8.813 8.813 0 0 0 .577-3.185c0-1.167-.274-2.053-.822-2.66-.549-.607-1.301-.91-2.258-.91-.98 0-1.826.315-2.537.945-.712.63-1.255 1.44-1.628 2.433a8.624 8.624 0 0 0-.56 3.062c0 1.143.262 2.047.787 2.713.526.665 1.22.997 2.083.997z" fill="#0069CC"/></g></svg>
|
After Width: | Height: | Size: 2.2 KiB |
|
@ -15,4 +15,5 @@ $light-green: lighten($green, 25%);
|
||||||
$dark-green: darken($green, 20%);
|
$dark-green: darken($green, 20%);
|
||||||
$orange: #F28900;
|
$orange: #F28900;
|
||||||
$orange-bg: lighten($orange, 35%);
|
$orange-bg: lighten($orange, 35%);
|
||||||
|
$yellow: #FEF3B8;
|
||||||
$light-yellow: #FFFFDE;
|
$light-yellow: #FFFFDE;
|
||||||
|
|
64
app/assets/stylesheets/new_design/confirmations.scss
Normal file
64
app/assets/stylesheets/new_design/confirmations.scss
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
@import "colors";
|
||||||
|
@import "constants";
|
||||||
|
|
||||||
|
.devise-confirmations {
|
||||||
|
.one-column-centered {
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirmation-icon,
|
||||||
|
.confirmation-preamble,
|
||||||
|
.confirmation-instructions,
|
||||||
|
.confirmation-separator {
|
||||||
|
font-size: 1.15em;
|
||||||
|
margin-bottom: $default-padding * 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirmation-icon {
|
||||||
|
display: block;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirmation-instructions {
|
||||||
|
color: #000000;
|
||||||
|
background-color: $yellow;
|
||||||
|
margin-left: -15px;
|
||||||
|
margin-right: -15px;
|
||||||
|
padding: 15px 20px 17px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirmation-separator {
|
||||||
|
height: 1px;
|
||||||
|
margin-left: -12px;
|
||||||
|
margin-right: -12px;
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid #DDDDDD;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirmation-resend {
|
||||||
|
p {
|
||||||
|
margin-bottom: $default-padding;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
input,
|
||||||
|
button {
|
||||||
|
margin-bottom: $default-spacer;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=email] {
|
||||||
|
width: auto;
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-right: $default-spacer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
30
app/assets/stylesheets/new_design/link-sent.scss
Normal file
30
app/assets/stylesheets/new_design/link-sent.scss
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
@import "constants";
|
||||||
|
@import "colors";
|
||||||
|
|
||||||
|
#link-sent {
|
||||||
|
padding-top: 2 * $default-padding;
|
||||||
|
padding-bottom: 2 * $default-padding;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 600px;
|
||||||
|
|
||||||
|
b {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
text-align: left;
|
||||||
|
margin: 6 * $default-spacer auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.mail {
|
||||||
|
color: #000000;
|
||||||
|
background-color: $yellow;
|
||||||
|
padding: $default-padding;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.help {
|
||||||
|
border-top: 1px solid $grey;
|
||||||
|
padding-top: 6 * $default-spacer;
|
||||||
|
margin-bottom: 2 * $default-spacer;
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,7 +22,7 @@ footer {
|
||||||
.footer-columns {
|
.footer-columns {
|
||||||
@extend %horizontal-list;
|
@extend %horizontal-list;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
margin: 0 -20px;
|
margin: 0 -15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-column {
|
.footer-column {
|
||||||
|
|
|
@ -51,7 +51,7 @@
|
||||||
.brouillon,
|
.brouillon,
|
||||||
.en-construction,
|
.en-construction,
|
||||||
.en-instruction {
|
.en-instruction {
|
||||||
max-width: 600px;
|
max-width: 650px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
require 'zxcvbn'
|
require 'zxcvbn'
|
||||||
|
|
||||||
class Administrateurs::ActivateController < ApplicationController
|
class Administrateurs::ActivateController < ApplicationController
|
||||||
|
include TrustedDeviceConcern
|
||||||
|
|
||||||
layout "new_application"
|
layout "new_application"
|
||||||
|
|
||||||
def new
|
def new
|
||||||
@administrateur = Administrateur.find_inactive_by_token(params[:token])
|
@administrateur = Administrateur.find_inactive_by_token(params[:token])
|
||||||
|
|
||||||
if !@administrateur
|
if @administrateur
|
||||||
|
# the administrateur activates its account from an email
|
||||||
|
trust_device
|
||||||
|
else
|
||||||
flash.alert = "Le lien de validation d'administrateur a expiré, #{helpers.contact_link('contactez-nous', tags: 'lien expiré')} pour obtenir un nouveau lien."
|
flash.alert = "Le lien de validation d'administrateur a expiré, #{helpers.contact_link('contactez-nous', tags: 'lien expiré')} pour obtenir un nouveau lien."
|
||||||
redirect_to root_path
|
redirect_to root_path
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
class Gestionnaires::ActivateController < ApplicationController
|
class Gestionnaires::ActivateController < ApplicationController
|
||||||
|
include TrustedDeviceConcern
|
||||||
|
|
||||||
layout "new_application"
|
layout "new_application"
|
||||||
|
|
||||||
def new
|
def new
|
||||||
@gestionnaire = Gestionnaire.with_reset_password_token(params[:token])
|
@gestionnaire = Gestionnaire.with_reset_password_token(params[:token])
|
||||||
|
|
||||||
if !@gestionnaire
|
if @gestionnaire
|
||||||
|
# the gestionnaire activates its account from an email
|
||||||
|
trust_device
|
||||||
|
else
|
||||||
flash.alert = "Le lien de validation du compte instructeur a expiré, #{helpers.contact_link('contactez-nous', tags: 'lien expiré')} pour obtenir un nouveau lien."
|
flash.alert = "Le lien de validation du compte instructeur a expiré, #{helpers.contact_link('contactez-nous', tags: 'lien expiré')} pour obtenir un nouveau lien."
|
||||||
redirect_to root_path
|
redirect_to root_path
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,5 +6,19 @@ module Manager
|
||||||
flash[:notice] = "Instructeur réinvité."
|
flash[:notice] = "Instructeur réinvité."
|
||||||
redirect_to manager_gestionnaire_path(gestionnaire)
|
redirect_to manager_gestionnaire_path(gestionnaire)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def enable_feature
|
||||||
|
gestionnaire = Gestionnaire.find(params[:id])
|
||||||
|
|
||||||
|
params[:features].each do |key, enable|
|
||||||
|
if enable
|
||||||
|
gestionnaire.enable_feature(key.to_sym)
|
||||||
|
else
|
||||||
|
gestionnaire.disable_feature(key.to_sym)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,9 +4,10 @@ class Users::ConfirmationsController < Devise::ConfirmationsController
|
||||||
layout "new_application"
|
layout "new_application"
|
||||||
|
|
||||||
# GET /resource/confirmation/new
|
# GET /resource/confirmation/new
|
||||||
# def new
|
def new
|
||||||
# super
|
# Allow displaying the user email in the message
|
||||||
# end
|
self.resource = resource_class.new(email: user_email_param)
|
||||||
|
end
|
||||||
|
|
||||||
# POST /resource/confirmation
|
# POST /resource/confirmation
|
||||||
# def create
|
# def create
|
||||||
|
@ -20,6 +21,10 @@ class Users::ConfirmationsController < Devise::ConfirmationsController
|
||||||
|
|
||||||
# protected
|
# protected
|
||||||
|
|
||||||
|
def user_email_param
|
||||||
|
params.permit(user: :email).dig(:user, :email)
|
||||||
|
end
|
||||||
|
|
||||||
# The path used after resending confirmation instructions.
|
# The path used after resending confirmation instructions.
|
||||||
# def after_resending_confirmation_instructions_path_for(resource_name)
|
# def after_resending_confirmation_instructions_path_for(resource_name)
|
||||||
# super(resource_name)
|
# super(resource_name)
|
||||||
|
|
|
@ -22,20 +22,22 @@ class Users::RegistrationsController < Devise::RegistrationsController
|
||||||
|
|
||||||
# POST /resource
|
# POST /resource
|
||||||
def create
|
def create
|
||||||
user = User.find_by(email: params[:user][:email])
|
# Handle existing user trying to sign up again
|
||||||
if user.present?
|
existing_user = User.find_by(email: params[:user][:email])
|
||||||
if user.confirmed?
|
if existing_user.present?
|
||||||
UserMailer.new_account_warning(user).deliver_later
|
if existing_user.confirmed?
|
||||||
else
|
UserMailer.new_account_warning(existing_user).deliver_later
|
||||||
user.resend_confirmation_instructions
|
|
||||||
end
|
|
||||||
flash.notice = t('devise.registrations.signed_up_but_unconfirmed')
|
flash.notice = t('devise.registrations.signed_up_but_unconfirmed')
|
||||||
redirect_to root_path
|
return redirect_to root_path
|
||||||
else
|
else
|
||||||
super
|
existing_user.resend_confirmation_instructions
|
||||||
|
return redirect_to after_inactive_sign_up_path_for(existing_user)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
super
|
||||||
|
end
|
||||||
|
|
||||||
# GET /resource/edit
|
# GET /resource/edit
|
||||||
# def edit
|
# def edit
|
||||||
# super
|
# super
|
||||||
|
@ -78,7 +80,8 @@ class Users::RegistrationsController < Devise::RegistrationsController
|
||||||
# end
|
# end
|
||||||
|
|
||||||
# The path used after sign up for inactive accounts.
|
# The path used after sign up for inactive accounts.
|
||||||
# def after_inactive_sign_up_path_for(resource)
|
def after_inactive_sign_up_path_for(resource)
|
||||||
# super(resource)
|
flash.discard(:notice) # Remove devise's default message (as we have a custom page to explain it)
|
||||||
# end
|
new_confirmation_path(resource, :user => { email: resource.email })
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
class Users::SessionsController < Sessions::SessionsController
|
class Users::SessionsController < Sessions::SessionsController
|
||||||
|
include TrustedDeviceConcern
|
||||||
|
include ActionView::Helpers::DateHelper
|
||||||
|
|
||||||
layout "new_application"
|
layout "new_application"
|
||||||
|
|
||||||
# GET /resource/sign_in
|
# GET /resource/sign_in
|
||||||
|
@ -23,13 +26,22 @@ class Users::SessionsController < Sessions::SessionsController
|
||||||
current_user.update(loged_in_with_france_connect: '')
|
current_user.update(loged_in_with_france_connect: '')
|
||||||
end
|
end
|
||||||
|
|
||||||
if user_signed_in?
|
if gestionnaire_signed_in?
|
||||||
|
if trusted_device? || !current_gestionnaire.feature_enabled?(:enable_email_login_token)
|
||||||
|
set_flash_message :notice, :signed_in
|
||||||
|
redirect_to gestionnaire_procedures_path
|
||||||
|
else
|
||||||
|
gestionnaire = current_gestionnaire
|
||||||
|
login_token = gestionnaire.login_token!
|
||||||
|
GestionnaireMailer.send_login_token(gestionnaire, login_token).deliver_later
|
||||||
|
|
||||||
|
[:user, :gestionnaire, :administrateur].each { |role| sign_out(role) }
|
||||||
|
|
||||||
|
redirect_to link_sent_path(email: gestionnaire.email)
|
||||||
|
end
|
||||||
|
elsif user_signed_in?
|
||||||
|
set_flash_message :notice, :signed_in
|
||||||
redirect_to after_sign_in_path_for(:user)
|
redirect_to after_sign_in_path_for(:user)
|
||||||
elsif gestionnaire_signed_in?
|
|
||||||
location = stored_location_for(:gestionnaire) || gestionnaire_procedures_path
|
|
||||||
redirect_to location
|
|
||||||
elsif administrateur_signed_in?
|
|
||||||
redirect_to admin_path
|
|
||||||
else
|
else
|
||||||
flash.alert = 'Mauvais couple login / mot de passe'
|
flash.alert = 'Mauvais couple login / mot de passe'
|
||||||
new
|
new
|
||||||
|
@ -37,6 +49,10 @@ class Users::SessionsController < Sessions::SessionsController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def link_sent
|
||||||
|
@email = params[:email]
|
||||||
|
end
|
||||||
|
|
||||||
# DELETE /resource/sign_out
|
# DELETE /resource/sign_out
|
||||||
def destroy
|
def destroy
|
||||||
if gestionnaire_signed_in?
|
if gestionnaire_signed_in?
|
||||||
|
@ -68,6 +84,27 @@ class Users::SessionsController < Sessions::SessionsController
|
||||||
redirect_to new_user_session_path
|
redirect_to new_user_session_path
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def sign_in_by_link
|
||||||
|
gestionnaire = Gestionnaire.find(params[:id])
|
||||||
|
if gestionnaire&.login_token_valid?(params[:jeton])
|
||||||
|
trust_device
|
||||||
|
flash.notice = "Merci d’avoir confirmé votre connexion. Votre navigateur est maintenant authentifié pour #{TRUSTED_DEVICE_PERIOD.to_i / ActiveSupport::Duration::SECONDS_PER_DAY} jours."
|
||||||
|
|
||||||
|
user = User.find_by(email: gestionnaire.email)
|
||||||
|
administrateur = Administrateur.find_by(email: gestionnaire.email)
|
||||||
|
[user, gestionnaire, administrateur].compact.each { |resource| sign_in(resource) }
|
||||||
|
|
||||||
|
if administrateur.present?
|
||||||
|
redirect_to admin_procedures_path
|
||||||
|
else
|
||||||
|
redirect_to gestionnaire_procedures_path
|
||||||
|
end
|
||||||
|
else
|
||||||
|
flash[:alert] = 'Votre lien est invalide ou expiré, veuillez-vous reconnecter.'
|
||||||
|
redirect_to new_user_session_path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def error_procedure
|
def error_procedure
|
||||||
|
@ -92,7 +129,6 @@ class Users::SessionsController < Sessions::SessionsController
|
||||||
resource.remember_me = remember_me
|
resource.remember_me = remember_me
|
||||||
sign_in resource
|
sign_in resource
|
||||||
resource.force_sync_credentials
|
resource.force_sync_credentials
|
||||||
set_flash_message :notice, :signed_in
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,7 +14,8 @@ class GestionnaireDashboard < Administrate::BaseDashboard
|
||||||
updated_at: Field::DateTime,
|
updated_at: Field::DateTime,
|
||||||
current_sign_in_at: Field::DateTime,
|
current_sign_in_at: Field::DateTime,
|
||||||
dossiers: Field::HasMany,
|
dossiers: Field::HasMany,
|
||||||
procedures: Field::HasMany
|
procedures: Field::HasMany,
|
||||||
|
features: FeaturesField
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
# COLLECTION_ATTRIBUTES
|
# COLLECTION_ATTRIBUTES
|
||||||
|
@ -35,7 +36,8 @@ class GestionnaireDashboard < Administrate::BaseDashboard
|
||||||
:id,
|
:id,
|
||||||
:email,
|
:email,
|
||||||
:current_sign_in_at,
|
:current_sign_in_at,
|
||||||
:created_at
|
:created_at,
|
||||||
|
:features
|
||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
# FORM_ATTRIBUTES
|
# FORM_ATTRIBUTES
|
||||||
|
|
|
@ -34,4 +34,12 @@ class GestionnaireMailer < ApplicationMailer
|
||||||
|
|
||||||
mail(to: recipient.email, subject: subject)
|
mail(to: recipient.email, subject: subject)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def send_login_token(gestionnaire, login_token)
|
||||||
|
@gestionnaire_id = gestionnaire.id
|
||||||
|
@login_token = login_token
|
||||||
|
subject = "Connexion sécurisée à demarches-simplifiees.fr"
|
||||||
|
|
||||||
|
mail(to: gestionnaire.email, subject: subject)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -19,11 +19,17 @@ class Administration < ApplicationRecord
|
||||||
if administrateur.save
|
if administrateur.save
|
||||||
AdministrationMailer.new_admin_email(administrateur, self).deliver_later
|
AdministrationMailer.new_admin_email(administrateur, self).deliver_later
|
||||||
administrateur.invite!(id)
|
administrateur.invite!(id)
|
||||||
|
|
||||||
User.create({
|
User.create({
|
||||||
email: email,
|
email: email,
|
||||||
password: password,
|
password: password,
|
||||||
confirmed_at: Time.zone.now
|
confirmed_at: Time.zone.now
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Gestionnaire.create({
|
||||||
|
email: email,
|
||||||
|
password: password
|
||||||
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
administrateur
|
administrateur
|
||||||
|
|
25
app/models/concerns/trusted_device_concern.rb
Normal file
25
app/models/concerns/trusted_device_concern.rb
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
module TrustedDeviceConcern
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
TRUSTED_DEVICE_COOKIE_NAME = :trusted_device
|
||||||
|
TRUSTED_DEVICE_PERIOD = 1.month
|
||||||
|
|
||||||
|
def trust_device
|
||||||
|
cookies.encrypted[TRUSTED_DEVICE_COOKIE_NAME] = {
|
||||||
|
value: JSON.generate({ created_at: Time.zone.now }),
|
||||||
|
expires: TRUSTED_DEVICE_PERIOD,
|
||||||
|
httponly: true
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def trusted_device?
|
||||||
|
trusted_device_cookie.present? &&
|
||||||
|
Time.zone.now - TRUSTED_DEVICE_PERIOD < JSON.parse(trusted_device_cookie)['created_at']
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def trusted_device_cookie
|
||||||
|
cookies.encrypted[TRUSTED_DEVICE_COOKIE_NAME]
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,6 +1,7 @@
|
||||||
class Gestionnaire < ApplicationRecord
|
class Gestionnaire < ApplicationRecord
|
||||||
include CredentialsSyncableConcern
|
include CredentialsSyncableConcern
|
||||||
include EmailSanitizableConcern
|
include EmailSanitizableConcern
|
||||||
|
include ActiveRecord::SecureToken
|
||||||
|
|
||||||
devise :database_authenticatable, :registerable, :async,
|
devise :database_authenticatable, :registerable, :async,
|
||||||
:recoverable, :rememberable, :trackable, :validatable
|
:recoverable, :rememberable, :trackable, :validatable
|
||||||
|
@ -144,6 +145,20 @@ class Gestionnaire < ApplicationRecord
|
||||||
Dossier.where(id: dossiers_id_with_notifications(dossiers)).group(:procedure_id).count
|
Dossier.where(id: dossiers_id_with_notifications(dossiers)).group(:procedure_id).count
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def login_token!
|
||||||
|
login_token = Gestionnaire.generate_unique_secure_token
|
||||||
|
encrypted_login_token = BCrypt::Password.create(login_token)
|
||||||
|
update(encrypted_login_token: encrypted_login_token, login_token_created_at: Time.zone.now)
|
||||||
|
login_token
|
||||||
|
end
|
||||||
|
|
||||||
|
def login_token_valid?(login_token)
|
||||||
|
BCrypt::Password.new(encrypted_login_token) == login_token
|
||||||
|
30.minutes.ago < login_token_created_at
|
||||||
|
rescue BCrypt::Errors::InvalidHash
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
def dossiers_id_with_notifications(dossiers)
|
def dossiers_id_with_notifications(dossiers)
|
||||||
dossiers = dossiers.followed_by(self)
|
dossiers = dossiers.followed_by(self)
|
||||||
|
|
||||||
|
@ -190,6 +205,23 @@ class Gestionnaire < ApplicationRecord
|
||||||
GestionnaireMailer.invite_gestionnaire(self, reset_password_token).deliver_later
|
GestionnaireMailer.invite_gestionnaire(self, reset_password_token).deliver_later
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def feature_enabled?(feature)
|
||||||
|
Flipflop.feature_set.feature(feature)
|
||||||
|
features[feature.to_s]
|
||||||
|
end
|
||||||
|
|
||||||
|
def disable_feature(feature)
|
||||||
|
Flipflop.feature_set.feature(feature)
|
||||||
|
features.delete(feature.to_s)
|
||||||
|
save
|
||||||
|
end
|
||||||
|
|
||||||
|
def enable_feature(feature)
|
||||||
|
Flipflop.feature_set.feature(feature)
|
||||||
|
features[feature.to_s] = true
|
||||||
|
save
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def annotations_hash(demande, annotations_privees, avis, messagerie)
|
def annotations_hash(demande, annotations_privees, avis, messagerie)
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
require Rails.root.join('lib', 'percentile')
|
||||||
|
|
||||||
class Procedure < ApplicationRecord
|
class Procedure < ApplicationRecord
|
||||||
MAX_DUREE_CONSERVATION = 36
|
MAX_DUREE_CONSERVATION = 36
|
||||||
|
|
||||||
|
@ -304,16 +306,16 @@ class Procedure < ApplicationRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def mean_traitement_time
|
def usual_traitement_time
|
||||||
mean_time(:en_construction_at, :processed_at)
|
percentile_time(:en_construction_at, :processed_at, 90)
|
||||||
end
|
end
|
||||||
|
|
||||||
def mean_verification_time
|
def usual_verification_time
|
||||||
mean_time(:en_construction_at, :en_instruction_at)
|
percentile_time(:en_construction_at, :en_instruction_at, 90)
|
||||||
end
|
end
|
||||||
|
|
||||||
def mean_instruction_time
|
def usual_instruction_time
|
||||||
mean_time(:en_instruction_at, :processed_at)
|
percentile_time(:en_instruction_at, :processed_at, 90)
|
||||||
end
|
end
|
||||||
|
|
||||||
PATH_AVAILABLE = :available
|
PATH_AVAILABLE = :available
|
||||||
|
@ -421,14 +423,14 @@ class Procedure < ApplicationRecord
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
def mean_time(start_attribute, end_attribute)
|
def percentile_time(start_attribute, end_attribute, p)
|
||||||
times = dossiers
|
times = dossiers
|
||||||
.state_termine
|
.state_termine
|
||||||
.pluck(start_attribute, end_attribute)
|
.pluck(start_attribute, end_attribute)
|
||||||
.map { |(start_date, end_date)| end_date - start_date }
|
.map { |(start_date, end_date)| end_date - start_date }
|
||||||
|
|
||||||
if times.present?
|
if times.present?
|
||||||
times.sum.fdiv(times.size).ceil
|
times.percentile(p).ceil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
|
:ruby
|
||||||
|
url = if field.resource.class.name == 'Gestionnaire'
|
||||||
|
enable_feature_manager_gestionnaire_path(field.resource.id)
|
||||||
|
else
|
||||||
|
enable_feature_manager_administrateur_path(field.resource.id)
|
||||||
|
end
|
||||||
|
|
||||||
%table#features
|
%table#features
|
||||||
- Flipflop.feature_set.features.each do |feature|
|
- Flipflop.feature_set.features.each do |feature|
|
||||||
- if !feature.group || feature.group.key != :production
|
- if !feature.group || feature.group.key != :production
|
||||||
%tr
|
%tr
|
||||||
%td= feature.title
|
%td= feature.title
|
||||||
%td
|
%td
|
||||||
= check_box_tag "enable-feature", "enable", field.data[feature.name], data: { url: enable_feature_manager_administrateur_path(field.resource.id), key: feature.key }
|
= check_box_tag "enable-feature", "enable", field.data[feature.name], data: { url: url, key: feature.key }
|
||||||
|
|
12
app/views/gestionnaire_mailer/send_login_token.html.haml
Normal file
12
app/views/gestionnaire_mailer/send_login_token.html.haml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
%p
|
||||||
|
Bonjour,
|
||||||
|
|
||||||
|
%p
|
||||||
|
Veuillez cliquer sur le lien suivant pour vous connecter sur le site demarches-simplifiees.fr :
|
||||||
|
= link_to(sign_in_by_link_url(@gestionnaire_id, jeton: @login_token), sign_in_by_link_url(@gestionnaire_id, jeton: @login_token))
|
||||||
|
|
||||||
|
%p
|
||||||
|
Bonne journée,
|
||||||
|
|
||||||
|
%p
|
||||||
|
L'équipe demarches-simplifiees.fr
|
|
@ -31,10 +31,10 @@
|
||||||
%strong votre dossier passera directement en instruction
|
%strong votre dossier passera directement en instruction
|
||||||
|
|
||||||
/ FIXME: remove the custom procedure switch at some point
|
/ FIXME: remove the custom procedure switch at some point
|
||||||
- if dossier.procedure.mean_verification_time && show_time_means
|
- if dossier.procedure.usual_verification_time && show_time_means
|
||||||
- cache(dossier.procedure, expires_in: 1.week) do
|
- cache(dossier.procedure, expires_in: 1.week) do
|
||||||
%p
|
%p
|
||||||
Le temps moyen de vérification pour cette démarche est de #{distance_of_time_in_words(dossier.procedure.mean_verification_time)}.
|
Habituellement, les dossiers de cette démarche sont vérifiés dans un délai de #{distance_of_time_in_words(dossier.procedure.usual_verification_time)}.
|
||||||
|
|
||||||
- elsif dossier.en_instruction?
|
- elsif dossier.en_instruction?
|
||||||
.en-instruction
|
.en-instruction
|
||||||
|
@ -46,10 +46,10 @@
|
||||||
avec le résultat.
|
avec le résultat.
|
||||||
|
|
||||||
/ FIXME: remove the custom procedure switch at some point
|
/ FIXME: remove the custom procedure switch at some point
|
||||||
- if dossier.procedure.mean_instruction_time && show_time_means
|
- if dossier.procedure.usual_instruction_time && show_time_means
|
||||||
- cache(dossier.procedure, expires_in: 1.week) do
|
- cache(dossier.procedure, expires_in: 1.week) do
|
||||||
%p
|
%p
|
||||||
Le temps moyen d’instruction pour cette démarche est de #{distance_of_time_in_words(dossier.procedure.mean_instruction_time)}.
|
Habituellement, les dossiers de cette démarche sont traités dans un délai de #{distance_of_time_in_words(dossier.procedure.usual_instruction_time)}.
|
||||||
|
|
||||||
- elsif dossier.accepte?
|
- elsif dossier.accepte?
|
||||||
.accepte
|
.accepte
|
||||||
|
|
|
@ -1,17 +1,31 @@
|
||||||
- content_for(:title, 'Renvoyer les instructions de confirmation de compte')
|
- content_for(:title, 'Confirmer votre adresse email')
|
||||||
|
|
||||||
- content_for :footer do
|
- content_for :footer do
|
||||||
= render partial: 'root/footer'
|
= render partial: 'root/footer'
|
||||||
|
|
||||||
.container.devise-container
|
.container.devise-container.devise-confirmations
|
||||||
.one-column-centered
|
.one-column-centered
|
||||||
= devise_error_messages!
|
= devise_error_messages!
|
||||||
|
|
||||||
|
%img.confirmation-icon{ src: image_url("user/confirmation-email.svg"), alt: "" }
|
||||||
|
|
||||||
|
%p.confirmation-preamble
|
||||||
|
= succeed '.' do
|
||||||
|
Avant d’effectuer votre démarche, nous avons besoin de vérifier votre adresse
|
||||||
|
- if resource.email.present?
|
||||||
|
%strong= resource.email
|
||||||
|
|
||||||
|
%p.confirmation-instructions
|
||||||
|
Ouvrez votre boîte email, et
|
||||||
|
%strong cliquez sur le lien d’activation
|
||||||
|
dans le message que vous avez reçu.
|
||||||
|
|
||||||
|
%hr.confirmation-separator
|
||||||
|
|
||||||
|
.confirmation-resend
|
||||||
|
%p Si vous n’avez pas reçu notre message, nous pouvons vous le renvoyer.
|
||||||
|
|
||||||
= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { class: 'form' }) do |f|
|
= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { class: 'form' }) do |f|
|
||||||
|
|
||||||
%h1 Renvoyer les instructions de confirmation de compte
|
|
||||||
|
|
||||||
= f.label :email, 'Email'
|
= f.label :email, 'Email'
|
||||||
= f.email_field :email, autofocus: true
|
= f.email_field :email, placeholder: 'Email', class: 'small', autofocus: true
|
||||||
|
= f.submit 'Renvoyer un email de confirmation', class: 'button'
|
||||||
= f.submit 'Renvoyer les instructions de confirmation', class: 'button primary'
|
|
||||||
|
|
14
app/views/users/sessions/link_sent.html.haml
Normal file
14
app/views/users/sessions/link_sent.html.haml
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
- content_for(:title, 'Lien de connexion par email')
|
||||||
|
|
||||||
|
- content_for :footer do
|
||||||
|
= render partial: 'root/footer'
|
||||||
|
|
||||||
|
#link-sent.container
|
||||||
|
= image_tag('user/confirmation-email.svg')
|
||||||
|
%h1 Encore une petite étape :)
|
||||||
|
|
||||||
|
%p.mail
|
||||||
|
Ouvrez votre boite email <b>#{@email}</b> puis cliquez sur le lien d'activation du message <b>Connexion sécurisée à demarches-simplifiees.fr</b>.
|
||||||
|
|
||||||
|
%p.help
|
||||||
|
En cas de difficultés, nous restons joignables sur #{link_to 'contact@demarches-simplifiees.fr', 'mailto:contact@demarches-simplifiees.fr'}.
|
|
@ -20,6 +20,7 @@ Flipflop.configure do
|
||||||
feature :web_hook
|
feature :web_hook
|
||||||
feature :publish_draft
|
feature :publish_draft
|
||||||
feature :support_form
|
feature :support_form
|
||||||
|
feature :enable_email_login_token
|
||||||
|
|
||||||
group :production do
|
group :production do
|
||||||
feature :remote_storage,
|
feature :remote_storage,
|
||||||
|
|
|
@ -26,6 +26,7 @@ Rails.application.routes.draw do
|
||||||
|
|
||||||
resources :gestionnaires, only: [:index, :show] do
|
resources :gestionnaires, only: [:index, :show] do
|
||||||
post 'reinvite', on: :member
|
post 'reinvite', on: :member
|
||||||
|
put 'enable_feature', on: :member
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :dossiers, only: [:show]
|
resources :dossiers, only: [:show]
|
||||||
|
@ -88,6 +89,8 @@ Rails.application.routes.draw do
|
||||||
devise_scope :user do
|
devise_scope :user do
|
||||||
get '/users/sign_in/demo' => redirect("/users/sign_in")
|
get '/users/sign_in/demo' => redirect("/users/sign_in")
|
||||||
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 'lien-envoye/:email' => 'users/sessions#link_sent', constraints: { email: /.*/ }, as: 'link_sent'
|
||||||
end
|
end
|
||||||
|
|
||||||
devise_scope :gestionnaire do
|
devise_scope :gestionnaire do
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
class AddEncryptedLoginTokenColumnToGestionnaire < ActiveRecord::Migration[5.2]
|
||||||
|
def change
|
||||||
|
add_column :gestionnaires, :encrypted_login_token, :text
|
||||||
|
add_column :gestionnaires, :login_token_created_at, :datetime
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,5 @@
|
||||||
|
class AddFeaturesColumnToGestionnaires < ActiveRecord::Migration[5.2]
|
||||||
|
def change
|
||||||
|
add_column :gestionnaires, :features, :jsonb, null: false, default: {}
|
||||||
|
end
|
||||||
|
end
|
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 2018_10_30_141238) do
|
ActiveRecord::Schema.define(version: 2018_11_08_151929) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
@ -365,6 +365,9 @@ ActiveRecord::Schema.define(version: 2018_10_30_141238) do
|
||||||
t.string "last_sign_in_ip"
|
t.string "last_sign_in_ip"
|
||||||
t.datetime "created_at"
|
t.datetime "created_at"
|
||||||
t.datetime "updated_at"
|
t.datetime "updated_at"
|
||||||
|
t.text "encrypted_login_token"
|
||||||
|
t.datetime "login_token_created_at"
|
||||||
|
t.jsonb "features", default: {}, null: false
|
||||||
t.index ["email"], name: "index_gestionnaires_on_email", unique: true
|
t.index ["email"], name: "index_gestionnaires_on_email", unique: true
|
||||||
t.index ["reset_password_token"], name: "index_gestionnaires_on_reset_password_token", unique: true
|
t.index ["reset_password_token"], name: "index_gestionnaires_on_reset_password_token", unique: true
|
||||||
end
|
end
|
||||||
|
|
31
lib/percentile.rb
Normal file
31
lib/percentile.rb
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
# Adapted from https://github.com/thirtysixthspan/descriptive_statistics
|
||||||
|
|
||||||
|
# Copyright (c) 2010-2014 Derrick Parkhurst (derrick.parkhurst@gmail.com)
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be included in
|
||||||
|
# all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
class Array
|
||||||
|
def percentile(p)
|
||||||
|
values = self.sort
|
||||||
|
|
||||||
|
if values.empty?
|
||||||
|
return []
|
||||||
|
elsif values.size == 1
|
||||||
|
return values.first
|
||||||
|
elsif p == 100
|
||||||
|
return values.last
|
||||||
|
end
|
||||||
|
|
||||||
|
rank = p / 100.0 * (values.size - 1)
|
||||||
|
lower, upper = values[rank.floor, 2]
|
||||||
|
lower + (upper - lower) * (rank - rank.floor)
|
||||||
|
end
|
||||||
|
end
|
15
lib/tasks/2018_10_30_admin_has_gestionnaire.rake
Normal file
15
lib/tasks/2018_10_30_admin_has_gestionnaire.rake
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
namespace :'2018_10_30_admin_has_gestionnaire' do
|
||||||
|
task run: :environment do
|
||||||
|
admin_without_gestionnaire_ids = Administrateur
|
||||||
|
.find_by_sql('SELECT administrateurs.id FROM administrateurs LEFT OUTER JOIN gestionnaires ON gestionnaires.email = administrateurs.email WHERE gestionnaires.email IS NULL')
|
||||||
|
.pluck(:id)
|
||||||
|
|
||||||
|
admin_without_gestionnaire_ids.each do |admin_id|
|
||||||
|
admin = Administrateur.find(admin_id)
|
||||||
|
g = Gestionnaire.new
|
||||||
|
g.email = admin.email
|
||||||
|
g.encrypted_password = admin.encrypted_password
|
||||||
|
g.save(validate: false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,8 @@
|
||||||
|
namespace :'activate_trusted_device_for_a-f' do
|
||||||
|
task run: :environment do
|
||||||
|
letters_a_to_f = ('a'..'f').to_a
|
||||||
|
Gestionnaire
|
||||||
|
.where("substr(email, 1, 1) IN (?)", letters_a_to_f)
|
||||||
|
.update_all(features: { "enable_email_login_token" => true })
|
||||||
|
end
|
||||||
|
end
|
20
spec/controllers/administrateur/activate_controller_spec.rb
Normal file
20
spec/controllers/administrateur/activate_controller_spec.rb
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
describe Administrateurs::ActivateController, type: :controller do
|
||||||
|
describe '#new' do
|
||||||
|
let(:admin) { create(:administrateur) }
|
||||||
|
let(:token) { admin.send(:set_reset_password_token) }
|
||||||
|
|
||||||
|
before { allow(controller).to receive(:trust_device) }
|
||||||
|
|
||||||
|
context 'when the token is ok' do
|
||||||
|
before { get :new, params: { token: token } }
|
||||||
|
|
||||||
|
it { expect(controller).to have_received(:trust_device) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the token is bad' do
|
||||||
|
before { get :new, params: { token: 'bad' } }
|
||||||
|
|
||||||
|
it { expect(controller).not_to have_received(:trust_device) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
20
spec/controllers/gestionnaires/activate_controller_spec.rb
Normal file
20
spec/controllers/gestionnaires/activate_controller_spec.rb
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
describe Gestionnaires::ActivateController, type: :controller do
|
||||||
|
describe '#new' do
|
||||||
|
let(:gestionnaire) { create(:gestionnaire) }
|
||||||
|
let(:token) { gestionnaire.send(:set_reset_password_token) }
|
||||||
|
|
||||||
|
before { allow(controller).to receive(:trust_device) }
|
||||||
|
|
||||||
|
context 'when the token is ok' do
|
||||||
|
before { get :new, params: { token: token } }
|
||||||
|
|
||||||
|
it { expect(controller).to have_received(:trust_device) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the token is bad' do
|
||||||
|
before { get :new, params: { token: 'bad' } }
|
||||||
|
|
||||||
|
it { expect(controller).not_to have_received(:trust_device) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -74,8 +74,7 @@ describe Users::RegistrationsController, type: :controller do
|
||||||
subject
|
subject
|
||||||
end
|
end
|
||||||
|
|
||||||
it { expect(response).to redirect_to(root_path) }
|
it { expect(response).to redirect_to(new_user_confirmation_path(user: { email: user[:email] })) }
|
||||||
it { expect(flash.notice).to eq(I18n.t('devise.registrations.signed_up_but_unconfirmed')) }
|
|
||||||
it { expect(UserMailer).not_to have_received(:new_account_warning) }
|
it { expect(UserMailer).not_to have_received(:new_account_warning) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -27,7 +27,7 @@ describe Users::SessionsController, type: :controller do
|
||||||
let(:password) { 'un super mot de passe' }
|
let(:password) { 'un super mot de passe' }
|
||||||
|
|
||||||
let(:user) { create(:user, email: email, password: password) }
|
let(:user) { create(:user, email: email, password: password) }
|
||||||
let(:gestionnaire) { create(:gestionnaire, email: email, password: password) }
|
let(:gestionnaire) { create(:gestionnaire, :with_trusted_device, email: email, password: password) }
|
||||||
let(:administrateur) { create(:administrateur, email: email, password: password) }
|
let(:administrateur) { create(:administrateur, email: email, password: password) }
|
||||||
|
|
||||||
it 'signs user in' do
|
it 'signs user in' do
|
||||||
|
@ -41,18 +41,39 @@ describe Users::SessionsController, type: :controller do
|
||||||
|
|
||||||
it 'signs gestionnaire in' do
|
it 'signs gestionnaire in' do
|
||||||
post :create, params: { user: { email: gestionnaire.email, password: gestionnaire.password } }
|
post :create, params: { user: { email: gestionnaire.email, password: gestionnaire.password } }
|
||||||
expect(@response.redirect?).to be(true)
|
|
||||||
|
expect(subject).to redirect_to link_sent_path(email: gestionnaire.email)
|
||||||
|
expect(subject.current_user).to be(nil)
|
||||||
|
expect(subject.current_gestionnaire).to be(nil)
|
||||||
|
expect(subject.current_administrateur).to be(nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the device is trusted' do
|
||||||
|
before do
|
||||||
|
allow(controller).to receive(:trusted_device?).and_return(true)
|
||||||
|
post :create, params: { user: { email: gestionnaire.email, password: gestionnaire.password } }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'directly log the gestionnaire' do
|
||||||
|
expect(subject).to redirect_to gestionnaire_procedures_path
|
||||||
expect(subject.current_user).to be(nil)
|
expect(subject.current_user).to be(nil)
|
||||||
expect(subject.current_gestionnaire).to eq(gestionnaire)
|
expect(subject.current_gestionnaire).to eq(gestionnaire)
|
||||||
expect(subject.current_administrateur).to be(nil)
|
expect(subject.current_administrateur).to be(nil)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'signs administrateur in' do
|
||||||
|
# an admin has always an gestionnaire role
|
||||||
|
before { gestionnaire }
|
||||||
|
|
||||||
it 'signs administrateur in' do
|
it 'signs administrateur in' do
|
||||||
post :create, params: { user: { email: administrateur.email, password: administrateur.password } }
|
post :create, params: { user: { email: administrateur.email, password: administrateur.password } }
|
||||||
expect(@response.redirect?).to be(true)
|
|
||||||
|
expect(subject).to redirect_to link_sent_path(email: gestionnaire.email)
|
||||||
expect(subject.current_user).to be(nil)
|
expect(subject.current_user).to be(nil)
|
||||||
expect(subject.current_gestionnaire).to be(nil)
|
expect(subject.current_gestionnaire).to be(nil)
|
||||||
expect(subject.current_administrateur).to eq(administrateur)
|
expect(subject.current_administrateur).to eq(nil)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context {
|
context {
|
||||||
|
@ -63,10 +84,16 @@ describe Users::SessionsController, type: :controller do
|
||||||
|
|
||||||
it 'signs user + gestionnaire + administrateur in' do
|
it 'signs user + gestionnaire + administrateur in' do
|
||||||
post :create, params: { user: { email: administrateur.email, password: administrateur.password } }
|
post :create, params: { user: { email: administrateur.email, password: administrateur.password } }
|
||||||
expect(@response.redirect?).to be(true)
|
|
||||||
expect(subject.current_user).to eq(user)
|
expect(subject).to redirect_to link_sent_path(email: gestionnaire.email)
|
||||||
expect(subject.current_gestionnaire).to eq(gestionnaire)
|
|
||||||
expect(subject.current_administrateur).to eq(administrateur)
|
# TODO: fix me
|
||||||
|
# Strange behaviour: sign_out(:user) does not work in spec
|
||||||
|
# but seems to work in live
|
||||||
|
# expect(controller.current_user).to be(nil)
|
||||||
|
|
||||||
|
expect(subject.current_gestionnaire).to be(nil)
|
||||||
|
expect(subject.current_administrateur).to be(nil)
|
||||||
expect(user.reload.loged_in_with_france_connect).to be(nil)
|
expect(user.reload.loged_in_with_france_connect).to be(nil)
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
|
@ -219,4 +246,75 @@ describe Users::SessionsController, type: :controller do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#sign_in_by_link' do
|
||||||
|
context 'when the gestionnaire has non other account' do
|
||||||
|
let(:gestionnaire) { create(:gestionnaire) }
|
||||||
|
before do
|
||||||
|
allow(controller).to receive(:trust_device)
|
||||||
|
post :sign_in_by_link, params: { id: gestionnaire.id, login_token: login_token }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the token is valid' do
|
||||||
|
let(:login_token) { gestionnaire.login_token! }
|
||||||
|
|
||||||
|
it { is_expected.to redirect_to gestionnaire_procedures_path }
|
||||||
|
it { expect(controller.current_gestionnaire).to eq(gestionnaire) }
|
||||||
|
it { expect(controller).to have_received(:trust_device) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the token is invalid' do
|
||||||
|
let(:login_token) { 'invalid_token' }
|
||||||
|
|
||||||
|
it { is_expected.to redirect_to new_user_session_path }
|
||||||
|
it { expect(controller.current_gestionnaire).to be_nil }
|
||||||
|
it { expect(controller).not_to have_received(:trust_device) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the gestionnaire has an user and admin account' do
|
||||||
|
let(:email) { 'unique@plop.com' }
|
||||||
|
let(:password) { 'un super mot de passe' }
|
||||||
|
|
||||||
|
let!(:user) { create(:user, email: email, password: password) }
|
||||||
|
let!(:gestionnaire) { create(:gestionnaire, email: email, password: password) }
|
||||||
|
let!(:administrateur) { create(:administrateur, email: email, password: password) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
post :sign_in_by_link, params: { id: gestionnaire.id, login_token: login_token }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the token is valid' do
|
||||||
|
let(:login_token) { gestionnaire.login_token! }
|
||||||
|
|
||||||
|
it { expect(controller.current_gestionnaire).to eq(gestionnaire) }
|
||||||
|
it { expect(controller.current_administrateur).to eq(administrateur) }
|
||||||
|
it { expect(controller.current_user).to eq(user) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#trust_device and #trusted_device?' do
|
||||||
|
subject { controller.trusted_device? }
|
||||||
|
|
||||||
|
context 'when the trusted cookie is not present' do
|
||||||
|
it { is_expected.to be false }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the cookie is outdated' do
|
||||||
|
before do
|
||||||
|
Timecop.freeze(Time.zone.now - TrustedDeviceConcern::TRUSTED_DEVICE_PERIOD - 1.minute)
|
||||||
|
controller.trust_device
|
||||||
|
Timecop.return
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to be false }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the cookie is ok' do
|
||||||
|
before { controller.trust_device }
|
||||||
|
|
||||||
|
it { is_expected.to be true }
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,4 +4,10 @@ FactoryBot.define do
|
||||||
email { generate(:gestionnaire_email) }
|
email { generate(:gestionnaire_email) }
|
||||||
password { 'password' }
|
password { 'password' }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
trait :with_trusted_device do
|
||||||
|
after(:create) do |gestionnaire|
|
||||||
|
gestionnaire.update(features: { "enable_email_login_token" => true })
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,20 +1,26 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
feature 'Administrator connection' do
|
feature 'Administrator connection' do
|
||||||
let(:admin) { create(:administrateur) }
|
include ActiveJob::TestHelper
|
||||||
|
|
||||||
|
let(:email) { 'admin1@admin.com' }
|
||||||
|
let(:password) { 'mon chien aime les bananes' }
|
||||||
|
let!(:admin) { create(:administrateur, email: email, password: password) }
|
||||||
|
let!(:gestionnaire) { create(:gestionnaire, :with_trusted_device, email: email, password: password) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
visit new_administrateur_session_path
|
visit new_administrateur_session_path
|
||||||
end
|
end
|
||||||
|
|
||||||
scenario 'administrator is on sign in page' do
|
scenario 'administrator is on sign in page' do
|
||||||
expect(page).to have_css('#new_user')
|
expect(page).to have_css('#new_user')
|
||||||
end
|
end
|
||||||
|
|
||||||
context "admin fills form and log in" do
|
context "admin fills form and log in" do
|
||||||
before do
|
before do
|
||||||
page.find_by_id('user_email').set admin.email
|
sign_in_with(email, password, true)
|
||||||
page.find_by_id('user_password').set admin.password
|
|
||||||
page.click_on 'Se connecter'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
scenario 'a menu button is available' do
|
scenario 'a menu button is available' do
|
||||||
expect(page).to have_css('#admin_menu')
|
expect(page).to have_css('#admin_menu')
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,7 +4,7 @@ feature 'The gestionnaire part' do
|
||||||
include ActiveJob::TestHelper
|
include ActiveJob::TestHelper
|
||||||
|
|
||||||
let(:password) { 'secret_password' }
|
let(:password) { 'secret_password' }
|
||||||
let!(:gestionnaire) { create(:gestionnaire, password: password) }
|
let!(:gestionnaire) { create(:gestionnaire, :with_trusted_device, password: password) }
|
||||||
|
|
||||||
let!(:procedure) { create(:procedure, :published, gestionnaires: [gestionnaire]) }
|
let!(:procedure) { create(:procedure, :published, gestionnaires: [gestionnaire]) }
|
||||||
let!(:dossier) { create(:dossier, state: Dossier.states.fetch(:en_construction), procedure: procedure) }
|
let!(:dossier) { create(:dossier, state: Dossier.states.fetch(:en_construction), procedure: procedure) }
|
||||||
|
@ -116,7 +116,7 @@ feature 'The gestionnaire part' do
|
||||||
|
|
||||||
log_out
|
log_out
|
||||||
|
|
||||||
log_in(gestionnaire.email, password)
|
log_in(gestionnaire.email, password, check_email: false)
|
||||||
|
|
||||||
click_on procedure.libelle
|
click_on procedure.libelle
|
||||||
click_on dossier.user.email
|
click_on dossier.user.email
|
||||||
|
@ -173,14 +173,13 @@ feature 'The gestionnaire part' do
|
||||||
expect(page).to have_text("Dossier envoyé")
|
expect(page).to have_text("Dossier envoyé")
|
||||||
end
|
end
|
||||||
|
|
||||||
def log_in(email, password)
|
def log_in(email, password, check_email: true)
|
||||||
visit '/'
|
visit '/'
|
||||||
click_on 'Connexion'
|
click_on 'Connexion'
|
||||||
expect(page).to have_current_path(new_user_session_path)
|
expect(page).to have_current_path(new_user_session_path)
|
||||||
|
|
||||||
fill_in 'user_email', with: email
|
sign_in_with(email, password, check_email)
|
||||||
fill_in 'user_password', with: password
|
|
||||||
click_on 'Se connecter'
|
|
||||||
expect(page).to have_current_path(gestionnaire_procedures_path)
|
expect(page).to have_current_path(gestionnaire_procedures_path)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -196,7 +195,7 @@ feature 'The gestionnaire part' do
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_mail(to, content)
|
def test_mail(to, content)
|
||||||
mail = ActionMailer::Base.deliveries.first
|
mail = ActionMailer::Base.deliveries.last
|
||||||
expect(mail.to).to match([to])
|
expect(mail.to).to match([to])
|
||||||
expect(mail.body.parts.map(&:to_s)).to all(include(content))
|
expect(mail.body.parts.map(&:to_s)).to all(include(content))
|
||||||
end
|
end
|
||||||
|
|
|
@ -23,7 +23,7 @@ describe 'Dossier details:' do
|
||||||
visit dossier_path(dossier)
|
visit dossier_path(dossier)
|
||||||
end
|
end
|
||||||
|
|
||||||
it { expect(page).to have_text("Le temps moyen de vérification pour cette démarche est de 10 jours.") }
|
it { expect(page).to have_text("Habituellement, les dossiers de cette démarche sont vérifiés dans un délai de 10 jours.") }
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when the dossier is in instruction" do
|
context "when the dossier is in instruction" do
|
||||||
|
@ -34,7 +34,7 @@ describe 'Dossier details:' do
|
||||||
visit dossier_path(dossier)
|
visit dossier_path(dossier)
|
||||||
end
|
end
|
||||||
|
|
||||||
it { expect(page).to have_text("Le temps moyen d’instruction pour cette démarche est de 2 mois.") }
|
it { expect(page).to have_text("Habituellement, les dossiers de cette démarche sont traités dans un délai de 2 mois.") }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,7 @@ feature 'Invitations' do
|
||||||
|
|
||||||
# Create the account
|
# Create the account
|
||||||
sign_up_with invite.email, user_password
|
sign_up_with invite.email, user_password
|
||||||
expect(page).to have_content("lien d'activation")
|
expect(page).to have_content('lien d’activation')
|
||||||
|
|
||||||
# Confirm the account
|
# Confirm the account
|
||||||
# (The user should be redirected to the dossier they was invited on)
|
# (The user should be redirected to the dossier they was invited on)
|
||||||
|
|
|
@ -1,15 +1,18 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
feature 'Signin up:' do
|
feature 'Signin up:' do
|
||||||
|
let(:user_email) { generate :user_email }
|
||||||
|
let(:user_password) { 'testpassword' }
|
||||||
|
|
||||||
scenario 'a new user can sign-up' do
|
scenario 'a new user can sign-up' do
|
||||||
visit root_path
|
visit root_path
|
||||||
click_on 'Connexion'
|
click_on 'Connexion'
|
||||||
click_on 'Créer un compte'
|
click_on 'Créer un compte'
|
||||||
|
|
||||||
sign_up_with 'testuser@exemple.fr'
|
sign_up_with user_email, user_password
|
||||||
expect(page).to have_content "Nous vous avons envoyé un email contenant un lien d'activation"
|
expect(page).to have_content "nous avons besoin de vérifier votre adresse #{user_email}"
|
||||||
|
|
||||||
click_confirmation_link_for 'testuser@exemple.fr'
|
click_confirmation_link_for user_email
|
||||||
expect(page).to have_content 'Votre compte a été activé'
|
expect(page).to have_content 'Votre compte a été activé'
|
||||||
expect(page).to have_current_path dossiers_path
|
expect(page).to have_current_path dossiers_path
|
||||||
end
|
end
|
||||||
|
@ -25,12 +28,37 @@ feature 'Signin up:' do
|
||||||
expect(page).to have_current_path new_user_session_path
|
expect(page).to have_current_path new_user_session_path
|
||||||
click_on 'Créer un compte'
|
click_on 'Créer un compte'
|
||||||
|
|
||||||
sign_up_with 'testuser@exemple.fr'
|
sign_up_with user_email, user_password
|
||||||
expect(page).to have_content "Nous vous avons envoyé un email contenant un lien d'activation"
|
expect(page).to have_content "nous avons besoin de vérifier votre adresse #{user_email}"
|
||||||
|
|
||||||
click_confirmation_link_for 'testuser@exemple.fr'
|
click_confirmation_link_for user_email
|
||||||
expect(page).to have_content 'Votre compte a été activé'
|
expect(page).to have_content 'Votre compte a été activé'
|
||||||
expect(page).to have_content procedure.libelle
|
expect(page).to have_content procedure.libelle
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when a user is not confirmed yet' do
|
||||||
|
before do
|
||||||
|
visit root_path
|
||||||
|
click_on 'Connexion'
|
||||||
|
click_on 'Créer un compte'
|
||||||
|
|
||||||
|
sign_up_with user_email, user_password
|
||||||
|
end
|
||||||
|
|
||||||
|
# Ideally, when signing-in with an unconfirmed account,
|
||||||
|
# the user would be redirected to the "resend email confirmation" page.
|
||||||
|
#
|
||||||
|
# However the check for unconfirmed accounts is made by Warden every time a page is loaded –
|
||||||
|
# and much earlier than SessionsController#create.
|
||||||
|
#
|
||||||
|
# For now only test the default behavior (an error message is displayed).
|
||||||
|
scenario 'they get an error message' do
|
||||||
|
visit root_path
|
||||||
|
click_on 'Connexion'
|
||||||
|
|
||||||
|
sign_in_with user_email, user_password
|
||||||
|
expect(page).to have_content 'Vous devez confirmer votre adresse email pour continuer'
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,4 +7,8 @@ class GestionnaireMailerPreview < ActionMailer::Preview
|
||||||
def send_dossier
|
def send_dossier
|
||||||
GestionnaireMailer.send_dossier(Gestionnaire.first, Dossier.first, Gestionnaire.last)
|
GestionnaireMailer.send_dossier(Gestionnaire.first, Dossier.first, Gestionnaire.last)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def send_login_token
|
||||||
|
GestionnaireMailer.send_login_token(Gestionnaire.first, "token")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -20,6 +20,12 @@ describe Administration, type: :model do
|
||||||
expect(user).to be_present
|
expect(user).to be_present
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'creates a corresponding gestionnaire account for the email' do
|
||||||
|
subject
|
||||||
|
gestionnaire = Gestionnaire.find_by(email: valid_email)
|
||||||
|
expect(gestionnaire).to be_present
|
||||||
|
end
|
||||||
|
|
||||||
context 'when there already is a user account with the same email' do
|
context 'when there already is a user account with the same email' do
|
||||||
before { create(:user, email: valid_email) }
|
before { create(:user, email: valid_email) }
|
||||||
it 'still creates an admin account' do
|
it 'still creates an admin account' do
|
||||||
|
|
|
@ -705,22 +705,34 @@ describe Procedure do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#mean_instruction_time' do
|
describe '#usual_instruction_time' do
|
||||||
let(:procedure) { create(:procedure) }
|
let(:procedure) { create(:procedure) }
|
||||||
|
|
||||||
context 'when there is only one dossier' do
|
|
||||||
let(:dossier) { create(:dossier, procedure: procedure) }
|
|
||||||
|
|
||||||
context 'which is termine' do
|
|
||||||
before do
|
before do
|
||||||
dossier.accepte!
|
processed_delays.each do |delay|
|
||||||
processed_date = Time.zone.parse('12/12/2012')
|
dossier = create :dossier, :accepte, procedure: procedure
|
||||||
instruction_date = processed_date - 1.day
|
instruction_date = 1.month.ago
|
||||||
dossier.update(en_instruction_at: instruction_date, processed_at: processed_date)
|
processed_date = instruction_date + delay
|
||||||
|
dossier.update!(en_instruction_at: instruction_date, processed_at: processed_date)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it { expect(procedure.mean_instruction_time).to eq(1.day.to_i) }
|
context 'when there are several processed dossiers' do
|
||||||
end
|
let(:processed_delays) { [1.day, 2.days, 2.days, 2.days, 2.days, 3.days, 3.days, 3.days, 3.days, 12.days] }
|
||||||
|
|
||||||
|
it 'returns a time representative of the dossier instruction delay' do
|
||||||
|
expect(procedure.usual_instruction_time).to be_between(3.days, 4.days)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when there is only one processed dossier' do
|
||||||
|
let(:processed_delays) { [1.day] }
|
||||||
|
it { expect(procedure.usual_instruction_time).to eq(1.day) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'where there is no processed dossier' do
|
||||||
|
let(:processed_delays) { [] }
|
||||||
|
it { expect(procedure.usual_instruction_time).to be_nil }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -17,12 +17,23 @@ module FeatureHelpers
|
||||||
dossier
|
dossier
|
||||||
end
|
end
|
||||||
|
|
||||||
def sign_in_with(email, password)
|
def sign_in_with(email, password, sign_in_by_link = false)
|
||||||
fill_in :user_email, with: email
|
fill_in :user_email, with: email
|
||||||
fill_in :user_password, with: password
|
fill_in :user_password, with: password
|
||||||
|
|
||||||
|
perform_enqueued_jobs do
|
||||||
click_on 'Se connecter'
|
click_on 'Se connecter'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if sign_in_by_link
|
||||||
|
mail = ActionMailer::Base.deliveries.last
|
||||||
|
message = mail.body.parts.join(&:to_s)
|
||||||
|
login_token = message[/connexion-par-jeton\/(.*)/, 1]
|
||||||
|
|
||||||
|
visit sign_in_by_link_path(login_token)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def sign_up_with(email, password = 'testpassword')
|
def sign_up_with(email, password = 'testpassword')
|
||||||
fill_in :user_email, with: email
|
fill_in :user_email, with: email
|
||||||
fill_in :user_password, with: password
|
fill_in :user_password, with: password
|
||||||
|
|
Loading…
Reference in a new issue