diff --git a/app/javascript/dashboard/helper/URLHelper.js b/app/javascript/dashboard/helper/URLHelper.js index 84e2a9b91..a4b3f32b4 100644 --- a/app/javascript/dashboard/helper/URLHelper.js +++ b/app/javascript/dashboard/helper/URLHelper.js @@ -145,3 +145,34 @@ export const extractFilenameFromUrl = url => { return match ? match[1] : url; } }; + +/** + * Normalizes a comma/newline separated list of domains + * @param {string} domains - The comma/newline separated list of domains + * @returns {string} - The normalized list of domains + * - Converts newlines to commas + * - Trims whitespace + * - Lowercases entries + * - Removes empty values + * - De-duplicates while preserving original order + */ +export const sanitizeAllowedDomains = domains => { + if (!domains) return ''; + + const tokens = domains + .replace(/\r\n/g, '\n') + .replace(/\s*\n\s*/g, ',') + .split(',') + .map(d => d.trim().toLowerCase()) + .filter(d => d.length > 0); + + // De-duplicate while preserving order using Set and filter index + const seen = new Set(); + const unique = tokens.filter(d => { + if (seen.has(d)) return false; + seen.add(d); + return true; + }); + + return unique.join(','); +}; diff --git a/app/javascript/dashboard/helper/specs/URLHelper.spec.js b/app/javascript/dashboard/helper/specs/URLHelper.spec.js index 3a286d8af..224a1df8b 100644 --- a/app/javascript/dashboard/helper/specs/URLHelper.spec.js +++ b/app/javascript/dashboard/helper/specs/URLHelper.spec.js @@ -8,6 +8,7 @@ import { timeStampAppendedURL, getHostNameFromURL, extractFilenameFromUrl, + sanitizeAllowedDomains, } from '../URLHelper'; describe('#URL Helpers', () => { @@ -318,4 +319,32 @@ describe('#URL Helpers', () => { ).toBe('file.doc'); }); }); + + describe('sanitizeAllowedDomains', () => { + it('returns empty string for falsy input', () => { + expect(sanitizeAllowedDomains('')).toBe(''); + expect(sanitizeAllowedDomains(null)).toBe(''); + expect(sanitizeAllowedDomains(undefined)).toBe(''); + }); + + it('trims whitespace and converts newlines to commas', () => { + const input = ' example.com \n foo.bar\nbar.baz '; + expect(sanitizeAllowedDomains(input)).toBe('example.com,foo.bar,bar.baz'); + }); + + it('handles Windows newlines and mixed spacing', () => { + const input = ' example.com\r\n\tfoo.bar , bar.baz '; + expect(sanitizeAllowedDomains(input)).toBe('example.com,foo.bar,bar.baz'); + }); + + it('removes empty values from repeated commas', () => { + const input = ',,example.com,,foo.bar,,'; + expect(sanitizeAllowedDomains(input)).toBe('example.com,foo.bar'); + }); + + it('lowercases entries and de-duplicates preserving order', () => { + const input = 'Example.com,FOO.bar,example.com,Bar.Baz,foo.BAR'; + expect(sanitizeAllowedDomains(input)).toBe('example.com,foo.bar,bar.baz'); + }); + }); }); diff --git a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json index a525921db..1c54adcb2 100644 --- a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json @@ -618,6 +618,11 @@ "SETTINGS_POPUP": { "MESSENGER_HEADING": "Messenger Script", "MESSENGER_SUB_HEAD": "Place this button inside your body tag", + "ALLOWED_DOMAINS": { + "TITLE": "Allowed Domains", + "SUBTITLE": "Add wildcard or regular domains separated by commas (leave blank to allow all), e.g. *.chatwoot.dev, chatwoot.com.", + "PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)" + }, "INBOX_AGENTS": "Agents", "INBOX_AGENTS_SUB_TEXT": "Add or remove agents from this inbox", "AGENT_ASSIGNMENT": "Conversation Assignment", diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/ConfigurationPage.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/ConfigurationPage.vue index 9d35541ff..186fcc48e 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/ConfigurationPage.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/ConfigurationPage.vue @@ -7,7 +7,9 @@ import SmtpSettings from '../SmtpSettings.vue'; import { useVuelidate } from '@vuelidate/core'; import { required } from '@vuelidate/validators'; import NextButton from 'dashboard/components-next/button/Button.vue'; +import TextArea from 'next/textarea/TextArea.vue'; import WhatsappReauthorize from '../channels/whatsapp/Reauthorize.vue'; +import { sanitizeAllowedDomains } from 'dashboard/helper/URLHelper'; export default { components: { @@ -15,6 +17,7 @@ export default { ImapSettings, SmtpSettings, NextButton, + TextArea, WhatsappReauthorize, }, mixins: [inboxMixin], @@ -33,6 +36,8 @@ export default { whatsAppInboxAPIKey: '', isRequestingReauthorization: false, isSyncingTemplates: false, + allowedDomains: '', + isUpdatingAllowedDomains: false, }; }, validations: { @@ -57,6 +62,7 @@ export default { methods: { setDefaults() { this.hmacMandatory = this.inbox.hmac_mandatory || false; + this.allowedDomains = this.inbox.allowed_domains || ''; }, handleHmacFlag() { this.updateInbox(); @@ -76,6 +82,28 @@ export default { useAlert(this.$t('INBOX_MGMT.EDIT.API.ERROR_MESSAGE')); } }, + async updateAllowedDomains() { + this.isUpdatingAllowedDomains = true; + const sanitizedAllowedDomains = sanitizeAllowedDomains( + this.allowedDomains + ); + try { + const payload = { + id: this.inbox.id, + formData: false, + channel: { + allowed_domains: sanitizedAllowedDomains, + }, + }; + await this.$store.dispatch('inboxes/updateInbox', payload); + this.allowedDomains = sanitizedAllowedDomains; + useAlert(this.$t('INBOX_MGMT.EDIT.API.SUCCESS_MESSAGE')); + } catch (error) { + useAlert(this.$t('INBOX_MGMT.EDIT.API.ERROR_MESSAGE')); + } finally { + this.isUpdatingAllowedDomains = false; + } + }, async updateWhatsAppInboxAPIKey() { try { const payload = { @@ -180,6 +208,30 @@ export default { /> + +
+