Merge pull request #6466 from betagouv/main

2021-09-14-01
This commit is contained in:
LeSim 2021-09-14 16:27:40 +02:00 committed by GitHub
commit f44f0a1649
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
72 changed files with 731 additions and 407 deletions

View file

@ -76,12 +76,11 @@ gem 'sentry-rails'
gem 'sentry-ruby' gem 'sentry-ruby'
gem 'sib-api-v3-sdk' gem 'sib-api-v3-sdk'
gem 'skylight' gem 'skylight'
gem 'smart_listing'
gem 'spreadsheet_architect' gem 'spreadsheet_architect'
gem 'typhoeus' gem 'typhoeus'
gem 'warden' gem 'warden'
gem 'webpacker' gem 'webpacker'
gem 'zipline' gem 'zipline', github: 'fringd/zipline', ref: 'd637bbff2' # Unreleased 1.3.0, with a fix for Ruby 3.0 kwargs
gem 'zxcvbn-ruby', require: 'zxcvbn' gem 'zxcvbn-ruby', require: 'zxcvbn'
group :test do group :test do

View file

@ -1,3 +1,12 @@
GIT
remote: https://github.com/fringd/zipline.git
revision: d637bbff262f59718d23a65f50b50163b8ba749f
ref: d637bbff2
specs:
zipline (1.3.0)
actionpack (>= 3.2.1, < 7.0)
zip_tricks (>= 4.2.1, < 6.0)
GIT GIT
remote: https://github.com/mina-deploy/mina.git remote: https://github.com/mina-deploy/mina.git
revision: 84fa84c7f7f94f9518ef9b7099396ab6676b5881 revision: 84fa84c7f7f94f9518ef9b7099396ab6676b5881
@ -171,13 +180,6 @@ GEM
coderay (1.1.3) coderay (1.1.3)
coercible (1.0.0) coercible (1.0.0)
descendants_tracker (~> 0.0.1) descendants_tracker (~> 0.0.1)
coffee-rails (5.0.0)
coffee-script (>= 2.2.0)
railties (>= 5.2.0)
coffee-script (2.4.1)
coffee-script-source
execjs
coffee-script-source (1.12.2)
concurrent-ruby (1.1.9) concurrent-ruby (1.1.9)
connection_pool (2.2.3) connection_pool (2.2.3)
crack (0.4.5) crack (0.4.5)
@ -690,11 +692,6 @@ GEM
tilt (~> 2.0) tilt (~> 2.0)
skylight (5.0.1) skylight (5.0.1)
activesupport (>= 5.2.0) activesupport (>= 5.2.0)
smart_listing (1.2.3)
coffee-rails
jquery-rails
kaminari (>= 0.17)
rails (>= 3.2)
spreadsheet_architect (4.1.0) spreadsheet_architect (4.1.0)
axlsx_styler (>= 1.0.0, < 2) axlsx_styler (>= 1.0.0, < 2)
caxlsx (>= 2.0.2, < 4) caxlsx (>= 2.0.2, < 4)
@ -774,10 +771,7 @@ GEM
xpath (3.2.0) xpath (3.2.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
zeitwerk (2.4.2) zeitwerk (2.4.2)
zip_tricks (5.5.0) zip_tricks (5.6.0)
zipline (1.3.0)
actionpack (>= 3.2.1, < 7.0)
zip_tricks (>= 4.2.1, < 6.0)
zxcvbn-ruby (1.2.0) zxcvbn-ruby (1.2.0)
PLATFORMS PLATFORMS
@ -885,7 +879,6 @@ DEPENDENCIES
sib-api-v3-sdk sib-api-v3-sdk
simple_xlsx_reader simple_xlsx_reader
skylight skylight
smart_listing
spreadsheet_architect spreadsheet_architect
spring spring
spring-commands-rspec spring-commands-rspec
@ -897,7 +890,7 @@ DEPENDENCIES
webdrivers (~> 4.0) webdrivers (~> 4.0)
webmock webmock
webpacker webpacker
zipline zipline!
zxcvbn-ruby zxcvbn-ruby
BUNDLED WITH BUNDLED WITH

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

@ -1,40 +0,0 @@
@import "colors";
@import "constants";
$strength-bg: #EEEEEE;
$strength-color-0: $lighter-red;
$strength-color-1: #FF5000;
$strength-color-2: $orange;
$strength-color-3: #FFD000;
$strength-color-4: $green;
.password-strength {
margin-top: -24px;
width: 100%;
height: 12px;
background: $strength-bg;
display: block;
margin-bottom: $default-spacer;
text-align: center;
border-radius: 8px;
&.strength-0 {
background: linear-gradient(to right, $strength-color-0 00%, $strength-bg 20%);
}
&.strength-1 {
background: linear-gradient(to right, $strength-color-1 20%, $strength-bg 40%);
}
&.strength-2 {
background: linear-gradient(to right, $strength-color-2 40%, $strength-bg 60%);
}
&.strength-3 {
background: linear-gradient(to right, $strength-color-3 60%, $strength-bg 80%);
}
&.strength-4 {
background: $strength-color-4;
}
}

View file

@ -1,14 +0,0 @@
class Administrateurs::PasswordsController < ApplicationController
def test_strength
@score, @words, @length = ZxcvbnService.new(password_params[:password]).complexity
@min_length = PASSWORD_MIN_LENGTH
@min_complexity = PASSWORD_COMPLEXITY_FOR_ADMIN
render 'shared/password/test_strength'
end
private
def password_params
params.require(:administrateur).permit(:password)
end
end

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

@ -16,7 +16,7 @@ module Manager
if super_admin_signed_in? && current_super_admin.otp_required_for_login? if super_admin_signed_in? && current_super_admin.otp_required_for_login?
super super
elsif super_admin_signed_in? elsif super_admin_signed_in?
redirect_to edit_super_admin_otp_path SUPER_ADMIN_OTP_ENABLED ? (redirect_to edit_super_admin_otp_path) : super
else else
redirect_to new_super_admin_session_path redirect_to new_super_admin_session_path
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

@ -1,19 +1,8 @@
class SuperAdmins::PasswordsController < Devise::PasswordsController class SuperAdmins::PasswordsController < Devise::PasswordsController
include DevisePopulatedResource
def update def update
super super
self.resource.disable_otp! self.resource.disable_otp!
end end
def test_strength
@score, @words, @length = ZxcvbnService.new(password_params[:password]).complexity
@min_length = PASSWORD_MIN_LENGTH
@min_complexity = PASSWORD_COMPLEXITY_FOR_ADMIN
render 'shared/password/test_strength'
end
private
def password_params
params.require(:super_admin).permit(:password)
end
end end

View file

@ -4,7 +4,7 @@ module Users
layout 'procedure_context', only: [:identite, :update_identite, :siret, :update_siret] layout 'procedure_context', only: [:identite, :update_identite, :siret, :update_siret]
ACTIONS_ALLOWED_TO_ANY_USER = [:index, :recherche, :new] ACTIONS_ALLOWED_TO_ANY_USER = [:index, :recherche, :new, :transferer_all]
ACTIONS_ALLOWED_TO_OWNER_OR_INVITE = [:show, :demande, :messagerie, :brouillon, :update_brouillon, :modifier, :update, :create_commentaire] ACTIONS_ALLOWED_TO_OWNER_OR_INVITE = [:show, :demande, :messagerie, :brouillon, :update_brouillon, :modifier, :update, :create_commentaire]
before_action :ensure_ownership!, except: ACTIONS_ALLOWED_TO_ANY_USER + ACTIONS_ALLOWED_TO_OWNER_OR_INVITE before_action :ensure_ownership!, except: ACTIONS_ALLOWED_TO_ANY_USER + ACTIONS_ALLOWED_TO_OWNER_OR_INVITE
@ -19,7 +19,12 @@ module Users
@user_dossiers = current_user.dossiers.includes(:procedure).order_by_updated_at.page(page) @user_dossiers = current_user.dossiers.includes(:procedure).order_by_updated_at.page(page)
@dossiers_invites = current_user.dossiers_invites.includes(:procedure).order_by_updated_at.page(page) @dossiers_invites = current_user.dossiers_invites.includes(:procedure).order_by_updated_at.page(page)
@dossiers_supprimes = current_user.deleted_dossiers.order_by_updated_at.page(page) @dossiers_supprimes = current_user.deleted_dossiers.order_by_updated_at.page(page)
@statut = statut(@user_dossiers, @dossiers_invites, @dossiers_supprimes, params[:statut]) @dossier_transfers = DossierTransfer
.includes(dossiers: :user)
.with_dossiers
.where(email: current_user.email)
.page(page)
@statut = statut(@user_dossiers, @dossiers_invites, @dossiers_supprimes, @dossier_transfers, params[:statut])
end end
def show def show
@ -273,16 +278,25 @@ module Users
@dossier || (dossier_id.present? && Dossier.find_by(id: dossier_id.to_i)) @dossier || (dossier_id.present? && Dossier.find_by(id: dossier_id.to_i))
end end
def transferer
@transfer = DossierTransfer.new(dossiers: [dossier])
end
def transferer_all
@transfer = DossierTransfer.new(dossiers: current_user.dossiers)
end
private private
# if the status tab is filled, then this tab # if the status tab is filled, then this tab
# else first filled tab # else first filled tab
# else mes-dossiers # else mes-dossiers
def statut(mes_dossiers, dossiers_invites, dossiers_supprimes, params_statut) def statut(mes_dossiers, dossiers_invites, dossiers_supprimes, dossier_transfers, params_statut)
tabs = { tabs = {
'mes-dossiers' => mes_dossiers.present?, 'mes-dossiers' => mes_dossiers.present?,
'dossiers-invites' => dossiers_invites.present?, 'dossiers-invites' => dossiers_invites.present?,
'dossiers-supprimes' => dossiers_supprimes.present? 'dossiers-supprimes' => dossiers_supprimes.present?,
'dossiers-transferes' => dossier_transfers.present?
} }
if tabs[params_statut] if tabs[params_statut]
params_statut params_statut

View file

@ -1,4 +1,6 @@
class Users::PasswordsController < Devise::PasswordsController class Users::PasswordsController < Devise::PasswordsController
include DevisePopulatedResource
after_action :try_to_authenticate_instructeur, only: [:update] after_action :try_to_authenticate_instructeur, only: [:update]
after_action :try_to_authenticate_administrateur, only: [:update] after_action :try_to_authenticate_administrateur, only: [:update]
@ -8,19 +10,9 @@ class Users::PasswordsController < Devise::PasswordsController
# end # end
# POST /resource/password # POST /resource/password
def create # def create
# Check the credentials associated to the mail to generate a correct reset link # super
email = params[:user][:email] # end
if Administrateur.by_email(email)
@devise_mapping = Devise.mappings[:administrateur]
params[:administrateur] = params[:user]
# uncomment to check password complexity for Instructeur
# elsif Instructeur.by_email(email)
# @devise_mapping = Devise.mappings[:instructeur]
# params[:instructeur] = params[:user]
end
super
end
# GET /resource/password/edit?reset_password_token=abcdef # GET /resource/password/edit?reset_password_token=abcdef
# def edit # def edit
@ -37,16 +29,16 @@ class Users::PasswordsController < Devise::PasswordsController
@email = params[:email] @email = params[:email]
end end
# protected protected
# def after_resetting_password_path_for(resource) # def after_resetting_password_path_for(resource)
# super(resource) # super(resource)
# end # end
# The path used after sending reset password instructions def after_sending_reset_password_instructions_path_for(resource_name)
# def after_sending_reset_password_instructions_path_for(resource_name) flash.discard(:notice)
# super(resource_name) users_password_reset_link_sent_path(email: resource.email)
# end end
def try_to_authenticate_instructeur def try_to_authenticate_instructeur
if user_signed_in? if user_signed_in?
@ -67,20 +59,4 @@ class Users::PasswordsController < Devise::PasswordsController
end end
end end
end end
def test_strength
@score, @words, @length = ZxcvbnService.new(password_params[:password]).complexity
@min_length = PASSWORD_MIN_LENGTH
@min_complexity = PASSWORD_COMPLEXITY_FOR_USER
render 'shared/password/test_strength'
end
def password_params
params.require(:user).permit(:reset_password_token, :password)
end
def after_sending_reset_password_instructions_path_for(resource_name)
flash.discard(:notice)
users_password_reset_link_sent_path(email: resource.email)
end
end end

View file

@ -0,0 +1,31 @@
module Users
class TransfersController < UserController
def create
transfer = DossierTransfer.new(transfer_params)
transfer.save!
redirect_to dossiers_path
end
def update
DossierTransfer.accept(params[:id], current_user)
redirect_to dossiers_path
end
def destroy
transfer = DossierTransfer.find_by!(id: params[:id], dossiers: { user: current_user })
transfer.destroy
redirect_to dossiers_path
end
private
def transfer_params
transfer_params = params.require(:dossier_transfer).permit(:email, :dossiers)
if transfer_params[:dossiers].present?
transfer_params.merge(dossiers: [current_user.dossiers.find(transfer_params[:dossiers])])
else
transfer_params.merge(dossiers: current_user.dossiers)
end
end
end
end

View file

@ -0,0 +1,7 @@
class Cron::PurgeStaleTransfersJob < Cron::CronJob
self.schedule_expression = "every day at midnight"
def perform
DossierTransfer.stale.destroy_all
end
end

View file

@ -67,7 +67,7 @@ class DossierMailer < ApplicationMailer
def notify_brouillon_near_deletion(dossiers, to_email) def notify_brouillon_near_deletion(dossiers, to_email)
I18n.with_locale(dossiers.first.user_locale) do I18n.with_locale(dossiers.first.user_locale) do
@subject = default_i18n_subject(count: dossiers.count) @subject = default_i18n_subject(count: dossiers.size)
@dossiers = dossiers @dossiers = dossiers
mail(to: to_email, subject: @subject) mail(to: to_email, subject: @subject)
@ -75,7 +75,7 @@ class DossierMailer < ApplicationMailer
end end
def notify_brouillon_deletion(dossier_hashes, to_email) def notify_brouillon_deletion(dossier_hashes, to_email)
@subject = default_i18n_subject(count: dossier_hashes.count) @subject = default_i18n_subject(count: dossier_hashes.size)
@dossier_hashes = dossier_hashes @dossier_hashes = dossier_hashes
mail(to: to_email, subject: @subject) mail(to: to_email, subject: @subject)
@ -109,7 +109,7 @@ class DossierMailer < ApplicationMailer
def notify_automatic_deletion_to_user(deleted_dossiers, to_email) def notify_automatic_deletion_to_user(deleted_dossiers, to_email)
I18n.with_locale(deleted_dossiers.first.user_locale) do I18n.with_locale(deleted_dossiers.first.user_locale) do
@state = deleted_dossiers.first.state @state = deleted_dossiers.first.state
@subject = default_i18n_subject(count: deleted_dossiers.count) @subject = default_i18n_subject(count: deleted_dossiers.size)
@deleted_dossiers = deleted_dossiers @deleted_dossiers = deleted_dossiers
mail(to: to_email, subject: @subject) mail(to: to_email, subject: @subject)
@ -117,7 +117,7 @@ class DossierMailer < ApplicationMailer
end end
def notify_automatic_deletion_to_administration(deleted_dossiers, to_email) def notify_automatic_deletion_to_administration(deleted_dossiers, to_email)
@subject = default_i18n_subject(count: deleted_dossiers.count) @subject = default_i18n_subject(count: deleted_dossiers.size)
@deleted_dossiers = deleted_dossiers @deleted_dossiers = deleted_dossiers
mail(to: to_email, subject: @subject) mail(to: to_email, subject: @subject)
@ -126,7 +126,7 @@ class DossierMailer < ApplicationMailer
def notify_near_deletion_to_user(dossiers, to_email) def notify_near_deletion_to_user(dossiers, to_email)
I18n.with_locale(dossiers.first.user_locale) do I18n.with_locale(dossiers.first.user_locale) do
@state = dossiers.first.state @state = dossiers.first.state
@subject = default_i18n_subject(count: dossiers.count, state: @state) @subject = default_i18n_subject(count: dossiers.size, state: @state)
@dossiers = dossiers @dossiers = dossiers
mail(to: to_email, subject: @subject) mail(to: to_email, subject: @subject)
@ -135,7 +135,7 @@ class DossierMailer < ApplicationMailer
def notify_near_deletion_to_administration(dossiers, to_email) def notify_near_deletion_to_administration(dossiers, to_email)
@state = dossiers.first.state @state = dossiers.first.state
@subject = default_i18n_subject(count: dossiers.count, state: @state) @subject = default_i18n_subject(count: dossiers.size, state: @state)
@dossiers = dossiers @dossiers = dossiers
mail(to: to_email, subject: @subject) mail(to: to_email, subject: @subject)
@ -157,6 +157,15 @@ class DossierMailer < ApplicationMailer
end end
end end
def notify_transfer(transfer)
I18n.with_locale(transfer.user_locale) do
@subject = default_i18n_subject(count: transfer.dossiers.size)
@transfer = transfer
mail(to: transfer.email, subject: @subject)
end
end
protected protected
# This is an override of `default_i18n_subject` method # This is an override of `default_i18n_subject` method

View file

@ -0,0 +1,23 @@
module PasswordComplexityConcern
extend ActiveSupport::Concern
# Allows adding a condition to the password complexity validation.
# Default is yes. Can be overridden in included classes.
def validate_password_complexity?
true
end
included do
# Add a validator for password complexity.
#
# The validator triggers as soon as the password is long enough (to avoid presenting
# two errors when the password is too short, one about length and one about complexity).
validates :password, password_complexity: true, if: -> { password_has_minimum_length? && validate_password_complexity? }
end
private
def password_has_minimum_length?
self.class.password_length.include?(password.try(:size))
end
end

View file

@ -27,6 +27,7 @@
# termine_close_to_expiration_notice_sent_at :datetime # termine_close_to_expiration_notice_sent_at :datetime
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# dossier_transfer_id :bigint
# groupe_instructeur_id :bigint # groupe_instructeur_id :bigint
# revision_id :bigint # revision_id :bigint
# user_id :integer # user_id :integer
@ -122,6 +123,9 @@ class Dossier < ApplicationRecord
has_many :types_de_champ, through: :revision has_many :types_de_champ, through: :revision
has_many :types_de_champ_private, through: :revision has_many :types_de_champ_private, through: :revision
belongs_to :transfer, class_name: 'DossierTransfer', foreign_key: 'dossier_transfer_id', optional: true, inverse_of: :dossiers, dependent: :destroy
has_many :transfer_logs, class_name: 'DossierTransferLog', dependent: :destroy
accepts_nested_attributes_for :champs accepts_nested_attributes_for :champs
accepts_nested_attributes_for :champs_private accepts_nested_attributes_for :champs_private

View file

@ -0,0 +1,53 @@
# == Schema Information
#
# Table name: dossier_transfers
#
# id :bigint not null, primary key
# email :string not null
# created_at :datetime not null
# updated_at :datetime not null
#
class DossierTransfer < ApplicationRecord
has_many :dossiers, dependent: :nullify
EXPIRATION_LIMIT = 2.weeks
scope :pending, -> { where('created_at > ?', (Time.zone.now - EXPIRATION_LIMIT)) }
scope :stale, -> { where('created_at < ?', (Time.zone.now - EXPIRATION_LIMIT)) }
scope :with_dossiers, -> { joins(:dossiers) }
after_create_commit :send_notification
def self.initiate(email, dossiers)
create(email: email, dossiers: dossiers)
end
def self.accept(id, current_user)
transfer = pending.find_by(id: id, email: current_user.email)
if transfer && transfer.dossiers.present?
Invite
.where(dossier: transfer.dossiers, email: transfer.email)
.destroy_all
DossierTransferLog.create(transfer.dossiers.map do |dossier|
{
dossier: dossier,
from: dossier.user.email,
to: transfer.email
}
end)
transfer.dossiers.update_all(user_id: current_user.id)
transfer.destroy
end
end
def user_locale
User.find_by(email: email)&.locale || I18n.default_locale
end
private
def send_notification
DossierMailer.notify_transfer(self).deliver_later
end
end

View file

@ -0,0 +1,14 @@
# == Schema Information
#
# Table name: dossier_transfer_logs
#
# id :bigint not null, primary key
# from :string not null
# to :string not null
# created_at :datetime not null
# updated_at :datetime not null
# dossier_id :bigint not null
#
class DossierTransferLog < ApplicationRecord
belongs_to :dossier
end

View file

@ -25,11 +25,11 @@
# updated_at :datetime # updated_at :datetime
# #
class SuperAdmin < ApplicationRecord class SuperAdmin < ApplicationRecord
include PasswordComplexityConcern
devise :rememberable, :trackable, :validatable, :lockable, :async, :recoverable, devise :rememberable, :trackable, :validatable, :lockable, :async, :recoverable,
:two_factor_authenticatable, :otp_secret_encryption_key => Rails.application.secrets.otp_secret_key :two_factor_authenticatable, :otp_secret_encryption_key => Rails.application.secrets.otp_secret_key
validates :password, password_complexity: true, if: -> (u) { Devise.password_length.include?(u.password.try(:size)) }
def enable_otp! def enable_otp!
self.otp_secret = SuperAdmin.generate_otp_secret self.otp_secret = SuperAdmin.generate_otp_secret
self.otp_required_for_login = true self.otp_required_for_login = true

View file

@ -31,6 +31,7 @@
# #
class User < ApplicationRecord class User < ApplicationRecord
include EmailSanitizableConcern include EmailSanitizableConcern
include PasswordComplexityConcern
enum loged_in_with_france_connect: { enum loged_in_with_france_connect: {
particulier: 'particulier', particulier: 'particulier',
@ -57,7 +58,9 @@ class User < ApplicationRecord
before_validation -> { sanitize_email(:email) } before_validation -> { sanitize_email(:email) }
validates :password, password_complexity: true, if: -> (u) { u.administrateur.present? && Devise.password_length.include?(u.password.try(:size)) } def validate_password_complexity?
administrateur?
end
# Override of Devise::Models::Confirmable#send_confirmation_instructions # Override of Devise::Models::Confirmable#send_confirmation_instructions
def send_confirmation_instructions def send_confirmation_instructions

View file

@ -16,7 +16,6 @@
= f.label :password do = f.label :password do
Mot de passe Mot de passe
= render 'password_complexity/field', { form: f, test_complexity: true }
= render partial: 'shared/password/edit_password', locals: { form: f, controller: 'administrateurs/passwords' }
= f.submit 'Continuer', class: 'button large primary expand', id: "submit-password", data: { disable_with: "Envoi..." } = f.submit 'Continuer', class: 'button large primary expand', id: "submit-password", data: { disable_with: "Envoi..." }

View file

@ -1,6 +0,0 @@
- content_for(:title, 'Changement de mot de passe')
- content_for :footer do
= render partial: 'root/footer'
= render 'shared/password/edit', test_password_strength: administrateurs_password_test_strength_path

View file

@ -14,8 +14,9 @@
= f.hidden_field :reset_password_token = f.hidden_field :reset_password_token
= f.label 'Nouveau mot de passe' = f.label 'Nouveau mot de passe'
= render 'password_complexity/field', { form: f, test_complexity: populated_resource.validate_password_complexity? }
= render partial: 'shared/password/edit_password', locals: { form: f, controller: 'super_admins/passwords' } = f.label 'Confirmez le nouveau mot de passe'
= f.password_field :password_confirmation, autocomplete: 'off'
= f.submit 'Changer le mot de passe', class: 'button large primary expand', id: "submit-password", data: { disable_with: "Envoi…" }
= f.submit 'Changer le mot de passe', class: 'button large primary expand', id: "submit-password", data: { disable_with: "Envoi..." }

View file

@ -3,7 +3,7 @@
%p= t(:hello, scope: [:views, :shared, :greetings]) %p= t(:hello, scope: [:views, :shared, :greetings])
%p %p
= t('.header', count: @deleted_dossiers.count) = t('.header', count: @deleted_dossiers.size)
%ul %ul
- @deleted_dossiers.each do |d| - @deleted_dossiers.each do |d|
%li n° #{d.dossier_id} (#{d.procedure.libelle}) %li n° #{d.dossier_id} (#{d.procedure.libelle})

View file

@ -3,12 +3,12 @@
%p= t(:hello, scope: [:views, :shared, :greetings]) %p= t(:hello, scope: [:views, :shared, :greetings])
%p %p
= t('.header', count: @deleted_dossiers.count) = t('.header', count: @deleted_dossiers.size)
%ul %ul
- @deleted_dossiers.each do |d| - @deleted_dossiers.each do |d|
%li n° #{d.dossier_id} (#{d.procedure.libelle}) %li n° #{d.dossier_id} (#{d.procedure.libelle})
- if @state == Dossier.states.fetch(:en_construction) - if @state == Dossier.states.fetch(:en_construction)
%p= t('.footer_en_construction', count: @deleted_dossiers.count) %p= t('.footer_en_construction', count: @deleted_dossiers.size)
= render partial: "layouts/mailers/signature" = render partial: "layouts/mailers/signature"

View file

@ -3,7 +3,7 @@
%p= t(:hello, scope: [:views, :shared, :greetings]) %p= t(:hello, scope: [:views, :shared, :greetings])
%p %p
= t('.header', count: @dossier_hashes.count) = t('.header', count: @dossier_hashes.size)
%ul %ul
- @dossier_hashes.each do |d| - @dossier_hashes.each do |d|
%li n° #{d[:id]} (#{d[:procedure_libelle]}) %li n° #{d[:id]} (#{d[:procedure_libelle]})

View file

@ -3,11 +3,11 @@
%p= t(:hello, scope: [:views, :shared, :greetings]) %p= t(:hello, scope: [:views, :shared, :greetings])
%p %p
= t('.header', count: @dossiers.count) = t('.header', count: @dossiers.size)
%ul %ul
- @dossiers.each do |d| - @dossiers.each do |d|
%li= link_to("n° #{d.id} (#{d.procedure.libelle})", dossier_url(d)) %li= link_to("n° #{d.id} (#{d.procedure.libelle})", dossier_url(d))
%p= sanitize(t('.footer', count: @dossiers.count)) %p= sanitize(t('.footer', count: @dossiers.size))
= render partial: "layouts/mailers/signature" = render partial: "layouts/mailers/signature"

View file

@ -4,9 +4,9 @@
%p %p
- if @state == Dossier.states.fetch(:en_construction) - if @state == Dossier.states.fetch(:en_construction)
= t('.header_en_construction', count: @dossiers.count) = t('.header_en_construction', count: @dossiers.size)
- else - else
= t('.header_termine', count: @dossiers.count) = t('.header_termine', count: @dossiers.size)
%ul %ul
- @dossiers.each do |d| - @dossiers.each do |d|
%li %li
@ -14,8 +14,8 @@
%p %p
- if @state == Dossier.states.fetch(:en_construction) - if @state == Dossier.states.fetch(:en_construction)
= sanitize(t('.footer_en_construction', count: @dossiers.count)) = sanitize(t('.footer_en_construction', count: @dossiers.size))
- else - else
= sanitize(t('.footer_termine', count: @dossiers.count)) = sanitize(t('.footer_termine', count: @dossiers.size))
= render partial: "layouts/mailers/signature" = render partial: "layouts/mailers/signature"

View file

@ -4,18 +4,18 @@
%p %p
- if @state == Dossier.states.fetch(:en_construction) - if @state == Dossier.states.fetch(:en_construction)
= t('.header_en_construction', count: @dossiers.count) = t('.header_en_construction', count: @dossiers.size)
- else - else
= t('.header_termine', count: @dossiers.count) = t('.header_termine', count: @dossiers.size)
%ul %ul
- @dossiers.each do |d| - @dossiers.each do |d|
%li %li
#{link_to("n° #{d.id} (#{d.procedure.libelle})", dossier_url(d))}. Retrouvez le dossier au format #{link_to("PDF", dossier_url(d, format: :pdf))} #{link_to("n° #{d.id} (#{d.procedure.libelle})", dossier_url(d))}. Retrouvez le dossier au format #{link_to("PDF", dossier_url(d, format: :pdf))}
%p %p
= sanitize(t('.footer', count: @dossiers.count)) = sanitize(t('.footer', count: @dossiers.size))
%p %p
- if @state == Dossier.states.fetch(:en_construction) - if @state == Dossier.states.fetch(:en_construction)
= sanitize(t('.footer_en_construction', count: @dossiers.count)) = sanitize(t('.footer_en_construction', count: @dossiers.size))
= render partial: "layouts/mailers/signature" = render partial: "layouts/mailers/signature"

View file

@ -0,0 +1,9 @@
- content_for(:title, "#{@subject}")
%p= t(:hello, scope: [:views, :shared, :greetings])
%p
= t('.body', count: @transfer.dossiers.size)
= link_to t('.transfer_link'), dossiers_url(statut: 'dossiers-transferes')
= render partial: "layouts/mailers/signature"

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

@ -1,4 +1,4 @@
#strength-label{ style: 'font-weight: bold' } #complexity-label{ style: 'font-weight: bold' }
- if @length > 0 - if @length > 0
- if @length < @min_length - if @length < @min_length
Le mot de passe doit faire au moins #{@min_length} caractères. Le mot de passe doit faire au moins #{@min_length} caractères.

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

@ -1,21 +0,0 @@
- content_for(:title, 'Changement de mot de passe')
- content_for :footer do
= render partial: 'root/footer'
.container.devise-container
.one-column-centered
= devise_error_messages!
= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :patch, class: 'form' }) do |f|
%h1 Changement de mot de passe
= f.hidden_field :reset_password_token, value: params[:token]
= f.label 'Nouveau mot de passe'
= render partial 'shared/password/edit_password', locals: { form: f, controller: controller }
= f.submit 'Changer le mot de passe', class: 'button large primary expand', id: "submit-password", data: { disable_with: "Envoi..." }

View file

@ -1,10 +0,0 @@
= form.password_field :password, autofocus: true, autocomplete: 'off', placeholder: 'Mot de passe', data: { remote: true, url: url_for(controller: controller, action: 'test_strength') }
#strength-bar.password-strength
&nbsp;
.explication
#strength-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

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

View file

@ -1,3 +0,0 @@
<%= render_to_element('#strength-label', partial: 'shared/password/password_strength_label', outer: true) %>
<%= render_to_element('#strength-bar', partial: 'shared/password/password_strength', outer: true) %>
<%= raw("document.querySelector('#submit-password').disabled = #{@score < @min_complexity || @length < @min_length};") %>

View file

@ -1,17 +0,0 @@
- content_for(:title, 'Mot de passe oublié')
- content_for :footer do
= render partial: 'root/footer'
.container.devise-container
.one-column-centered
= devise_error_messages!
= form_for(resource, as: resource_name, url: password_path(resource_name), html: { class: 'form' }) do |f|
%h1 Mot de passe oublié
= f.label :email, 'Email'
= f.email_field :email, autofocus: true
= f.submit 'Demander un nouveau mot de passe', class: 'button large expand primary'

View file

@ -1,7 +1,8 @@
- has_edit_action = !dossier.read_only?
- has_delete_action = dossier.can_be_deleted_by_user? - has_delete_action = dossier.can_be_deleted_by_user?
- has_new_dossier_action = dossier.procedure.accepts_new_dossiers? - has_new_dossier_action = dossier.procedure.accepts_new_dossiers?
- has_transfer_action = dossier.user == current_user
- has_actions = has_delete_action || has_new_dossier_action - has_actions = has_edit_action || has_delete_action || has_new_dossier_action || has_transfer_action
- if has_actions - if has_actions
.dropdown.user-dossier-actions .dropdown.user-dossier-actions
@ -9,7 +10,7 @@
= t('views.users.dossiers.dossier_action.actions') = t('views.users.dossiers.dossier_action.actions')
#actions-menu.dropdown-content.fade-in-down #actions-menu.dropdown-content.fade-in-down
%ul.dropdown-items %ul.dropdown-items
- if !dossier.read_only? - if has_edit_action
- if dossier.brouillon? - if dossier.brouillon?
%li %li
= link_to(url_for_dossier(dossier)) do = link_to(url_for_dossier(dossier)) do
@ -23,6 +24,13 @@
.dropdown-description .dropdown-description
= t('views.users.dossiers.dossier_action.edit_dossier') = t('views.users.dossiers.dossier_action.edit_dossier')
- if has_transfer_action
%li
= link_to transferer_dossier_path(dossier) do
%span.icon.person
.dropdown-description
= t('views.users.dossiers.dossier_action.transfer_dossier')
- if has_new_dossier_action - if has_new_dossier_action
%li %li
= link_to procedure_lien(dossier.procedure) do = link_to procedure_lien(dossier.procedure) do

View file

@ -0,0 +1,32 @@
- if dossier_transfers.present?
%ul.dossiers-transfers.mb-2
- dossier_transfers.each do |transfer|
%li.mb-4
.transfer-details.mb-2
Demande de transfert Nº #{transfer.id} envoyé par #{transfer.dossiers.first.user.email}
%table.table.dossiers-table.hoverable
%thead
%tr
%th.number-col= t('views.users.dossiers.dossiers_list.n_dossier')
%th= t('views.users.dossiers.dossiers_list.procedure')
%th= t('views.users.dossiers.dossiers_list.status')
%th Date de dépot
%tbody
- transfer.dossiers.each do |dossier|
%tr{ data: { 'transfer-id': transfer.id } }
%td.number-col
%span.icon.folder
= dossier.id
%td= dossier.procedure.libelle
%td= status_badge(dossier.state)
%td{ style: 'padding: 18px;' }= (dossier.en_construction_at || dossier.created_at).strftime('%d/%m/%Y')
.transfer-actions.mt-4
= link_to "Accepter", transfer_path(transfer), class: "button primary", method: :put
= link_to "Rejeter", transfer_path(transfer), class: "button danger", method: :delete
= paginate(dossier_transfers)
- else
.blank-tab
%h2.empty-text Aucune demande de transfert de dossiers ne vous a été adressée.

View file

@ -33,6 +33,11 @@
active: @statut == 'dossiers-supprimes', active: @statut == 'dossiers-supprimes',
badge: number_with_html_delimiter(@dossiers_supprimes.count)) badge: number_with_html_delimiter(@dossiers_supprimes.count))
- if @dossier_transfers.count > 0
= tab_item(t('pluralize.dossiers_transferes', count: @dossier_transfers.count),
dossiers_path(statut: 'dossiers-transferes'),
active: @statut == 'dossiers-transferes',
badge: number_with_html_delimiter(@dossier_transfers.count))
.container .container
- if @statut == "mes-dossiers" - if @statut == "mes-dossiers"
@ -43,3 +48,6 @@
- if @statut == "dossiers-supprimes" - if @statut == "dossiers-supprimes"
= render partial: "deleted_dossiers_list", locals: { deleted_dossiers: @dossiers_supprimes } = render partial: "deleted_dossiers_list", locals: { deleted_dossiers: @dossiers_supprimes }
- if @statut == "dossiers-transferes"
= render partial: "transfered_dossiers_list", locals: { dossier_transfers: @dossier_transfers }

