Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ajout du champ notification_email pour les usagers, modifiable uniquement via l'api (par rdv-insertion) #5003

Draft
wants to merge 3 commits into
base: production
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/blueprints/user_blueprint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ class UserBlueprint < Blueprinter::Base
:notify_by_email, :invitation_created_at, :invitation_accepted_at, :created_at, :case_number, :address_details,
:logement, :notes

field :notification_email, if: ->(_field_name, _user, options) { options[:include_notification_email] }
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A voir si on ne souhaite pas conditionner de cette façon, on pourrait toujours afficher le champ si il est rempli par exemple.


association :responsible, blueprint: UserBlueprint

association :user_profiles, blueprint: UserProfileBlueprint, view: :without_user do |user, options|
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/api/rdvinsertion/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ class Api::Rdvinsertion::UsersController < Api::Rdvinsertion::AgentAuthBaseContr
before_action :set_user, only: [:show]

def show
render_record @user, agent_context: fake_agent_context
render_record @user, agent_context: fake_agent_context, include_notification_email: true
end

private
Expand Down
6 changes: 5 additions & 1 deletion app/controllers/api/v1/agent_auth_base_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ def current_organisation
end
end

def rdvinsertion_request?
request.headers.include?("X-Agent-Auth-Signature")
end

# Rescuable exceptions

rescue_from Pundit::NotAuthorizedError, with: :not_authorized
Expand Down Expand Up @@ -65,7 +69,7 @@ def record_invalid(exception)

def authenticate_agent
if request.headers.include?("X-Agent-Auth-Signature")
# Bypass DeviseTokenAuth
# Bypass DeviseTokenAuth for rdv-insertion
authenticate_agent_with_shared_secret
elsif request.headers["HTTP_ACCESS_TOKEN"] && request.headers["HTTP_UID"]
# Use DeviseTokenAuth
Expand Down
16 changes: 10 additions & 6 deletions app/controllers/api/v1/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ def index
end

def show
render_record @user, agent_context: pundit_user
render_record @user, agent_context: pundit_user, include_notification_email: rdvinsertion_request?
end

def create
Expand All @@ -19,13 +19,13 @@ def create
authorize(user, policy_class: Agent::UserPolicy)
user.skip_confirmation_notification!
user.save!
render_record user
render_record user, include_notification_email: rdvinsertion_request?
end

def update
@user.skip_reconfirmation!
@user.update!(user_params)
render_record @user
render_record @user, include_notification_email: rdvinsertion_request?
end

def rdv_invitation_token
Expand All @@ -47,14 +47,18 @@ def set_user
end

