diff --git a/app/assets/stylesheets/new_design/link-sent.scss b/app/assets/stylesheets/new_design/link-sent.scss new file mode 100644 index 000000000..778d330b1 --- /dev/null +++ b/app/assets/stylesheets/new_design/link-sent.scss @@ -0,0 +1,30 @@ +@import "constants"; +@import "colors"; + +#link-sent { + padding-top: 2 * $default-padding; + padding-bottom: 2 * $default-padding; + text-align: center; + max-width: 600px; + + b { + font-weight: bold; + } + + p { + text-align: left; + margin: 6 * $default-spacer auto; + } + + p.mail { + color: #000000; + background-color: $yellow; + padding: $default-padding; + } + + p.help { + border-top: 1px solid $grey; + padding-top: 6 * $default-spacer; + margin-bottom: 2 * $default-spacer; + } +} diff --git a/app/controllers/administrateurs/activate_controller.rb b/app/controllers/administrateurs/activate_controller.rb index d354c0375..5d2654587 100644 --- a/app/controllers/administrateurs/activate_controller.rb +++ b/app/controllers/administrateurs/activate_controller.rb @@ -1,12 +1,17 @@ require 'zxcvbn' class Administrateurs::ActivateController < ApplicationController + include TrustedDeviceConcern + layout "new_application" def new @administrateur = Administrateur.find_inactive_by_token(params[:token]) - if !@administrateur + if @administrateur + # the administrateur activates its account from an email + trust_device + else flash.alert = "Le lien de validation d'administrateur a expiré, #{helpers.contact_link('contactez-nous', tags: 'lien expiré')} pour obtenir un nouveau lien." redirect_to root_path end diff --git a/app/controllers/gestionnaires/activate_controller.rb b/app/controllers/gestionnaires/activate_controller.rb index 873757f47..12fdae36a 100644 --- a/app/controllers/gestionnaires/activate_controller.rb +++ b/app/controllers/gestionnaires/activate_controller.rb @@ -1,10 +1,15 @@ class Gestionnaires::ActivateController < ApplicationController + include TrustedDeviceConcern + layout "new_application" def new @gestionnaire = Gestionnaire.with_reset_password_token(params[:token]) - if !@gestionnaire + if @gestionnaire + # the gestionnaire activates its account from an email + trust_device + else flash.alert = "Le lien de validation du compte instructeur a expiré, #{helpers.contact_link('contactez-nous', tags: 'lien expiré')} pour obtenir un nouveau lien." redirect_to root_path end diff --git a/app/controllers/manager/gestionnaires_controller.rb b/app/controllers/manager/gestionnaires_controller.rb index 1caaf9156..0b83d772e 100644 --- a/app/controllers/manager/gestionnaires_controller.rb +++ b/app/controllers/manager/gestionnaires_controller.rb @@ -6,5 +6,19 @@ module Manager flash[:notice] = "Instructeur réinvité." redirect_to manager_gestionnaire_path(gestionnaire) end + + def enable_feature + gestionnaire = Gestionnaire.find(params[:id]) + + params[:features].each do |key, enable| + if enable + gestionnaire.enable_feature(key.to_sym) + else + gestionnaire.disable_feature(key.to_sym) + end + end + + head :ok + end end end diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb index 5d43d18e6..610ce28b7 100644 --- a/app/controllers/users/sessions_controller.rb +++ b/app/controllers/users/sessions_controller.rb @@ -1,4 +1,7 @@ class Users::SessionsController < Sessions::SessionsController + include TrustedDeviceConcern + include ActionView::Helpers::DateHelper + layout "new_application" # GET /resource/sign_in @@ -23,13 +26,22 @@ class Users::SessionsController < Sessions::SessionsController current_user.update(loged_in_with_france_connect: '') end - if user_signed_in? + if gestionnaire_signed_in? + if trusted_device? || !current_gestionnaire.feature_enabled?(:enable_email_login_token) + set_flash_message :notice, :signed_in + redirect_to gestionnaire_procedures_path + else + gestionnaire = current_gestionnaire + login_token = gestionnaire.login_token! + GestionnaireMailer.send_login_token(gestionnaire, login_token).deliver_later + + [:user, :gestionnaire, :administrateur].each { |role| sign_out(role) } + + redirect_to link_sent_path(email: gestionnaire.email) + end + elsif user_signed_in? + set_flash_message :notice, :signed_in redirect_to after_sign_in_path_for(:user) - elsif gestionnaire_signed_in? - location = stored_location_for(:gestionnaire) || gestionnaire_procedures_path - redirect_to location - elsif administrateur_signed_in? - redirect_to admin_path else flash.alert = 'Mauvais couple login / mot de passe' new @@ -37,6 +49,10 @@ class Users::SessionsController < Sessions::SessionsController end end + def link_sent + @email = params[:email] + end + # DELETE /resource/sign_out def destroy if gestionnaire_signed_in? @@ -68,6 +84,27 @@ class Users::SessionsController < Sessions::SessionsController redirect_to new_user_session_path end + def sign_in_by_link + gestionnaire = Gestionnaire.find(params[:id]) + if gestionnaire&.login_token_valid?(params[:jeton]) + trust_device + flash.notice = "Merci d’avoir confirmé votre connexion. Votre navigateur est maintenant authentifié pour #{TRUSTED_DEVICE_PERIOD.to_i / ActiveSupport::Duration::SECONDS_PER_DAY} jours." + + user = User.find_by(email: gestionnaire.email) + administrateur = Administrateur.find_by(email: gestionnaire.email) + [user, gestionnaire, administrateur].compact.each { |resource| sign_in(resource) } + + if administrateur.present? + redirect_to admin_procedures_path + else + redirect_to gestionnaire_procedures_path + end + else + flash[:alert] = 'Votre lien est invalide ou expiré, veuillez-vous reconnecter.' + redirect_to new_user_session_path + end + end + private def error_procedure @@ -92,7 +129,6 @@ class Users::SessionsController < Sessions::SessionsController resource.remember_me = remember_me sign_in resource resource.force_sync_credentials - set_flash_message :notice, :signed_in end end end diff --git a/app/dashboards/gestionnaire_dashboard.rb b/app/dashboards/gestionnaire_dashboard.rb index a1666edf7..2f609a677 100644 --- a/app/dashboards/gestionnaire_dashboard.rb +++ b/app/dashboards/gestionnaire_dashboard.rb @@ -14,7 +14,8 @@ class GestionnaireDashboard < Administrate::BaseDashboard updated_at: Field::DateTime, current_sign_in_at: Field::DateTime, dossiers: Field::HasMany, - procedures: Field::HasMany + procedures: Field::HasMany, + features: FeaturesField }.freeze # COLLECTION_ATTRIBUTES @@ -35,7 +36,8 @@ class GestionnaireDashboard < Administrate::BaseDashboard :id, :email, :current_sign_in_at, - :created_at + :created_at, + :features ].freeze # FORM_ATTRIBUTES diff --git a/app/mailers/gestionnaire_mailer.rb b/app/mailers/gestionnaire_mailer.rb index 105eb7243..a3473f65f 100644 --- a/app/mailers/gestionnaire_mailer.rb +++ b/app/mailers/gestionnaire_mailer.rb @@ -34,4 +34,12 @@ class GestionnaireMailer < ApplicationMailer mail(to: recipient.email, subject: subject) end + + def send_login_token(gestionnaire, login_token) + @gestionnaire_id = gestionnaire.id + @login_token = login_token + subject = "Connexion sécurisée à demarches-simplifiees.fr" + + mail(to: gestionnaire.email, subject: subject) + end end diff --git a/app/models/administration.rb b/app/models/administration.rb index 1d350b240..8d98bab4d 100644 --- a/app/models/administration.rb +++ b/app/models/administration.rb @@ -19,11 +19,17 @@ class Administration < ApplicationRecord if administrateur.save AdministrationMailer.new_admin_email(administrateur, self).deliver_later administrateur.invite!(id) + User.create({ email: email, password: password, confirmed_at: Time.zone.now }) + + Gestionnaire.create({ + email: email, + password: password + }) end administrateur diff --git a/app/models/concerns/trusted_device_concern.rb b/app/models/concerns/trusted_device_concern.rb new file mode 100644 index 000000000..5aff22882 --- /dev/null +++ b/app/models/concerns/trusted_device_concern.rb @@ -0,0 +1,25 @@ +module TrustedDeviceConcern + extend ActiveSupport::Concern + + TRUSTED_DEVICE_COOKIE_NAME = :trusted_device + TRUSTED_DEVICE_PERIOD = 1.month + + def trust_device + cookies.encrypted[TRUSTED_DEVICE_COOKIE_NAME] = { + value: JSON.generate({ created_at: Time.zone.now }), + expires: TRUSTED_DEVICE_PERIOD, + httponly: true + } + end + + def trusted_device? + trusted_device_cookie.present? && + Time.zone.now - TRUSTED_DEVICE_PERIOD < JSON.parse(trusted_device_cookie)['created_at'] + end + + private + + def trusted_device_cookie + cookies.encrypted[TRUSTED_DEVICE_COOKIE_NAME] + end +end diff --git a/app/models/gestionnaire.rb b/app/models/gestionnaire.rb index 4829a736b..7f6fdcf3b 100644 --- a/app/models/gestionnaire.rb +++ b/app/models/gestionnaire.rb @@ -1,6 +1,7 @@ class Gestionnaire < ApplicationRecord include CredentialsSyncableConcern include EmailSanitizableConcern + include ActiveRecord::SecureToken devise :database_authenticatable, :registerable, :async, :recoverable, :rememberable, :trackable, :validatable @@ -144,6 +145,20 @@ 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 + 30.minutes.ago < login_token_created_at + rescue BCrypt::Errors::InvalidHash + false + end + def dossiers_id_with_notifications(dossiers) dossiers = dossiers.followed_by(self) @@ -190,6 +205,23 @@ class Gestionnaire < ApplicationRecord GestionnaireMailer.invite_gestionnaire(self, reset_password_token).deliver_later end + def feature_enabled?(feature) + Flipflop.feature_set.feature(feature) + features[feature.to_s] + end + + def disable_feature(feature) + Flipflop.feature_set.feature(feature) + features.delete(feature.to_s) + save + end + + def enable_feature(feature) + Flipflop.feature_set.feature(feature) + features[feature.to_s] = true + save + end + private def annotations_hash(demande, annotations_privees, avis, messagerie) diff --git a/app/views/fields/features_field/_show.html.haml b/app/views/fields/features_field/_show.html.haml index d3d864800..27e2741ca 100644 --- a/app/views/fields/features_field/_show.html.haml +++ b/app/views/fields/features_field/_show.html.haml @@ -1,7 +1,14 @@ +:ruby + url = if field.resource.class.name == 'Gestionnaire' + enable_feature_manager_gestionnaire_path(field.resource.id) + else + enable_feature_manager_administrateur_path(field.resource.id) + end + %table#features - 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 } + = check_box_tag "enable-feature", "enable", field.data[feature.name], data: { url: url, key: feature.key } diff --git a/app/views/gestionnaire_mailer/send_login_token.html.haml b/app/views/gestionnaire_mailer/send_login_token.html.haml new file mode 100644 index 000000000..c2c2374f4 --- /dev/null +++ b/app/views/gestionnaire_mailer/send_login_token.html.haml @@ -0,0 +1,12 @@ +%p + Bonjour, + +%p + Veuillez cliquer sur le lien suivant pour vous connecter sur le site demarches-simplifiees.fr :  + = link_to(sign_in_by_link_url(@gestionnaire_id, jeton: @login_token), sign_in_by_link_url(@gestionnaire_id, jeton: @login_token)) + +%p + Bonne journée, + +%p + L'équipe demarches-simplifiees.fr diff --git a/app/views/users/sessions/link_sent.html.haml b/app/views/users/sessions/link_sent.html.haml new file mode 100644 index 000000000..cd7f78c25 --- /dev/null +++ b/app/views/users/sessions/link_sent.html.haml @@ -0,0 +1,14 @@ +- content_for(:title, 'Lien de connexion par email') + +- content_for :footer do + = render partial: 'root/footer' + +#link-sent.container + = image_tag('user/confirmation-email.svg') + %h1 Encore une petite étape :) + + %p.mail + Ouvrez votre boite email #{@email} puis cliquez sur le lien d'activation du message Connexion sécurisée à demarches-simplifiees.fr. + + %p.help + En cas de difficultés, nous restons joignables sur #{link_to 'contact@demarches-simplifiees.fr', 'mailto:contact@demarches-simplifiees.fr'}. diff --git a/config/features.rb b/config/features.rb index 6ee9d203e..6ec364588 100644 --- a/config/features.rb +++ b/config/features.rb @@ -20,6 +20,7 @@ Flipflop.configure do feature :web_hook feature :publish_draft feature :support_form + feature :enable_email_login_token group :production do feature :remote_storage, diff --git a/config/routes.rb b/config/routes.rb index 1c5f6ac70..caa4e0487 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -26,6 +26,7 @@ Rails.application.routes.draw do resources :gestionnaires, only: [:index, :show] do post 'reinvite', on: :member + put 'enable_feature', on: :member end resources :dossiers, only: [:show] @@ -88,6 +89,8 @@ Rails.application.routes.draw do devise_scope :user do get '/users/sign_in/demo' => redirect("/users/sign_in") get '/users/no_procedure' => 'users/sessions#no_procedure' + get 'connexion-par-jeton/:id' => 'users/sessions#sign_in_by_link', as: 'sign_in_by_link' + get 'lien-envoye/:email' => 'users/sessions#link_sent', constraints: { email: /.*/ }, as: 'link_sent' end devise_scope :gestionnaire do diff --git a/db/migrate/20181108091339_add_encrypted_login_token_column_to_gestionnaire.rb b/db/migrate/20181108091339_add_encrypted_login_token_column_to_gestionnaire.rb new file mode 100644 index 000000000..9d92083f8 --- /dev/null +++ b/db/migrate/20181108091339_add_encrypted_login_token_column_to_gestionnaire.rb @@ -0,0 +1,6 @@ +class AddEncryptedLoginTokenColumnToGestionnaire < ActiveRecord::Migration[5.2] + def change + add_column :gestionnaires, :encrypted_login_token, :text + add_column :gestionnaires, :login_token_created_at, :datetime + end +end diff --git a/db/migrate/20181108151929_add_features_column_to_gestionnaires.rb b/db/migrate/20181108151929_add_features_column_to_gestionnaires.rb new file mode 100644 index 000000000..c90c6ebb6 --- /dev/null +++ b/db/migrate/20181108151929_add_features_column_to_gestionnaires.rb @@ -0,0 +1,5 @@ +class AddFeaturesColumnToGestionnaires < ActiveRecord::Migration[5.2] + def change + add_column :gestionnaires, :features, :jsonb, null: false, default: {} + end +end diff --git a/db/schema.rb b/db/schema.rb index f366acca0..8e642e1f6 100644 --- a/db/schema.rb +++ b/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: 2018_10_30_141238) do +ActiveRecord::Schema.define(version: 2018_11_08_151929) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -365,6 +365,9 @@ ActiveRecord::Schema.define(version: 2018_10_30_141238) do t.string "last_sign_in_ip" t.datetime "created_at" t.datetime "updated_at" + t.text "encrypted_login_token" + t.datetime "login_token_created_at" + t.jsonb "features", default: {}, null: false t.index ["email"], name: "index_gestionnaires_on_email", unique: true t.index ["reset_password_token"], name: "index_gestionnaires_on_reset_password_token", unique: true end diff --git a/lib/tasks/2018_10_30_admin_has_gestionnaire.rake b/lib/tasks/2018_10_30_admin_has_gestionnaire.rake new file mode 100644 index 000000000..c78969b78 --- /dev/null +++ b/lib/tasks/2018_10_30_admin_has_gestionnaire.rake @@ -0,0 +1,15 @@ +namespace :'2018_10_30_admin_has_gestionnaire' do + task run: :environment do + admin_without_gestionnaire_ids = Administrateur + .find_by_sql('SELECT administrateurs.id FROM administrateurs LEFT OUTER JOIN gestionnaires ON gestionnaires.email = administrateurs.email WHERE gestionnaires.email IS NULL') + .pluck(:id) + + admin_without_gestionnaire_ids.each do |admin_id| + admin = Administrateur.find(admin_id) + g = Gestionnaire.new + g.email = admin.email + g.encrypted_password = admin.encrypted_password + g.save(validate: false) + end + end +end diff --git a/lib/tasks/2018_11_08_activate_trusted_device_for_a-f.rake b/lib/tasks/2018_11_08_activate_trusted_device_for_a-f.rake new file mode 100644 index 000000000..fe55cb3ee --- /dev/null +++ b/lib/tasks/2018_11_08_activate_trusted_device_for_a-f.rake @@ -0,0 +1,8 @@ +namespace :'activate_trusted_device_for_a-f' do + task run: :environment do + letters_a_to_f = ('a'..'f').to_a + Gestionnaire + .where("substr(email, 1, 1) IN (?)", letters_a_to_f) + .update_all(features: { "enable_email_login_token" => true }) + end +end diff --git a/spec/controllers/administrateur/activate_controller_spec.rb b/spec/controllers/administrateur/activate_controller_spec.rb new file mode 100644 index 000000000..a6e99f851 --- /dev/null +++ b/spec/controllers/administrateur/activate_controller_spec.rb @@ -0,0 +1,20 @@ +describe Administrateurs::ActivateController, type: :controller do + describe '#new' do + let(:admin) { create(:administrateur) } + let(:token) { admin.send(:set_reset_password_token) } + + before { allow(controller).to receive(:trust_device) } + + context 'when the token is ok' do + before { get :new, params: { token: token } } + + it { expect(controller).to have_received(:trust_device) } + end + + context 'when the token is bad' do + before { get :new, params: { token: 'bad' } } + + it { expect(controller).not_to have_received(:trust_device) } + end + end +end diff --git a/spec/controllers/gestionnaires/activate_controller_spec.rb b/spec/controllers/gestionnaires/activate_controller_spec.rb new file mode 100644 index 000000000..fa7ce7857 --- /dev/null +++ b/spec/controllers/gestionnaires/activate_controller_spec.rb @@ -0,0 +1,20 @@ +describe Gestionnaires::ActivateController, type: :controller do + describe '#new' do + let(:gestionnaire) { create(:gestionnaire) } + let(:token) { gestionnaire.send(:set_reset_password_token) } + + before { allow(controller).to receive(:trust_device) } + + context 'when the token is ok' do + before { get :new, params: { token: token } } + + it { expect(controller).to have_received(:trust_device) } + end + + context 'when the token is bad' do + before { get :new, params: { token: 'bad' } } + + it { expect(controller).not_to have_received(:trust_device) } + end + end +end diff --git a/spec/controllers/users/sessions_controller_spec.rb b/spec/controllers/users/sessions_controller_spec.rb index af12c7ce2..3fcc5e9de 100644 --- a/spec/controllers/users/sessions_controller_spec.rb +++ b/spec/controllers/users/sessions_controller_spec.rb @@ -27,7 +27,7 @@ describe Users::SessionsController, type: :controller do let(:password) { 'un super mot de passe' } let(:user) { create(:user, email: email, password: password) } - let(:gestionnaire) { create(:gestionnaire, email: email, password: password) } + let(:gestionnaire) { create(:gestionnaire, :with_trusted_device, email: email, password: password) } let(:administrateur) { create(:administrateur, email: email, password: password) } it 'signs user in' do @@ -41,18 +41,39 @@ describe Users::SessionsController, type: :controller do it 'signs gestionnaire in' do post :create, params: { user: { email: gestionnaire.email, password: gestionnaire.password } } - expect(@response.redirect?).to be(true) + + expect(subject).to redirect_to link_sent_path(email: gestionnaire.email) expect(subject.current_user).to be(nil) - expect(subject.current_gestionnaire).to eq(gestionnaire) + expect(subject.current_gestionnaire).to be(nil) expect(subject.current_administrateur).to be(nil) end - it 'signs administrateur in' do - post :create, params: { user: { email: administrateur.email, password: administrateur.password } } - expect(@response.redirect?).to be(true) - expect(subject.current_user).to be(nil) - expect(subject.current_gestionnaire).to be(nil) - expect(subject.current_administrateur).to eq(administrateur) + context 'when the device is trusted' do + before do + allow(controller).to receive(:trusted_device?).and_return(true) + post :create, params: { user: { email: gestionnaire.email, password: gestionnaire.password } } + end + + it 'directly log the gestionnaire' do + expect(subject).to redirect_to gestionnaire_procedures_path + expect(subject.current_user).to be(nil) + expect(subject.current_gestionnaire).to eq(gestionnaire) + expect(subject.current_administrateur).to be(nil) + end + end + + context 'signs administrateur in' do + # an admin has always an gestionnaire role + before { gestionnaire } + + it 'signs administrateur in' do + post :create, params: { user: { email: administrateur.email, password: administrateur.password } } + + expect(subject).to redirect_to link_sent_path(email: gestionnaire.email) + expect(subject.current_user).to be(nil) + expect(subject.current_gestionnaire).to be(nil) + expect(subject.current_administrateur).to eq(nil) + end end context { @@ -63,10 +84,16 @@ describe Users::SessionsController, type: :controller do it 'signs user + gestionnaire + administrateur in' do post :create, params: { user: { email: administrateur.email, password: administrateur.password } } - expect(@response.redirect?).to be(true) - expect(subject.current_user).to eq(user) - expect(subject.current_gestionnaire).to eq(gestionnaire) - expect(subject.current_administrateur).to eq(administrateur) + + expect(subject).to redirect_to link_sent_path(email: gestionnaire.email) + + # TODO: fix me + # Strange behaviour: sign_out(:user) does not work in spec + # but seems to work in live + # expect(controller.current_user).to be(nil) + + expect(subject.current_gestionnaire).to be(nil) + expect(subject.current_administrateur).to be(nil) expect(user.reload.loged_in_with_france_connect).to be(nil) end } @@ -219,4 +246,75 @@ describe Users::SessionsController, type: :controller do end end end + + describe '#sign_in_by_link' do + context 'when the gestionnaire has non other account' do + let(:gestionnaire) { create(:gestionnaire) } + before do + allow(controller).to receive(:trust_device) + post :sign_in_by_link, params: { id: gestionnaire.id, login_token: login_token } + end + + context 'when the token is valid' do + let(:login_token) { gestionnaire.login_token! } + + it { is_expected.to redirect_to gestionnaire_procedures_path } + it { expect(controller.current_gestionnaire).to eq(gestionnaire) } + it { expect(controller).to have_received(:trust_device) } + end + + context 'when the token is invalid' do + let(:login_token) { 'invalid_token' } + + it { is_expected.to redirect_to new_user_session_path } + it { expect(controller.current_gestionnaire).to be_nil } + it { expect(controller).not_to have_received(:trust_device) } + end + end + + context 'when the gestionnaire has an user and admin account' do + let(:email) { 'unique@plop.com' } + let(:password) { 'un super mot de passe' } + + let!(:user) { create(:user, email: email, password: password) } + let!(:gestionnaire) { create(:gestionnaire, email: email, password: password) } + let!(:administrateur) { create(:administrateur, email: email, password: password) } + + before do + post :sign_in_by_link, params: { id: gestionnaire.id, login_token: login_token } + end + + context 'when the token is valid' do + let(:login_token) { gestionnaire.login_token! } + + it { expect(controller.current_gestionnaire).to eq(gestionnaire) } + it { expect(controller.current_administrateur).to eq(administrateur) } + it { expect(controller.current_user).to eq(user) } + end + end + end + + describe '#trust_device and #trusted_device?' do + subject { controller.trusted_device? } + + context 'when the trusted cookie is not present' do + it { is_expected.to be false } + end + + context 'when the cookie is outdated' do + before do + Timecop.freeze(Time.zone.now - TrustedDeviceConcern::TRUSTED_DEVICE_PERIOD - 1.minute) + controller.trust_device + Timecop.return + end + + it { is_expected.to be false } + end + + context 'when the cookie is ok' do + before { controller.trust_device } + + it { is_expected.to be true } + end + end end diff --git a/spec/factories/gestionnaire.rb b/spec/factories/gestionnaire.rb index dc07d61e9..b93610555 100644 --- a/spec/factories/gestionnaire.rb +++ b/spec/factories/gestionnaire.rb @@ -4,4 +4,10 @@ FactoryBot.define do email { generate(:gestionnaire_email) } password { 'password' } end + + trait :with_trusted_device do + after(:create) do |gestionnaire| + gestionnaire.update(features: { "enable_email_login_token" => true }) + end + end end diff --git a/spec/features/admin/connection_spec.rb b/spec/features/admin/connection_spec.rb index 3b6168311..1647e5bb7 100644 --- a/spec/features/admin/connection_spec.rb +++ b/spec/features/admin/connection_spec.rb @@ -1,20 +1,26 @@ require 'spec_helper' feature 'Administrator connection' do - let(:admin) { create(:administrateur) } + include ActiveJob::TestHelper + + let(:email) { 'admin1@admin.com' } + let(:password) { 'mon chien aime les bananes' } + let!(:admin) { create(:administrateur, email: email, password: password) } + let!(:gestionnaire) { create(:gestionnaire, :with_trusted_device, email: email, password: password) } + before do visit new_administrateur_session_path end + scenario 'administrator is on sign in page' do expect(page).to have_css('#new_user') end context "admin fills form and log in" do before do - page.find_by_id('user_email').set admin.email - page.find_by_id('user_password').set admin.password - page.click_on 'Se connecter' + sign_in_with(email, password, true) end + scenario 'a menu button is available' do expect(page).to have_css('#admin_menu') end diff --git a/spec/features/new_gestionnaire/gestionnaire_spec.rb b/spec/features/new_gestionnaire/gestionnaire_spec.rb index 1461b0ae0..e38f6c3f7 100644 --- a/spec/features/new_gestionnaire/gestionnaire_spec.rb +++ b/spec/features/new_gestionnaire/gestionnaire_spec.rb @@ -4,7 +4,7 @@ feature 'The gestionnaire part' do include ActiveJob::TestHelper let(:password) { 'secret_password' } - let!(:gestionnaire) { create(:gestionnaire, password: password) } + let!(:gestionnaire) { create(:gestionnaire, :with_trusted_device, password: password) } let!(:procedure) { create(:procedure, :published, gestionnaires: [gestionnaire]) } let!(:dossier) { create(:dossier, state: Dossier.states.fetch(:en_construction), procedure: procedure) } @@ -116,7 +116,7 @@ feature 'The gestionnaire part' do log_out - log_in(gestionnaire.email, password) + log_in(gestionnaire.email, password, check_email: false) click_on procedure.libelle click_on dossier.user.email @@ -173,14 +173,13 @@ feature 'The gestionnaire part' do expect(page).to have_text("Dossier envoyé") end - def log_in(email, password) + def log_in(email, password, check_email: true) visit '/' click_on 'Connexion' expect(page).to have_current_path(new_user_session_path) - fill_in 'user_email', with: email - fill_in 'user_password', with: password - click_on 'Se connecter' + sign_in_with(email, password, check_email) + expect(page).to have_current_path(gestionnaire_procedures_path) end @@ -196,7 +195,7 @@ feature 'The gestionnaire part' do end def test_mail(to, content) - mail = ActionMailer::Base.deliveries.first + mail = ActionMailer::Base.deliveries.last expect(mail.to).to match([to]) expect(mail.body.parts.map(&:to_s)).to all(include(content)) end diff --git a/spec/mailers/previews/gestionnaire_mailer_preview.rb b/spec/mailers/previews/gestionnaire_mailer_preview.rb index d38ca04e2..d7707bd22 100644 --- a/spec/mailers/previews/gestionnaire_mailer_preview.rb +++ b/spec/mailers/previews/gestionnaire_mailer_preview.rb @@ -7,4 +7,8 @@ class GestionnaireMailerPreview < ActionMailer::Preview def send_dossier GestionnaireMailer.send_dossier(Gestionnaire.first, Dossier.first, Gestionnaire.last) end + + def send_login_token + GestionnaireMailer.send_login_token(Gestionnaire.first, "token") + end end diff --git a/spec/models/administration_spec.rb b/spec/models/administration_spec.rb index 91444ad3b..77f3594f2 100644 --- a/spec/models/administration_spec.rb +++ b/spec/models/administration_spec.rb @@ -20,6 +20,12 @@ describe Administration, type: :model do expect(user).to be_present end + it 'creates a corresponding gestionnaire account for the email' do + subject + gestionnaire = Gestionnaire.find_by(email: valid_email) + expect(gestionnaire).to be_present + end + context 'when there already is a user account with the same email' do before { create(:user, email: valid_email) } it 'still creates an admin account' do diff --git a/spec/support/feature_helpers.rb b/spec/support/feature_helpers.rb index 0f0504446..f0d0f6447 100644 --- a/spec/support/feature_helpers.rb +++ b/spec/support/feature_helpers.rb @@ -17,10 +17,21 @@ module FeatureHelpers dossier end - def sign_in_with(email, password) + def sign_in_with(email, password, sign_in_by_link = false) fill_in :user_email, with: email fill_in :user_password, with: password - click_on 'Se connecter' + + perform_enqueued_jobs do + click_on 'Se connecter' + end + + if sign_in_by_link + mail = ActionMailer::Base.deliveries.last + message = mail.body.parts.join(&:to_s) + login_token = message[/connexion-par-jeton\/(.*)/, 1] + + visit sign_in_by_link_path(login_token) + end end def sign_up_with(email, password = 'testpassword')