feat: Add UI to manage web widget allowed domains (#12495)
## Summary - add allowed domains controls in the web widget configuration page. <img width="1064" height="699" alt="Screenshot 2025-09-23 at 8 52 21 PM" src="https://github.com/user-attachments/assets/8afd60b6-c81d-4f52-9cbe-07e70ad003d2" /> fixes: https://linear.app/chatwoot/issue/CW-5661/add-the-options-for-configure-allowed-domains-for-web-widget --------- Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: iamsivin <iamsivin@gmail.com>
This commit is contained in:
@@ -145,3 +145,34 @@ export const extractFilenameFromUrl = url => {
|
|||||||
return match ? match[1] : 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(',');
|
||||||
|
};
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
timeStampAppendedURL,
|
timeStampAppendedURL,
|
||||||
getHostNameFromURL,
|
getHostNameFromURL,
|
||||||
extractFilenameFromUrl,
|
extractFilenameFromUrl,
|
||||||
|
sanitizeAllowedDomains,
|
||||||
} from '../URLHelper';
|
} from '../URLHelper';
|
||||||
|
|
||||||
describe('#URL Helpers', () => {
|
describe('#URL Helpers', () => {
|
||||||
@@ -318,4 +319,32 @@ describe('#URL Helpers', () => {
|
|||||||
).toBe('file.doc');
|
).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');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -618,6 +618,11 @@
|
|||||||
"SETTINGS_POPUP": {
|
"SETTINGS_POPUP": {
|
||||||
"MESSENGER_HEADING": "Messenger Script",
|
"MESSENGER_HEADING": "Messenger Script",
|
||||||
"MESSENGER_SUB_HEAD": "Place this button inside your body tag",
|
"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": "Agents",
|
||||||
"INBOX_AGENTS_SUB_TEXT": "Add or remove agents from this inbox",
|
"INBOX_AGENTS_SUB_TEXT": "Add or remove agents from this inbox",
|
||||||
"AGENT_ASSIGNMENT": "Conversation Assignment",
|
"AGENT_ASSIGNMENT": "Conversation Assignment",
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import SmtpSettings from '../SmtpSettings.vue';
|
|||||||
import { useVuelidate } from '@vuelidate/core';
|
import { useVuelidate } from '@vuelidate/core';
|
||||||
import { required } from '@vuelidate/validators';
|
import { required } from '@vuelidate/validators';
|
||||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||||
|
import TextArea from 'next/textarea/TextArea.vue';
|
||||||
import WhatsappReauthorize from '../channels/whatsapp/Reauthorize.vue';
|
import WhatsappReauthorize from '../channels/whatsapp/Reauthorize.vue';
|
||||||
|
import { sanitizeAllowedDomains } from 'dashboard/helper/URLHelper';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -15,6 +17,7 @@ export default {
|
|||||||
ImapSettings,
|
ImapSettings,
|
||||||
SmtpSettings,
|
SmtpSettings,
|
||||||
NextButton,
|
NextButton,
|
||||||
|
TextArea,
|
||||||
WhatsappReauthorize,
|
WhatsappReauthorize,
|
||||||
},
|
},
|
||||||
mixins: [inboxMixin],
|
mixins: [inboxMixin],
|
||||||
@@ -33,6 +36,8 @@ export default {
|
|||||||
whatsAppInboxAPIKey: '',
|
whatsAppInboxAPIKey: '',
|
||||||
isRequestingReauthorization: false,
|
isRequestingReauthorization: false,
|
||||||
isSyncingTemplates: false,
|
isSyncingTemplates: false,
|
||||||
|
allowedDomains: '',
|
||||||
|
isUpdatingAllowedDomains: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
validations: {
|
validations: {
|
||||||
@@ -57,6 +62,7 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
setDefaults() {
|
setDefaults() {
|
||||||
this.hmacMandatory = this.inbox.hmac_mandatory || false;
|
this.hmacMandatory = this.inbox.hmac_mandatory || false;
|
||||||
|
this.allowedDomains = this.inbox.allowed_domains || '';
|
||||||
},
|
},
|
||||||
handleHmacFlag() {
|
handleHmacFlag() {
|
||||||
this.updateInbox();
|
this.updateInbox();
|
||||||
@@ -76,6 +82,28 @@ export default {
|
|||||||
useAlert(this.$t('INBOX_MGMT.EDIT.API.ERROR_MESSAGE'));
|
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() {
|
async updateWhatsAppInboxAPIKey() {
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
@@ -180,6 +208,30 @@ export default {
|
|||||||
/>
|
/>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|
||||||
|
<SettingsSection
|
||||||
|
:title="$t('INBOX_MGMT.SETTINGS_POPUP.ALLOWED_DOMAINS.TITLE')"
|
||||||
|
:sub-title="$t('INBOX_MGMT.SETTINGS_POPUP.ALLOWED_DOMAINS.SUBTITLE')"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col w-full max-w-3xl gap-4">
|
||||||
|
<TextArea
|
||||||
|
v-model="allowedDomains"
|
||||||
|
:placeholder="
|
||||||
|
$t('INBOX_MGMT.SETTINGS_POPUP.ALLOWED_DOMAINS.PLACEHOLDER')
|
||||||
|
"
|
||||||
|
auto-height
|
||||||
|
min-height="8rem"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<NextButton
|
||||||
|
:label="$t('INBOX_MGMT.SETTINGS_POPUP.UPDATE')"
|
||||||
|
:is-loading="isUpdatingAllowedDomains"
|
||||||
|
@click="updateAllowedDomains"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SettingsSection>
|
||||||
|
|
||||||
<SettingsSection
|
<SettingsSection
|
||||||
:title="$t('INBOX_MGMT.SETTINGS_POPUP.HMAC_VERIFICATION')"
|
:title="$t('INBOX_MGMT.SETTINGS_POPUP.HMAC_VERIFICATION')"
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user