Merge branch 'dev' into fix_3417_departement_with_empty_value
This commit is contained in:
commit
b2f5ff8690
47 changed files with 564 additions and 244 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -25,3 +25,4 @@ storage/
|
|||
/node_modules
|
||||
yarn-debug.log*
|
||||
.yarn-integrity
|
||||
/.vscode
|
||||
|
|
1
Gemfile
1
Gemfile
|
@ -99,6 +99,7 @@ group :development, :test do
|
|||
gem 'pry-byebug'
|
||||
gem 'rspec-rails'
|
||||
gem 'rspec_junit_formatter'
|
||||
gem 'ruby-debug-ide', require: false
|
||||
gem 'spring' # Spring speeds up development by keeping your application running in the background
|
||||
gem 'spring-commands-rspec'
|
||||
end
|
||||
|
|
|
@ -510,6 +510,8 @@ GEM
|
|||
unicode-display_width (~> 1.4.0)
|
||||
rubocop-rspec-focused (1.0.0)
|
||||
rubocop (>= 0.51)
|
||||
ruby-debug-ide (0.6.1)
|
||||
rake (>= 0.8.1)
|
||||
ruby-progressbar (1.10.0)
|
||||
ruby_dep (1.5.0)
|
||||
ruby_parser (3.12.0)
|
||||
|
@ -713,6 +715,7 @@ DEPENDENCIES
|
|||
rspec_junit_formatter
|
||||
rubocop
|
||||
rubocop-rspec-focused
|
||||
ruby-debug-ide
|
||||
sanitize-url
|
||||
sassc-rails
|
||||
scenic
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
@import "colors";
|
||||
|
||||
.direct-upload {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
padding: 2px 4px;
|
||||
padding: 4px 15px;
|
||||
margin: 0 3px 3px 0;
|
||||
border: 1px solid rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid $border-grey;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
line-height: 13px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.direct-upload--pending {
|
||||
|
|
|
@ -111,6 +111,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.direct-upload {
|
||||
margin-bottom: 2 * $default-padding;
|
||||
}
|
||||
|
||||
.add-row {
|
||||
margin-bottom: 2 * $default-padding;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
class ApplicationController < ActionController::Base
|
||||
include TrustedDeviceConcern
|
||||
|
||||
MAINTENANCE_MESSAGE = 'Le site est actuellement en maintenance. Il sera à nouveau disponible dans un court instant.'
|
||||
|
||||
# Prevent CSRF attacks by raising an exception.
|
||||
|
@ -6,6 +8,7 @@ class ApplicationController < ActionController::Base
|
|||
protect_from_forgery with: :exception, if: -> { !Rails.env.test? }
|
||||
before_action :load_navbar_left_pannel_partial_url
|
||||
before_action :set_raven_context
|
||||
before_action :redirect_if_untrusted
|
||||
before_action :authorize_request_for_profiler
|
||||
before_action :reject, if: -> { Flipflop.maintenance_mode? }
|
||||
|
||||
|
@ -151,4 +154,34 @@ class ApplicationController < ActionController::Base
|
|||
redirect_to root_path
|
||||
end
|
||||
end
|
||||
|
||||
def redirect_if_untrusted
|
||||
if gestionnaire_signed_in? &&
|
||||
sensitive_path &&
|
||||
current_gestionnaire.feature_enabled?(:enable_email_login_token) &&
|
||||
!trusted_device?
|
||||
|
||||
# return at this location
|
||||
# after the device is trusted
|
||||
store_location_for(:user, request.fullpath)
|
||||
|
||||
send_login_token_or_bufferize(current_gestionnaire)
|
||||
redirect_to link_sent_path(email: current_gestionnaire.email)
|
||||
end
|
||||
end
|
||||
|
||||
def sensitive_path
|
||||
path = request.path_info
|
||||
|
||||
if path == '/' ||
|
||||
path == '/users/sign_out' ||
|
||||
path.start_with?('/connexion-par-jeton') ||
|
||||
path.start_with?('/api/') ||
|
||||
path.start_with?('/lien-envoye')
|
||||
|
||||
false
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -29,6 +29,17 @@ module Manager
|
|||
redirect_to manager_dossier_path(dossier)
|
||||
end
|
||||
|
||||
def hide
|
||||
dossier = Dossier.find(params[:id])
|
||||
deleted_dossier = dossier.hide!(current_administration)
|
||||
|
||||
DossierMailer.notify_deletion_to_user(deleted_dossier, dossier.user.email).deliver_later
|
||||
logger.info("Le dossier #{dossier.id} est supprimé par #{current_administration.email}")
|
||||
flash[:notice] = "Le dossier #{dossier.id} est supprimé"
|
||||
|
||||
redirect_to manager_dossier_path(dossier)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def unfiltered_list?
|
||||
|
|
|
@ -23,20 +23,7 @@ class Users::SessionsController < Sessions::SessionsController
|
|||
current_user.update(loged_in_with_france_connect: nil)
|
||||
end
|
||||
|
||||
if gestionnaire_signed_in?
|
||||
if trusted_device? || !current_gestionnaire.feature_enabled?(:enable_email_login_token)
|
||||
set_flash_message :notice, :signed_in
|
||||
redirect_to after_sign_in_path_for(:user)
|
||||
else
|
||||
gestionnaire = current_gestionnaire
|
||||
|
||||
send_login_token_or_bufferize(gestionnaire)
|
||||
|
||||
[:user, :gestionnaire, :administrateur].each { |role| sign_out(role) }
|
||||
|
||||
redirect_to link_sent_path(email: gestionnaire.email)
|
||||
end
|
||||
elsif user_signed_in?
|
||||
if gestionnaire_signed_in? || user_signed_in?
|
||||
set_flash_message :notice, :signed_in
|
||||
redirect_to after_sign_in_path_for(:user)
|
||||
else
|
||||
|
@ -83,32 +70,35 @@ class Users::SessionsController < Sessions::SessionsController
|
|||
|
||||
def sign_in_by_link
|
||||
gestionnaire = Gestionnaire.find(params[:id])
|
||||
if gestionnaire&.login_token_valid?(params[:jeton])
|
||||
trust_device
|
||||
flash.notice = "Merci d’avoir confirmé votre connexion. Votre navigateur est maintenant authentifié pour #{TRUSTED_DEVICE_PERIOD.to_i / ActiveSupport::Duration::SECONDS_PER_DAY} jours."
|
||||
trusted_device_token = gestionnaire
|
||||
.trusted_device_tokens
|
||||
.find_by(token: params[:jeton])
|
||||
|
||||
user = User.find_by(email: gestionnaire.email)
|
||||
administrateur = Administrateur.find_by(email: gestionnaire.email)
|
||||
[user, gestionnaire, administrateur].compact.each { |resource| sign_in(resource) }
|
||||
if trusted_device_token&.token_valid?
|
||||
trust_device(trusted_device_token.created_at)
|
||||
|
||||
period = ((trusted_device_token.created_at + TRUSTED_DEVICE_PERIOD) - Time.zone.now).to_i / ActiveSupport::Duration::SECONDS_PER_DAY
|
||||
|
||||
flash.notice = "Merci d’avoir confirmé votre connexion. Votre navigateur est maintenant authentifié pour #{period} jours."
|
||||
|
||||
# redirect to procedure'url if stored by store_location_for(:user) in dossiers_controller
|
||||
# redirect to root_path otherwise
|
||||
redirect_to after_sign_in_path_for(:user)
|
||||
|
||||
if gestionnaire_signed_in?
|
||||
redirect_to after_sign_in_path_for(:user)
|
||||
else
|
||||
redirect_to new_user_session_path
|
||||
end
|
||||
else
|
||||
flash[:alert] = 'Votre lien est invalide ou expiré, veuillez-vous reconnecter.'
|
||||
redirect_to new_user_session_path
|
||||
flash[:alert] = 'Votre lien est invalide ou expiré, un nouveau vient de vous être envoyé.'
|
||||
|
||||
send_login_token_or_bufferize(gestionnaire)
|
||||
redirect_to link_sent_path(email: gestionnaire.email)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def send_login_token_or_bufferize(gestionnaire)
|
||||
if !gestionnaire.young_login_token?
|
||||
login_token = gestionnaire.login_token!
|
||||
GestionnaireMailer.send_login_token(gestionnaire, login_token).deliver_later
|
||||
end
|
||||
end
|
||||
|
||||
def try_to_authenticate(klass, remember_me = false)
|
||||
resource = klass.find_for_database_authentication(email: params[:user][:email])
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
require "administrate/field/base"
|
||||
|
||||
class ProcedureLinkField < Administrate::Field::Base
|
||||
class ProcedureLinkField < Administrate::Field::String
|
||||
def name
|
||||
"Lien démarche"
|
||||
end
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { getJSON, debounce } from '@utils';
|
||||
import { DirectUpload } from 'activestorage';
|
||||
import Uploader from '../../shared/activestorage/uploader';
|
||||
|
||||
export default {
|
||||
props: ['state', 'index', 'item'],
|
||||
|
@ -181,7 +181,12 @@ export default {
|
|||
const file = input.files[0];
|
||||
if (file) {
|
||||
this.isUploading = true;
|
||||
uploadFile(this.state.directUploadUrl, file).then(({ signed_id }) => {
|
||||
const controller = new Uploader(
|
||||
input,
|
||||
file,
|
||||
this.state.directUploadUrl
|
||||
);
|
||||
controller.start().then(signed_id => {
|
||||
this.pieceJustificativeTemplate = signed_id;
|
||||
this.isUploading = false;
|
||||
this.debouncedSave();
|
||||
|
@ -247,17 +252,3 @@ const EXCLUDE_FROM_REPETITION = [
|
|||
function castBoolean(value) {
|
||||
return value && value != 0;
|
||||
}
|
||||
|
||||
function uploadFile(directUploadUrl, file) {
|
||||
const upload = new DirectUpload(file, directUploadUrl);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
upload.create((error, blob) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(blob);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import Rails from 'rails-ujs';
|
|||
import * as ActiveStorage from 'activestorage';
|
||||
import jQuery from 'jquery';
|
||||
|
||||
import '../shared/activestorage/progress';
|
||||
import '../shared/activestorage/ujs';
|
||||
import '../shared/sentry';
|
||||
import '../shared/rails-ujs-fix';
|
||||
import '../shared/safari-11-file-xhr-workaround';
|
||||
|
|
|
@ -5,7 +5,7 @@ import * as ActiveStorage from 'activestorage';
|
|||
import Chartkick from 'chartkick';
|
||||
import Highcharts from 'highcharts';
|
||||
|
||||
import '../shared/activestorage/progress';
|
||||
import '../shared/activestorage/ujs';
|
||||
import '../shared/sentry';
|
||||
import '../shared/rails-ujs-fix';
|
||||
import '../shared/safari-11-file-xhr-workaround';
|
||||
|
|
92
app/javascript/shared/activestorage/progress-bar.js
Normal file
92
app/javascript/shared/activestorage/progress-bar.js
Normal file
|
@ -0,0 +1,92 @@
|
|||
const PENDING_CLASS = 'direct-upload--pending';
|
||||
const ERROR_CLASS = 'direct-upload--error';
|
||||
const COMPLETE_CLASS = 'direct-upload--complete';
|
||||
|
||||
/**
|
||||
ProgressBar is and utility class responsible for
|
||||
rendering upload progress bar. It is used to handle
|
||||
direct-upload form ujs events but also in the
|
||||
Uploader delegate used with uploads on json api.
|
||||
*/
|
||||
export default class ProgressBar {
|
||||
static init(input, id, file) {
|
||||
clearErrors(input);
|
||||
const html = this.render(id, file.name);
|
||||
input.insertAdjacentHTML('beforebegin', html);
|
||||
}
|
||||
|
||||
static start(id) {
|
||||
const element = getDirectUploadElement(id);
|
||||
|
||||
element.classList.remove(PENDING_CLASS);
|
||||
}
|
||||
|
||||
static progress(id, progress) {
|
||||
const element = getDirectUploadProgressElement(id);
|
||||
|
||||
element.style.width = `${progress}%`;
|
||||
}
|
||||
|
||||
static error(id, error) {
|
||||
const element = getDirectUploadElement(id);
|
||||
|
||||
element.classList.add(ERROR_CLASS);
|
||||
element.setAttribute('title', error);
|
||||
}
|
||||
|
||||
static end(id) {
|
||||
const element = getDirectUploadElement(id);
|
||||
|
||||
element.classList.add(COMPLETE_CLASS);
|
||||
}
|
||||
|
||||
static render(id, filename) {
|
||||
return `<div id="direct-upload-${id}" class="direct-upload ${PENDING_CLASS}">
|
||||
<div class="direct-upload__progress" style="width: 0%"></div>
|
||||
<span class="direct-upload__filename">${filename}</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
constructor(input, id, file) {
|
||||
this.constructor.init(input, id, file);
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
start() {
|
||||
this.constructor.start(this.id);
|
||||
}
|
||||
|
||||
progress(progress) {
|
||||
this.constructor.progress(this.id, progress);
|
||||
}
|
||||
|
||||
error(error) {
|
||||
this.constructor.error(this.id, error);
|
||||
}
|
||||
|
||||
end() {
|
||||
this.constructor.end(this.id);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
const element = getDirectUploadElement(this.id);
|
||||
element.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function clearErrors(input) {
|
||||
const errorElements = input.parentElement.querySelectorAll(`.${ERROR_CLASS}`);
|
||||
for (let element of errorElements) {
|
||||
element.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function getDirectUploadElement(id) {
|
||||
return document.getElementById(`direct-upload-${id}`);
|
||||
}
|
||||
|
||||
function getDirectUploadProgressElement(id) {
|
||||
return document.querySelector(
|
||||
`#direct-upload-${id} .direct-upload__progress`
|
||||
);
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
addEventListener('direct-upload:initialize', event => {
|
||||
const target = event.target,
|
||||
detail = event.detail,
|
||||
id = detail.id,
|
||||
file = detail.file;
|
||||
|
||||
target.insertAdjacentHTML(
|
||||
'beforebegin',
|
||||
'\n<div id="direct-upload-' +
|
||||
id +
|
||||
'" class="direct-upload direct-upload--pending">\n<div id="direct-upload-progress-' +
|
||||
id +
|
||||
'" class="direct-upload__progress" style="width: 0%"></div>\n<span class="direct-upload__filename">' +
|
||||
file.name +
|
||||
'</span>\n</div>\n'
|
||||
);
|
||||
});
|
||||
|
||||
addEventListener('direct-upload:start', event => {
|
||||
const id = event.detail.id,
|
||||
element = document.getElementById('direct-upload-' + id);
|
||||
|
||||
element.classList.remove('direct-upload--pending');
|
||||
});
|
||||
|
||||
addEventListener('direct-upload:progress', event => {
|
||||
const id = event.detail.id,
|
||||
progress = event.detail.progress,
|
||||
progressElement = document.getElementById('direct-upload-progress-' + id);
|
||||
|
||||
progressElement.style.width = `${progress} %`;
|
||||
});
|
||||
|
||||
addEventListener('direct-upload:error', event => {
|
||||
event.preventDefault();
|
||||
const id = event.detail.id,
|
||||
error = event.detail.error,
|
||||
element = document.getElementById('direct-upload-' + id);
|
||||
|
||||
element.classList.add('direct-upload--error');
|
||||
element.setAttribute('title', error);
|
||||
});
|
||||
|
||||
addEventListener('direct-upload:end', event => {
|
||||
const id = event.detail.id,
|
||||
element = document.getElementById('direct-upload-' + id);
|
||||
|
||||
element.classList.add('direct-upload--complete');
|
||||
});
|
27
app/javascript/shared/activestorage/ujs.js
Normal file
27
app/javascript/shared/activestorage/ujs.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
import ProgressBar from './progress-bar';
|
||||
|
||||
const INITIALIZE_EVENT = 'direct-upload:initialize';
|
||||
const START_EVENT = 'direct-upload:start';
|
||||
const PROGRESS_EVENT = 'direct-upload:progress';
|
||||
const ERROR_EVENT = 'direct-upload:error';
|
||||
const END_EVENT = 'direct-upload:end';
|
||||
|
||||
addEventListener(INITIALIZE_EVENT, ({ target, detail: { id, file } }) => {
|
||||
ProgressBar.init(target, id, file);
|
||||
});
|
||||
|
||||
addEventListener(START_EVENT, ({ detail: { id } }) => {
|
||||
ProgressBar.start(id);
|
||||
});
|
||||
|
||||
addEventListener(PROGRESS_EVENT, ({ detail: { id, progress } }) => {
|
||||
ProgressBar.progress(id, progress);
|
||||
});
|
||||
|
||||
addEventListener(ERROR_EVENT, ({ detail: { id, error } }) => {
|
||||
ProgressBar.error(id, error);
|
||||
});
|
||||
|
||||
addEventListener(END_EVENT, ({ detail: { id } }) => {
|
||||
ProgressBar.end(id);
|
||||
});
|
43
app/javascript/shared/activestorage/uploader.js
Normal file
43
app/javascript/shared/activestorage/uploader.js
Normal file
|
@ -0,0 +1,43 @@
|
|||
import { DirectUpload } from 'activestorage';
|
||||
import ProgressBar from './progress-bar';
|
||||
|
||||
/**
|
||||
Uploader class is a delegate for DirectUpload instance
|
||||
used to track lifecycle and progress of un upload.
|
||||
*/
|
||||
export default class Uploader {
|
||||
constructor(input, file, directUploadUrl) {
|
||||
this.directUpload = new DirectUpload(file, directUploadUrl, this);
|
||||
this.progressBar = new ProgressBar(input, this.directUpload.id, file);
|
||||
}
|
||||
|
||||
start() {
|
||||
this.progressBar.start();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.directUpload.create((error, attributes) => {
|
||||
if (error) {
|
||||
this.progressBar.error(error);
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(attributes.signed_id);
|
||||
}
|
||||
this.progressBar.end();
|
||||
this.progressBar.destroy();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
uploadRequestDidProgress(event) {
|
||||
const progress = (event.loaded / event.total) * 100;
|
||||
if (progress) {
|
||||
this.progressBar.progress(progress);
|
||||
}
|
||||
}
|
||||
|
||||
directUploadWillStoreFileWithXHR(xhr) {
|
||||
xhr.upload.addEventListener('progress', event =>
|
||||
this.uploadRequestDidProgress(event)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -27,7 +27,7 @@ module ActiveStorage
|
|||
|
||||
def publicize(url)
|
||||
search = %r{^https://[^/]+/v1/AUTH_[a-f0-9]{32}}
|
||||
replace = "https://#{ENV['APP_HOST']}/direct-upload"
|
||||
replace = 'https://static.demarches-simplifiees.fr'
|
||||
url.gsub(search, replace)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,10 +4,10 @@ module TrustedDeviceConcern
|
|||
TRUSTED_DEVICE_COOKIE_NAME = :trusted_device
|
||||
TRUSTED_DEVICE_PERIOD = 1.month
|
||||
|
||||
def trust_device
|
||||
def trust_device(start_at)
|
||||
cookies.encrypted[TRUSTED_DEVICE_COOKIE_NAME] = {
|
||||
value: JSON.generate({ created_at: Time.zone.now }),
|
||||
expires: TRUSTED_DEVICE_PERIOD,
|
||||
value: JSON.generate({ created_at: start_at }),
|
||||
expires: start_at + TRUSTED_DEVICE_PERIOD,
|
||||
httponly: true
|
||||
}
|
||||
end
|
||||
|
@ -17,6 +17,13 @@ module TrustedDeviceConcern
|
|||
(Time.zone.now - TRUSTED_DEVICE_PERIOD) < trusted_device_cookie_created_at
|
||||
end
|
||||
|
||||
def send_login_token_or_bufferize(gestionnaire)
|
||||
if !gestionnaire.young_login_token?
|
||||
login_token = gestionnaire.create_trusted_device_token
|
||||
GestionnaireMailer.send_login_token(gestionnaire, login_token).deliver_later
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def trusted_device_cookie_created_at
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
class DeletedDossier < ApplicationRecord
|
||||
belongs_to :procedure
|
||||
|
||||
def self.create_from_dossier(dossier)
|
||||
DeletedDossier.create!(dossier_id: dossier.id, procedure: dossier.procedure, state: dossier.state, deleted_at: Time.now.utc)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -258,9 +258,8 @@ class Dossier < ApplicationRecord
|
|||
end
|
||||
|
||||
def delete_and_keep_track
|
||||
now = Time.zone.now
|
||||
deleted_dossier = DeletedDossier.create!(dossier_id: id, procedure: procedure, state: state, deleted_at: now)
|
||||
update(hidden_at: now)
|
||||
deleted_dossier = DeletedDossier.create_from_dossier(self)
|
||||
update(hidden_at: deleted_dossier.deleted_at)
|
||||
|
||||
if en_construction?
|
||||
administration_emails = followers_gestionnaires.present? ? followers_gestionnaires.pluck(:email) : [procedure.administrateur.email]
|
||||
|
@ -318,6 +317,13 @@ class Dossier < ApplicationRecord
|
|||
log_dossier_operation(nil, :accepter, automatic_operation: true)
|
||||
end
|
||||
|
||||
def hide!(administration)
|
||||
update(hidden_at: Time.zone.now)
|
||||
|
||||
log_administration_dossier_operation(administration, :supprimer)
|
||||
DeletedDossier.create_from_dossier(self)
|
||||
end
|
||||
|
||||
def refuser!(gestionnaire, motivation)
|
||||
self.motivation = motivation
|
||||
self.en_instruction_at ||= Time.zone.now
|
||||
|
@ -356,6 +362,13 @@ class Dossier < ApplicationRecord
|
|||
)
|
||||
end
|
||||
|
||||
def log_administration_dossier_operation(administration, operation)
|
||||
dossier_operation_logs.create(
|
||||
administration: administration,
|
||||
operation: DossierOperationLog.operations.fetch(operation)
|
||||
)
|
||||
end
|
||||
|
||||
def update_state_dates
|
||||
if en_construction? && !self.en_construction_at
|
||||
self.en_construction_at = Time.zone.now
|
||||
|
|
|
@ -4,9 +4,11 @@ class DossierOperationLog < ApplicationRecord
|
|||
repasser_en_construction: 'repasser_en_construction',
|
||||
accepter: 'accepter',
|
||||
refuser: 'refuser',
|
||||
classer_sans_suite: 'classer_sans_suite'
|
||||
classer_sans_suite: 'classer_sans_suite',
|
||||
supprimer: 'supprimer'
|
||||
}
|
||||
|
||||
belongs_to :dossier
|
||||
belongs_to :gestionnaire
|
||||
belongs_to :administration
|
||||
end
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
class Gestionnaire < ApplicationRecord
|
||||
include CredentialsSyncableConcern
|
||||
include EmailSanitizableConcern
|
||||
include ActiveRecord::SecureToken
|
||||
|
||||
LOGIN_TOKEN_VALIDITY = 45.minutes
|
||||
LOGIN_TOKEN_YOUTH = 15.minutes
|
||||
|
||||
devise :database_authenticatable, :registerable, :async,
|
||||
:recoverable, :rememberable, :trackable, :validatable
|
||||
|
@ -20,6 +16,7 @@ class Gestionnaire < ApplicationRecord
|
|||
has_many :followed_dossiers, through: :follows, source: :dossier
|
||||
has_many :avis
|
||||
has_many :dossiers_from_avis, through: :avis, source: :dossier
|
||||
has_many :trusted_device_tokens
|
||||
|
||||
def visible_procedures
|
||||
procedures.merge(Procedure.avec_lien.or(Procedure.archivees))
|
||||
|
@ -135,18 +132,9 @@ class Gestionnaire < ApplicationRecord
|
|||
Dossier.where(id: dossiers_id_with_notifications(dossiers)).group(:procedure_id).count
|
||||
end
|
||||
|
||||
def login_token!
|
||||
login_token = Gestionnaire.generate_unique_secure_token
|
||||
encrypted_login_token = BCrypt::Password.create(login_token)
|
||||
update(encrypted_login_token: encrypted_login_token, login_token_created_at: Time.zone.now)
|
||||
login_token
|
||||
end
|
||||
|
||||
def login_token_valid?(login_token)
|
||||
BCrypt::Password.new(encrypted_login_token) == login_token &&
|
||||
LOGIN_TOKEN_VALIDITY.ago < login_token_created_at
|
||||
rescue BCrypt::Errors::InvalidHash
|
||||
false
|
||||
def create_trusted_device_token
|
||||
trusted_device_token = trusted_device_tokens.create
|
||||
trusted_device_token.token
|
||||
end
|
||||
|
||||
def dossiers_id_with_notifications(dossiers)
|
||||
|
@ -213,8 +201,8 @@ class Gestionnaire < ApplicationRecord
|
|||
end
|
||||
|
||||
def young_login_token?
|
||||
login_token_created_at.present? &&
|
||||
LOGIN_TOKEN_YOUTH.ago < login_token_created_at
|
||||
trusted_device_token = trusted_device_tokens.order(created_at: :desc).first
|
||||
trusted_device_token&.token_young?
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -7,11 +7,10 @@ class Service < ApplicationRecord
|
|||
enum type_organisme: {
|
||||
administration_centrale: 'administration_centrale',
|
||||
association: 'association',
|
||||
commune: 'commune',
|
||||
departement: 'departement',
|
||||
collectivite_territoriale: 'collectivite_territoriale',
|
||||
etablissement_enseignement: 'etablissement_enseignement',
|
||||
prefecture: 'prefecture',
|
||||
region: 'region',
|
||||
operateur_d_etat: "operateur_d_etat",
|
||||
service_deconcentre_de_l_etat: 'service_deconcentre_de_l_etat',
|
||||
autre: 'autre'
|
||||
}
|
||||
|
||||
|
|
15
app/models/trusted_device_token.rb
Normal file
15
app/models/trusted_device_token.rb
Normal file
|
@ -0,0 +1,15 @@
|
|||
class TrustedDeviceToken < ApplicationRecord
|
||||
LOGIN_TOKEN_VALIDITY = 1.week
|
||||
LOGIN_TOKEN_YOUTH = 15.minutes
|
||||
|
||||
belongs_to :gestionnaire
|
||||
has_secure_token
|
||||
|
||||
def token_valid?
|
||||
LOGIN_TOKEN_VALIDITY.ago < created_at
|
||||
end
|
||||
|
||||
def token_young?
|
||||
LOGIN_TOKEN_YOUTH.ago < created_at
|
||||
end
|
||||
end
|
|
@ -10,7 +10,11 @@
|
|||
- else
|
||||
%small Désactivée
|
||||
|
||||
%p.notice Les attestations, si elles sont activées, sont délivrées par email aux usagers lorsque leurs dossiers sont acceptés, et sont également disponibles au téléchargement sur leur espace personnel.
|
||||
%p.notice
|
||||
L’attestation, si elle est activée, est émise au moment où un dossier est accepté.
|
||||
%br
|
||||
L’email d’accusé d’acceptation envoyé à l’usager comporte alors un lien vers l’attestation ;
|
||||
celle-ci est également disponible au téléchargement depuis l’espace personnel de l’usager.
|
||||
|
||||
.image-upload
|
||||
- if @attestation_template.logo.present?
|
||||
|
|
|
@ -4,14 +4,20 @@
|
|||
Bonjour,
|
||||
|
||||
%p
|
||||
Les comptes administrateurs sont destinés aux administrations publiques souhaitant mettre en place des démarches dématérialisées avec demarches-simplifiees.fr. Il ne semble pas que ce soit votre cas.
|
||||
Les comptes administrateurs sont destinés aux administrations publiques souhaitant mettre en place sur leurs sites internet des démarches dématérialisées pour leurs usagers. Il ne semble pas que ce soit votre cas.
|
||||
|
||||
|
||||
%p
|
||||
Pour les usagers qui souhaitent remplir une démarche, l’entrée dans demarches-simplifiees.fr se fait via un lien fourni par l’administration responsable, sur son propre site web. Ce lien vous permettra de créer un compte et de remplir le formulaire dans la foulée.
|
||||
Pour les usagers ou les administrations publiques (collectivités, etc.) qui souhaitent remplir une démarche ou un déposer un dossier en ligne, l’entrée dans demarches-simplifiees.fr se fait via un lien fourni par l’administration responsable, sur son propre site web. Ce lien vous permettra de créer un compte et de remplir le formulaire dans la foulée.
|
||||
|
||||
%p
|
||||
Si par contre vous rencontrez des problèmes lors de l'utilisation de demarches-simplifiees.fr en tant qu'usager, merci d'expliciter le problème rencontré sur notre
|
||||
= link_to("formulaire de contact", contact_url)
|
||||
\.
|
||||
|
||||
%p
|
||||
Si vous avez fait une demande de compte administrateur légitime avec une adresse email grand public (Orange, Wanadoo etc), merci de nous contacter sur notre
|
||||
= link_to("formulaire de contact administrateur", contact_admin_url)
|
||||
\.
|
||||
|
||||
= render partial: "layouts/mailers/signature"
|
||||
|
|
|
@ -23,7 +23,7 @@ as well as a link to its edit page.
|
|||
<h1 class="main-content__page-title">
|
||||
<%= content_for(:title) %>
|
||||
<% if dossier.hidden_at %>
|
||||
(SUPPRIMÉ)
|
||||
(Supprimé)
|
||||
<% end %>
|
||||
</h1>
|
||||
|
||||
|
@ -31,6 +31,9 @@ as well as a link to its edit page.
|
|||
<% if dossier.termine? %>
|
||||
<%= link_to 'Repasser en instruction', change_state_to_instruction_manager_dossier_path(dossier), method: :post, class: 'button', data: { confirm: "Repasser en instruction ?" } %>
|
||||
<% end %>
|
||||
<% if dossier.hidden_at.nil? %>
|
||||
<%= link_to 'Supprimer le dossier', hide_manager_dossier_path(dossier), method: :post, class: 'button', data: { confirm: "Confirmez vous la suppression du dossier ?" } %>
|
||||
<% end %>
|
||||
<div>
|
||||
</header>
|
||||
|
||||
|
|
|
@ -33,6 +33,8 @@ as well as a link to its edit page.
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<%= link_to 'aperçu', apercu_procedure_path(procedure), class: 'button' %>
|
||||
|
||||
<% if !procedure.whitelisted? %>
|
||||
<%= link_to 'whitelister', whitelist_manager_procedure_path(procedure), method: :post, class: 'button' %>
|
||||
<% end %>
|
||||
|
|
|
@ -5,7 +5,10 @@
|
|||
.title-container
|
||||
%span.icon.folder
|
||||
%h1= dossier.procedure.libelle
|
||||
%h2 Dossier nº #{dossier.id} - Déposé le #{l(dossier.en_construction_at, format: '%d %B %Y')}
|
||||
%h2
|
||||
Dossier nº #{dossier.id}
|
||||
- if dossier.en_construction_at.present?
|
||||
= "- Déposé le #{l(dossier.en_construction_at, format: '%d %B %Y')}"
|
||||
|
||||
- if current_user.owns?(dossier)
|
||||
.header-actions
|
||||
|
|
|
@ -75,18 +75,18 @@
|
|||
name: :save_draft,
|
||||
value: true,
|
||||
class: 'button send secondary',
|
||||
data: { disable: true }
|
||||
data: { 'disable-with': "Envoi en cours…" }
|
||||
|
||||
- if dossier.can_transition_to_en_construction?
|
||||
= f.button 'Soumettre le dossier',
|
||||
class: 'button send primary',
|
||||
disabled: !current_user.owns?(dossier),
|
||||
data: { disable: true }
|
||||
data: { 'disable-with': "Envoi en cours…" }
|
||||
|
||||
- else
|
||||
= f.button 'Enregistrer les modifications du dossier',
|
||||
class: 'button send primary',
|
||||
data: { disable: true }
|
||||
data: { 'disable-with': "Envoi en cours…" }
|
||||
|
||||
- if dossier.brouillon? && !current_user.owns?(dossier)
|
||||
.send-notice.invite-cannot-submit
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
fr:
|
||||
type_organisme:
|
||||
administration_centrale: 'administration centrale'
|
||||
association: 'association'
|
||||
commune: 'commune'
|
||||
departement: 'département'
|
||||
etablissement_enseignement: 'établissement d’enseignement'
|
||||
prefecture: 'préfecture'
|
||||
region: 'région'
|
||||
autre: 'autre'
|
||||
administration_centrale: 'Administration centrale'
|
||||
association: 'Association'
|
||||
collectivite_territoriale: 'Collectivité territoriale'
|
||||
etablissement_enseignement: 'Établissement d’enseignement'
|
||||
operateur_d_etat: "Opérateur d'État"
|
||||
service_deconcentre_de_l_etat: "Service déconcentré de l'État"
|
||||
autre: 'Autre'
|
||||
|
|
|
@ -14,6 +14,7 @@ Rails.application.routes.draw do
|
|||
|
||||
resources :dossiers, only: [:index, :show] do
|
||||
post 'change_state_to_instruction', on: :member
|
||||
post 'hide', on: :member
|
||||
end
|
||||
|
||||
resources :administrateurs, only: [:index, :show, :new, :create] do
|
||||
|
|
11
db/migrate/20190201164951_create_trusted_device_tokens.rb
Normal file
11
db/migrate/20190201164951_create_trusted_device_tokens.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
class CreateTrustedDeviceTokens < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
create_table :trusted_device_tokens do |t|
|
||||
t.string :token, null: false
|
||||
t.references :gestionnaire, foreign_key: true
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
add_index :trusted_device_tokens, :token, unique: true
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
class AddAdministrationColumnToLogDossierOperation < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
add_reference :dossier_operation_logs, :administration, foreign_key: true
|
||||
end
|
||||
end
|
15
db/schema.rb
15
db/schema.rb
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 2019_01_10_163655) do
|
||||
ActiveRecord::Schema.define(version: 2019_02_13_144145) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
@ -219,6 +219,8 @@ ActiveRecord::Schema.define(version: 2019_01_10_163655) do
|
|||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.boolean "automatic_operation", default: false, null: false
|
||||
t.bigint "administration_id"
|
||||
t.index ["administration_id"], name: "index_dossier_operation_logs_on_administration_id"
|
||||
t.index ["dossier_id"], name: "index_dossier_operation_logs_on_dossier_id"
|
||||
t.index ["gestionnaire_id"], name: "index_dossier_operation_logs_on_gestionnaire_id"
|
||||
end
|
||||
|
@ -518,6 +520,15 @@ ActiveRecord::Schema.define(version: 2019_01_10_163655) do
|
|||
t.string "version", null: false
|
||||
end
|
||||
|
||||
create_table "trusted_device_tokens", force: :cascade do |t|
|
||||
t.string "token", null: false
|
||||
t.bigint "gestionnaire_id"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["gestionnaire_id"], name: "index_trusted_device_tokens_on_gestionnaire_id"
|
||||
t.index ["token"], name: "index_trusted_device_tokens_on_token", unique: true
|
||||
end
|
||||
|
||||
create_table "types_de_champ", id: :serial, force: :cascade do |t|
|
||||
t.string "libelle"
|
||||
t.string "type_champ"
|
||||
|
@ -597,6 +608,7 @@ ActiveRecord::Schema.define(version: 2019_01_10_163655) do
|
|||
add_foreign_key "champs", "champs", column: "parent_id"
|
||||
add_foreign_key "closed_mails", "procedures"
|
||||
add_foreign_key "commentaires", "dossiers"
|
||||
add_foreign_key "dossier_operation_logs", "administrations"
|
||||
add_foreign_key "dossier_operation_logs", "dossiers"
|
||||
add_foreign_key "dossier_operation_logs", "gestionnaires"
|
||||
add_foreign_key "dossiers", "users"
|
||||
|
@ -608,6 +620,7 @@ ActiveRecord::Schema.define(version: 2019_01_10_163655) do
|
|||
add_foreign_key "received_mails", "procedures"
|
||||
add_foreign_key "refused_mails", "procedures"
|
||||
add_foreign_key "services", "administrateurs"
|
||||
add_foreign_key "trusted_device_tokens", "gestionnaires"
|
||||
add_foreign_key "types_de_champ", "types_de_champ", column: "parent_id"
|
||||
add_foreign_key "without_continuation_mails", "procedures"
|
||||
end
|
||||
|
|
17
lib/tasks/2019_02_18_migrate_service_organisme.rake
Normal file
17
lib/tasks/2019_02_18_migrate_service_organisme.rake
Normal file
|
@ -0,0 +1,17 @@
|
|||
namespace :after_party do
|
||||
desc 'Deployment task: migrate service organisme'
|
||||
task migrate_service_organisme: :environment do
|
||||
table = {
|
||||
'commune': 'collectivite_territoriale',
|
||||
'departement': 'collectivite_territoriale',
|
||||
'region': 'collectivite_territoriale',
|
||||
'prefecture': 'service_deconcentre_de_l_etat'
|
||||
}
|
||||
|
||||
table.each do |(old_name, new_name)|
|
||||
Service.where(type_organisme: old_name).update_all(type_organisme: new_name)
|
||||
end
|
||||
|
||||
AfterParty::TaskRecord.create version: '20190201121252'
|
||||
end
|
||||
end
|
|
@ -9,6 +9,7 @@ describe ApplicationController, type: :controller do
|
|||
.map(&:filter)
|
||||
|
||||
expect(before_actions).to include(:set_raven_context)
|
||||
expect(before_actions).to include(:redirect_if_untrusted)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -145,4 +146,56 @@ describe ApplicationController, type: :controller do
|
|||
it { expect(flash[:alert]).to eq(ApplicationController::MAINTENANCE_MESSAGE) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#redirect_if_unstrusted' do
|
||||
let(:current_gestionnaire) { create(:gestionnaire) }
|
||||
|
||||
before do
|
||||
allow(current_gestionnaire).to receive(:feature_enabled?).and_return(feature_enabled)
|
||||
allow(@controller).to receive(:current_gestionnaire).and_return(current_gestionnaire)
|
||||
|
||||
allow(@controller).to receive(:redirect_to)
|
||||
allow(@controller).to receive(:trusted_device?).and_return(trusted_device)
|
||||
allow(@controller).to receive(:gestionnaire_signed_in?).and_return(gestionnaire_signed_in)
|
||||
allow(@controller).to receive(:sensitive_path).and_return(sensitive_path)
|
||||
allow(@controller).to receive(:send_login_token_or_bufferize)
|
||||
allow(@controller).to receive(:store_location_for)
|
||||
end
|
||||
|
||||
subject { @controller.send(:redirect_if_untrusted) }
|
||||
|
||||
context 'when the path is sensitive' do
|
||||
let(:sensitive_path) { true }
|
||||
|
||||
context 'when the gestionnaire is signed_in' do
|
||||
let(:gestionnaire_signed_in) { true }
|
||||
|
||||
context 'when the feature is activated' do
|
||||
let(:feature_enabled) { true }
|
||||
|
||||
context 'when the device is trusted' do
|
||||
let(:trusted_device) { true }
|
||||
|
||||
before { subject }
|
||||
|
||||
it { expect(@controller).not_to have_received(:redirect_to) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the feature is activated' do
|
||||
let(:feature_enabled) { true }
|
||||
|
||||
context 'when the device is not trusted' do
|
||||
let(:trusted_device) { false }
|
||||
|
||||
before { subject }
|
||||
|
||||
it { expect(@controller).to have_received(:redirect_to) }
|
||||
it { expect(@controller).to have_received(:send_login_token_or_bufferize) }
|
||||
it { expect(@controller).to have_received(:store_location_for) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
14
spec/controllers/manager/dossiers_controller_spec.rb
Normal file
14
spec/controllers/manager/dossiers_controller_spec.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
describe Manager::DossiersController, type: :controller do
|
||||
describe '#hide' do
|
||||
let(:administration) { create :administration }
|
||||
let!(:dossier) { create(:dossier) }
|
||||
|
||||
before do
|
||||
sign_in administration
|
||||
post :hide, params: { id: dossier.id }
|
||||
dossier.reload
|
||||
end
|
||||
|
||||
it { expect(dossier.hidden_at).not_to be_nil }
|
||||
end
|
||||
end
|
|
@ -15,7 +15,7 @@ describe NewAdministrateur::ServicesController, type: :controller do
|
|||
nom: 'super service',
|
||||
organisme: 'organisme',
|
||||
siret: '01234567891234',
|
||||
type_organisme: 'region',
|
||||
type_organisme: 'association',
|
||||
email: 'email@toto.com',
|
||||
telephone: '1234',
|
||||
horaires: 'horaires',
|
||||
|
@ -30,7 +30,7 @@ describe NewAdministrateur::ServicesController, type: :controller do
|
|||
it { expect(Service.last.nom).to eq('super service') }
|
||||
it { expect(Service.last.organisme).to eq('organisme') }
|
||||
it { expect(Service.last.siret).to eq('01234567891234') }
|
||||
it { expect(Service.last.type_organisme).to eq(Service.type_organismes.fetch(:region)) }
|
||||
it { expect(Service.last.type_organisme).to eq(Service.type_organismes.fetch(:association)) }
|
||||
it { expect(Service.last.email).to eq('email@toto.com') }
|
||||
it { expect(Service.last.telephone).to eq('1234') }
|
||||
it { expect(Service.last.horaires).to eq('horaires') }
|
||||
|
@ -49,7 +49,7 @@ describe NewAdministrateur::ServicesController, type: :controller do
|
|||
|
||||
describe '#update' do
|
||||
let!(:service) { create(:service, administrateur: admin) }
|
||||
let(:service_params) { { nom: 'nom', type_organisme: Service.type_organismes.fetch(:region) } }
|
||||
let(:service_params) { { nom: 'nom', type_organisme: Service.type_organismes.fetch(:association) } }
|
||||
|
||||
before do
|
||||
sign_in admin
|
||||
|
@ -65,12 +65,12 @@ describe NewAdministrateur::ServicesController, type: :controller do
|
|||
it { expect(flash.alert).to be_nil }
|
||||
it { expect(flash.notice).to eq('nom modifié') }
|
||||
it { expect(Service.last.nom).to eq('nom') }
|
||||
it { expect(Service.last.type_organisme).to eq(Service.type_organismes.fetch(:region)) }
|
||||
it { expect(Service.last.type_organisme).to eq(Service.type_organismes.fetch(:association)) }
|
||||
it { expect(response).to redirect_to(services_path(procedure_id: procedure.id)) }
|
||||
end
|
||||
|
||||
context 'when updating a service with invalid data' do
|
||||
let(:service_params) { { nom: '', type_organisme: Service.type_organismes.fetch(:region) } }
|
||||
let(:service_params) { { nom: '', type_organisme: Service.type_organismes.fetch(:association) } }
|
||||
|
||||
it { expect(flash.alert).not_to be_nil }
|
||||
it { expect(response).to render_template(:edit) }
|
||||
|
|
|
@ -40,6 +40,7 @@ describe Sessions::SessionsController, type: :controller do
|
|||
@request.env["devise.mapping"] = Devise.mappings[:gestionnaire]
|
||||
|
||||
allow_any_instance_of(described_class).to receive(:gestionnaire_signed_in?).and_return(true)
|
||||
allow_any_instance_of(described_class).to receive(:current_gestionnaire).and_return(gestionnaire)
|
||||
end
|
||||
|
||||
it 'calls sign out for gestionnaire' do
|
||||
|
|
|
@ -28,31 +28,15 @@ describe Users::SessionsController, type: :controller do
|
|||
context 'when the device is not trusted' do
|
||||
let(:trusted_device) { false }
|
||||
|
||||
it 'redirects to the confirmation link path' do
|
||||
it 'redirects to the root path' do
|
||||
subject
|
||||
|
||||
expect(controller).to redirect_to link_sent_path(email: email)
|
||||
expect(controller).to redirect_to(root_path)
|
||||
|
||||
# do not know why, should be test related
|
||||
expect(controller.current_user).to eq(user)
|
||||
|
||||
expect(controller.current_gestionnaire).to be(nil)
|
||||
expect(controller.current_administrateur).to be(nil)
|
||||
expect(user.loged_in_with_france_connect).to be(nil)
|
||||
expect(GestionnaireMailer).to have_received(:send_login_token)
|
||||
end
|
||||
|
||||
context 'and the user try to connect multiple times in a short period' do
|
||||
before do
|
||||
allow_any_instance_of(Gestionnaire).to receive(:young_login_token?).and_return(true)
|
||||
allow(GestionnaireMailer).to receive(:send_login_token)
|
||||
end
|
||||
|
||||
it 'does not renew nor send a new login token' do
|
||||
subject
|
||||
|
||||
expect(GestionnaireMailer).not_to have_received(:send_login_token)
|
||||
end
|
||||
expect(controller.current_gestionnaire).to eq(gestionnaire)
|
||||
expect(controller.current_administrateur).to eq(administrateur)
|
||||
expect(user.loged_in_with_france_connect).to eq(nil)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -69,7 +53,6 @@ describe Users::SessionsController, type: :controller do
|
|||
expect(controller.current_gestionnaire).to eq(gestionnaire)
|
||||
expect(controller.current_administrateur).to eq(administrateur)
|
||||
expect(user.loged_in_with_france_connect).to be(nil)
|
||||
expect(GestionnaireMailer).not_to have_received(:send_login_token)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -192,50 +175,57 @@ describe Users::SessionsController, type: :controller do
|
|||
describe '#sign_in_by_link' do
|
||||
context 'when the gestionnaire has non other account' do
|
||||
let(:gestionnaire) { create(:gestionnaire) }
|
||||
let!(:good_jeton) { gestionnaire.login_token! }
|
||||
let!(:good_jeton) { gestionnaire.create_trusted_device_token }
|
||||
let(:logged) { false }
|
||||
|
||||
before do
|
||||
if logged
|
||||
sign_in gestionnaire
|
||||
end
|
||||
allow(controller).to receive(:trust_device)
|
||||
allow(controller).to receive(:send_login_token_or_bufferize)
|
||||
post :sign_in_by_link, params: { id: gestionnaire.id, jeton: jeton }
|
||||
end
|
||||
|
||||
context 'when the token is valid' do
|
||||
let(:jeton) { good_jeton }
|
||||
context 'when the gestionnaire is not logged in' do
|
||||
context 'when the token is valid' do
|
||||
let(:jeton) { good_jeton }
|
||||
|
||||
# TODO when the gestionnaire has no other account, and the token is valid, and the user signing in was not starting a demarche,
|
||||
# redirect to root_path, then redirect to gestionnaire_procedures_path (see root_controller)
|
||||
it { is_expected.to redirect_to root_path }
|
||||
it { expect(controller.current_gestionnaire).to eq(gestionnaire) }
|
||||
it { expect(controller).to have_received(:trust_device) }
|
||||
it { is_expected.to redirect_to new_user_session_path }
|
||||
it { expect(controller.current_gestionnaire).to be_nil }
|
||||
it { expect(controller).to have_received(:trust_device) }
|
||||
end
|
||||
|
||||
context 'when the token is invalid' do
|
||||
let(:jeton) { 'invalid_token' }
|
||||
|
||||
it { is_expected.to redirect_to link_sent_path(email: gestionnaire.email) }
|
||||
it { expect(controller.current_gestionnaire).to be_nil }
|
||||
it { expect(controller).not_to have_received(:trust_device) }
|
||||
it { expect(controller).to have_received(:send_login_token_or_bufferize) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the token is invalid' do
|
||||
let(:jeton) { 'invalid_token' }
|
||||
context 'when the gestionnaire is logged in' do
|
||||
let(:logged) { true }
|
||||
|
||||
it { is_expected.to redirect_to new_user_session_path }
|
||||
it { expect(controller.current_gestionnaire).to be_nil }
|
||||
it { expect(controller).not_to have_received(:trust_device) }
|
||||
end
|
||||
end
|
||||
context 'when the token is valid' do
|
||||
let(:jeton) { good_jeton }
|
||||
|
||||
context 'when the gestionnaire has an user and admin account' do
|
||||
let(:email) { 'unique@plop.com' }
|
||||
let(:password) { 'un super mot de passe' }
|
||||
# redirect to root_path, then redirect to gestionnaire_procedures_path (see root_controller)
|
||||
it { is_expected.to redirect_to root_path }
|
||||
it { expect(controller.current_gestionnaire).to eq(gestionnaire) }
|
||||
it { expect(controller).to have_received(:trust_device) }
|
||||
end
|
||||
|
||||
let!(:user) { create(:user, email: email, password: password) }
|
||||
let!(:administrateur) { create(:administrateur, email: email, password: password) }
|
||||
let(:gestionnaire) { administrateur.gestionnaire }
|
||||
context 'when the token is invalid' do
|
||||
let(:jeton) { 'invalid_token' }
|
||||
|
||||
before do
|
||||
post :sign_in_by_link, params: { id: gestionnaire.id, jeton: jeton }
|
||||
end
|
||||
|
||||
context 'when the token is valid' do
|
||||
let(:jeton) { gestionnaire.login_token! }
|
||||
|
||||
it { expect(controller.current_gestionnaire).to eq(gestionnaire) }
|
||||
it { expect(controller.current_administrateur).to eq(administrateur) }
|
||||
it { expect(controller.current_user).to eq(user) }
|
||||
it { is_expected.to redirect_to link_sent_path(email: gestionnaire.email) }
|
||||
it { expect(controller.current_gestionnaire).to eq(gestionnaire) }
|
||||
it { expect(controller).not_to have_received(:trust_device) }
|
||||
it { expect(controller).to have_received(:send_login_token_or_bufferize) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -249,16 +239,15 @@ describe Users::SessionsController, type: :controller do
|
|||
|
||||
context 'when the cookie is outdated' do
|
||||
before do
|
||||
Timecop.freeze(Time.zone.now - TrustedDeviceConcern::TRUSTED_DEVICE_PERIOD - 1.minute)
|
||||
controller.trust_device
|
||||
Timecop.return
|
||||
emission_date = Time.zone.now - TrustedDeviceConcern::TRUSTED_DEVICE_PERIOD - 1.minute
|
||||
controller.trust_device(emission_date)
|
||||
end
|
||||
|
||||
it { is_expected.to be false }
|
||||
end
|
||||
|
||||
context 'when the cookie is ok' do
|
||||
before { controller.trust_device }
|
||||
before { controller.trust_device(Time.zone.now) }
|
||||
|
||||
it { is_expected.to be true }
|
||||
end
|
||||
|
|
|
@ -2,7 +2,7 @@ FactoryBot.define do
|
|||
factory :service do
|
||||
nom { 'service' }
|
||||
organisme { 'organisme' }
|
||||
type_organisme { Service.type_organismes.fetch(:commune) }
|
||||
type_organisme { Service.type_organismes.fetch(:association) }
|
||||
administrateur { create(:administrateur) }
|
||||
email { 'email@toto.com' }
|
||||
telephone { '1234' }
|
||||
|
|
|
@ -1,20 +1,17 @@
|
|||
describe ActiveStorage::Service::DsProxyService do
|
||||
let(:private_host) { 'storage.sbg1.cloud.ovh.net:443' }
|
||||
let(:public_host) { 'www.demarches-simplifiees.fr' }
|
||||
let(:public_host) { 'static.demarches-simplifiees.fr' }
|
||||
let(:auth) { 'AUTH_a24c37ed11a84896914514384898c34b' }
|
||||
let(:bucket) { 'test_local' }
|
||||
let(:key) { '2R6rr89nFeSRkSgXHd3smvEf' }
|
||||
let(:temp_url_params) { 'temp_url_sig=5ab8cfc3ba5da2598a6c88cc6b1b461fe4e115bc&temp_url_expires=1547598179' }
|
||||
let(:storage_service) { storage_service = double(ActiveStorage::Service) }
|
||||
|
||||
subject do
|
||||
allow(ENV).to receive(:[]).with('APP_HOST').and_return(public_host)
|
||||
ActiveStorage::Service::DsProxyService.new(wrapped: storage_service)
|
||||
end
|
||||
let(:storage_service) { storage_service = double(ActiveStorage::Service) }
|
||||
subject { ActiveStorage::Service::DsProxyService.new(wrapped: storage_service) }
|
||||
|
||||
describe '#url' do
|
||||
let(:private_url) { "https://#{private_host}/v1/#{auth}/#{bucket}/#{key}?#{temp_url_params}" }
|
||||
let(:public_url) { "https://#{public_host}/direct-upload/#{bucket}/#{key}?#{temp_url_params}" }
|
||||
let(:public_url) { "https://#{public_host}/#{bucket}/#{key}?#{temp_url_params}" }
|
||||
|
||||
before do
|
||||
expect(storage_service).to receive(:url).and_return(private_url)
|
||||
|
@ -28,7 +25,7 @@ describe ActiveStorage::Service::DsProxyService do
|
|||
describe '#url_for_direct_upload' do
|
||||
let(:download_params) { 'inline&filename=documents_top_confidentiels.bmp' }
|
||||
let(:private_url) { "https://#{private_host}/v1/#{auth}/#{bucket}/#{key}?#{temp_url_params}&#{download_params}" }
|
||||
let(:public_url) { "https://#{public_host}/direct-upload/#{bucket}/#{key}?#{temp_url_params}&#{download_params}" }
|
||||
let(:public_url) { "https://#{public_host}/#{bucket}/#{key}?#{temp_url_params}&#{download_params}" }
|
||||
|
||||
before do
|
||||
expect(storage_service).to receive(:url_for_direct_upload).and_return(private_url)
|
||||
|
|
|
@ -892,4 +892,21 @@ describe Dossier do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#hide!' do
|
||||
let(:dossier) { create(:dossier) }
|
||||
let(:administration) { create(:administration) }
|
||||
let(:last_operation) { dossier.dossier_operation_logs.last }
|
||||
|
||||
before do
|
||||
Timecop.freeze
|
||||
dossier.hide!(administration)
|
||||
end
|
||||
|
||||
after { Timecop.return }
|
||||
|
||||
it { expect(dossier.hidden_at).to eq(Time.zone.now) }
|
||||
it { expect(last_operation.operation).to eq('supprimer') }
|
||||
it { expect(last_operation.administration).to eq(administration) }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -392,44 +392,24 @@ describe Gestionnaire, type: :model do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#login_token_valid?' do
|
||||
let!(:gestionnaire) { create(:gestionnaire) }
|
||||
let!(:good_token) { gestionnaire.login_token! }
|
||||
|
||||
it { expect(gestionnaire.login_token_valid?(good_token)).to be true }
|
||||
it { expect(gestionnaire.login_token_valid?('bad_token')).to be false }
|
||||
|
||||
context 'when the token as expired' do
|
||||
before { gestionnaire.update(login_token_created_at: (Gestionnaire::LOGIN_TOKEN_VALIDITY + 1.minute).ago) }
|
||||
|
||||
it { expect(gestionnaire.login_token_valid?(good_token)).to be false }
|
||||
end
|
||||
|
||||
context 'when the gestionnaire does not have a token' do
|
||||
before { gestionnaire.update(encrypted_login_token: nil) }
|
||||
|
||||
it { expect(gestionnaire.login_token_valid?(nil)).to be false }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#young_login_token?' do
|
||||
let!(:gestionnaire) { create(:gestionnaire) }
|
||||
|
||||
context 'when there is a token' do
|
||||
let!(:good_token) { gestionnaire.login_token! }
|
||||
let!(:good_token) { gestionnaire.create_trusted_device_token }
|
||||
|
||||
context 'when the token has just been created' do
|
||||
it { expect(gestionnaire.young_login_token?).to be true }
|
||||
end
|
||||
|
||||
context 'when the token is a bit old' do
|
||||
before { gestionnaire.update(login_token_created_at: (Gestionnaire::LOGIN_TOKEN_YOUTH + 1.minute).ago) }
|
||||
before { gestionnaire.trusted_device_tokens.first.update(created_at: (TrustedDeviceToken::LOGIN_TOKEN_YOUTH + 1.minute).ago) }
|
||||
it { expect(gestionnaire.young_login_token?).to be false }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are no token' do
|
||||
it { expect(gestionnaire.young_login_token?).to be false }
|
||||
it { expect(gestionnaire.young_login_token?).to be_falsey }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ describe Service, type: :model do
|
|||
{
|
||||
nom: 'service des jardins',
|
||||
organisme: 'mairie des iles',
|
||||
type_organisme: Service.type_organismes.fetch(:commune),
|
||||
type_organisme: Service.type_organismes.fetch(:association),
|
||||
email: 'super@email.com',
|
||||
telephone: '1212202',
|
||||
horaires: 'du lundi au vendredi',
|
||||
|
|
29
spec/models/trusted_device_token_spec.rb
Normal file
29
spec/models/trusted_device_token_spec.rb
Normal file
|
@ -0,0 +1,29 @@
|
|||
RSpec.describe TrustedDeviceToken, type: :model do
|
||||
describe '#token_valid?' do
|
||||
let(:token) { TrustedDeviceToken.create }
|
||||
|
||||
context 'when the token is create after login_token_validity' do
|
||||
it { expect(token.token_valid?).to be true }
|
||||
end
|
||||
|
||||
context 'when the token is create before login_token_validity' do
|
||||
before { token.update(created_at: (TrustedDeviceToken::LOGIN_TOKEN_VALIDITY + 1.minute).ago) }
|
||||
|
||||
it { expect(token.token_valid?).to be false }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#token_young?' do
|
||||
let(:token) { TrustedDeviceToken.create }
|
||||
|
||||
context 'when the token is create after login_token_youth' do
|
||||
it { expect(token.token_young?).to be true }
|
||||
end
|
||||
|
||||
context 'when the token is create before login_token_youth' do
|
||||
before { token.update(created_at: (TrustedDeviceToken::LOGIN_TOKEN_YOUTH + 1.minute).ago) }
|
||||
|
||||
it { expect(token.token_young?).to be false }
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue