mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-11-04 04:57:51 +00:00 
			
		
		
		
	## 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>
		
			
				
	
	
		
			179 lines
		
	
	
		
			4.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			179 lines
		
	
	
		
			4.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
export const frontendURL = (path, params) => {
 | 
						|
  const stringifiedParams = params ? `?${new URLSearchParams(params)}` : '';
 | 
						|
  return `/app/${path}${stringifiedParams}`;
 | 
						|
};
 | 
						|
 | 
						|
export const conversationUrl = ({
 | 
						|
  accountId,
 | 
						|
  activeInbox,
 | 
						|
  id,
 | 
						|
  label,
 | 
						|
  teamId,
 | 
						|
  conversationType = '',
 | 
						|
  foldersId,
 | 
						|
}) => {
 | 
						|
  let url = `accounts/${accountId}/conversations/${id}`;
 | 
						|
  if (activeInbox) {
 | 
						|
    url = `accounts/${accountId}/inbox/${activeInbox}/conversations/${id}`;
 | 
						|
  } else if (label) {
 | 
						|
    url = `accounts/${accountId}/label/${label}/conversations/${id}`;
 | 
						|
  } else if (teamId) {
 | 
						|
    url = `accounts/${accountId}/team/${teamId}/conversations/${id}`;
 | 
						|
  } else if (foldersId && foldersId !== 0) {
 | 
						|
    url = `accounts/${accountId}/custom_view/${foldersId}/conversations/${id}`;
 | 
						|
  } else if (conversationType === 'mention') {
 | 
						|
    url = `accounts/${accountId}/mentions/conversations/${id}`;
 | 
						|
  } else if (conversationType === 'participating') {
 | 
						|
    url = `accounts/${accountId}/participating/conversations/${id}`;
 | 
						|
  } else if (conversationType === 'unattended') {
 | 
						|
    url = `accounts/${accountId}/unattended/conversations/${id}`;
 | 
						|
  }
 | 
						|
  return url;
 | 
						|
};
 | 
						|
 | 
						|
export const conversationListPageURL = ({
 | 
						|
  accountId,
 | 
						|
  conversationType = '',
 | 
						|
  inboxId,
 | 
						|
  label,
 | 
						|
  teamId,
 | 
						|
  customViewId,
 | 
						|
}) => {
 | 
						|
  let url = `accounts/${accountId}/dashboard`;
 | 
						|
  if (label) {
 | 
						|
    url = `accounts/${accountId}/label/${label}`;
 | 
						|
  } else if (teamId) {
 | 
						|
    url = `accounts/${accountId}/team/${teamId}`;
 | 
						|
  } else if (inboxId) {
 | 
						|
    url = `accounts/${accountId}/inbox/${inboxId}`;
 | 
						|
  } else if (customViewId) {
 | 
						|
    url = `accounts/${accountId}/custom_view/${customViewId}`;
 | 
						|
  } else if (conversationType) {
 | 
						|
    const urlMap = {
 | 
						|
      mention: 'mentions/conversations',
 | 
						|
      unattended: 'unattended/conversations',
 | 
						|
    };
 | 
						|
    url = `accounts/${accountId}/${urlMap[conversationType]}`;
 | 
						|
  }
 | 
						|
  return frontendURL(url);
 | 
						|
};
 | 
						|
 | 
						|
export const isValidURL = value => {
 | 
						|
  /* eslint-disable no-useless-escape */
 | 
						|
  const URL_REGEX =
 | 
						|
    /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/gm;
 | 
						|
  return URL_REGEX.test(value);
 | 
						|
};
 | 
						|
 | 
						|
export const getArticleSearchURL = ({
 | 
						|
  host,
 | 
						|
  portalSlug,
 | 
						|
  pageNumber,
 | 
						|
  locale,
 | 
						|
  status,
 | 
						|
  authorId,
 | 
						|
  categorySlug,
 | 
						|
  sort,
 | 
						|
  query,
 | 
						|
}) => {
 | 
						|
  const queryParams = new URLSearchParams({});
 | 
						|
 | 
						|
  const params = {
 | 
						|
    page: pageNumber,
 | 
						|
    locale,
 | 
						|
    status,
 | 
						|
    author_id: authorId,
 | 
						|
    category_slug: categorySlug,
 | 
						|
    sort,
 | 
						|
    query,
 | 
						|
  };
 | 
						|
 | 
						|
  Object.entries(params).forEach(([key, value]) => {
 | 
						|
    if (value !== null && value !== undefined) {
 | 
						|
      queryParams.set(key, value);
 | 
						|
    }
 | 
						|
  });
 | 
						|
 | 
						|
  return `${host}/${portalSlug}/articles?${queryParams.toString()}`;
 | 
						|
};
 | 
						|
 | 
						|
export const hasValidAvatarUrl = avatarUrl => {
 | 
						|
  try {
 | 
						|
    const { host: avatarUrlHost } = new URL(avatarUrl);
 | 
						|
    const isFromGravatar = ['www.gravatar.com', 'gravatar'].includes(
 | 
						|
      avatarUrlHost
 | 
						|
    );
 | 
						|
    return avatarUrl && !isFromGravatar;
 | 
						|
  } catch (error) {
 | 
						|
    return false;
 | 
						|
  }
 | 
						|
};
 | 
						|
 | 
						|
export const timeStampAppendedURL = dataUrl => {
 | 
						|
  const url = new URL(dataUrl);
 | 
						|
  if (!url.searchParams.has('t')) {
 | 
						|
    url.searchParams.append('t', Date.now());
 | 
						|
  }
 | 
						|
 | 
						|
  return url.toString();
 | 
						|
};
 | 
						|
 | 
						|
export const getHostNameFromURL = url => {
 | 
						|
  try {
 | 
						|
    return new URL(url).hostname;
 | 
						|
  } catch (error) {
 | 
						|
    return null;
 | 
						|
  }
 | 
						|
};
 | 
						|
 | 
						|
/**
 | 
						|
 * Extracts filename from a URL
 | 
						|
 * @param {string} url - The URL to extract filename from
 | 
						|
 * @returns {string} - The extracted filename or original URL if extraction fails
 | 
						|
 */
 | 
						|
export const extractFilenameFromUrl = url => {
 | 
						|
  if (!url || typeof url !== 'string') return url;
 | 
						|
 | 
						|
  try {
 | 
						|
    const urlObj = new URL(url);
 | 
						|
    const pathname = urlObj.pathname;
 | 
						|
    const filename = pathname.split('/').pop();
 | 
						|
    return filename || url;
 | 
						|
  } catch (error) {
 | 
						|
    // If URL parsing fails, try to extract filename using regex
 | 
						|
    const match = url.match(/\/([^/?#]+)(?:[?#]|$)/);
 | 
						|
    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(',');
 | 
						|
};
 |