View file

@ -0,0 +1,9 @@
.container.mt-4
- dossier = @transfer.dossiers.first
Transferer le dossier #{dossier_display_state(dossier.state, lower: true)} nº #{@dossier.id} déposé par #{demandeur_dossier(dossier)} le #{try_format_date(dossier.created_at)} vers le compte dun autre usager :
= form_for @transfer, url: transfers_path, html: { class: 'form mt-2' } do |f|
= f.label :email, 'Email du compte destinataire'
= f.email_field :email
= f.hidden_field :dossiers, value: dossier.id
= f.submit "Envoyer la demande de transfert", class: 'button primary'

View file

@ -0,0 +1,7 @@
.container.mt-4
Transferer les #{@transfer.dossiers.size} dossiers de votre compte vers le compte dun autre usager :
= form_for @transfer, url: transfers_path, html: { class: 'form mt-2' } do |f|
= f.label :email, 'Email du compte destinataire'
= f.email_field :email
= f.submit "Envoyer la demande de transfert", class: 'button primary'

View file

@ -1,22 +0,0 @@
- content_for(:title, 'Changement de mot de passe')
- content_for :footer do
= render partial: 'root/footer'
.container.devise-container
.one-column-centered
= devise_error_messages!
= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :patch, class: 'form' }) do |f|
%h1 Changement de mot de passe
= f.hidden_field :reset_password_token
= f.label 'Nouveau mot de passe'
= f.password_field :password, autofocus: true, autocomplete: 'off'
= f.label 'Confirmez le nouveau mot de passe'
= f.password_field :password_confirmation, autocomplete: 'off'
= f.submit 'Changer le mot de passe', class: 'button primary'

