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 {
/>
+
+
+
+