Auth hardening: - payments: per-customer JWT authorization on dual-use /payments routes (balance/methods/invoice/checkout/setup/portal/toggle-ppa) via authorizeCustomer + PAYMENTS_AUTH=enforce; portal retains+sends magic-link JWT (sessionStorage) - hub: fail-closed Stripe webhook, /accept/doc-pdf IDOR gate, telephony field-name guard (SQLi), modem-bridge private-IP guard (SSRF), ALWAYS_ENFORCE expansion Leaked-credential cleanup (token already rotated in ERPNext): - de-hardcode ERPNext API token -> env in bulk_submit.py, import_items.py, apps/client/deploy.sh; placeholder in apps/ops/infra/nginx.conf (nginx injects) - ops prod build no longer bakes VITE_ERP_TOKEN (.env.production empty) - de-hardcode legacy DB password -> env in import_items.py - gitignore legacy migration PII exports (tsv/json) Conversations/UI: - floating (un-docked) conversation panel; full-width mailbox - per-message real sender from email headers; unified scroll; header spacing - campaign pre-send review (subject/from/channel), Gmail send channel, clone-as-draft Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
74 lines
2.5 KiB
Vue
74 lines
2.5 KiB
Vue
<template>
|
|
<!-- Editable "From" field: a plain text input (free text always preserved)
|
|
with an auto-suggest dropdown of validated Mailjet senders. Type freely,
|
|
or pick a suggestion. -->
|
|
<q-input
|
|
:model-value="modelValue"
|
|
@update:model-value="onInput"
|
|
@focus="menuOpen = true"
|
|
:label="label"
|
|
outlined dense
|
|
:placeholder="placeholder"
|
|
:hint="hint"
|
|
spellcheck="false"
|
|
autocomplete="off"
|
|
>
|
|
<template #append>
|
|
<q-icon name="expand_more" class="cursor-pointer" @click.stop="menuOpen = !menuOpen" />
|
|
</template>
|
|
<q-menu
|
|
:model-value="menuOpen && filtered.length > 0"
|
|
@update:model-value="v => menuOpen = v"
|
|
no-focus no-refocus fit anchor="bottom left" self="top left"
|
|
>
|
|
<q-list dense style="min-width:320px">
|
|
<q-item v-for="s in filtered" :key="s" clickable v-close-popup
|
|
:active="s === modelValue" @click="pick(s)">
|
|
<q-item-section class="text-body2">{{ s }}</q-item-section>
|
|
</q-item>
|
|
</q-list>
|
|
</q-menu>
|
|
</q-input>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed } from 'vue'
|
|
|
|
// Validated Mailjet senders — single source of truth for the From picker.
|
|
// Mailjet only delivers from a validated sender; an unvalidated address
|
|
// silently falls back to noreply@targo.ca, so we surface the known-good ones.
|
|
// The field stays free-text, so a newly-validated address can just be typed.
|
|
const SENDERS = [
|
|
'Facturation Targo <facturation@targo.ca>',
|
|
'Service TARGO <support@targointernet.com>',
|
|
'TARGO <noreply@targo.ca>',
|
|
]
|
|
|
|
const props = defineProps({
|
|
modelValue: { type: String, default: '' },
|
|
label: { type: String, default: 'Expéditeur (From)' },
|
|
placeholder: { type: String, default: 'Facturation Targo <facturation@targo.ca>' },
|
|
hint: { type: String, default: 'Expéditeur validé Mailjet — saisie libre, ou choisir une suggestion' },
|
|
})
|
|
const emit = defineEmits(['update:modelValue'])
|
|
|
|
const menuOpen = ref(false)
|
|
|
|
const filtered = computed(() => {
|
|
const q = (props.modelValue || '').toLowerCase().trim()
|
|
// Empty, or already an exact match (pre-filled / just picked) → show the
|
|
// full list so the dropdown stays a useful picker. Otherwise substring-filter.
|
|
if (!q || SENDERS.some(s => s.toLowerCase() === q)) return SENDERS
|
|
return SENDERS.filter(s => s.toLowerCase().includes(q))
|
|
})
|
|
|
|
function onInput (val) {
|
|
emit('update:modelValue', val == null ? '' : val)
|
|
menuOpen.value = true
|
|
}
|
|
function pick (s) {
|
|
emit('update:modelValue', s)
|
|
menuOpen.value = false
|
|
}
|
|
</script>
|