View file

@ -72,3 +72,6 @@ DS_ENV="staging"
# Active la localisation # Active la localisation
# LOCALIZATION_ENABLED="true" # LOCALIZATION_ENABLED="true"
# Désactivé l'OTP pour SuperAdmin
# SUPER_ADMIN_OTP_ENABLED = "disabled" # "enabled" par défaut

View file

@ -0,0 +1 @@
SUPER_ADMIN_OTP_ENABLED = ENV.fetch("SUPER_ADMIN_OTP_ENABLED", "enabled") == "enabled"

View file

@ -1,75 +0,0 @@
SmartListing.configure do |config|
config.global_options({
# :param_names => { # param names
# :page => :page,
# :per_page => :per_page,
# :sort => :sort,
# },
# :array => false, # controls whether smart list should be using arrays or AR collections
# :max_count => nil, # limit number of rows
# :unlimited_per_page => false, # allow infinite page size
# :paginate => true, # allow pagination
# :memorize_per_page => false, # save per page settings in the cookie
:page_sizes => [10, 20, 50, 100] # set available page sizes array
# :kaminari_options => {:theme => "smart_listing"}, # Kaminari's paginate helper options
})
config.constants :classes, {
#:main => "smart-listing",
#:editable => "editable",
#:content => "content",
#:loading => "loading",
#:status => "smart-listing-status",
#:item_actions => "actions",
#:new_item_placeholder => "new-item-placeholder",
#:new_item_action => "new-item-action",
#:new_item_button => "btn",
#:hidden => "hidden",
#:autoselect => "autoselect",
#:callback => "callback",
#:pagination_per_page => "pagination-per-page text-center",
#:pagination_count => "count",
#:inline_editing => "info",
#:no_records => "no-records",
#:limit => "smart-listing-limit",
#:limit_alert => "smart-listing-limit-alert",
#:controls => "smart-listing-controls",
#:controls_reset => "reset",
#:filtering => "filter",
#:filtering_search => "glyphicon-search",
#:filtering_cancel => "glyphicon-remove",
#:filtering_disabled => "disabled",
#:sortable => "sortable",
#:icon_new => "glyphicon glyphicon-plus",
#:icon_edit => "glyphicon glyphicon-pencil",
#:icon_trash => "glyphicon glyphicon-trash",
#:icon_inactive => "glyphicon glyphicon-circle",
#:icon_show => "glyphicon glyphicon-share-alt",
#:icon_sort_none => "glyphicon glyphicon-resize-vertical",
#:icon_sort_up => "glyphicon glyphicon-chevron-up",
#:icon_sort_down => "glyphicon glyphicon-chevron-down",
}
config.constants :data_attributes, {
# :main => "smart-listing",
# :confirmation => "confirmation",
# :id => "id",
# :href => "href",
# :callback_href => "callback-href",
# :max_count => "max-count",
# :inline_edit_backup => "smart-listing-edit-backup",
# :params => "params",
# :observed => "observed",
# :href => "href",
# :autoshow => "autoshow",
# :popover => "slpopover",
}
config.constants :selectors, {
# :item_action_destroy => "a.destroy",
# :edit_cancel => "button.cancel",
# :row => "tr",
# :head => "thead",
# :filtering_icon => "i"
}
end

