diff --git a/Gemfile.lock b/Gemfile.lock index f5347d04d..f8460ab99 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -203,7 +203,7 @@ GEM ffi (1.9.23) fission (0.5.0) CFPropertyList (~> 2.2) - flipflop (2.3.1) + flipflop (2.4.0) activesupport (>= 4.0) fog (1.42.0) fog-aliyun (>= 0.1.0) diff --git a/app/controllers/admin/gestionnaires_controller.rb b/app/controllers/admin/gestionnaires_controller.rb index 11e9f52c0..6bd61d4e2 100644 --- a/app/controllers/admin/gestionnaires_controller.rb +++ b/app/controllers/admin/gestionnaires_controller.rb @@ -23,7 +23,7 @@ class Admin::GestionnairesController < AdminController procedure_id = params[:procedure_id] if @gestionnaire.nil? - new_gestionnaire! + invite_gestionnaire(params[:gestionnaire][:email]) else assign_gestionnaire! end @@ -42,22 +42,23 @@ class Admin::GestionnairesController < AdminController private - def new_gestionnaire! - attributes = params.require(:gestionnaire).permit(:email) - .merge(password: SecureRandom.hex(5)) + def invite_gestionnaire(email) + password = SecureRandom.hex @gestionnaire = Gestionnaire.create( - attributes.merge( - administrateurs: [current_administrateur] - ) + email: email, + password: password, + password_confirmation: password, + administrateurs: [current_administrateur] ) if @gestionnaire.errors.messages.empty? + @gestionnaire.invite! + if User.exists?(email: @gestionnaire.email) GestionnaireMailer.user_to_gestionnaire(@gestionnaire.email).deliver_now! else - User.create(attributes) - GestionnaireMailer.new_gestionnaire(@gestionnaire.email, @gestionnaire.password).deliver_now! + User.create(email: email, password: password) end flash.notice = 'Accompagnateur ajouté' else diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 870441667..4db6b6ab2 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,10 +1,13 @@ class ApplicationController < ActionController::Base + MAINTENANCE_MESSAGE = 'Le site est actuellement en maintenance. Il sera à nouveau disponible dans un court instant.' + # Prevent CSRF attacks by raising an exception. # For APIs, you may want to use :null_session instead. protect_from_forgery with: :exception before_action :load_navbar_left_pannel_partial_url before_action :set_raven_context before_action :authorize_request_for_profiler + before_action :reject, if: -> { Flipflop.maintenance_mode? } before_action :staging_authenticate @@ -135,4 +138,23 @@ class ApplicationController < ActionController::Base ) # END OF FIXME end + + def reject + authorized_request = + request.path_info == '/' || + request.path_info.start_with?('/manager') || + request.path_info.start_with?('/administrations') + + api_request = request.path_info.start_with?('/api/') + + if administration_signed_in? || authorized_request + flash.now.alert = MAINTENANCE_MESSAGE + elsif api_request + render json: { error: MAINTENANCE_MESSAGE }.to_json, status: :service_unavailable + else + %i(user gestionnaire administrateur).each { |role| sign_out(role) } + flash[:alert] = MAINTENANCE_MESSAGE + redirect_to root_path + end + end end diff --git a/app/controllers/gestionnaires/activate_controller.rb b/app/controllers/gestionnaires/activate_controller.rb new file mode 100644 index 000000000..5eac9f317 --- /dev/null +++ b/app/controllers/gestionnaires/activate_controller.rb @@ -0,0 +1,47 @@ +class Gestionnaires::ActivateController < ApplicationController + layout "new_application" + + def new + @gestionnaire = Gestionnaire.with_reset_password_token(params[:token]) + + if !@gestionnaire + flash.alert = "Le lien de validation du compte accompagnateur a expiré, contactez-nous à contact@demarches-simplifiees.fr pour obtenir un nouveau lien." + redirect_to root_path + end + end + + def create + password = create_gestionnaire_params[:password] + gestionnaire = Gestionnaire.reset_password_by_token({ + password: password, + password_confirmation: password, + reset_password_token: create_gestionnaire_params[:reset_password_token] + }) + + if gestionnaire && gestionnaire.errors.empty? + sign_in(gestionnaire, scope: :gestionnaire) + try_to_authenticate(User, gestionnaire.email, password) + try_to_authenticate(Administrateur, gestionnaire.email, password) + flash.notice = "Mot de passe enregistré" + redirect_to gestionnaire_procedures_path + else + flash.alert = gestionnaire.errors.full_messages + redirect_to gestionnaire_activate_path(token: create_gestionnaire_params[:reset_password_token]) + end + end + + private + + def create_gestionnaire_params + params.require(:gestionnaire).permit(:reset_password_token, :password) + end + + def try_to_authenticate(klass, email, password) + resource = klass.find_for_database_authentication(email: email) + + if resource&.valid_password?(password) + sign_in resource + resource.force_sync_credentials + end + end +end diff --git a/app/controllers/manager/administrateurs_controller.rb b/app/controllers/manager/administrateurs_controller.rb index fccc55c5e..0b5cb39b3 100644 --- a/app/controllers/manager/administrateurs_controller.rb +++ b/app/controllers/manager/administrateurs_controller.rb @@ -19,6 +19,20 @@ module Manager redirect_to manager_administrateur_path(params[:id]) end + def enable_feature + administrateur = Administrateur.find(params[:id]) + + params[:features].each do |key, enable| + if enable + administrateur.enable_feature(key.to_sym) + else + administrateur.disable_feature(key.to_sym) + end + end + + head :ok + end + private def create_administrateur_params diff --git a/app/controllers/new_gestionnaire/procedures_controller.rb b/app/controllers/new_gestionnaire/procedures_controller.rb index dad304e23..81f36b586 100644 --- a/app/controllers/new_gestionnaire/procedures_controller.rb +++ b/app/controllers/new_gestionnaire/procedures_controller.rb @@ -237,9 +237,10 @@ module NewGestionnaire when 'user', 'etablissement', 'entreprise' if filter['column'] == 'date_creation' + date = filter['value'].to_date rescue nil dossiers .includes(filter['table']) - .where("#{filter['table'].pluralize}.#{filter['column']} = ?", filter['value'].to_date) + .where("#{filter['table'].pluralize}.#{filter['column']} = ?", date) else dossiers .includes(filter['table']) diff --git a/app/dashboards/administrateur_dashboard.rb b/app/dashboards/administrateur_dashboard.rb index d8f7afe5e..7f581595d 100644 --- a/app/dashboards/administrateur_dashboard.rb +++ b/app/dashboards/administrateur_dashboard.rb @@ -15,6 +15,7 @@ class AdministrateurDashboard < Administrate::BaseDashboard procedures: Field::HasMany.with_options(limit: 20), registration_state: Field::String.with_options(searchable: false), current_sign_in_at: Field::DateTime, + features: FeaturesField }.freeze # COLLECTION_ATTRIBUTES @@ -38,6 +39,7 @@ class AdministrateurDashboard < Administrate::BaseDashboard :updated_at, :registration_state, :current_sign_in_at, + :features, :procedures, ].freeze diff --git a/app/fields/features_field.rb b/app/fields/features_field.rb new file mode 100644 index 000000000..a6ed5cbe2 --- /dev/null +++ b/app/fields/features_field.rb @@ -0,0 +1,4 @@ +require "administrate/field/base" + +class FeaturesField < Administrate::Field::Base +end diff --git a/app/lib/flipflop/strategies/user_preference_strategy.rb b/app/lib/flipflop/strategies/user_preference_strategy.rb index c4b0824d2..63c22b778 100644 --- a/app/lib/flipflop/strategies/user_preference_strategy.rb +++ b/app/lib/flipflop/strategies/user_preference_strategy.rb @@ -11,29 +11,12 @@ module Flipflop::Strategies def enabled?(feature) # Can only check features if we have the user's session. if request? - legacy_enabled?(feature) || find_current_administrateur&.feature_enabled?(feature) + find_current_administrateur&.feature_enabled?(feature) end end private - def legacy_enabled?(feature) - if self.class.legacy_features_map.present? - ids = self.class.legacy_features_map["#{feature}_allowed_for_admin_ids"] - ids.present? && find_current_administrateur&.id&.in?(ids) - end - end - - LEGACY_CONFIG_FILE = Rails.root.join("config", "initializers", "features.yml") - - def self.legacy_features_map - @@legacy_features_map = begin - if File.exist?(LEGACY_CONFIG_FILE) - YAML.load_file(LEGACY_CONFIG_FILE) - end - end - end - def find_current_administrateur if request.session["warden.user.administrateur.key"] administrateur_id = request.session["warden.user.administrateur.key"][0][0] diff --git a/app/mailers/gestionnaire_mailer.rb b/app/mailers/gestionnaire_mailer.rb index 88e67be03..fbb090012 100644 --- a/app/mailers/gestionnaire_mailer.rb +++ b/app/mailers/gestionnaire_mailer.rb @@ -1,8 +1,12 @@ class GestionnaireMailer < ApplicationMailer layout 'mailers/layout' - def new_gestionnaire(email, password) - send_mail(email, password, "Vous avez été nommé accompagnateur sur demarches-simplifiees.fr") + def invite_gestionnaire(gestionnaire, reset_password_token) + @reset_password_token = reset_password_token + @gestionnaire = gestionnaire + mail(to: gestionnaire.email, + subject: "demarches-simplifiees.fr - Activez votre compte accompagnateur", + reply_to: "contact@demarches-simplifiees.fr") end def user_to_gestionnaire(email) diff --git a/app/models/gestionnaire.rb b/app/models/gestionnaire.rb index 73ee2a1b9..d96c603df 100644 --- a/app/models/gestionnaire.rb +++ b/app/models/gestionnaire.rb @@ -144,6 +144,12 @@ class Gestionnaire < ApplicationRecord Follow.where(gestionnaire: self, dossier: dossier).update_all(attributes) end + def invite! + reset_password_token = set_reset_password_token + + GestionnaireMailer.invite_gestionnaire(self, reset_password_token).deliver_now! + end + private def valid_couple_table_attr?(table, column) diff --git a/app/serializers/champ_serializer.rb b/app/serializers/champ_serializer.rb index 981c2b9cc..454d3b944 100644 --- a/app/serializers/champ_serializer.rb +++ b/app/serializers/champ_serializer.rb @@ -1,5 +1,15 @@ class ChampSerializer < ActiveModel::Serializer + include Rails.application.routes.url_helpers + attributes :value has_one :type_de_champ + + def value + if object.piece_justificative_file.attached? + url_for(object.piece_justificative_file) + else + object.value + end + end end diff --git a/app/views/admin/procedures/_modal_transfer.html.haml b/app/views/admin/procedures/_modal_transfer.html.haml index 66edd79ab..12e2fe6cc 100644 --- a/app/views/admin/procedures/_modal_transfer.html.haml +++ b/app/views/admin/procedures/_modal_transfer.html.haml @@ -7,11 +7,11 @@ %span{ "aria-hidden" => "true" } × %h4#myModalLabel.modal-title - Transférer la procédure à un autre administrateur + Envoyer une copie de cette procédure à un autre administrateur .modal-body %p - Cette fonctionnalité vous permet de transmettre un clone de votre procédure à un autre administrateur. + Cette fonctionnalité vous permet de d'envoyer une copie de votre procédure à un autre administrateur. %div{ style:'margin-top:20px' } = text_field_tag :email_admin, '', { class: 'form-control', diff --git a/app/views/admin/procedures/show.html.haml b/app/views/admin/procedures/show.html.haml index 202d490f5..7dcbb1d6a 100644 --- a/app/views/admin/procedures/show.html.haml +++ b/app/views/admin/procedures/show.html.haml @@ -15,7 +15,7 @@ %a#transfer.btn.btn-small.btn-default{ "data-target" => "#transferModal", "data-toggle" => "modal", :type => "button", style: 'float: right; margin-top: 10px; margin-right: 10px;' } %i.fa.fa-exchange - Transférer + Envoyer une copie = render partial: '/admin/procedures/modal_transfer' diff --git a/app/views/fields/features_field/_show.html.haml b/app/views/fields/features_field/_show.html.haml new file mode 100644 index 000000000..a9ce32535 --- /dev/null +++ b/app/views/fields/features_field/_show.html.haml @@ -0,0 +1,26 @@ +%table#features + - Flipflop.feature_set.features.each do |feature| + - if !feature.group || feature.group.key != :production + %tr + %td= feature.title + %td + = check_box_tag "enable-feature", "enable", field.data[feature.name], data: { url: enable_feature_manager_administrateur_path(field.resource.id), key: feature.key } + +:javascript + window.onload = function() { + $('#features input[type=checkbox]').on('change', function(evt) { + let url = $(evt.target).data('url'); + let key = $(evt.target).data('key'); + let features = {}; + features[key] = $(evt.target).prop('checked'); + $.ajax(url, { + method: 'put', + contentType: 'application/json', + dataType: 'json', + data: JSON.stringify({ + features: features + }) + }); + }); + }; + diff --git a/app/views/gestionnaire_mailer/invite_gestionnaire.html.haml b/app/views/gestionnaire_mailer/invite_gestionnaire.html.haml new file mode 100644 index 000000000..3b5fa28d3 --- /dev/null +++ b/app/views/gestionnaire_mailer/invite_gestionnaire.html.haml @@ -0,0 +1,19 @@ +- content_for(:title, 'Activation de votre compte accompagnateur') + +Bonjour, +%br +%br +Vous venez d'être nommé accompagnateur sur demarches-simplifiees.fr. +%br +Votre compte a été créé pour l'adresse email #{@gestionnaire.email}. Pour l’activer, je vous invite à cliquer sur le lien suivant :  += link_to(gestionnaire_activate_url(token: @reset_password_token), gestionnaire_activate_url(token: @reset_password_token)) +%br +%br +Par ailleurs, notre site de documentation qui regroupe l'ensemble des informations relatives à demarches-simplifiees.fr ainsi que des tutoriels d’utilisation est à votre disposition :  += link_to('https://demarches-simplifiees.gitbook.io/demarches-simplifiees/', 'https://demarches-simplifiees.gitbook.io/demarches-simplifiees/') +%br +%br +Bonne journée, +%br +%br +L'équipe demarches-simplifiees.fr diff --git a/app/views/gestionnaire_mailer/new_gestionnaire.text.erb b/app/views/gestionnaire_mailer/new_gestionnaire.text.erb deleted file mode 100644 index b599994c7..000000000 --- a/app/views/gestionnaire_mailer/new_gestionnaire.text.erb +++ /dev/null @@ -1,11 +0,0 @@ -Bienvenue sur demarches-simplifiees.fr, - -Vous venez d'être nommé accompagnateur sur demarches-simplifiees.fr. Pour mémoire, voici quelques informations utiles : - - URL : <%= new_gestionnaire_session_url %> - Login : <%= @email %> - Mot de passe : <%= @args %> - -Bonne journée, - -L'équipe demarches-simplifiees.fr diff --git a/app/views/gestionnaires/activate/new.html.haml b/app/views/gestionnaires/activate/new.html.haml new file mode 100644 index 000000000..fb16700ce --- /dev/null +++ b/app/views/gestionnaires/activate/new.html.haml @@ -0,0 +1,7 @@ +.container + = form_for @gestionnaire, url: { controller: 'gestionnaires/activate', action: :create }, html: { class: "form" } do |f| + %br + %h1= @gestionnaire.email + = f.password_field :password, placeholder: 'Mot de passe' + = f.hidden_field :reset_password_token, value: params[:token] + = f.submit 'Définir le mot de passe', class: 'button large primary expand' diff --git a/config/environments/test.rb b/config/environments/test.rb index 9de8d2433..380b51502 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -31,6 +31,8 @@ Rails.application.configure do # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test + config.active_storage.service = :local + # Randomize the order test cases are executed. config.active_support.test_order = :random diff --git a/config/features.rb b/config/features.rb index c582f0be4..14e779b0c 100644 --- a/config/features.rb +++ b/config/features.rb @@ -1,18 +1,26 @@ Flipflop.configure do - strategy :cookie + strategy :cookie, + secure: Rails.env.production?, + httponly: true strategy :active_record strategy :user_preference strategy :default group :champs do - feature :champ_pj - feature :champ_siret + feature :champ_pj, + title: "Champ pièce justificative" + feature :champ_siret, + title: "Champ SIRET" end + feature :web_hook + group :production do feature :remote_storage, default: Rails.env.production? || Rails.env.staging? feature :weekly_overview, default: Rails.env.production? end + + feature :maintenance_mode end diff --git a/config/initializers/features.yml b/config/initializers/features.yml deleted file mode 100644 index 71aea0c20..000000000 --- a/config/initializers/features.yml +++ /dev/null @@ -1,8 +0,0 @@ -remote_storage: false -weekly_overview: false -champ_pj_allowed_for_admin_ids: - - 0 -champ_siret_allowed_for_admin_ids: - - 0 -web_hook_allowed_for_admin_ids: - - 0 diff --git a/config/routes.rb b/config/routes.rb index 7876ffc0b..56601ac28 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -8,6 +8,7 @@ Rails.application.routes.draw do resources :administrateurs, only: [:index, :show, :new, :create] do post 'reinvite', on: :member + put 'enable_feature', on: :member end resources :demandes, only: [:index] @@ -114,6 +115,11 @@ Rails.application.routes.draw do resource :dossiers end + namespace :gestionnaire do + get 'activate' => '/gestionnaires/activate#new' + patch 'activate' => '/gestionnaires/activate#create' + end + namespace :admin do get 'activate' => '/administrateurs/activate#new' patch 'activate' => '/administrateurs/activate#create' diff --git a/spec/controllers/admin/gestionnaires_controller_spec.rb b/spec/controllers/admin/gestionnaires_controller_spec.rb index 9f9f6091a..6fe2da4d0 100644 --- a/spec/controllers/admin/gestionnaires_controller_spec.rb +++ b/spec/controllers/admin/gestionnaires_controller_spec.rb @@ -149,8 +149,7 @@ describe Admin::GestionnairesController, type: :controller do context 'Email notification' do it 'Notification email is sent when accompagnateur is create' do - expect(GestionnaireMailer).to receive(:new_gestionnaire).and_return(GestionnaireMailer) - expect(GestionnaireMailer).to receive(:deliver_now!) + expect_any_instance_of(Gestionnaire).to receive(:invite!) subject end end diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 89d601eaa..ce2dbc1c1 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -82,4 +82,57 @@ describe ApplicationController, type: :controller do end end end + + describe 'reject before action' do + let(:path_info) { '/one_path' } + + before do + allow(@controller).to receive(:redirect_to) + allow(@controller).to receive(:sign_out) + allow(@controller).to receive(:render) + @request.path_info = path_info + end + + context 'when no administration is logged in' do + before { @controller.send(:reject) } + + it { expect(@controller).to have_received(:sign_out).with(:user) } + it { expect(@controller).to have_received(:sign_out).with(:gestionnaire) } + it { expect(@controller).to have_received(:sign_out).with(:administrateur) } + it { expect(flash[:alert]).to eq(ApplicationController::MAINTENANCE_MESSAGE) } + it { expect(@controller).to have_received(:redirect_to).with(root_path) } + + context 'when the path is safe' do + %w(/ /manager /administrations).each do |path| + let(:path_info) { path } + + it { expect(@controller).not_to have_received(:sign_out) } + it { expect(@controller).not_to have_received(:redirect_to) } + it { expect(flash.alert).to eq(ApplicationController::MAINTENANCE_MESSAGE) } + end + end + + context 'when the path is api related' do + let(:path_info) { '/api/some-stuff' } + let(:json_error) { { error: ApplicationController::MAINTENANCE_MESSAGE }.to_json } + it { expect(@controller).not_to have_received(:sign_out) } + it { expect(@controller).not_to have_received(:redirect_to) } + it { expect(flash.alert).to be_nil } + it { expect(@controller).to have_received(:render).with({ json: json_error, status: :service_unavailable }) } + end + end + + context 'when a administration is logged in' do + let(:current_administration) { create(:administration) } + + before do + sign_in(current_administration) + @controller.send(:reject) + end + + it { expect(@controller).not_to have_received(:sign_out) } + it { expect(@controller).not_to have_received(:redirect_to) } + it { expect(flash[:alert]).to eq(ApplicationController::MAINTENANCE_MESSAGE) } + end + end end diff --git a/spec/serializers/champ_serializer_spec.rb b/spec/serializers/champ_serializer_spec.rb new file mode 100644 index 000000000..96b8ce784 --- /dev/null +++ b/spec/serializers/champ_serializer_spec.rb @@ -0,0 +1,22 @@ +describe ChampSerializer do + describe '#attributes' do + subject { ChampSerializer.new(champ).serializable_hash } + + context 'when type champ is piece justificative' do + include Rails.application.routes.url_helpers + + let(:champ) { create(:champ, type_de_champ: create(:type_de_champ_piece_justificative)) } + + before { champ.piece_justificative_file.attach({ filename: __FILE__, io: File.open(__FILE__) }) } + after { champ.piece_justificative_file.purge } + + it { is_expected.to include(value: url_for(champ.piece_justificative_file)) } + end + + context 'when type champ is not piece justificative' do + let(:champ) { create(:champ, value: "blah") } + + it { is_expected.to include(value: "blah") } + end + end +end