Skip to content

Commit 94803b0

Browse files
committed
feat: implement user sendable mail template validation and UI enhancements
- Added validation in MailSendService to restrict sending of internal templates, ensuring only user sendable templates can be sent. - Introduced a utility function in MailTemplatesService to identify user sendable templates based on a defined prefix. - Updated mail template modal in the frontend to display appropriate messages for internal templates, enhancing user clarity on sending capabilities. - Adjusted template fetching logic to differentiate between sendable and internal templates, improving user experience in template selection.
1 parent 342ebb0 commit 94803b0

3 files changed

Lines changed: 98 additions & 13 deletions

File tree

apps/api/src/management/mail/mail-send.service.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { IdentitiesCrudService } from '~/management/identities/identities-crud.s
55
import { PasswdadmService } from '~/settings/passwdadm.service';
66
import { MailadmService } from '~/settings/mailadm.service';
77
import { IdentityState } from '~/management/identities/_enums/states.enum';
8+
import { isUserSendableMailTemplate } from './mail-templates.service';
89

910
@Injectable()
1011
export class MailSendService {
@@ -28,6 +29,11 @@ export class MailSendService {
2829
if (!template) {
2930
throw new BadRequestException('Template requis');
3031
}
32+
if (!isUserSendableMailTemplate(template)) {
33+
throw new BadRequestException(
34+
'Template interne Sesame : mode lecture seule (aperçu uniquement, envoi manuel non autorisé).',
35+
);
36+
}
3137
const subject = String(args.subject || '').trim();
3238
if (!subject) {
3339
throw new BadRequestException('Sujet requis');

apps/api/src/management/mail/mail-templates.service.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from
66
import { parse } from 'yaml'
77
import { resolveConfigVariables } from '~/_common/functions/resolve-config-variables.function'
88

9+
/** Préfixe des templates envoyables manuellement depuis l’UI (hors flux internes Sesame). */
10+
export const USER_SENDABLE_MAIL_TEMPLATE_PREFIX = 'mail_'
11+
12+
export function isUserSendableMailTemplate(templateName: string): boolean {
13+
return String(templateName || '')
14+
.trim()
15+
.startsWith(USER_SENDABLE_MAIL_TEMPLATE_PREFIX)
16+
}
17+
918
@Injectable()
1019
export class MailTemplatesService implements OnApplicationBootstrap {
1120
private readonly logger = new Logger(MailTemplatesService.name)
@@ -72,8 +81,14 @@ export class MailTemplatesService implements OnApplicationBootstrap {
7281
.map((e) => e.name)
7382
.filter((name) => name.endsWith('.hbs'))
7483
.map((name) => name.replace(/\.hbs$/, ''))
75-
.filter((name) => name.startsWith('mail_'))
76-
.sort((a, b) => a.localeCompare(b))
84+
.sort((a, b) => {
85+
const aSendable = isUserSendableMailTemplate(a)
86+
const bSendable = isUserSendableMailTemplate(b)
87+
if (aSendable !== bSendable) {
88+
return aSendable ? -1 : 1
89+
}
90+
return a.localeCompare(b)
91+
})
7792
}
7893

7994
public async renderPreviewHtml(template: string, variables?: Record<string, unknown>): Promise<string> {

apps/web/src/components/pages/identities/modals/mail-template.vue

Lines changed: 75 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,18 @@ q-dialog(
3636
:loading="templatesLoading"
3737
color="teal-7"
3838
)
39+
q-banner.q-mt-md(
40+
v-if="isInternalTemplate"
41+
rounded
42+
dense
43+
class="bg-blue-1 text-grey-9"
44+
)
45+
template(#avatar)
46+
q-icon(name="mdi-eye-outline" color="primary")
47+
.text-body2
48+
| Mode lecture seule : template interne Sesame. L'aperçu est disponible ; l'envoi manuel n'est pas autorisé pour ce template.
3949
q-input.q-mt-md(
50+
v-if="!isInternalTemplate"
4051
v-model="mailSubject"
4152
label="Sujet du mail"
4253
hint="Sujet affiché par le client mail (obligatoire)"
@@ -46,7 +57,7 @@ q-dialog(
4657
autocomplete="off"
4758
)
4859
q-banner.q-mt-md(
49-
v-if="mailPathsReady && recipientSourceOptions.length === 0"
60+
v-if="!isInternalTemplate && mailPathsReady && recipientSourceOptions.length === 0"
5061
rounded
5162
dense
5263
class="bg-orange-1 text-grey-9"
@@ -62,7 +73,7 @@ q-dialog(
6273
strong Paramètres → Serveur SMTP
6374
| .
6475
q-select.q-mt-md(
65-
v-if="recipientSourceOptions.length > 1"
76+
v-if="!isInternalTemplate && recipientSourceOptions.length > 1"
6677
v-model="recipientAddressSource"
6778
:options="recipientSourceOptions"
6879
label="Adresse du destinataire"
@@ -204,7 +215,18 @@ q-dialog(
204215
:loading="templatesLoading"
205216
color="teal-7"
206217
)
218+
q-banner.q-mt-md(
219+
v-if="isInternalTemplate"
220+
rounded
221+
dense
222+
class="bg-blue-1 text-grey-9"
223+
)
224+
template(#avatar)
225+
q-icon(name="mdi-eye-outline" color="primary")
226+
.text-body2
227+
| Mode lecture seule : template interne Sesame. L'aperçu est disponible ; l'envoi manuel n'est pas autorisé pour ce template.
207228
q-input.q-mt-md(
229+
v-if="!isInternalTemplate"
208230
v-model="mailSubject"
209231
label="Sujet du mail"
210232
hint="Sujet affiché par le client mail (obligatoire)"
@@ -214,7 +236,7 @@ q-dialog(
214236
autocomplete="off"
215237
)
216238
q-banner.q-mt-md(
217-
v-if="mailPathsReady && recipientSourceOptions.length === 0"
239+
v-if="!isInternalTemplate && mailPathsReady && recipientSourceOptions.length === 0"
218240
rounded
219241
dense
220242
class="bg-orange-1 text-grey-9"
@@ -230,7 +252,7 @@ q-dialog(
230252
strong Paramètres → Serveur SMTP
231253
| .
232254
q-select.q-mt-md(
233-
v-if="recipientSourceOptions.length > 1"
255+
v-if="!isInternalTemplate && recipientSourceOptions.length > 1"
234256
v-model="recipientAddressSource"
235257
:options="recipientSourceOptions"
236258
label="Adresse du destinataire"
@@ -340,6 +362,18 @@ q-dialog(
340362
@click="cancelSync"
341363
)
342364
q-btn(
365+
v-if="isInternalTemplate"
366+
outline
367+
no-caps
368+
padding="sm lg"
369+
color="grey-7"
370+
icon-right="mdi-eye-outline"
371+
label="Aperçu seul — envoi non disponible"
372+
disable
373+
:title="sendButtonTitle"
374+
)
375+
q-btn(
376+
v-else
343377
unelevated
344378
no-caps
345379
padding="sm lg"
@@ -474,11 +508,13 @@ const checkboxLabel = computed(() => {
474508
return `Envoyer le mail à toutes les identités synchronisées (${props.allIdentitiesCount})`
475509
})
476510
477-
const showSendAll = computed(() => {
478-
const total = Number(props.allIdentitiesCount || 0)
479-
const selected = selectedRows.value.length
480-
return total > Math.max(selected, 1)
481-
})
511+
const SENDABLE_TEMPLATE_PREFIX = 'mail_'
512+
513+
function isSendableTemplateName(name: string): boolean {
514+
return String(name || '')
515+
.trim()
516+
.startsWith(SENDABLE_TEMPLATE_PREFIX)
517+
}
482518
483519
const initAllIdentities = ref(false)
484520
const templateName = ref<string>('')
@@ -495,6 +531,20 @@ const mailPaths = ref<{ personnel: string; principal: string }>({ personnel: '',
495531
const mailPathsReady = ref(false)
496532
const recipientAddressSource = ref<'principal' | 'personnel' | null>(null)
497533
534+
const isInternalTemplate = computed(() => {
535+
const name = String(templateName.value || '').trim()
536+
return name.length > 0 && !isSendableTemplateName(name)
537+
})
538+
539+
const showSendAll = computed(() => {
540+
if (isInternalTemplate.value) {
541+
return false
542+
}
543+
const total = Number(props.allIdentitiesCount || 0)
544+
const selected = selectedRows.value.length
545+
return total > Math.max(selected, 1)
546+
})
547+
498548
const recipientSourceOptions = computed(() => {
499549
const opts: { label: string; value: 'principal' | 'personnel' }[] = []
500550
if (mailPaths.value.personnel) {
@@ -508,6 +558,7 @@ const recipientSourceOptions = computed(() => {
508558
509559
const canSendMailTemplate = computed(
510560
() =>
561+
!isInternalTemplate.value &&
511562
mailPathsReady.value &&
512563
String(mailSubject.value || '').trim().length > 0 &&
513564
(recipientAddressSource.value === 'principal' || recipientAddressSource.value === 'personnel'),
@@ -517,6 +568,9 @@ const sendButtonTitle = computed(() => {
517568
if (canSendMailTemplate.value) {
518569
return ''
519570
}
571+
if (isInternalTemplate.value) {
572+
return 'Template interne Sesame : aperçu uniquement, envoi non autorisé.'
573+
}
520574
if (!mailPathsReady.value) {
521575
return 'Chargement des paramètres SMTP…'
522576
}
@@ -596,7 +650,16 @@ async function fetchTemplates() {
596650
try {
597651
const res = await (useNuxtApp() as any).$http.get('/management/mail/templates', { method: 'GET' })
598652
const list = res?._data?.data || []
599-
templates.value = Array.isArray(list) ? list.map((t: string) => ({ label: t, value: t })) : []
653+
templates.value = Array.isArray(list)
654+
? list.map((t: string) => {
655+
const name = String(t || '').trim()
656+
const sendable = isSendableTemplateName(name)
657+
return {
658+
label: sendable ? name : `${name} (interne — aperçu seul)`,
659+
value: name,
660+
}
661+
})
662+
: []
600663
} finally {
601664
templatesLoading.value = false
602665
}
@@ -637,7 +700,8 @@ async function refreshPreview() {
637700
onMounted(async () => {
638701
await Promise.all([fetchTemplatesConfig(), fetchTemplates(), fetchMailPathsConfig()])
639702
if (templates.value.length && !templates.value.some((t) => t.value === templateName.value)) {
640-
templateName.value = templates.value[0].value
703+
const firstSendable = templates.value.find((t) => isSendableTemplateName(t.value))
704+
templateName.value = firstSendable?.value ?? templates.value[0].value
641705
}
642706
await refreshPreview()
643707
})

0 commit comments

Comments
 (0)