View file

@ -190,6 +190,7 @@ en:
edit_dossier: "Edit the file" edit_dossier: "Edit the file"
start_other_dossier: "Start an other file" start_other_dossier: "Start an other file"
delete_dossier: "Delete the file" delete_dossier: "Delete the file"
transfer_dossier: "Transfer the file"
edit_draft: "Edit the draft" edit_draft: "Edit the draft"
actions: "Actions" actions: "Actions"
sessions: sessions:
@ -328,6 +329,10 @@ en:
zero: deleted file zero: deleted file
one: deleted file one: deleted file
other: deleted files other: deleted files
dossiers_transferes:
zero: transfer request
one: transfer request
other: transfer requests
dossier_trouve: dossier_trouve:
zero: 0 file found zero: 0 file found
one: 1 file found one: 1 file found

View file

@ -182,6 +182,7 @@ fr:
edit_dossier: "Modifier le dossier" edit_dossier: "Modifier le dossier"
start_other_dossier: "Commencer un autre dossier" start_other_dossier: "Commencer un autre dossier"
delete_dossier: "Supprimer le dossier" delete_dossier: "Supprimer le dossier"
transfer_dossier: "Transferer le dossier"
edit_draft: "Modifier le brouillon" edit_draft: "Modifier le brouillon"
actions: "Actions" actions: "Actions"
sessions: sessions:
@ -332,6 +333,10 @@ fr:
zero: dossier supprimé zero: dossier supprimé
one: dossier supprimé one: dossier supprimé
other: dossiers supprimés other: dossiers supprimés
dossiers_transferes:
zero: demande de transfert
one: demande de transfert
other: demandes de transfert
dossier_trouve: dossier_trouve:
zero: 0 dossier trouvé zero: 0 dossier trouvé
one: 1 dossier trouvé one: 1 dossier trouvé

