diff --git a/packages/core/src/libraries/domain.test.ts b/packages/core/src/libraries/domain.test.ts index c88f59ef1..537bcd143 100644 --- a/packages/core/src/libraries/domain.test.ts +++ b/packages/core/src/libraries/domain.test.ts @@ -45,6 +45,7 @@ beforeAll(() => { SystemContext.shared.hostnameProviderConfig = { zoneId: 'fake_zone_id', apiToken: '', + blockedDomains: ['blocked.com'], }; }); @@ -70,6 +71,12 @@ describe('addDomain()', () => { value: mockFallbackOrigin, }); }); + + it('should throw for blocked domain', async () => { + await expect(addDomain('hi.blocked.com')).rejects.toMatchError( + new RequestError({ code: 'domain.domain_is_not_allowed' }) + ); + }); }); describe('syncDomainStatus()', () => { diff --git a/packages/core/src/libraries/domain.ts b/packages/core/src/libraries/domain.ts index 73a244c9f..b0223abba 100644 --- a/packages/core/src/libraries/domain.ts +++ b/packages/core/src/libraries/domain.ts @@ -11,6 +11,7 @@ import { deleteCustomHostname, getFallbackOrigin, } from '#src/utils/cloudflare/index.js'; +import { isSubdomainOf } from '#src/utils/domain.js'; import { clearCustomDomainCache } from '#src/utils/tenant.js'; export type DomainLibrary = ReturnType; @@ -71,6 +72,14 @@ export const createDomainLibrary = (queries: Queries) => { const { hostnameProviderConfig } = SystemContext.shared; assertThat(hostnameProviderConfig, 'domain.not_configured'); + const { blockedDomains } = hostnameProviderConfig; + assertThat( + !(blockedDomains ?? []).some( + (domain) => hostname === domain || isSubdomainOf(hostname, domain) + ), + 'domain.domain_is_not_allowed' + ); + const [fallbackOrigin, cloudflareData] = await Promise.all([ getFallbackOrigin(hostnameProviderConfig), createCustomHostname(hostnameProviderConfig, hostname), diff --git a/packages/core/src/utils/domain.test.ts b/packages/core/src/utils/domain.test.ts new file mode 100644 index 000000000..0db616ba8 --- /dev/null +++ b/packages/core/src/utils/domain.test.ts @@ -0,0 +1,12 @@ +import { isSubdomainOf } from './domain.js'; + +describe('isSubdomainOf()', () => { + it('should return true if the given domain is a subdomain of a domain', () => { + expect(isSubdomainOf('subdomain.domain.com', 'domain.com')).toBe(true); + }); + + it('should return false if the given domain is not a subdomain of a domain', () => { + expect(isSubdomainOf('subdomain.domain.com', 'domain.org')).toBe(false); + expect(isSubdomainOf('subdomaindomain.com', 'domain.org')).toBe(false); + }); +}); diff --git a/packages/core/src/utils/domain.ts b/packages/core/src/utils/domain.ts new file mode 100644 index 000000000..9c97c2fad --- /dev/null +++ b/packages/core/src/utils/domain.ts @@ -0,0 +1,6 @@ +/** + * Checks if the given domain is a subdomain of a domain. + */ +export const isSubdomainOf = (subdomain: string, domain: string): boolean => { + return subdomain.endsWith(`.${domain}`); +}; diff --git a/packages/phrases/src/locales/de/errors/domain.ts b/packages/phrases/src/locales/de/errors/domain.ts index 9c3a30389..c45990f77 100644 --- a/packages/phrases/src/locales/de/errors/domain.ts +++ b/packages/phrases/src/locales/de/errors/domain.ts @@ -7,6 +7,8 @@ const domain = { limit_to_one_domain: 'Sie können nur eine benutzerdefinierte Domain haben.', hostname_already_exists: 'Diese Domain existiert bereits auf unserem Server.', cloudflare_not_found: 'Hostname in Cloudflare nicht gefunden', + /** UNTRANSLATED */ + domain_is_not_allowed: 'This domain is not allowed.', }; export default Object.freeze(domain); diff --git a/packages/phrases/src/locales/en/errors/domain.ts b/packages/phrases/src/locales/en/errors/domain.ts index 85db1f63d..176619c1f 100644 --- a/packages/phrases/src/locales/en/errors/domain.ts +++ b/packages/phrases/src/locales/en/errors/domain.ts @@ -6,6 +6,7 @@ const domain = { limit_to_one_domain: 'You can only have one custom domain.', hostname_already_exists: 'This domain already exists in our server.', cloudflare_not_found: 'Can not find hostname in Cloudflare', + domain_is_not_allowed: 'This domain is not allowed.', }; export default Object.freeze(domain); diff --git a/packages/phrases/src/locales/es/errors/domain.ts b/packages/phrases/src/locales/es/errors/domain.ts index ac544896d..431c536ed 100644 --- a/packages/phrases/src/locales/es/errors/domain.ts +++ b/packages/phrases/src/locales/es/errors/domain.ts @@ -6,6 +6,8 @@ const domain = { limit_to_one_domain: 'Solo puedes tener un dominio personalizado.', hostname_already_exists: 'Este dominio ya existe en nuestro servidor.', cloudflare_not_found: 'No se puede encontrar el nombre de host en Cloudflare', + /** UNTRANSLATED */ + domain_is_not_allowed: 'This domain is not allowed.', }; export default Object.freeze(domain); diff --git a/packages/phrases/src/locales/fr/errors/domain.ts b/packages/phrases/src/locales/fr/errors/domain.ts index fcf1d3209..db9922b94 100644 --- a/packages/phrases/src/locales/fr/errors/domain.ts +++ b/packages/phrases/src/locales/fr/errors/domain.ts @@ -6,6 +6,8 @@ const domain = { limit_to_one_domain: "Vous ne pouvez avoir qu'un seul domaine personnalisé", hostname_already_exists: 'Ce domaine existe déjà sur notre serveur.', cloudflare_not_found: "Impossible de trouver le nom d'hôte dans Cloudflare", + /** UNTRANSLATED */ + domain_is_not_allowed: 'This domain is not allowed.', }; export default Object.freeze(domain); diff --git a/packages/phrases/src/locales/it/errors/domain.ts b/packages/phrases/src/locales/it/errors/domain.ts index 08f00e481..d6dab0a8d 100644 --- a/packages/phrases/src/locales/it/errors/domain.ts +++ b/packages/phrases/src/locales/it/errors/domain.ts @@ -6,6 +6,8 @@ const domain = { limit_to_one_domain: 'Puoi avere solo un dominio personalizzato.', hostname_already_exists: 'Questo dominio esiste già nel nostro server.', cloudflare_not_found: 'Impossibile trovare il nome host in Cloudflare.', + /** UNTRANSLATED */ + domain_is_not_allowed: 'This domain is not allowed.', }; export default Object.freeze(domain); diff --git a/packages/phrases/src/locales/ja/errors/domain.ts b/packages/phrases/src/locales/ja/errors/domain.ts index 334cc0d11..9014a45f6 100644 --- a/packages/phrases/src/locales/ja/errors/domain.ts +++ b/packages/phrases/src/locales/ja/errors/domain.ts @@ -6,6 +6,8 @@ const domain = { limit_to_one_domain: 'カスタムドメインは1つしか持てません。', hostname_already_exists: 'サーバーには既にこのドメインが存在しています。', cloudflare_not_found: 'Cloudflare からホスト名が見つかりませんでした。', + /** UNTRANSLATED */ + domain_is_not_allowed: 'This domain is not allowed.', }; export default Object.freeze(domain); diff --git a/packages/phrases/src/locales/ko/errors/domain.ts b/packages/phrases/src/locales/ko/errors/domain.ts index 955104c83..289722e37 100644 --- a/packages/phrases/src/locales/ko/errors/domain.ts +++ b/packages/phrases/src/locales/ko/errors/domain.ts @@ -6,6 +6,8 @@ const domain = { limit_to_one_domain: '하나의 맞춤 도메인만 사용할 수 있습니다.', hostname_already_exists: '이 도메인은 이미 서버에 존재합니다.', cloudflare_not_found: 'Cloudflare에서 호스트 이름을 찾을 수 없습니다.', + /** UNTRANSLATED */ + domain_is_not_allowed: 'This domain is not allowed.', }; export default Object.freeze(domain); diff --git a/packages/phrases/src/locales/pl-pl/errors/domain.ts b/packages/phrases/src/locales/pl-pl/errors/domain.ts index 8a0ae939c..e4789fa46 100644 --- a/packages/phrases/src/locales/pl-pl/errors/domain.ts +++ b/packages/phrases/src/locales/pl-pl/errors/domain.ts @@ -6,6 +6,8 @@ const domain = { limit_to_one_domain: 'Możesz mieć tylko jedną niestandardową domenę.', hostname_already_exists: 'Ta domena już istnieje na naszym serwerze.', cloudflare_not_found: 'Nie można znaleźć nazwy hosta w Cloudflare', + /** UNTRANSLATED */ + domain_is_not_allowed: 'This domain is not allowed.', }; export default Object.freeze(domain); diff --git a/packages/phrases/src/locales/pt-br/errors/domain.ts b/packages/phrases/src/locales/pt-br/errors/domain.ts index 0eeb3ab12..3ac205c89 100644 --- a/packages/phrases/src/locales/pt-br/errors/domain.ts +++ b/packages/phrases/src/locales/pt-br/errors/domain.ts @@ -6,6 +6,8 @@ const domain = { limit_to_one_domain: 'Você só pode ter um domínio personalizado.', hostname_already_exists: 'Este domínio já existe em nosso servidor.', cloudflare_not_found: 'Não é possível encontrar o nome do host no Cloudflare', + /** UNTRANSLATED */ + domain_is_not_allowed: 'This domain is not allowed.', }; export default Object.freeze(domain); diff --git a/packages/phrases/src/locales/pt-pt/errors/domain.ts b/packages/phrases/src/locales/pt-pt/errors/domain.ts index b82daa5b6..6df409ffd 100644 --- a/packages/phrases/src/locales/pt-pt/errors/domain.ts +++ b/packages/phrases/src/locales/pt-pt/errors/domain.ts @@ -6,6 +6,8 @@ const domain = { limit_to_one_domain: 'Você só pode ter um domínio personalizado.', hostname_already_exists: 'Este domínio já existe em nosso servidor.', cloudflare_not_found: 'Não é possível encontrar o nome de host no Cloudflare', + /** UNTRANSLATED */ + domain_is_not_allowed: 'This domain is not allowed.', }; export default Object.freeze(domain); diff --git a/packages/phrases/src/locales/ru/errors/domain.ts b/packages/phrases/src/locales/ru/errors/domain.ts index ff35698b9..33b89d63e 100644 --- a/packages/phrases/src/locales/ru/errors/domain.ts +++ b/packages/phrases/src/locales/ru/errors/domain.ts @@ -6,6 +6,8 @@ const domain = { limit_to_one_domain: 'Вы можете использовать только один пользовательский домен.', hostname_already_exists: 'Этот домен уже существует на нашем сервере.', cloudflare_not_found: 'Не удается найти имя хоста в Cloudflare', + /** UNTRANSLATED */ + domain_is_not_allowed: 'This domain is not allowed.', }; export default Object.freeze(domain); diff --git a/packages/phrases/src/locales/tr-tr/errors/domain.ts b/packages/phrases/src/locales/tr-tr/errors/domain.ts index 92e8128d5..c3c67f933 100644 --- a/packages/phrases/src/locales/tr-tr/errors/domain.ts +++ b/packages/phrases/src/locales/tr-tr/errors/domain.ts @@ -6,6 +6,8 @@ const domain = { limit_to_one_domain: 'Sadece bir özel alan adınız olabilir.', hostname_already_exists: 'Bu alan adı sunucumuzda zaten mevcut.', cloudflare_not_found: "Cloudflare'da alan adı bulunamadı.", + /** UNTRANSLATED */ + domain_is_not_allowed: 'This domain is not allowed.', }; export default Object.freeze(domain); diff --git a/packages/phrases/src/locales/zh-cn/errors/domain.ts b/packages/phrases/src/locales/zh-cn/errors/domain.ts index 8e7156820..40c79c6e9 100644 --- a/packages/phrases/src/locales/zh-cn/errors/domain.ts +++ b/packages/phrases/src/locales/zh-cn/errors/domain.ts @@ -6,6 +6,8 @@ const domain = { limit_to_one_domain: '仅限一个自定义域名。', hostname_already_exists: '该域名在我们的服务器中已存在。', cloudflare_not_found: '在 Cloudflare 中找不到主机名', + /** UNTRANSLATED */ + domain_is_not_allowed: 'This domain is not allowed.', }; export default Object.freeze(domain); diff --git a/packages/phrases/src/locales/zh-hk/errors/domain.ts b/packages/phrases/src/locales/zh-hk/errors/domain.ts index 690d0be48..94c1c257a 100644 --- a/packages/phrases/src/locales/zh-hk/errors/domain.ts +++ b/packages/phrases/src/locales/zh-hk/errors/domain.ts @@ -6,6 +6,8 @@ const domain = { limit_to_one_domain: '您只能擁有一個自定義域名。', hostname_already_exists: '此域名已存在於我們的伺服器中。', cloudflare_not_found: '無法在 Cloudflare 中找到主機名', + /** UNTRANSLATED */ + domain_is_not_allowed: 'This domain is not allowed.', }; export default Object.freeze(domain); diff --git a/packages/phrases/src/locales/zh-tw/errors/domain.ts b/packages/phrases/src/locales/zh-tw/errors/domain.ts index 56a78bd08..a029fe145 100644 --- a/packages/phrases/src/locales/zh-tw/errors/domain.ts +++ b/packages/phrases/src/locales/zh-tw/errors/domain.ts @@ -6,6 +6,8 @@ const domain = { limit_to_one_domain: '您只能擁有一個自訂網域。', hostname_already_exists: '此網域名稱已經存在我們的伺服器中。', cloudflare_not_found: '無法找到 Cloudflare 中的主機名', + /** UNTRANSLATED */ + domain_is_not_allowed: 'This domain is not allowed.', }; export default Object.freeze(domain); diff --git a/packages/schemas/src/types/system.ts b/packages/schemas/src/types/system.ts index 63aa87b25..5a63d603e 100644 --- a/packages/schemas/src/types/system.ts +++ b/packages/schemas/src/types/system.ts @@ -145,6 +145,7 @@ export const demoSocialGuard: Readonly<{ export const hostnameProviderDataGuard = z.object({ zoneId: z.string(), apiToken: z.string(), // Requires zone permission for "SSL and Certificates Edit" + blockedDomains: z.string().array().optional(), // Optional list of blocked domains }); export type HostnameProviderData = z.infer;