feat: Enhanced WhatsApp template support with security optimizations

- Add comprehensive WhatsApp template parameter support for header/footer/buttons
- Implement coupon code template support with 15-character validation
- Support all WhatsApp template types (text, media, interactive)
- Add advanced parameter types (currency, date/time, location)
- Remove restrictions on DOCUMENT, IMAGE, VIDEO template formats

Security improvements:
- Fix XSS vulnerability by replacing v-html with v-dompurify-html
- Add input sanitization and parameter validation
- Implement URL validation with scheme and length checks
- Add protection against DoS attacks with length limits

Performance optimizations:
- Replace recursive template fetching with iterative pagination
- Add safety limits to prevent infinite loops and stack overflow
- Optimize template processing for better memory usage

Frontend enhancements:
- Enhanced template picker with header/footer/button display
- Component-based parameter input organized by type
- Real-time validation for coupon codes and parameters
- Improved template preview with sanitized HTML rendering
- Fix Vue linting issues with proper computed properties

Maintains full backward compatibility with existing template configurations.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Sojan Jose
2025-07-21 17:03:11 +04:00
co-authored by Claude
parent 647a65d481
commit ee176480da
4 changed files with 471 additions and 33 deletions
@@ -1,6 +1,6 @@
<script>
// TODO: Remove this when we support all formats
const formatsToRemove = ['DOCUMENT', 'IMAGE', 'VIDEO'];
// Support all template formats now
const formatsToRemove = [];
export default {
props: {
@@ -34,8 +34,30 @@ export default {
},
methods: {
getTemplatebody(template) {
return template.components.find(component => component.type === 'BODY')
.text;
return (
template.components.find(component => component.type === 'BODY')
?.text || ''
);
},
getTemplateHeader(template) {
return template.components.find(component => component.type === 'HEADER');
},
getTemplateFooter(template) {
return template.components.find(component => component.type === 'FOOTER');
},
getTemplateButtons(template) {
return template.components.find(
component => component.type === 'BUTTONS'
);
},
hasMediaContent(template) {
const header = this.getTemplateHeader(template);
return (
header &&
(header.format === 'IMAGE' ||
header.format === 'VIDEO' ||
header.format === 'DOCUMENT')
);
},
},
};
@@ -74,17 +96,68 @@ export default {
{{ template.language }}
</span>
</div>
<div>
<p class="font-medium">
{{ $t('WHATSAPP_TEMPLATES.PICKER.LABELS.TEMPLATE_BODY') }}
<!-- Header -->
<div v-if="getTemplateHeader(template)" class="mb-3">
<p class="font-medium text-xs text-n-slate-11">
{{ $t('WHATSAPP_TEMPLATES.PICKER.HEADER') || 'HEADER' }}
</p>
<p class="label-body">{{ getTemplatebody(template) }}</p>
<div
v-if="getTemplateHeader(template).format === 'TEXT'"
class="label-body text-sm"
>
{{ getTemplateHeader(template).text }}
</div>
<div
v-else-if="hasMediaContent(template)"
class="text-sm text-n-slate-11 italic"
>
{{
$t('WHATSAPP_TEMPLATES.PICKER.MEDIA_CONTENT', {
format: getTemplateHeader(template).format,
}) || `${getTemplateHeader(template).format} media content`
}}
</div>
</div>
<div class="mt-5">
<p class="font-medium">
{{ $t('WHATSAPP_TEMPLATES.PICKER.LABELS.CATEGORY') }}
<!-- Body -->
<div>
<p class="font-medium text-xs text-n-slate-11">
{{ $t('WHATSAPP_TEMPLATES.PICKER.BODY') || 'BODY' }}
</p>
<p>{{ template.category }}</p>
<p class="label-body text-sm">{{ getTemplatebody(template) }}</p>
</div>
<!-- Footer -->
<div v-if="getTemplateFooter(template)" class="mt-3">
<p class="font-medium text-xs text-n-slate-11">
{{ $t('WHATSAPP_TEMPLATES.PICKER.FOOTER') || 'FOOTER' }}
</p>
<p class="label-body text-sm">
{{ getTemplateFooter(template).text }}
</p>
</div>
<!-- Buttons -->
<div v-if="getTemplateButtons(template)" class="mt-3">
<p class="font-medium text-xs text-n-slate-11">
{{ $t('WHATSAPP_TEMPLATES.PICKER.BUTTONS') || 'BUTTONS' }}
</p>
<div class="flex flex-wrap gap-1 mt-1">
<span
v-for="button in getTemplateButtons(template).buttons"
:key="button.text"
class="px-2 py-1 text-xs bg-n-slate-3 rounded text-n-slate-12"
>
{{ button.text }}
</span>
</div>
</div>
<div class="mt-3">
<p class="font-medium text-xs text-n-slate-11">
{{ $t('WHATSAPP_TEMPLATES.PICKER.CATEGORY') || 'CATEGORY' }}
</p>
<p class="text-sm">{{ template.category }}</p>
</div>
</div>
</button>