View file

@ -1,15 +0,0 @@
fr:
smart_listing:
msgs:
destroy_confirmation: Destroy?
no_items: No items
actions:
destroy: Destroy
edit: Edit
show: Show
new: New item
views:
pagination:
per_page: Par page
unlimited: Unlimited
total: Total

View file

@ -0,0 +1,12 @@
fr:
dossier_mailer:
notify_transfer:
subject:
one: Une demande de transfert de dossier vous est adressée
other: Une demande de transfert de dossiers vous est adressée
transfer_link: demande de transfer
body:
one: |
Accéder à la demande de transfert du dossier en cliquant sur le lien suivant :
other: |
Accéder à la demande de transfert de %{count} dossiers en cliquant sur le lien suivant :

View file

@ -88,10 +88,6 @@ Rails.application.routes.draw do
passwords: 'super_admins/passwords' passwords: 'super_admins/passwords'
} }
devise_scope :super_admin do
get '/super_admins/password/test_strength' => 'super_admins/passwords#test_strength'
end
get 'super_admins/edit_otp', to: 'super_admins#edit_otp', as: 'edit_super_admin_otp' get 'super_admins/edit_otp', to: 'super_admins#edit_otp', as: 'edit_super_admin_otp'
put 'super_admins/enable_otp', to: 'super_admins#enable_otp', as: 'enable_super_admin_otp' put 'super_admins/enable_otp', to: 'super_admins#enable_otp', as: 'enable_super_admin_otp'
@ -110,9 +106,7 @@ Rails.application.routes.draw do
get '/users/password/reset-link-sent' => 'users/passwords#reset_link_sent' get '/users/password/reset-link-sent' => 'users/passwords#reset_link_sent'
end end
devise_scope :administrateur do get 'password_complexity' => 'password_complexity#show', as: 'show_password_complexity'
get '/administrateurs/password/test_strength' => 'administrateurs/passwords#test_strength'
end
# #
# Main routes # Main routes
@ -264,12 +258,16 @@ Rails.application.routes.draw do
post 'commentaire' => 'dossiers#create_commentaire' post 'commentaire' => 'dossiers#create_commentaire'
post 'ask_deletion' post 'ask_deletion'
get 'attestation' get 'attestation'
get 'transferer', to: 'dossiers#transferer'
end end
collection do collection do
get 'transferer', to: 'dossiers#transferer_all'
get 'recherche' get 'recherche'
resources :transfers, only: [:create, :update, :destroy]
end end
end end
resource :feedback, only: [:create] resource :feedback, only: [:create]
get 'demarches' => 'demarches#index' get 'demarches' => 'demarches#index'

