diff --git a/app/controllers/widgets_controller.rb b/app/controllers/widgets_controller.rb index 7f45ce636..913319303 100644 --- a/app/controllers/widgets_controller.rb +++ b/app/controllers/widgets_controller.rb @@ -77,13 +77,23 @@ class WidgetsController < ActionController::Base end def allow_iframe_requests - if @web_widget.allowed_domains.blank? + if @web_widget.allowed_domains.blank? || embedded_from_non_web_origin? response.headers.delete('X-Frame-Options') else domains = @web_widget.allowed_domains.split(',').map(&:strip).join(' ') response.headers['Content-Security-Policy'] = "frame-ancestors #{domains}" end end + + # Mobile WebViews (iOS/Android) load content from file:// or null origins, + # which cannot match any domain in frame-ancestors. When the per-inbox flag + # is enabled, skip frame-ancestors for these requests. + def embedded_from_non_web_origin? + return false unless @web_widget.allow_mobile_webview? + + origin = request.headers['Origin'] + origin.blank? || origin == 'null' || origin&.start_with?('file://') + end end WidgetsController.prepend_mod_with('WidgetsController') diff --git a/app/javascript/dashboard/components-next/Settings/SettingsToggleSection.vue b/app/javascript/dashboard/components-next/Settings/SettingsToggleSection.vue index 62e8b44fa..ab3f8488e 100644 --- a/app/javascript/dashboard/components-next/Settings/SettingsToggleSection.vue +++ b/app/javascript/dashboard/components-next/Settings/SettingsToggleSection.vue @@ -14,6 +14,10 @@ defineProps({ type: Boolean, default: false, }, + hideToggle: { + type: Boolean, + default: false, + }, }); const modelValue = defineModel({ type: Boolean, default: false }); @@ -28,7 +32,8 @@ const modelValue = defineModel({ type: Boolean, default: false }); {{ header }} - +
+
{{ description }} diff --git a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json index 0455bdfa1..d0d1573b3 100644 --- a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json @@ -710,8 +710,20 @@ "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)" + "DESCRIPTION": "Restrict which websites can embed your chat widget. For security, only add domains you own and trust. Add one or more domains separated by commas. Leave blank to allow all domains (not recommended for production).", + "PLACEHOLDER": "example.com, www.example.com, app.example.com" + }, + "ALLOW_MOBILE_WEBVIEW": { + "LABEL": "Enable widget in mobile apps", + "SUBTITLE": "Check this if you embed the widget in iOS or Android apps. Mobile apps don't send domain information, so they would be blocked by domain restrictions unless this is enabled." + }, + "IDENTITY_VALIDATION": { + "TITLE": "Identity Validation", + "DESCRIPTION": "Verify user authenticity by generating secure tokens. This prevents unauthorized users from impersonating others in your chat.", + "SECRET_KEY": "Secret Key", + "VIEW_DOCS": "View documentation", + "REQUIRE_LABEL": "Require identity validation for all conversations", + "REQUIRE_DESCRIPTION": "When enabled, users must provide a valid identity token to start conversations. Requests without valid tokens will be rejected." }, "INBOX_AGENTS": "Agents", "INBOX_AGENTS_SUB_TEXT": "Add or remove agents from this inbox", diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue index 0ff1ac61e..247bb30d8 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue @@ -218,13 +218,10 @@ export default { return getInboxIconByType(type, medium, 'line'); }, bannerMaxWidth() { - const narrowTabs = [ - 'collaborators', - 'configuration', - 'bot-configuration', - ]; + const narrowTabs = ['collaborators', 'bot-configuration']; + const wideIfWebWidget = ['configuration', 'inbox-settings']; if (narrowTabs.includes(this.selectedTabKey)) return 'max-w-4xl'; - if (this.selectedTabKey === 'inbox-settings') { + if (wideIfWebWidget.includes(this.selectedTabKey)) { return this.isAWebWidgetInbox ? 'max-w-7xl' : 'max-w-4xl'; } return 'max-w-7xl'; @@ -354,6 +351,8 @@ export default { this.$nextTick(() => { this.setTabFromRouteParam(); }); + } else { + this.selectedFeatureFlags = newInbox?.selected_feature_flags || []; } }, immediate: true, @@ -1164,7 +1163,11 @@ export default {
-
+
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 35362528e..0bfabb7b6 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/ConfigurationPage.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/ConfigurationPage.vue @@ -2,6 +2,8 @@ import { useAlert } from 'dashboard/composables'; import inboxMixin from 'shared/mixins/inboxMixin'; import SettingsFieldSection from 'dashboard/components-next/Settings/SettingsFieldSection.vue'; +import SettingsToggleSection from 'dashboard/components-next/Settings/SettingsToggleSection.vue'; +import SettingsAccordion from 'dashboard/components-next/Settings/SettingsAccordion.vue'; import ImapSettings from '../ImapSettings.vue'; import SmtpSettings from '../SmtpSettings.vue'; import { useVuelidate } from '@vuelidate/core'; @@ -14,6 +16,8 @@ import { sanitizeAllowedDomains } from 'dashboard/helper/URLHelper'; export default { components: { SettingsFieldSection, + SettingsToggleSection, + SettingsAccordion, ImapSettings, SmtpSettings, NextButton, @@ -33,11 +37,13 @@ export default { data() { return { hmacMandatory: false, + allowMobileWebview: false, whatsAppInboxAPIKey: '', isRequestingReauthorization: false, isSyncingTemplates: false, allowedDomains: '', isUpdatingAllowedDomains: false, + isSettingDefaults: false, }; }, validations: { @@ -58,14 +64,28 @@ export default { inbox() { this.setDefaults(); }, + allowMobileWebview() { + if (!this.isSettingDefaults) this.handleMobileWebviewFlag(); + }, + hmacMandatory() { + if (!this.isSettingDefaults && this.isAWebWidgetInbox) + this.handleHmacFlag(); + }, }, mounted() { this.setDefaults(); }, methods: { setDefaults() { + this.isSettingDefaults = true; this.hmacMandatory = this.inbox.hmac_mandatory || false; + this.allowMobileWebview = ( + this.inbox.selected_feature_flags || [] + ).includes('allow_mobile_webview'); this.allowedDomains = this.inbox.allowed_domains || ''; + this.$nextTick(() => { + this.isSettingDefaults = false; + }); }, handleHmacFlag() { this.updateInbox(); @@ -85,6 +105,26 @@ export default { useAlert(this.$t('INBOX_MGMT.EDIT.API.ERROR_MESSAGE')); } }, + async handleMobileWebviewFlag() { + try { + const currentFlags = this.inbox.selected_feature_flags || []; + const selectedFlags = this.allowMobileWebview + ? [...currentFlags, 'allow_mobile_webview'] + : currentFlags.filter(f => f !== 'allow_mobile_webview'); + + const payload = { + id: this.inbox.id, + formData: false, + channel: { + selected_feature_flags: selectedFlags, + }, + }; + await this.$store.dispatch('inboxes/updateInbox', payload); + useAlert(this.$t('INBOX_MGMT.EDIT.API.SUCCESS_MESSAGE')); + } catch (error) { + useAlert(this.$t('INBOX_MGMT.EDIT.API.ERROR_MESSAGE')); + } + }, async updateAllowedDomains() { this.isUpdatingAllowedDomains = true; const sanitizedAllowedDomains = sanitizeAllowedDomains( @@ -196,75 +236,86 @@ export default {
-
- + -