def user_params
attrs = %i[
base_attrs = %i[
first_name birth_name last_name email address phone_number
birth_date responsible_id caisse_affiliation affiliation_number
family_situation number_of_children notify_by_sms notify_by_email
]

attrs -= User::FranceconnectFrozenFieldsConcern::FROZEN_FIELDS if @user&.logged_once_with_franceconnect?
# Seul rdv-insertion peut enregistrer l'email de notification des usagers
# Celui ci permet de notifier plusieurs usagers avec le même email
base_attrs << :notification_email if rdvinsertion_request?
Comment on lines -56 to +58
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Je trouve ça étonnant qu'on ait un champ uniquement dispo à RDV-I dans notre API publique. Quel est le risque de rendre ce champ utilisable par n'importe qui ?


params.permit(attrs, organisation_ids: [], referent_agent_ids: [])
base_attrs -= User::FranceconnectFrozenFieldsConcern::FROZEN_FIELDS if @user&.logged_once_with_franceconnect?

params.permit(base_attrs, organisation_ids: [], referent_agent_ids: [])
end
end
6 changes: 5 additions & 1 deletion app/helpers/users_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ def birth_date_and_age_if_exist(user)
end

def notify_by_email_description(user)
if user.responsible_email.blank?
if user.preferred_email.blank?
"🔴 pas d'email renseigné"
elsif user.responsible_notify_by_email?
"🟢 Activées"
Expand All @@ -96,6 +96,10 @@ def clickable_user_email(user)
user.responsible_email.present? ? mail_to(user.responsible_email) : nil
end

def clickable_user_notification_email(user)
user.notification_email.present? ? mail_to(user.notification_email) : nil
end

def notify_by_sms_description(user)
if user.responsible_phone_number.blank?
"🔴 pas de numéro de téléphone renseigné"
Expand Down
2 changes: 1 addition & 1 deletion app/mailers/users/file_attente_mailer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ class Users::FileAttenteMailer < ApplicationMailer
@token = params[:token]
end

default to: -> { @user.email }
default to: -> { @user.preferred_email }

def new_creneau_available
subject = t("users.file_attente_mailer.new_creneau_available.title")
Expand Down
2 changes: 1 addition & 1 deletion app/mailers/users/rdv_mailer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class Users::RdvMailer < ApplicationMailer
@token = params[:token]
end

default to: -> { @user.email }
default to: -> { @user.preferred_email }

def rdv_created
self.ics_payload = @rdv.payload(:create, @user)
Expand Down
10 changes: 9 additions & 1 deletion app/models/concerns/user/notificable_concern.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@ module User::NotificableConcern
extend ActiveSupport::Concern

def notifiable_by_email?
email.present? && notify_by_email?
valid_email? && notify_by_email?
end

def valid_email?
email.present? || notification_email.present?
end

def preferred_email
email.presence || notification_email.presence
end

def notifiable_by_sms?
Expand Down
2 changes: 1 addition & 1 deletion app/models/concerns/user/responsability_concern.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ module User::ResponsabilityConcern
end

delegate(
:phone_number, :email, :address,
:phone_number, :email, :notification_email, :address,
:notify_by_email, :notify_by_email?, :notify_by_sms, :notify_by_sms?,
:phone_number_mobile?,
to: :responsible_or_self,
Expand Down
15 changes: 13 additions & 2 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def self.search_options

# Attributes
ONGOING_MARGIN = 1.hour.freeze
auto_strip_attributes :email, :first_name, :last_name, :birth_name
auto_strip_attributes :email, :notification_email, :first_name, :last_name, :birth_name

enum :caisse_affiliation, { aucune: 0, caf: 1, msa: 2 }
enum :family_situation, { single: 0, in_a_relationship: 1, divorced: 2 }
Expand Down Expand Up @@ -72,6 +72,7 @@ def self.search_options

# Hooks
before_save :set_email_to_null_if_blank
before_save :clear_notification_email_if_email_present

# Scopes
default_scope { where(deleted_at: nil) }
Expand All @@ -90,6 +91,11 @@ def email=(value)
super(value&.gsub(".@", "@")&.gsub("..", "."))
end

def notification_email=(value)
# On applique les mêmes corrections pour notification_email
super(value&.gsub(".@", "@")&.gsub("..", "."))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Je pense qu'on peut factoriser dans une méthode de classe du genre .sanitize_email(email) puis appeler super(sanitize_email(value))

end

def add_organisation(organisation)
self_and_relatives_and_responsible.each do |u|
u.organisations << organisation if u.organisation_ids.exclude?(organisation.id)
Expand Down Expand Up @@ -275,6 +281,10 @@ def set_email_to_null_if_blank
self.email = nil if email.blank?
end

def clear_notification_email_if_email_present
self.notification_email = nil if email.present?
end

def birth_date_validity
return unless birth_date.present? && (birth_date > Time.zone.today || birth_date < 130.years.ago)

Expand All @@ -297,7 +307,8 @@ def do_soft_delete(organisation)
first_name: "Usager supprimé",
last_name: "Usager supprimé",
deleted_at: Time.zone.now,
email: deleted_email
email: deleted_email,
notification_email: nil
)
reload # anonymizer operates outside the realm of rails knowledge
end
Expand Down
2 changes: 2 additions & 0 deletions app/views/admin/users/_responsible_information.html.slim
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ ul.list-unstyled.mb-5
li.mb-2= object_attribute_tag(user, :birth_date, birth_date_and_age(user))
li.mb-2= object_attribute_tag(user, :address)
li.mb-2= object_attribute_tag(user, :email, clickable_user_email(user))
- if user.notification_email.present?
li.mb-2= object_attribute_tag(user, :notification_email, clickable_user_notification_email(user))
li.mb-2= object_attribute_tag(user, :notify_by_email, notify_by_email_description(user))
li.mb-2
= object_attribute_tag(user, :phone_number, clickable_user_phone_number(user))
Expand Down
1 change: 1 addition & 0 deletions config/anonymizer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ tables:
- birth_name
- birth_date
- email
- notification_email
- unconfirmed_email
- address
- address_details
Expand Down
1 change: 1 addition & 0 deletions config/locales/models/user.fr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ fr:
responsability_type: Type d'usager
notify_by_sms: "Accepte les notifications par SMS"
notify_by_email: "Accepte les notifications par email"
notification_email: "Email de notification"
created_through: "Origine du compte"
notes: Remarques
ants_pre_demande_number: &ants_pre_demande_number_label Numéro de pré-demande ANTS
Expand Down
8 changes: 8 additions & 0 deletions db/migrate/20250121132708_add_notification_email_to_user.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class AddNotificationEmailToUser < ActiveRecord::Migration[7.1]
disable_ddl_transaction!

def change
add_column :users, :notification_email, :string
add_index :users, :notification_email, algorithm: :concurrently
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Un index partiel (WHERE NOT NULL) mer semble très judicieux ici !

end
end
4 changes: 3 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[7.1].define(version: 2025_01_08_115309) do
ActiveRecord::Schema[7.1].define(version: 2025_01_21_132708) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_stat_statements"
enable_extension "pgcrypto"
Expand Down Expand Up @@ -805,6 +805,7 @@
t.string "rdv_invitation_token"
t.virtual "text_search_terms", type: :tsvector, as: "(((((setweight(to_tsvector('simple'::regconfig, translate(lower((COALESCE(last_name, ''::character varying))::text), 'àâäéèêëïîôöùûüÿç'::text, 'aaaeeeeiioouuuyc'::text)), 'A'::\"char\") || setweight(to_tsvector('simple'::regconfig, translate(lower((COALESCE(first_name, ''::character varying))::text), 'àâäéèêëïîôöùûüÿç'::text, 'aaaeeeeiioouuuyc'::text)), 'B'::\"char\")) || setweight(to_tsvector('simple'::regconfig, translate(lower((COALESCE(birth_name, ''::character varying))::text), 'àâäéèêëïîôöùûüÿç'::text, 'aaaeeeeiioouuuyc'::text)), 'C'::\"char\")) || setweight(to_tsvector('simple'::regconfig, (COALESCE(email, ''::character varying))::text), 'D'::\"char\")) || setweight(to_tsvector('simple'::regconfig, (COALESCE(phone_number_formatted, ''::character varying))::text), 'D'::\"char\")) || setweight(to_tsvector('simple'::regconfig, COALESCE((id)::text, ''::text)), 'D'::\"char\"))", stored: true
t.datetime "rdv_invitation_token_updated_at"
t.string "notification_email"
t.index ["birth_date"], name: "index_users_on_birth_date"
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
t.index ["created_through"], name: "index_users_on_created_through"
Expand All @@ -816,6 +817,7 @@
t.index ["invited_by_id"], name: "index_users_on_invited_by_id"
t.index ["invited_by_type", "invited_by_id"], name: "index_users_on_invited_by_type_and_invited_by_id"
t.index ["last_name"], name: "index_users_on_last_name"
t.index ["notification_email"], name: "index_users_on_notification_email"
t.index ["phone_number_formatted"], name: "index_users_on_phone_number_formatted"
t.index ["rdv_invitation_token"], name: "index_users_on_rdv_invitation_token", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
Expand Down
3 changes: 2 additions & 1 deletion spec/requests/api/rdvinsertion/users_request_spec.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
RSpec.describe "rdv-insertion API: users" do
let(:agent) { create(:agent, basic_role_in_organisations: [organisation_rdv_insertion]) }
let(:user) { create(:user, organisations: [organisation_rdv_insertion, other_organisation_rdv_insertion, organisation_rdv_solidarites]) }
let(:user) { create(:user, :with_no_email, notification_email: "[email protected]", organisations: [organisation_rdv_insertion, other_organisation_rdv_insertion, organisation_rdv_solidarites]) }

let(:shared_secret) { "S3cr3T" }
let(:auth_headers) { api_auth_headers_with_shared_secret(agent, shared_secret) }
Expand All @@ -19,5 +19,6 @@
response_user_profiles = response.parsed_body.dig("user", "user_profiles")
response_organisation_ids = response_user_profiles.map { |user_profile| user_profile.dig("organisation", "id") }
expect(response_organisation_ids).to contain_exactly(organisation_rdv_insertion.id, other_organisation_rdv_insertion.id)
expect(parsed_response_body["user"]["notification_email"]).to eq("[email protected]")
end
end
56 changes: 56 additions & 0 deletions spec/requests/api/v1/swagger_doc/users_request_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
parameter name: "birth_name", in: :query, type: :string, description: "Nom de naissance", example: "Fripouille", required: false
parameter name: "birth_date", in: :query, type: :string, description: "Date de naissance", example: "1976-10-01", required: false
parameter name: "email", in: :query, type: :string, description: "Email", example: "[email protected]", required: false
parameter name: "notification_email", in: :query, type: :string, description: "Email de notification", required: false, document: false
parameter name: "phone_number", in: :query, type: :string, description: "Numéro de téléphone", example: "33600008012", required: false
parameter name: "address", in: :query, type: :string, description: "Adresse", example: "10 rue du Havre, Paris, 75016", required: false
parameter name: "caisse_affiliation", in: :query, type: :string, description: "Caisse d'affiliation", example: "caf", required: false
Expand Down Expand Up @@ -148,6 +149,22 @@
it { expect(user.reload.responsible).to eq(user_responsible) }
end

response 200, "cannot update notification_email field without shared secret auth", document: false do
before do
user.update(email: nil) # notification_email can't be set if devise email is set
end

let(:notification_email) { "[email protected]" }
let(:first_name) { "Alain" }
let(:last_name) { "Verse" }

run_test!

it { expect(user.reload.first_name).to eq(first_name) }
it { expect(user.reload.last_name).to eq(last_name) }
it { expect(user.reload.notification_email).to be_nil }
end

response 200, "updates a user with a minimal set of params", document: false do
let(:first_name) { "Alain" }
let(:last_name) { "Verse" }
Expand Down Expand Up @@ -199,6 +216,45 @@
let(:email) { existing_user.email }
end
end

patch "Mettre à jour un·e usager·ère via l'authentification shared secret", params: { document: false } do
with_shared_secret_authentication

tags "User"
produces "application/json"
operationId "updateUser"
description "Met à jour un·e usager·ère"

parameter name: :user_id, in: :path, type: :integer, description: "ID de l'usager·ère", example: 123
parameter name: "first_name", in: :query, type: :string, description: "Prénom", example: "Johnny", required: false
parameter name: "last_name", in: :query, type: :string, description: "Nom", example: "Silverhand", required: false
parameter name: "notification_email", in: :query, type: :string, description: "Email de notification", required: false, document: false

let(:user) { create(:user, :with_no_email, first_name: "Jean", last_name: "JACQUES", organisations: [organisation]) }
let(:user_id) { user.id }

before do
allow(Agent).to receive(:find_by).and_return(agent)
allow(ENV).to receive(:fetch).with("SHARED_SECRET_FOR_AGENTS_AUTH").and_return(shared_secret)
allow(ActiveSupport::SecurityUtils).to receive(:secure_compare).and_return(true)
end

let!(:shared_secret) { "S3cr3T" }
let!(:auth_headers) { api_auth_headers_with_shared_secret(agent, shared_secret) }
let!(:uid) { auth_headers["uid"].to_s }
let!(:"X-Agent-Auth-Signature") { auth_headers["X-Agent-Auth-Signature"].to_s }

response 200, "updates notification_email when authenticated through rdvinsertion", document: false do
let(:notification_email) { "[email protected]" }
let(:first_name) { "Alain" }
let(:last_name) { "Verse" }

run_test!

it { expect(parsed_response_body["user"]["notification_email"]).to eq(notification_email) }
it { expect(user.reload.notification_email).to eq(notification_email) }
end
end
end

path "/api/v1/users/{user_id}/rdv_invitation_token" do
Expand Down
Loading