commit
e70a869620
17 changed files with 297 additions and 53 deletions
1
Gemfile
1
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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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('<br>') }
|
||||
end
|
||||
end
|
||||
|
@ -36,6 +38,11 @@ class WebhookController < ActionController::Base
|
|||
"<a target='_blank' href='#{url}' rel='noopener'>#{model.model_name.human}##{model.id}</a>"
|
||||
end
|
||||
|
||||
def email_link_to_manager(user)
|
||||
url = emails_manager_user_url(user)
|
||||
"<a target='_blank' href='#{url}' rel='noopener'>Emails##{user.id}</a>"
|
||||
end
|
||||
|
||||
def verify_signature!
|
||||
if generate_body_signature(request.body.read) != request.headers['X-Helpscout-Signature']
|
||||
request_http_token_authentication
|
||||
|
|
12
app/helpers/email_helper.rb
Normal file
12
app/helpers/email_helper.rb
Normal file
|
@ -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
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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('');
|
||||
}, '');
|
||||
}
|
||||
|
|
|
@ -42,6 +42,7 @@ as well as a link to its edit page.
|
|||
</header>
|
||||
|
||||
<section class="main-content__body">
|
||||
<%= render partial: 'manager/application/user_meta', locals: {user: page.resource&.user} %>
|
||||
<dl>
|
||||
<% page.attributes.each do |attribute| %>
|
||||
<dt class="attribute-label" id="<%= attribute.name %>">
|
||||
|
|
43
app/views/manager/application/_user_meta.html.erb
Normal file
43
app/views/manager/application/_user_meta.html.erb
Normal file
|
@ -0,0 +1,43 @@
|
|||
<dl>
|
||||
<dt class="attribute-label" id="meta-usager">
|
||||
Emails
|
||||
</dt>
|
||||
<dd class="attribute-data attribute-data--meta-usager">
|
||||
<%= link_to('Voir les derniers emails', emails_manager_user_path(user)) %>
|
||||
</dd>
|
||||
</dl>
|
||||
<dl>
|
||||
<dt class="attribute-label" id="meta-usager">
|
||||
Usager
|
||||
</dt>
|
||||
<dd class="attribute-data attribute-data--meta-usager">
|
||||
<%= link_to('Voir son compte utilisateur', manager_user_path(user)) %>
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<dl>
|
||||
<dt class="attribute-label" id="meta-usager">
|
||||
Instructeur
|
||||
</dt>
|
||||
<dd class="attribute-data attribute-data--meta-usager">
|
||||
<% if user.instructeur.present? %>
|
||||
<%= link_to('Voir son compte instructeur', manager_instructeur_path(user.instructeur)) %>
|
||||
<% else %>
|
||||
Pas instructeur !
|
||||
<% end %>
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<dl>
|
||||
<dt class="attribute-label" id="meta-usager">
|
||||
Administrateur
|
||||
</dt>
|
||||
<dd class="attribute-data attribute-data--meta-usager">
|
||||
<% if user.administrateur.present? %>
|
||||
<%= link_to('Voir son compte administrateur', manager_administrateur_path(user.administrateur)) %>
|
||||
<% else %>
|
||||
Pas administrateur !
|
||||
<% end %>
|
||||
</dd>
|
||||
</dl>
|
||||
<hr />
|
|
@ -41,6 +41,7 @@ as well as a link to its edit page.
|
|||
</header>
|
||||
|
||||
<section class="main-content__body">
|
||||
<%= render partial: 'manager/application/user_meta', locals: {user: page.resource&.user} %>
|
||||
<dl>
|
||||
<% page.attributes.each do |attribute| %>
|
||||
<dt class="attribute-label" id="<%= attribute.name %>">
|
||||
|
|
117
app/views/manager/users/emails.html.erb
Normal file
117
app/views/manager/users/emails.html.erb
Normal file
|
@ -0,0 +1,117 @@
|
|||
<% content_for(:title) { "Emails vers #{@user.email}" } %>
|
||||
|
||||
<style>
|
||||
.hidden { display: none }
|
||||
.email-sent { color: green !important}
|
||||
.email-blocked { color: red }
|
||||
</style>
|
||||
<script type="text/javascript" charset="utf-8">
|
||||
function reveal_email(id) {
|
||||
document.querySelector(id).classList.toggle('hidden');
|
||||
}
|
||||
</script>
|
||||
<header class="main-content__header" role="banner">
|
||||
<h1 class="main-content__page-title">
|
||||
<%= content_for(:title) %>
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
<section class="main-content__body">
|
||||
<h2>Historique des email</h2>
|
||||
<% if @transactionnal_emails.present? %>
|
||||
<p>
|
||||
Cet historique contient les 30 derniers jours. Pour un recherche plus fine, il faut <a href="https://app-smtp.sendinblue.com/log">fouiller les logs</a>.
|
||||
</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="cell-label cell-label--string cell-label--false" scope="col" role="columnheader" aria-sort="none">
|
||||
Émetteur
|
||||
</th>
|
||||
<th class="cell-label cell-label--string cell-label--false" scope="col" role="columnheader" aria-sort="none">
|
||||
Sujet
|
||||
</th>
|
||||
<th class="cell-label cell-label--string cell-label--false" scope="col" role="columnheader" aria-sort="none">
|
||||
Date
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @transactionnal_emails&.transactional_emails&.reverse&.each do |email| %>
|
||||
<% matching_events = @events&.events&.select { |e| e.message_id == email.message_id } %>
|
||||
<tr class="<%= event_color_code(matching_events) %>">
|
||||
<td class="cell-data cell-data--string" style="">
|
||||
<%= email.from %>
|
||||
</td>
|
||||
<td class="cell-data cell-data--string" style="">
|
||||
<%= email.subject %>
|
||||
</td>
|
||||
<td class="cell-data cell-data--string" style="text-align: center;">
|
||||
<%= l(email.date, format: '%d/%m/%y à %H:%M') %>
|
||||
</td>
|
||||
<td class="cell-data cell-data--string" style="text-align: center;">
|
||||
<ul>
|
||||
|
||||
<% matching_events.each do |event|%>
|
||||
<li><%= event.event %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% else %>
|
||||
<p>Historique indisponible. Cet email n'existe pas chez Sendinblue, ou nous n'avons pas réussi à échanger.
|
||||
Vous pouvez éventuellement <a href="https://app-smtp.sendinblue.com/log">fouiller leurs logs</a>.</p>
|
||||
<% end %>
|
||||
|
||||
<h2>Problèmes potentiel</h2>
|
||||
|
||||
<% if @user.confirmed? %>
|
||||
<p><strong>Compte activé, n'arrive pas à se connecter</strong> ? <button class="btn btn-secondary btn-small" onclick="reveal_email('#activated-cant-connect')">Voir la suggestion d’email</button></p>
|
||||
<pre class="hidden" id="activated-cant-connect">
|
||||
Bonjour,
|
||||
votre compte est activé de notre côté.
|
||||
Vous pouvez vous connecter à votre compte de deux manières :
|
||||
- à cette adresse, afin de consulter vos dossiers : https://www.demarches-simplifiees.fr/users/sign_in
|
||||
- depuis la page de démarrage d’une démarche qu'on vous a communiqué, afin de déposer un dossier.
|
||||
|
||||
Si vous avez oublié votre mot de passe, vous pouvez aussi en demander un nouveau via:
|
||||
https://www.demarches-simplifiees.fr/users/password/new
|
||||
|
||||
Bien cordialement</pre>
|
||||
<% else %>
|
||||
<p><strong>Ce compte n'est pas activé</strong>. Vous pouvez lui <%= link_to('renvoyer l’email de confirmation', [:resend_confirmation_instructions, namespace, 'user'], method: :post, class: 'button') %>, puis un email. <button class="btn btn-secondary btn-small" onclick="reveal_email('#not-activated')">Voir la suggestion d’email</button> </p>
|
||||
<pre class="hidden" id="not-activated">
|
||||
Bonjour,
|
||||
|
||||
Votre compte n'a pas été confirmé. Je vous ai transmis à nouveau un code de confirmation
|
||||
dans un email séparé ; après avoir cliqué sur le lien qui s'y trouve, vous pourrez vous connecter
|
||||
à votre compte, voir les dossiers déposés et en déposer de nouveaux.
|
||||
|
||||
Si vous avez oublié votre mot de passe, vous pouvez aussi en demander un autre via:
|
||||
https://www.demarches-simplifiees.fr/users/password/new
|
||||
|
||||
Cordialement</pre>
|
||||
<% end %>
|
||||
<p><strong>Compte <a href="https://app-smtp.sendinblue.com/block">bloqué</a> chez Sendinblue ?</strong> Vous pouvez le <%= link_to('débloquer', manager_user_unblock_email_path(@user), method: :put, class: 'button', remote: true) %> puis lui envoyer <button class="btn btn-secondary btn-small" onclick="reveal_email('#unblock_email')">le mail suivant</button></p>
|
||||
<pre class="hidden" id="unblock_email">
|
||||
Bonjour,
|
||||
|
||||
votre email était bloqué par notre prestataire.
|
||||
Je l'ai débloqué, vous devriez recevoir les mails à venir.
|
||||
|
||||
Cela peut arriver si vous, ou ceux qui gèrent vos emails, marquent nos emails comme spam.
|
||||
|
||||
Nous vous invitons donc à autoriser les emails émis depuis demarches-simplifiees.fr
|
||||
|
||||
Bien cordialement</pre>
|
||||
<p><strong>Problème chez Sendinblue ?</strong> Regardez leur <a href="https://status.sendinblue.com/">page de status</a>. <button class="btn btn-secondary btn-small" onclick="reveal_email('#pb-sendinblue')">Voir la suggestion d’email</button></p>
|
||||
<pre class="hidden" id="pb-sendinblue">
|
||||
Bonjour,
|
||||
Désolé, notre prestataire d'envoi d'email subit actuellement des soucis avec sa plateforme ;
|
||||
vous allez recevoir cet email sous peu.
|
||||
|
||||
Bien cordialement,</pre>
|
||||
</section>
|
|
@ -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 %>
|
||||
<div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="main-content__body">
|
||||
<%= render partial: 'manager/application/user_meta', locals: {user: user} %>
|
||||
<dl>
|
||||
<% page.attributes.each do |attribute| %>
|
||||
<dt class="attribute-label" id="<%= attribute.name %>">
|
||||
|
|
5
config/initializers/sendinblue.rb
Normal file
5
config/initializers/sendinblue.rb
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue