commit
f44f0a1649
72 changed files with 731 additions and 407 deletions
3
Gemfile
3
Gemfile
|
@ -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
|
||||||
|
|
29
Gemfile.lock
29
Gemfile.lock
|
@ -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
|
||||||
|
|
40
app/assets/stylesheets/password_complexity.scss
Normal file
40
app/assets/stylesheets/password_complexity.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
13
app/controllers/concerns/devise_populated_resource.rb
Normal file
13
app/controllers/concerns/devise_populated_resource.rb
Normal 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
|
|
@ -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
|
||||||
|
|
15
app/controllers/password_complexity_controller.rb
Normal file
15
app/controllers/password_complexity_controller.rb
Normal 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
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
31
app/controllers/users/transfers_controller.rb
Normal file
31
app/controllers/users/transfers_controller.rb
Normal 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
|
7
app/jobs/cron/purge_stale_transfers_job.rb
Normal file
7
app/jobs/cron/purge_stale_transfers_job.rb
Normal 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
|
|
@ -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
|
||||||
|
|
23
app/models/concerns/password_complexity_concern.rb
Normal file
23
app/models/concerns/password_complexity_concern.rb
Normal 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
|
|
@ -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
|
||||||
|
|
||||||
|
|
53
app/models/dossier_transfer.rb
Normal file
53
app/models/dossier_transfer.rb
Normal 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
|
14
app/models/dossier_transfer_log.rb
Normal file
14
app/models/dossier_transfer_log.rb
Normal 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
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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..." }
|
||||||
|
|
|
@ -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
|
|
|
@ -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..." }
|
|
|
@ -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})
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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]})
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
9
app/views/dossier_mailer/notify_transfer.html.haml
Normal file
9
app/views/dossier_mailer/notify_transfer.html.haml
Normal 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"
|
1
app/views/password_complexity/_bar.html.haml
Normal file
1
app/views/password_complexity/_bar.html.haml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
#complexity-bar.password-complexity{ class: "complexity-#{@length < @min_length ? @score/2 : @score}" }
|
9
app/views/password_complexity/_field.html.haml
Normal file
9
app/views/password_complexity/_field.html.haml
Normal 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é.
|
|
@ -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.
|
3
app/views/password_complexity/show.js.erb
Normal file
3
app/views/password_complexity/show.js.erb
Normal 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};") %>
|
|
@ -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..." }
|
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
||||||
|
|
||||||
.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é.
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
#strength-bar.password-strength{ class: "strength-#{@length < @min_length ? @score/2 : @score}" }
|
|
|
@ -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};") %>
|
|
|
@ -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'
|
|
|
@ -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
|
||||||
|
|
32
app/views/users/dossiers/_transfered_dossiers_list.html.haml
Normal file
32
app/views/users/dossiers/_transfered_dossiers_list.html.haml
Normal 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.
|
|
@ -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 }
|
||||||
|
|
9
app/views/users/dossiers/transferer.html.haml
Normal file
9
app/views/users/dossiers/transferer.html.haml
Normal 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 d‘un 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'
|
7
app/views/users/dossiers/transferer_all.html.haml
Normal file
7
app/views/users/dossiers/transferer_all.html.haml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
.container.mt-4
|
||||||
|
Transferer les #{@transfer.dossiers.size} dossiers de votre compte vers le compte d‘un 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'
|
|
@ -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'
|
|
|
@ -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
|
||||||
|
|
1
config/initializers/otp.rb
Normal file
1
config/initializers/otp.rb
Normal file
|
@ -0,0 +1 @@
|
||||||
|
SUPER_ADMIN_OTP_ENABLED = ENV.fetch("SUPER_ADMIN_OTP_ENABLED", "enabled") == "enabled"
|
|
@ -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
|
|
|
@ -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
|
||||||
|
|
|
@ -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é
|
||||||
|
|
|
@ -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
|
|
12
config/locales/views/dossier_mailer/notify_transfer/fr.yml
Normal file
12
config/locales/views/dossier_mailer/notify_transfer/fr.yml
Normal 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 :
|
|
@ -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'
|
||||||
|
|
||||||
|
|
11
db/migrate/20210721140812_create_dossier_transfers.rb
Normal file
11
db/migrate/20210721140812_create_dossier_transfers.rb
Normal 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
|
11
db/migrate/20210721162213_create_dossier_transfer_logs.rb
Normal file
11
db/migrate/20210721162213_create_dossier_transfer_logs.rb
Normal 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
|
22
db/schema.rb
22
db/schema.rb
|
@ -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"
|
||||||
|
|
38
spec/controllers/concerns/devise_populated_resource_spec.rb
Normal file
38
spec/controllers/concerns/devise_populated_resource_spec.rb
Normal 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
|
|
@ -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
|
||||||
|
|
35
spec/controllers/password_complexity_controller_spec.rb
Normal file
35
spec/controllers/password_complexity_controller_spec.rb
Normal 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
|
|
@ -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
|
|
|
@ -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'
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
32
spec/features/users/transfer_dossier_spec.rb
Normal file
32
spec/features/users/transfer_dossier_spec.rb
Normal 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
|
|
@ -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
|
||||||
|
|
50
spec/models/dossier_transfer_spec.rb
Normal file
50
spec/models/dossier_transfer_spec.rb
Normal 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
|
|
@ -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 n’est pas assez complexe"]) }
|
it { expect(subject).to eq(["Le mot de passe n’est 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
|
||||||
|
|
|
@ -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 n’est pas assez complexe"]) }
|
it { expect(subject).to eq(["Le mot de passe n’est 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 'doesn’t enforce the password complexity' do
|
||||||
|
expect(subject).to be_empty
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
include SmartListing::Helper
|
|
||||||
include SmartListing::Helper::ControllerExtensions
|
|
||||||
|
|
||||||
module SmartListing
|
|
||||||
module Helper
|
|
||||||
def view_context
|
|
||||||
'mock'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -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 'doesn’t render the menu at all' do
|
it 'doesn’t render the menu at all' do
|
||||||
expect(subject).not_to have_selector('.dropdown')
|
expect(subject).not_to have_selector('.dropdown')
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue