diff --git a/Gemfile b/Gemfile index 761caed6a..c5a760d5e 100644 --- a/Gemfile +++ b/Gemfile @@ -69,6 +69,7 @@ gem 'rgeo-geojson' gem 'sanitize-url' gem 'sassc-rails' # Use SCSS for stylesheets gem 'sentry-raven' +gem 'sib-api-v3-sdk' gem 'skylight' gem 'smart_listing' gem 'spreadsheet_architect' diff --git a/Gemfile.lock b/Gemfile.lock index d15812e37..975be0b1d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -102,7 +102,7 @@ GEM rake (>= 10.4, < 14.0) ast (2.4.1) attr_required (1.0.1) - autoprefixer-rails (10.0.0.2) + autoprefixer-rails (10.0.1.0) execjs axe-matchers (2.6.1) dumb_delegator (~> 0.8) @@ -348,6 +348,7 @@ GEM rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) + json (2.3.1) json-jwt (1.13.0) activesupport (>= 4.2) aes_key_wrap @@ -658,6 +659,9 @@ GEM shellany (0.0.1) shoulda-matchers (4.4.1) activesupport (>= 4.2.0) + sib-api-v3-sdk (7.0.0) + json (~> 2.1, >= 2.1.0) + typhoeus (~> 1.0, >= 1.0.1) simple_xlsx_reader (1.0.4) nokogiri rubyzip @@ -860,6 +864,7 @@ DEPENDENCIES scss_lint sentry-raven shoulda-matchers + sib-api-v3-sdk simple_xlsx_reader skylight smart_listing diff --git a/app/controllers/manager/users_controller.rb b/app/controllers/manager/users_controller.rb index d9e33fcb9..43535c441 100644 --- a/app/controllers/manager/users_controller.rb +++ b/app/controllers/manager/users_controller.rb @@ -46,5 +46,27 @@ module Manager redirect_to manager_users_path end + + def emails + @user = User.find(params[:id]) + + transactionnal_api = ::SibApiV3Sdk::TransactionalEmailsApi.new + + @transactionnal_emails = transactionnal_api.get_transac_emails_list(email: @user.email) + @events = transactionnal_api.get_email_event_report(email: @user.email, days: 30) + + rescue ::SibApiV3Sdk::ApiError => e + flash.alert = "Impossible de récupérer les emails de cet utilisateur chez Sendinblue : #{e.message}" + end + + def unblock_user + @user = User.find(params[:id]) + + transactionnal_api = ::SibApiV3Sdk::TransactionalEmailsApi.new + transactionnal_api.smtp_blocked_contacts_email_delete(@user.email) + + rescue ::SibApiV3Sdk::ApiError => e + flash.alert = "Impossible de débloquer cet email auprès de Sendinblue : #{e.message}" + end end end diff --git a/app/controllers/webhook_controller.rb b/app/controllers/webhook_controller.rb index a8ab7193a..1a9a1288e 100644 --- a/app/controllers/webhook_controller.rb +++ b/app/controllers/webhook_controller.rb @@ -26,6 +26,8 @@ class WebhookController < ActionController::Base html << link_to_manager(administrateur, url) end + html << email_link_to_manager(user) + render json: { html: html.join('
') } end end @@ -36,6 +38,11 @@ class WebhookController < ActionController::Base "#{model.model_name.human}##{model.id}" end + def email_link_to_manager(user) + url = emails_manager_user_url(user) + "Emails##{user.id}" + end + def verify_signature! if generate_body_signature(request.body.read) != request.headers['X-Helpscout-Signature'] request_http_token_authentication diff --git a/app/helpers/email_helper.rb b/app/helpers/email_helper.rb new file mode 100644 index 000000000..9bd74dc65 --- /dev/null +++ b/app/helpers/email_helper.rb @@ -0,0 +1,12 @@ +module EmailHelper + def event_color_code(email_events) + unique_events = email_events.map(&:event) + if unique_events.include?('delivered') + return 'email-sent' + elsif unique_events.include?('blocked') || unique_events.include?('hardBounces') + return 'email-blocked' + else + return '' + end + end +end diff --git a/app/javascript/components/TypesDeChampEditor/OperationsQueue.js b/app/javascript/components/TypesDeChampEditor/OperationsQueue.js index f849e64bc..166f1891a 100644 --- a/app/javascript/components/TypesDeChampEditor/OperationsQueue.js +++ b/app/javascript/components/TypesDeChampEditor/OperationsQueue.js @@ -1,4 +1,4 @@ -import { to, getJSON } from '@utils'; +import { getJSON } from '@utils'; export default class OperationsQueue { constructor(baseUrl) { @@ -30,23 +30,27 @@ export default class OperationsQueue { async exec(operation) { const { path, method, payload, resolve, reject } = operation; const url = `${this.baseUrl}${path}`; - const [data, xhr] = await to(getJSON(url, payload, method)); - if (xhr) { - handleError(xhr, reject); - } else { + try { + const data = await getJSON(url, payload, method); resolve(data); + } catch (e) { + handleError(e, reject); } } } -function handleError(xhr, reject) { - try { - const { - errors: [message] - } = JSON.parse(xhr.responseText); +async function handleError({ response, message }, reject) { + if (response) { + try { + const { + errors: [message] + } = await response.json(); + reject(message); + } catch { + reject(await response.text()); + } + } else { reject(message); - } catch (e) { - reject(xhr.responseText); } } diff --git a/app/javascript/packs/application-old.js b/app/javascript/packs/application-old.js index 5f831fc68..e33dfc996 100644 --- a/app/javascript/packs/application-old.js +++ b/app/javascript/packs/application-old.js @@ -5,7 +5,6 @@ import jQuery from 'jquery'; import '../shared/page-update-event'; import '../shared/activestorage/ujs'; -import '../shared/rails-ujs-fix'; import '../shared/safari-11-file-xhr-workaround'; import '../shared/remote-input'; import '../shared/franceconnect'; diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index c58265eff..a742f3d29 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -7,7 +7,6 @@ import ReactRailsUJS from 'react_ujs'; import '../shared/page-update-event'; import '../shared/activestorage/ujs'; import '../shared/remote-poller'; -import '../shared/rails-ujs-fix'; import '../shared/safari-11-file-xhr-workaround'; import '../shared/remote-input'; import '../shared/franceconnect'; diff --git a/app/javascript/shared/rails-ujs-fix.js b/app/javascript/shared/rails-ujs-fix.js deleted file mode 100644 index b9031ce63..000000000 --- a/app/javascript/shared/rails-ujs-fix.js +++ /dev/null @@ -1,20 +0,0 @@ -import jQuery from 'jquery'; - -// rails-ujs installs CSRFProtection for its own ajax implementation. We might need -// CSRFProtection for jQuery initiated requests. This code is from jquery-ujs. -jQuery.ajaxPrefilter((options, originalOptions, xhr) => { - if (!options.crossDomain) { - CSRFProtection(xhr); - } -}); - -function csrfToken() { - return jQuery('meta[name=csrf-token]').attr('content'); -} - -function CSRFProtection(xhr) { - let token = csrfToken(); - if (token) { - xhr.setRequestHeader('X-CSRF-Token', token); - } -} diff --git a/app/javascript/shared/utils.js b/app/javascript/shared/utils.js index 595e277b8..ea0fd01f1 100644 --- a/app/javascript/shared/utils.js +++ b/app/javascript/shared/utils.js @@ -1,9 +1,8 @@ import Rails from '@rails/ujs'; -import $ from 'jquery'; import debounce from 'debounce'; export { debounce }; -export const { fire } = Rails; +export const { fire, csrfToken } = Rails; export function show(el) { el && el.classList.remove('hidden'); @@ -67,17 +66,20 @@ export function ajax(options) { }); } -export function getJSON(url, data, method = 'get') { - data = method !== 'get' && data ? JSON.stringify(data) : data; - return Promise.resolve( - $.ajax({ - method, - url, - data, - contentType: 'application/json', - dataType: 'json' - }) - ); +export function getJSON(url, data, method = 'GET') { + const { query, ...options } = fetchOptions(data, method); + + return fetch(`${url}${query}`, options).then((response) => { + if (response.ok) { + if (response.status === 204) { + return null; + } + return response.json(); + } + const error = new Error(response.statusText || response.status); + error.response = response; + throw error; + }); } export function scrollTo(container, scrollTo) { @@ -95,10 +97,6 @@ export function on(selector, eventName, fn) { ); } -export function to(promise) { - return promise.then((result) => [result]).catch((error) => [null, error]); -} - export function isNumeric(n) { return !isNaN(parseFloat(n)) && isFinite(n); } @@ -120,3 +118,50 @@ export function timeoutable(promise, timeoutDelay) { }); return Promise.race([promise, timeoutPromise]); } + +const FETCH_TIMEOUT = 30 * 1000; // 30 sec + +function fetchOptions(data, method = 'GET') { + const options = { + query: '', + method: method.toUpperCase(), + headers: { + accept: 'application/json', + 'x-csrf-token': csrfToken(), + 'x-requested-with': 'XMLHttpRequest' + }, + credentials: 'same-origin' + }; + + if (data) { + if (options.method === 'GET') { + options.query = objectToQuerystring(data); + } else { + options.headers['content-type'] = 'application/json'; + options.body = JSON.stringify(data); + } + } + + if (window.AbortController) { + const controller = new AbortController(); + options.signal = controller.signal; + + setTimeout(() => { + controller.abort(); + }, FETCH_TIMEOUT); + } + + return options; +} + +function objectToQuerystring(obj) { + return Object.keys(obj).reduce(function (query, key, i) { + return [ + query, + i === 0 ? '?' : '&', + encodeURIComponent(key), + '=', + encodeURIComponent(obj[key]) + ].join(''); + }, ''); +} diff --git a/app/views/manager/administrateurs/show.html.erb b/app/views/manager/administrateurs/show.html.erb index b33304288..b0d47c512 100644 --- a/app/views/manager/administrateurs/show.html.erb +++ b/app/views/manager/administrateurs/show.html.erb @@ -42,6 +42,7 @@ as well as a link to its edit page.
+ <%= render partial: 'manager/application/user_meta', locals: {user: page.resource&.user} %>
<% page.attributes.each do |attribute| %>
diff --git a/app/views/manager/application/_user_meta.html.erb b/app/views/manager/application/_user_meta.html.erb new file mode 100644 index 000000000..9197f3ab1 --- /dev/null +++ b/app/views/manager/application/_user_meta.html.erb @@ -0,0 +1,43 @@ +
+
+ Emails +
+
+ <%= link_to('Voir les derniers emails', emails_manager_user_path(user)) %> +
+
+
+
+ Usager +
+
+ <%= link_to('Voir son compte utilisateur', manager_user_path(user)) %> +
+
+ +
+
+ Instructeur +
+
+ <% if user.instructeur.present? %> + <%= link_to('Voir son compte instructeur', manager_instructeur_path(user.instructeur)) %> + <% else %> + Pas instructeur ! + <% end %> +
+
+ +
+
+ Administrateur +
+
+ <% if user.administrateur.present? %> + <%= link_to('Voir son compte administrateur', manager_administrateur_path(user.administrateur)) %> + <% else %> + Pas administrateur ! + <% end %> +
+
+
diff --git a/app/views/manager/instructeurs/show.html.erb b/app/views/manager/instructeurs/show.html.erb index 983e171e9..40682b598 100644 --- a/app/views/manager/instructeurs/show.html.erb +++ b/app/views/manager/instructeurs/show.html.erb @@ -41,6 +41,7 @@ as well as a link to its edit page.
+ <%= render partial: 'manager/application/user_meta', locals: {user: page.resource&.user} %>
<% page.attributes.each do |attribute| %>
diff --git a/app/views/manager/users/emails.html.erb b/app/views/manager/users/emails.html.erb new file mode 100644 index 000000000..13575b96a --- /dev/null +++ b/app/views/manager/users/emails.html.erb @@ -0,0 +1,117 @@ +<% content_for(:title) { "Emails vers #{@user.email}" } %> + + + + + +
+

Historique des email

+<% if @transactionnal_emails.present? %> +

+ Cet historique contient les 30 derniers jours. Pour un recherche plus fine, il faut fouiller les logs. +

+ + + + + + + + + + <% @transactionnal_emails&.transactional_emails&.reverse&.each do |email| %> + <% matching_events = @events&.events&.select { |e| e.message_id == email.message_id } %> + + + + + + + <% end %> + +
+ Émetteur + + Sujet + + Date +
+ <%= email.from %> + + <%= email.subject %> + + <%= l(email.date, format: '%d/%m/%y à %H:%M') %> + +
    + + <% matching_events.each do |event|%> +
  • <%= event.event %>
  • + <% end %> +
+
+<% else %> +

Historique indisponible. Cet email n'existe pas chez Sendinblue, ou nous n'avons pas réussi à échanger. + Vous pouvez éventuellement fouiller leurs logs.

+<% end %> + +

Problèmes potentiel

+ + <% if @user.confirmed? %> +

Compte activé, n'arrive pas à se connecter ?

+ + <% else %> +

Ce compte n'est pas activé. Vous pouvez lui <%= link_to('renvoyer l’email de confirmation', [:resend_confirmation_instructions, namespace, 'user'], method: :post, class: 'button') %>, puis un email.

+ + <% end %> +

Compte bloqué chez Sendinblue ? Vous pouvez le <%= link_to('débloquer', manager_user_unblock_email_path(@user), method: :put, class: 'button', remote: true) %> puis lui envoyer

+ +

Problème chez Sendinblue ? Regardez leur page de status.

+ +
diff --git a/app/views/manager/users/show.html.erb b/app/views/manager/users/show.html.erb index a14acc228..24b793aa9 100644 --- a/app/views/manager/users/show.html.erb +++ b/app/views/manager/users/show.html.erb @@ -34,10 +34,11 @@ as well as a link to its edit page. <% if !user.confirmed? %> <%= link_to('Renvoyer l’email de confirmation', [:resend_confirmation_instructions, namespace, page.resource], method: :post, class: 'button') %> <% end %> -
+
+ <%= render partial: 'manager/application/user_meta', locals: {user: user} %>
<% page.attributes.each do |attribute| %>
diff --git a/config/initializers/sendinblue.rb b/config/initializers/sendinblue.rb new file mode 100644 index 000000000..d523b2373 --- /dev/null +++ b/config/initializers/sendinblue.rb @@ -0,0 +1,5 @@ +require 'sib-api-v3-sdk' + +SibApiV3Sdk.configure do |config| + config.api_key['api-key'] = ENV.fetch('SENDINBLUE_API_V3_KEY', '') +end diff --git a/config/routes.rb b/config/routes.rb index b207161a6..c01dbf8b6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -31,6 +31,8 @@ Rails.application.routes.draw do delete 'delete', on: :member post 'resend_confirmation_instructions', on: :member put 'enable_feature', on: :member + get 'emails', on: :member + put 'unblock_email' end resources :instructeurs, only: [:index, :show] do