fix(ops): sécurité + robustesse dispatch/comms
- Sécurité : sanitize DOMPurify sur tout HTML entrant courriel/osTicket (v-html) → ferme le vecteur XSS (ConversationPanel + IssueDetail). Nouveau src/utils/sanitize.js, dompurify ajouté en dépendance directe. - Fix : piège TDZ (watches de la carte des tournées placés avant les const suggestDlg/ suggestMapDay/suggestGroups) qui cassait le montage de PlanificationPage → déplacés après les définitions. - Finition : étiquette de fenêtre du dispatch auto « Aujourd'hui + Demain » (au lieu de l'ISO). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
512c4a5f1b
commit
453bef006c
|
|
@ -31,7 +31,8 @@
|
|||
"vue-chartjs": "^5.3.3",
|
||||
"vue-email-editor": "^2.2.0",
|
||||
"vue-router": "^4.3.0",
|
||||
"vuedraggable": "^4.1.0"
|
||||
"vuedraggable": "^4.1.0",
|
||||
"dompurify": "^3.4.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@quasar/app-vite": "^1.10.0",
|
||||
|
|
|
|||
|
|
@ -352,7 +352,7 @@
|
|||
<div v-else-if="summaryErr" class="row items-center text-caption text-negative"><q-icon name="error_outline" class="q-mr-xs" />{{ summaryErr }} <q-btn flat dense size="sm" no-caps label="Réessayer" color="deep-purple-6" class="q-ml-sm" @click="summarizeThread" /></div>
|
||||
<div v-else-if="summaryText" class="conv-summary-card">
|
||||
<div class="row items-center q-mb-xs"><q-icon name="auto_awesome" color="deep-purple-5" size="16px" class="q-mr-xs" /><b class="text-deep-purple-7">Résumé IA</b><q-space /><q-btn flat dense round size="xs" icon="refresh" color="grey-6" @click="summarizeThread"><q-tooltip>Régénérer</q-tooltip></q-btn><q-btn flat dense round size="xs" icon="close" color="grey-6" @click="summaryText = ''" /></div>
|
||||
<div class="conv-summary-body" v-html="summaryHtml"></div>
|
||||
<div class="conv-summary-body" v-html="sanitizeHtml(summaryHtml)"></div>
|
||||
<div class="row q-mt-sm">
|
||||
<q-btn dense unelevated no-caps size="sm" color="positive" icon="confirmation_number" :label="'Créer un ticket — ' + ticketTypeLabel" :loading="ticketBusy" @click="createTicketFromSummary" />
|
||||
</div>
|
||||
|
|
@ -421,7 +421,7 @@
|
|||
<q-checkbox v-if="fwdAttCount" v-model="fwdDialog.includeAtts" dense color="primary" :label="'Inclure les pièces jointes (' + fwdAttCount + ')'" />
|
||||
<div class="fwd-preview">
|
||||
<div class="text-caption text-grey-6 q-mb-xs"><q-icon name="subdirectory_arrow_right" size="13px" style="vertical-align:-2px" /> Contenu transféré</div>
|
||||
<div class="fwd-preview-body" v-html="fwdPreviewHtml"></div>
|
||||
<div class="fwd-preview-body" v-html="sanitizeHtml(fwdPreviewHtml)"></div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
|
|
@ -566,7 +566,7 @@
|
|||
</template>
|
||||
<q-btn v-else flat dense round size="sm" icon="close" color="green-4" @click="closeOtherDraft"><q-tooltip>Masquer cet aperçu</q-tooltip></q-btn>
|
||||
</div>
|
||||
<div class="conv-draft-inline-body" v-html="msg.html || (msg.text || '').replace(/\n/g, '<br>')"></div>
|
||||
<div class="conv-draft-inline-body" v-html="sanitizeHtml(msg.html || (msg.text || '').replace(/\n/g, '<br>'))"></div>
|
||||
</div>
|
||||
<div v-else class="conv-msg" :class="'conv-msg-' + msg.from">
|
||||
<!-- En-tête repliable (accordéon, style Gmail) : qui · heure · aperçu — clic pour déplier/replier -->
|
||||
|
|
@ -1020,6 +1020,7 @@ import { ref, computed, watch, nextTick, onMounted, onUnmounted, defineAsyncComp
|
|||
const EmailEditor = defineAsyncComponent(() => import('vue-email-editor').then(m => m.EmailEditor))
|
||||
import { useQuasar } from 'quasar'
|
||||
import { useConversations } from 'src/composables/useConversations'
|
||||
import { sanitizeHtml } from 'src/utils/sanitize'
|
||||
import { getDoc, listDocs } from 'src/api/erp'
|
||||
import RecipientSelect from 'src/components/shared/RecipientSelect.vue'
|
||||
import { useSoftphone } from 'src/composables/useSoftphone'
|
||||
|
|
|
|||
|
|
@ -185,7 +185,7 @@
|
|||
</div>
|
||||
<div v-if="doc.resolution_details" class="q-mt-md">
|
||||
<div class="info-block-title">Resolution</div>
|
||||
<div class="modal-desc" v-html="doc.resolution_details"></div>
|
||||
<div class="modal-desc" v-html="sanitizeHtml(doc.resolution_details)"></div>
|
||||
</div>
|
||||
<div v-if="comms.length" class="q-mt-md">
|
||||
<div class="info-block-title">Echanges ({{ comms.length }})</div>
|
||||
|
|
@ -193,7 +193,7 @@
|
|||
<div class="row items-start">
|
||||
<div class="col">
|
||||
<div class="text-caption text-weight-bold">{{ comm.sender || comm.owner }}</div>
|
||||
<div class="modal-desc q-mt-xs" v-html="comm.content"></div>
|
||||
<div class="modal-desc q-mt-xs" v-html="sanitizeHtml(comm.content)"></div>
|
||||
</div>
|
||||
<div class="col-auto text-caption text-grey-6 text-right" style="min-width:90px">
|
||||
{{ formatDate(comm.creation) }}
|
||||
|
|
@ -217,7 +217,7 @@
|
|||
<span class="text-weight-bold">{{ c.comment_by || 'Systeme' }}</span>
|
||||
<span class="text-grey-5 q-ml-auto">{{ formatDateTime(c.creation) }}</span>
|
||||
</div>
|
||||
<div class="thread-body" v-html="c.content"></div>
|
||||
<div class="thread-body" v-html="sanitizeHtml(c.content)"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!doc.description && !doc.resolution_details && !comms.length && !comments.length" class="text-center text-grey-5 q-pa-lg">
|
||||
|
|
@ -265,6 +265,7 @@
|
|||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { Notify, Dialog } from 'quasar'
|
||||
import { sanitizeHtml } from 'src/utils/sanitize'
|
||||
import { formatDate, formatDateTime, erpFileUrl } from 'src/composables/useFormatters'
|
||||
import { createDoc, updateDoc, deleteDoc, listDocs } from 'src/api/erp'
|
||||
import { authFetch } from 'src/api/auth'
|
||||
|
|
|
|||
|
|
@ -1009,7 +1009,7 @@
|
|||
<template v-if="suggestDlg.view === 'map' && suggestWindow.length > 1">
|
||||
<q-space />
|
||||
<span class="text-caption text-grey-7">Jour :</span>
|
||||
<q-btn-toggle v-model="suggestMapDay" dense no-caps unelevated toggle-color="indigo" color="grey-3" text-color="grey-8" :options="suggestWindow.map(iso => ({ label: fmtDueLabel(iso), value: iso }))" />
|
||||
<q-btn-toggle v-model="suggestMapDay" dense no-caps unelevated toggle-color="indigo" color="grey-3" text-color="grey-8" :options="suggestWindow.map(iso => ({ label: windowDayLabel(iso), value: iso }))" />
|
||||
</template>
|
||||
</div>
|
||||
<div v-show="suggestDlg.view === 'map'">
|
||||
|
|
@ -3924,10 +3924,11 @@ const suggestWindow = computed(() => {
|
|||
if (!win.length) win = isos.filter(iso => iso >= t).slice(0, 2) // repli : 2 premiers jours futurs de la grille
|
||||
return win
|
||||
})
|
||||
const windowDayLabel = iso => iso === todayISO() ? "Aujourd'hui" : (iso === tomorrowISO() ? 'Demain' : (fmtDueLabel(iso) || iso))
|
||||
const suggestWindowLabel = computed(() => {
|
||||
const w = suggestWindow.value; if (!w.length) return '—'
|
||||
if ((assignDateFilter.value || []).length) return w.map(iso => fmtDueLabel(iso)).join(', ')
|
||||
return w.length === 1 ? fmtDueLabel(w[0]) : (fmtDueLabel(w[0]) + ' + ' + fmtDueLabel(w[w.length - 1]))
|
||||
if ((assignDateFilter.value || []).length) return w.map(windowDayLabel).join(', ')
|
||||
return w.length === 1 ? windowDayLabel(w[0]) : (windowDayLabel(w[0]) + ' + ' + windowDayLabel(w[w.length - 1]))
|
||||
})
|
||||
function suggestSelectTechs (which) { // 'all' | 'shift' | 'none'
|
||||
const techs = visibleTechs.value || []; const sel = {}
|
||||
|
|
|
|||
10
apps/ops/src/utils/sanitize.js
Normal file
10
apps/ops/src/utils/sanitize.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
// Sanitisation du HTML entrant (courriels clients, contenu osTicket/ERPNext) AVANT tout v-html.
|
||||
// Bloque les vecteurs XSS (<script>, gestionnaires on*, href javascript:) tout en gardant l'affichage riche.
|
||||
// DOMPurify v3 : config par défaut = sûre ET permissive (conserve img data:/cid réinlinés, liens, tableaux, styles inline).
|
||||
import DOMPurify from 'dompurify'
|
||||
|
||||
export function sanitizeHtml (html) {
|
||||
if (html == null || html === '') return ''
|
||||
// ADD_ATTR target → garde target="_blank" sur les liens ; le reste (scripts, on*, javascript:) est retiré par défaut.
|
||||
return DOMPurify.sanitize(String(html), { ADD_ATTR: ['target'] })
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user