View file

@ -0,0 +1,11 @@
class CreateDossierTransfers < ActiveRecord::Migration[6.1]
def change
create_table :dossier_transfers do |t|
t.string :email, null: false, index: true
t.timestamps
end
add_reference :dossiers, :dossier_transfer, foreign_key: true, null: true, index: true
end
end

View file

@ -0,0 +1,11 @@
class CreateDossierTransferLogs < ActiveRecord::Migration[6.1]
def change
create_table :dossier_transfer_logs do |t|
t.string :from, null: false
t.string :to, null: false
t.references :dossier, foreign_key: true, null: false, index: true
t.timestamps
end
end
end

View file

@ -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: 2021_08_26_161956) do ActiveRecord::Schema.define(version: 2021_08_27_161956) 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"
@ -268,6 +268,22 @@ ActiveRecord::Schema.define(version: 2021_08_26_161956) do
t.index ["keep_until"], name: "index_dossier_operation_logs_on_keep_until" t.index ["keep_until"], name: "index_dossier_operation_logs_on_keep_until"
end end
create_table "dossier_transfer_logs", force: :cascade do |t|
t.string "from", null: false
t.string "to", null: false
t.bigint "dossier_id", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["dossier_id"], name: "index_dossier_transfer_logs_on_dossier_id"
end
create_table "dossier_transfers", force: :cascade do |t|
t.string "email", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["email"], name: "index_dossier_transfers_on_email"
end
create_table "dossiers", id: :serial, force: :cascade do |t| create_table "dossiers", id: :serial, force: :cascade do |t|
t.boolean "autorisation_donnees" t.boolean "autorisation_donnees"
t.datetime "created_at" t.datetime "created_at"
@ -299,7 +315,9 @@ ActiveRecord::Schema.define(version: 2021_08_26_161956) do
t.datetime "declarative_triggered_at" t.datetime "declarative_triggered_at"
t.index "to_tsvector('french'::regconfig, (search_terms || private_search_terms))", name: "index_dossiers_on_search_terms_private_search_terms", using: :gin t.index "to_tsvector('french'::regconfig, (search_terms || private_search_terms))", name: "index_dossiers_on_search_terms_private_search_terms", using: :gin
t.index "to_tsvector('french'::regconfig, search_terms)", name: "index_dossiers_on_search_terms", using: :gin t.index "to_tsvector('french'::regconfig, search_terms)", name: "index_dossiers_on_search_terms", using: :gin
t.bigint "dossier_transfer_id"
t.index ["archived"], name: "index_dossiers_on_archived" t.index ["archived"], name: "index_dossiers_on_archived"
t.index ["dossier_transfer_id"], name: "index_dossiers_on_dossier_transfer_id"
t.index ["groupe_instructeur_id"], name: "index_dossiers_on_groupe_instructeur_id" t.index ["groupe_instructeur_id"], name: "index_dossiers_on_groupe_instructeur_id"
t.index ["hidden_at"], name: "index_dossiers_on_hidden_at" t.index ["hidden_at"], name: "index_dossiers_on_hidden_at"
t.index ["revision_id"], name: "index_dossiers_on_revision_id" t.index ["revision_id"], name: "index_dossiers_on_revision_id"
@ -797,6 +815,8 @@ ActiveRecord::Schema.define(version: 2021_08_26_161956) do
add_foreign_key "commentaires", "experts" add_foreign_key "commentaires", "experts"
add_foreign_key "dossier_operation_logs", "bill_signatures" add_foreign_key "dossier_operation_logs", "bill_signatures"
add_foreign_key "dossier_operation_logs", "instructeurs" add_foreign_key "dossier_operation_logs", "instructeurs"
add_foreign_key "dossier_transfer_logs", "dossiers"
add_foreign_key "dossiers", "dossier_transfers"
add_foreign_key "dossiers", "groupe_instructeurs" add_foreign_key "dossiers", "groupe_instructeurs"
add_foreign_key "dossiers", "procedure_revisions", column: "revision_id" add_foreign_key "dossiers", "procedure_revisions", column: "revision_id"
add_foreign_key "dossiers", "users" add_foreign_key "dossiers", "users"

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

@ -23,7 +23,7 @@ describe Instructeurs::ArchivesController, type: :controller do
end end
it 'displays archives' do it 'displays archives' do
get :index, { params: { procedure_id: procedure1.id } } get :index, params: { procedure_id: procedure1.id }
expect(assigns(:archives)).to eq([archive1]) expect(assigns(:archives)).to eq([archive1])
end end
@ -34,9 +34,7 @@ describe Instructeurs::ArchivesController, type: :controller do
let(:date_month) { Date.strptime(month, "%Y-%m") } let(:date_month) { Date.strptime(month, "%Y-%m") }
let(:archive) { create(:archive) } let(:archive) { create(:archive) }
let(:subject) do let(:subject) do
post :create, { post :create, params: { procedure_id: procedure1.id, type: 'monthly', month: month }
params: { procedure_id: procedure1.id, type: 'monthly', month: month }
}
end end
it "performs archive creation job" do it "performs archive creation job" do

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

View file

@ -1,12 +0,0 @@
describe SuperAdmins::PasswordsController, type: :controller do
describe '#test_strength' do
it 'calculate score' do
password = "bonjour"
@request.env["devise.mapping"] = Devise.mappings[:super_admin]
get 'test_strength', xhr: true, params: { super_admin: { password: password } }
expect(assigns(:score)).to be_present
end
end
end

View file

@ -2,6 +2,8 @@ feature 'As an administrateur', js: true do
let(:super_admin) { create(:super_admin) } let(:super_admin) { create(:super_admin) }
let(:admin_email) { 'new_admin@gouv.fr' } let(:admin_email) { 'new_admin@gouv.fr' }
let(:new_admin) { Administrateur.by_email(admin_email) } let(:new_admin) { Administrateur.by_email(admin_email) }
let(:weak_password) { '12345678' }
let(:strong_password) { 'a new, long, and complicated password!' }
before do before do
perform_enqueued_jobs do perform_enqueued_jobs do
@ -9,14 +11,21 @@ feature 'As an administrateur', js: true do
end end
end end
scenario 'I can register' do scenario 'I can register', js: true do
expect(new_admin.reload.user.active?).to be(false) expect(new_admin.reload.user.active?).to be(false)
confirmation_email = open_email(admin_email) confirmation_email = open_email(admin_email)
token_params = confirmation_email.body.match(/token=[^"]+/) token_params = confirmation_email.body.match(/token=[^"]+/)
visit "admin/activate?#{token_params}" visit "admin/activate?#{token_params}"
fill_in :administrateur_password, with: 'my-s3cure-p4ssword' fill_in :administrateur_password, with: weak_password
expect(page).to have_text('Mot de passe très vulnérable')
expect(page).to have_button('Continuer', disabled: true)
fill_in :administrateur_password, with: strong_password
expect(page).to have_text('Mot de passe suffisamment fort et sécurisé')
expect(page).to have_button('Continuer', disabled: false)
click_button 'Continuer' click_button 'Continuer'

View file

@ -27,11 +27,12 @@ feature 'Managing password:' do
end end
context 'for admins' do context 'for admins' do
let(:user) { create(:user) } let(:administrateur) { create(:administrateur) }
let(:administrateur) { create(:administrateur, user: user) } let(:user) { administrateur.user }
let(:new_password) { 'a new, long, and complicated password!' } let(:weak_password) { '12345678' }
let(:strong_password) { 'a new, long, and complicated password!' }
scenario 'an admin can reset their password' do scenario 'an admin can reset their password', js: true do
visit root_path visit root_path
click_on 'Connexion' click_on 'Connexion'
click_on 'Mot de passe oublié ?' click_on 'Mot de passe oublié ?'
@ -48,8 +49,51 @@ feature 'Managing password:' do
expect(page).to have_content 'Changement de mot de passe' expect(page).to have_content 'Changement de mot de passe'
fill_in 'user_password', with: new_password fill_in 'user_password', with: weak_password
fill_in 'user_password_confirmation', with: new_password fill_in 'user_password_confirmation', with: weak_password
expect(page).to have_text('Mot de passe très vulnérable')
expect(page).to have_button('Changer le mot de passe', disabled: true)
fill_in 'user_password', with: strong_password
fill_in 'user_password_confirmation', with: strong_password
expect(page).to have_text('Mot de passe suffisamment fort et sécurisé')
expect(page).to have_button('Changer le mot de passe', disabled: false)
click_on 'Changer le mot de passe'
expect(page).to have_content('Votre mot de passe a bien été modifié.')
end
end
context 'for super-admins' do
let(:super_admin) { create(:super_admin) }
let(:weak_password) { '12345678' }
let(:strong_password) { 'a new, long, and complicated password!' }
scenario 'a super-admin can reset their password', js: true do
visit manager_root_path
click_on 'Mot de passe oublié'
expect(page).to have_current_path(new_super_admin_password_path)
fill_in 'Email', with: super_admin.email
perform_enqueued_jobs do
click_on 'Demander un nouveau mot de passe'
end
expect(page).to have_text 'vous recevrez un lien vous permettant de récupérer votre mot de passe'
click_reset_password_link_for super_admin.email
expect(page).to have_content 'Changement de mot de passe'
fill_in 'super_admin_password', with: weak_password
fill_in 'super_admin_password_confirmation', with: weak_password
expect(page).to have_text('Mot de passe très vulnérable')
expect(page).to have_button('Changer le mot de passe', disabled: true)
fill_in 'super_admin_password', with: strong_password
fill_in 'super_admin_password_confirmation', with: strong_password
expect(page).to have_text('Mot de passe suffisamment fort et sécurisé')
expect(page).to have_button('Changer le mot de passe', disabled: false)
click_on 'Changer le mot de passe' click_on 'Changer le mot de passe'
expect(page).to have_content('Votre mot de passe a bien été modifié.') expect(page).to have_content('Votre mot de passe a bien été modifié.')
end end

View file

@ -0,0 +1,32 @@
describe 'Transfer dossier:' do
let(:user) { create(:user) }
let(:other_user) { create(:user) }
let(:procedure) { create(:simple_procedure) }
let(:dossier) { create(:dossier, :en_construction, :with_individual, :with_commentaires, user: user, procedure: procedure) }
before do
dossier
login_as user, scope: :user
visit dossiers_path
end
scenario 'the user can transfer dossier to another user' do
within(:css, "tr[data-dossier-id=\"#{dossier.id}\"]") do
click_on 'Actions'
click_on 'Transferer le dossier'
end
expect(page).to have_current_path(transferer_dossier_path(dossier))
expect(page).to have_content("Transferer le dossier en construction nº #{dossier.id}")
fill_in 'Email du compte destinataire', with: other_user.email
click_on 'Envoyer la demande de transfert'
logout
login_as other_user, scope: :user
visit dossiers_path
expect(page).to have_content("Demande de transfert Nº #{dossier.reload.transfer.id} envoyé par #{user.email}")
click_on 'Accepter'
expect(page).to have_current_path(dossiers_path)
end
end

View file

@ -69,6 +69,10 @@ class DossierMailerPreview < ActionMailer::Preview
DossierMailer.notify_brouillon_not_submitted(draft) DossierMailer.notify_brouillon_not_submitted(draft)
end end
def notify_transfer
DossierMailer.notify_transfer(transfer)
end
private private
def usager_email def usager_email
@ -79,24 +83,28 @@ class DossierMailerPreview < ActionMailer::Preview
"administration@example.com" "administration@example.com"
end end
def user
User.new(email: "usager@example.com", locale: I18n.locale)
end
def deleted_dossier def deleted_dossier
DeletedDossier.new(dossier_id: 1, procedure: procedure) DeletedDossier.new(dossier_id: 1, procedure: procedure)
end end
def draft def draft
Dossier.new(id: 47882, procedure: procedure, user: User.new(email: "usager@example.com")) Dossier.new(id: 47882, procedure: procedure, user: user)
end end
def dossier def dossier
Dossier.new(id: 47882, state: :en_instruction, procedure: procedure, user: User.new(email: "usager@example.com")) Dossier.new(id: 47882, state: :en_instruction, procedure: procedure, user: user)
end end
def dossier_en_construction def dossier_en_construction
Dossier.new(id: 47882, state: :en_construction, procedure: procedure, user: User.new(email: "usager@example.com")) Dossier.new(id: 47882, state: :en_construction, procedure: procedure, user: user)
end end
def dossier_accepte def dossier_accepte
Dossier.new(id: 47882, state: :accepte, procedure: procedure, user: User.new(email: "usager@example.com")) Dossier.new(id: 47882, state: :accepte, procedure: procedure, user: user)
end end
def procedure def procedure
@ -111,4 +119,8 @@ class DossierMailerPreview < ActionMailer::Preview
horaires: 'Du lundi au vendredi, de 9 h à 18 h' horaires: 'Du lundi au vendredi, de 9 h à 18 h'
) )
end end
def transfer
DossierTransfer.new(email: usager_email, dossiers: [dossier, dossier_accepte])
end
end end

View file

@ -0,0 +1,50 @@
require 'rails_helper'
RSpec.describe DossierTransfer, type: :model do
let(:user) { create(:user) }
let(:other_user) { create(:user) }
let(:dossier) { create(:dossier, user: user) }
describe 'initiate' do
subject { DossierTransfer.initiate(other_user.email, [dossier]) }
it 'should send transfer request' do
expect(subject.email).to eq(other_user.email)
expect(subject.dossiers).to eq([dossier])
expect(dossier.transfer).to eq(subject)
expect(dossier.user).to eq(user)
expect(dossier.transfer_logs.count).to eq(0)
end
describe 'accept' do
let(:transfer_log) { dossier.transfer_logs.first }
before do
DossierTransfer.accept(subject.id, other_user)
dossier.reload
end
it 'should transfer dossier' do
expect(DossierTransfer.count).to eq(0)
expect(dossier.transfer).to be_nil
expect(dossier.user).to eq(other_user)
expect(dossier.transfer_logs.count).to eq(1)
expect(transfer_log.dossier).to eq(dossier)
expect(transfer_log.from).to eq(user.email)
expect(transfer_log.to).to eq(other_user.email)
end
end
describe 'with_dossiers' do
before { subject }
it { expect(DossierTransfer.with_dossiers.count).to eq(1) }
context "when dossier discarded" do
before { dossier.discard! }
it { expect(DossierTransfer.with_dossiers.count).to eq(0) }
end
end
end
end

View file

@ -69,35 +69,37 @@ describe SuperAdmin, type: :model do
# 2 - somewhat guessable: protection from unthrottled online attacks. (guesses < 10^8) # 2 - somewhat guessable: protection from unthrottled online attacks. (guesses < 10^8)
# 3 - safely unguessable: moderate protection from offline slow-hash scenario. (guesses < 10^10) # 3 - safely unguessable: moderate protection from offline slow-hash scenario. (guesses < 10^10)
# 4 - very unguessable: strong protection from offline slow-hash scenario. (guesses >= 10^10) # 4 - very unguessable: strong protection from offline slow-hash scenario. (guesses >= 10^10)
passwords = ['pass', '12pass23', 'démarches ', 'démarches-simple', '{My-$3cure-p4ssWord}'] passwords = ['password', '12pass23', 'démarches ', 'démarches-simple', '{My-$3cure-p4ssWord}']
min_complexity = PASSWORD_COMPLEXITY_FOR_ADMIN min_complexity = PASSWORD_COMPLEXITY_FOR_ADMIN
let(:email) { 'mail@beta.gouv.fr' } let(:email) { 'mail@beta.gouv.fr' }
let(:super_admin) { build(:super_admin, email: email, password: password) } let(:super_admin) { build(:super_admin, email: email, password: password) }
subject do subject do
super_admin.save super_admin.valid?
super_admin.errors.full_messages super_admin.errors.full_messages
end end
context 'when password is too short' do context 'when the password is too short' do
let(:password) { 's' * (PASSWORD_MIN_LENGTH - 1) } let(:password) { 's' * (PASSWORD_MIN_LENGTH - 1) }
it { expect(subject).to eq(["Le mot de passe est trop court"]) } it 'reports an error about password length (but not about complexity)' do
expect(subject).to eq(["Le mot de passe est trop court"])
end
end end
context 'when password is too simple' do passwords[0..(min_complexity - 1)].each do |simple_password|
passwords[0..(min_complexity - 1)].each do |password| context 'when the password is long enough, but too simple' do
let(:password) { password } let(:password) { simple_password }
it { expect(subject).to eq(["Le mot de passe nest pas assez complexe"]) } it { expect(subject).to eq(["Le mot de passe nest pas assez complexe"]) }
end end
end end
context 'when password is acceptable' do context 'when the password is long and complex' do
let(:password) { passwords[min_complexity] } let(:password) { passwords[min_complexity] }
it { expect(subject).to eq([]) } it { expect(subject).to be_empty }
end end
end end
end end

View file

@ -363,45 +363,56 @@ describe User, type: :model do
# 2 - somewhat guessable: protection from unthrottled online attacks. (guesses < 10^8) # 2 - somewhat guessable: protection from unthrottled online attacks. (guesses < 10^8)
# 3 - safely unguessable: moderate protection from offline slow-hash scenario. (guesses < 10^10) # 3 - safely unguessable: moderate protection from offline slow-hash scenario. (guesses < 10^10)
# 4 - very unguessable: strong protection from offline slow-hash scenario. (guesses >= 10^10) # 4 - very unguessable: strong protection from offline slow-hash scenario. (guesses >= 10^10)
passwords = ['pass', '12pass23', 'démarches ', 'démarches-simple', '{My-$3cure-p4ssWord}'] passwords = ['password', '12pass23', 'démarches ', 'démarches-simple', '{My-$3cure-p4ssWord}']
min_complexity = PASSWORD_COMPLEXITY_FOR_ADMIN min_complexity = PASSWORD_COMPLEXITY_FOR_ADMIN
context 'administrateurs' do subject do
let(:email) { 'mail@beta.gouv.fr' } user.valid?
let(:administrateur) { build(:user, email: email, password: password, administrateur: build(:administrateur)) } user.errors.full_messages
end
subject do context 'for administrateurs' do
administrateur.save let(:user) { build(:user, email: 'admin@exemple.fr', password: password, administrateur: build(:administrateur)) }
administrateur.errors.full_messages
end
context 'when password is too short' do context 'when the password is too short' do
let(:password) { 's' * (PASSWORD_MIN_LENGTH - 1) } let(:password) { 's' * (PASSWORD_MIN_LENGTH - 1) }
it { expect(subject).to eq(["Le mot de passe est trop court"]) } it 'reports an error about password length (but not about complexity)' do
expect(subject).to eq(["Le mot de passe est trop court"])
end
end end
context 'when password is too simple' do passwords[0..(min_complexity - 1)].each do |simple_password|
passwords[0..(min_complexity - 1)].each do |password| context 'when the password is long enough, but too simple' do
let(:password) { password } let(:password) { simple_password }
it { expect(subject).to eq(["Le mot de passe nest pas assez complexe"]) } it { expect(subject).to eq(["Le mot de passe nest pas assez complexe"]) }
end end
end end
context 'when password is acceptable' do context 'when the password is long and complex' do
let(:password) { passwords[min_complexity] } let(:password) { passwords[min_complexity] }
it { expect(subject).to eq([]) } it { expect(subject).to be_empty }
end end
end end
context 'simple users' do context 'for simple users' do
passwords.each do |password| let(:user) { build(:user, email: 'user@exemple.fr', password: password) }
let(:user) { build(:user, email: 'some@email.fr', password: password) }
it 'has no complexity validation' do context 'when the password is too short' do
user.save let(:password) { 's' * (PASSWORD_MIN_LENGTH - 1) }
expect(user.errors.full_messages).to eq([])
it 'reports an error about password length (but not about complexity)' do
expect(subject).to eq(["Le mot de passe est trop court"])
end
end
context 'when the password is long enough, but simple' do
let(:password) { 'simple-password' }
it 'doesnt enforce the password complexity' do
expect(subject).to be_empty
end end
end end
end end

View file

@ -64,9 +64,9 @@ module FeatureHelpers
def click_reset_password_link_for(email) def click_reset_password_link_for(email)
reset_password_email = open_email(email) reset_password_email = open_email(email)
token_params = reset_password_email.body.match(/reset_password_token=[^"]+/) reset_password_url = reset_password_email.body.match(/http[s]?:\/\/[^\/]+(\/[^\s]+reset_password_token=[^\s"]+)/)[1]
visit "/users/password/edit?#{token_params}" visit reset_password_url
end end
# Add a new type de champ in the procedure editor # Add a new type de champ in the procedure editor

View file

@ -1,10 +0,0 @@
include SmartListing::Helper
include SmartListing::Helper::ControllerExtensions
module SmartListing
module Helper
def view_context
'mock'
end
end
end

View file

@ -1,11 +1,13 @@
describe 'users/dossiers/dossier_actions.html.haml', type: :view do describe 'users/dossiers/dossier_actions.html.haml', type: :view do
let(:procedure) { create(:procedure, :published) } let(:procedure) { create(:procedure, :published) }
let(:dossier) { create(:dossier, :en_construction, procedure: procedure) } let(:dossier) { create(:dossier, :en_construction, procedure: procedure) }
let(:user) { dossier.user }
subject { render 'users/dossiers/dossier_actions.html.haml', dossier: dossier } subject { render 'users/dossiers/dossier_actions.html.haml', dossier: dossier, current_user: user }
it { is_expected.to have_link('Commencer un autre dossier', href: commencer_url(path: procedure.path)) } it { is_expected.to have_link('Commencer un autre dossier', href: commencer_url(path: procedure.path)) }
it { is_expected.to have_link('Supprimer le dossier', href: ask_deletion_dossier_path(dossier)) } it { is_expected.to have_link('Supprimer le dossier', href: ask_deletion_dossier_path(dossier)) }
it { is_expected.to have_link('Transferer le dossier', href: transferer_dossier_path(dossier)) }
context 'when the dossier cannot be deleted' do context 'when the dossier cannot be deleted' do
let(:dossier) { create(:dossier, :accepte, procedure: procedure) } let(:dossier) { create(:dossier, :accepte, procedure: procedure) }
@ -20,6 +22,7 @@ describe 'users/dossiers/dossier_actions.html.haml', type: :view do
context 'when there are no actions to display' do context 'when there are no actions to display' do
let(:procedure) { create(:procedure, :closed) } let(:procedure) { create(:procedure, :closed) }
let(:dossier) { create(:dossier, :accepte, procedure: procedure) } let(:dossier) { create(:dossier, :accepte, procedure: procedure) }
let(:user) { create(:user) }
it 'doesnt render the menu at all' do it 'doesnt render the menu at all' do
expect(subject).not_to have_selector('.dropdown') expect(subject).not_to have_selector('.dropdown')

View file

@ -12,6 +12,7 @@ describe 'users/dossiers/index.html.haml', type: :view do
assign(:user_dossiers, Kaminari.paginate_array(user_dossiers).page(1)) assign(:user_dossiers, Kaminari.paginate_array(user_dossiers).page(1))
assign(:dossiers_invites, Kaminari.paginate_array(dossiers_invites).page(1)) assign(:dossiers_invites, Kaminari.paginate_array(dossiers_invites).page(1))
assign(:dossiers_supprimes, Kaminari.paginate_array(user_dossiers).page(1)) assign(:dossiers_supprimes, Kaminari.paginate_array(user_dossiers).page(1))
assign(:dossier_transfers, Kaminari.paginate_array([]).page(1))
assign(:statut, statut) assign(:statut, statut)
render render
end end