diff --git a/apps/dashboard/src/lib/components/Org/Settings/Domains.svelte b/apps/dashboard/src/lib/components/Org/Settings/Domains.svelte index 10a8f89f..bd36b099 100644 --- a/apps/dashboard/src/lib/components/Org/Settings/Domains.svelte +++ b/apps/dashboard/src/lib/components/Org/Settings/Domains.svelte @@ -25,7 +25,7 @@ import IconButton from '$lib/components/IconButton/index.svelte'; import { updateOrgSiteNameValidation } from '$lib/utils/functions/validator'; import { t } from '$lib/utils/functions/translations'; - import { sendDomainRequest } from '$lib/utils/services/org/domain'; + import { sanitizeDomain, sendDomainRequest } from '$lib/utils/functions/domain'; let siteName = ''; let customDomain = ''; @@ -81,16 +81,23 @@ async function handleSaveCustomDomain() { if (!isDomainValid) return; - const details = parse(customDomain); + const sanitizedDomain = sanitizeDomain(customDomain); + + const details = parse(sanitizedDomain); if (!details.subdomain) { errors.customDomain = $t('components.settings.domains.custom_domain_error'); return; } - isCustomDomainLoading = true; + if (sanitizedDomain.includes('classroomio')) { + errors.customDomain = $t('components.settings.domains.custom_domain_not_classroomio'); + return; + } + + isCustomDomainLoading = true; const { error } = await supabase .from('organization') - .update({ customDomain }) + .update({ customDomain: sanitizedDomain }) .match({ id: $currentOrg.id }); if (error) { @@ -101,7 +108,7 @@ } try { - const response = await sendDomainRequest('add_domain', customDomain); + const response = await sendDomainRequest('add_domain', sanitizedDomain); const data = await response.json(); console.log('added domain to vercel', data); } catch (error) { @@ -111,7 +118,7 @@ return; } snackbar.success('components.settings.domains.custom_domain_success'); - $currentOrg.customDomain = customDomain; + $currentOrg.customDomain = sanitizedDomain; isCustomDomainLoading = false; } @@ -200,7 +207,7 @@ $: setDefaults($currentOrg); $: resetErrors(siteName, customDomain); - $: isDomainValid = isValidDomain(customDomain, { subdomain: true }); + $: isDomainValid = isValidDomain(sanitizeDomain(customDomain), { subdomain: true }); diff --git a/apps/dashboard/src/lib/utils/functions/domain.ts b/apps/dashboard/src/lib/utils/functions/domain.ts new file mode 100644 index 00000000..4fa9cce8 --- /dev/null +++ b/apps/dashboard/src/lib/utils/functions/domain.ts @@ -0,0 +1,23 @@ +import { supabase } from '$lib/utils/functions/supabase'; + +export const sanitizeDomain = (domain: string) => { + return domain + .trim() + .toLowerCase() + .replace(/^(?:https?:\/\/)?(?:www\.)?/i, '') + .split('/')[0]; +}; + +export async function sendDomainRequest(key: string, domain: string): Promise { + const { data } = await supabase.auth.getSession(); + const accessToken = data.session?.access_token || ''; + + return fetch('/api/domain', { + method: 'POST', + body: JSON.stringify({ params: { key, domain } }), + headers: { + 'Content-Type': 'application/json', + Authorization: `${accessToken}` + } + }); +} diff --git a/apps/dashboard/src/lib/utils/functions/domains.ts b/apps/dashboard/src/lib/utils/functions/domains.ts deleted file mode 100644 index a106961f..00000000 --- a/apps/dashboard/src/lib/utils/functions/domains.ts +++ /dev/null @@ -1,133 +0,0 @@ -// Code from https://github.com/vercel/platforms/blob/1ed55b13bcae97b037c69ec40b4c32df21c2412c/lib/domains.ts - -import { - DomainResponse, - DomainConfigResponse, - DomainVerificationResponse -} from '$lib/utils/types/org'; -import { env } from '$env/dynamic/private'; - -export const addDomainToVercel = async (domain: string) => { - return await fetch( - `https://api.vercel.com/v10/projects/${env.PROJECT_ID_VERCEL}/domains${ - env.TEAM_ID_VERCEL ? `?teamId=${env.TEAM_ID_VERCEL}` : '' - }`, - { - method: 'POST', - headers: { - Authorization: `Bearer ${env.AUTH_BEARER_TOKEN}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - name: domain - // Optional: Redirect www. to root domain - // ...(domain.startsWith("www.") && { - // redirect: domain.replace("www.", ""), - // }), - }) - } - ).then((res) => res.json()); -}; - -export const removeDomainFromVercelProject = async (domain: string) => { - return await fetch( - `https://api.vercel.com/v9/projects/${env.PROJECT_ID_VERCEL}/domains/${domain}${ - env.TEAM_ID_VERCEL ? `?teamId=${env.TEAM_ID_VERCEL}` : '' - }`, - { - headers: { - Authorization: `Bearer ${env.AUTH_BEARER_TOKEN}` - }, - method: 'DELETE' - } - ).then((res) => res.json()); -}; - -export const removeDomainFromVercelTeam = async (domain: string) => { - return await fetch( - `https://api.vercel.com/v6/domains/${domain}${ - env.TEAM_ID_VERCEL ? `?teamId=${env.TEAM_ID_VERCEL}` : '' - }`, - { - headers: { - Authorization: `Bearer ${env.AUTH_BEARER_TOKEN}` - }, - method: 'DELETE' - } - ).then((res) => res.json()); -}; - -export const getDomainResponse = async ( - domain: string -): Promise => { - return await fetch( - `https://api.vercel.com/v9/projects/${env.PROJECT_ID_VERCEL}/domains/${domain}${ - env.TEAM_ID_VERCEL ? `?teamId=${env.TEAM_ID_VERCEL}` : '' - }`, - { - method: 'GET', - headers: { - Authorization: `Bearer ${env.AUTH_BEARER_TOKEN}`, - 'Content-Type': 'application/json' - } - } - ).then((res) => { - return res.json(); - }); -}; - -export const getConfigResponse = async (domain: string): Promise => { - return await fetch( - `https://api.vercel.com/v6/domains/${domain}/config${ - env.TEAM_ID_VERCEL ? `?teamId=${env.TEAM_ID_VERCEL}` : '' - }`, - { - method: 'GET', - headers: { - Authorization: `Bearer ${env.AUTH_BEARER_TOKEN}`, - 'Content-Type': 'application/json' - } - } - ).then((res) => res.json()); -}; - -export const verifyDomain = async (domain: string): Promise => { - return await fetch( - `https://api.vercel.com/v9/projects/${env.PROJECT_ID_VERCEL}/domains/${domain}/verify${ - env.TEAM_ID_VERCEL ? `?teamId=${env.TEAM_ID_VERCEL}` : '' - }`, - { - method: 'POST', - headers: { - Authorization: `Bearer ${env.AUTH_BEARER_TOKEN}`, - 'Content-Type': 'application/json' - } - } - ).then((res) => res.json()); -}; - -export const getSubdomain = (name: string, apexName: string) => { - if (name === apexName) return null; - return name.slice(0, name.length - apexName.length - 1); -}; - -export const getApexDomain = (url: string) => { - let domain; - try { - domain = new URL(url).hostname; - } catch (e) { - return ''; - } - const parts = domain.split('.'); - if (parts.length > 2) { - // if it's a subdomain (e.g. project.vercel.app), return the last 2 parts - return parts.slice(-2).join('.'); - } - // if it's a normal domain (e.g. training.yourdomain.com), we return the domain - return domain; -}; - -// courtesy of ChatGPT: https://sharegpt.com/c/pUYXtRs -export const validDomainRegex = new RegExp( - /^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/ -); diff --git a/apps/dashboard/src/lib/utils/services/org/domain.ts b/apps/dashboard/src/lib/utils/services/org/domain.ts index 9faf903e..41eb96b0 100644 --- a/apps/dashboard/src/lib/utils/services/org/domain.ts +++ b/apps/dashboard/src/lib/utils/services/org/domain.ts @@ -1,15 +1,128 @@ -import { supabase } from '$lib/utils/functions/supabase'; +// Code from https://github.com/vercel/platforms/blob/1ed55b13bcae97b037c69ec40b4c32df21c2412c/lib/domains.ts -export async function sendDomainRequest(key: string, domain: string): Promise { - const { data } = await supabase.auth.getSession(); - const accessToken = data.session?.access_token || ''; +import { + DomainResponse, + DomainConfigResponse, + DomainVerificationResponse +} from '$lib/utils/types/org'; +import { env } from '$env/dynamic/private'; - return fetch('/api/domain', { - method: 'POST', - body: JSON.stringify({ params: { key, domain } }), - headers: { - 'Content-Type': 'application/json', - Authorization: `${accessToken}` +export const addDomainToVercel = async (domain: string) => { + return await fetch( + `https://api.vercel.com/v10/projects/${env.PROJECT_ID_VERCEL}/domains${ + env.TEAM_ID_VERCEL ? `?teamId=${env.TEAM_ID_VERCEL}` : '' + }`, + { + method: 'POST', + headers: { + Authorization: `Bearer ${env.AUTH_BEARER_TOKEN}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: domain + // Optional: Redirect www. to root domain + // ...(domain.startsWith("www.") && { + // redirect: domain.replace("www.", ""), + // }), + }) } + ).then((res) => res.json()); +}; + +export const removeDomainFromVercelProject = async (domain: string) => { + return await fetch( + `https://api.vercel.com/v9/projects/${env.PROJECT_ID_VERCEL}/domains/${domain}${ + env.TEAM_ID_VERCEL ? `?teamId=${env.TEAM_ID_VERCEL}` : '' + }`, + { + headers: { + Authorization: `Bearer ${env.AUTH_BEARER_TOKEN}` + }, + method: 'DELETE' + } + ).then((res) => res.json()); +}; + +export const removeDomainFromVercelTeam = async (domain: string) => { + return await fetch( + `https://api.vercel.com/v6/domains/${domain}${ + env.TEAM_ID_VERCEL ? `?teamId=${env.TEAM_ID_VERCEL}` : '' + }`, + { + headers: { + Authorization: `Bearer ${env.AUTH_BEARER_TOKEN}` + }, + method: 'DELETE' + } + ).then((res) => res.json()); +}; + +export const getDomainResponse = async ( + domain: string +): Promise => { + return await fetch( + `https://api.vercel.com/v9/projects/${env.PROJECT_ID_VERCEL}/domains/${domain}${ + env.TEAM_ID_VERCEL ? `?teamId=${env.TEAM_ID_VERCEL}` : '' + }`, + { + method: 'GET', + headers: { + Authorization: `Bearer ${env.AUTH_BEARER_TOKEN}`, + 'Content-Type': 'application/json' + } + } + ).then((res) => { + return res.json(); }); -} +}; + +export const getConfigResponse = async (domain: string): Promise => { + return await fetch( + `https://api.vercel.com/v6/domains/${domain}/config${ + env.TEAM_ID_VERCEL ? `?teamId=${env.TEAM_ID_VERCEL}` : '' + }`, + { + method: 'GET', + headers: { + Authorization: `Bearer ${env.AUTH_BEARER_TOKEN}`, + 'Content-Type': 'application/json' + } + } + ).then((res) => res.json()); +}; + +export const verifyDomain = async (domain: string): Promise => { + return await fetch( + `https://api.vercel.com/v9/projects/${env.PROJECT_ID_VERCEL}/domains/${domain}/verify${ + env.TEAM_ID_VERCEL ? `?teamId=${env.TEAM_ID_VERCEL}` : '' + }`, + { + method: 'POST', + headers: { + Authorization: `Bearer ${env.AUTH_BEARER_TOKEN}`, + 'Content-Type': 'application/json' + } + } + ).then((res) => res.json()); +}; + +export const getSubdomain = (name: string, apexName: string) => { + if (name === apexName) return null; + return name.slice(0, name.length - apexName.length - 1); +}; + +export const getApexDomain = (url: string) => { + let domain; + try { + domain = new URL(url).hostname; + } catch (e) { + return ''; + } + const parts = domain.split('.'); + if (parts.length > 2) { + // if it's a subdomain (e.g. project.vercel.app), return the last 2 parts + return parts.slice(-2).join('.'); + } + // if it's a normal domain (e.g. training.yourdomain.com), we return the domain + return domain; +}; diff --git a/apps/dashboard/src/lib/utils/translations/de.json b/apps/dashboard/src/lib/utils/translations/de.json index d9b51154..21ac2160 100644 --- a/apps/dashboard/src/lib/utils/translations/de.json +++ b/apps/dashboard/src/lib/utils/translations/de.json @@ -949,7 +949,8 @@ "refresh": "Aktualisieren", "remove": "Entfernen", "custom_favicon": "Benutzerdefiniertes Favicon", - "custom_code": "Benutzerdefinierter Code" + "custom_code": "Benutzerdefinierter Code", + "custom_domain_not_classroomio": "Domain darf „classroomio“ nicht enthalten" }, "customize-lms": { "title": "LMS anpassen", diff --git a/apps/dashboard/src/lib/utils/translations/en.json b/apps/dashboard/src/lib/utils/translations/en.json index b14f2180..e38ffbd1 100644 --- a/apps/dashboard/src/lib/utils/translations/en.json +++ b/apps/dashboard/src/lib/utils/translations/en.json @@ -364,6 +364,7 @@ "save": "Save", "your_domain": "Your Domain", "custom_domain_error": "You can't add a top level domain as custom domain. Instead, use a subdomain like 'courses.[yourwebsite].com' or 'www.[yourwebsite].com'.", + "custom_domain_not_classroomio": "Domain cannot contain 'classroomio'", "custom_domain_success": "Custom domain added successfully", "dns_description": "Set the following record on your DNS provider to continue:", "dns_type": "Type", diff --git a/apps/dashboard/src/lib/utils/translations/es.json b/apps/dashboard/src/lib/utils/translations/es.json index 2a18d97a..fe85dd59 100644 --- a/apps/dashboard/src/lib/utils/translations/es.json +++ b/apps/dashboard/src/lib/utils/translations/es.json @@ -949,7 +949,8 @@ "refresh": "Refrescar", "remove": "Eliminar", "custom_favicon": "favicon personalizado", - "custom_code": "código personalizado" + "custom_code": "código personalizado", + "custom_domain_not_classroomio": "El dominio no puede contener 'classroomio'" }, "customize-lms": { "title": "Personalizar LMS", diff --git a/apps/dashboard/src/lib/utils/translations/fr.json b/apps/dashboard/src/lib/utils/translations/fr.json index 00a1d5a8..97bf8dda 100644 --- a/apps/dashboard/src/lib/utils/translations/fr.json +++ b/apps/dashboard/src/lib/utils/translations/fr.json @@ -949,7 +949,8 @@ "refresh": "Rafraîchir", "remove": "Retirer", "custom_favicon": "Icône de favori personnalisée", - "custom_code": "Code personnalisé" + "custom_code": "Code personnalisé", + "custom_domain_not_classroomio": "Le domaine ne peut pas contenir « classroomio »" }, "customize-lms": { "title": "Personnaliser le LMS", diff --git a/apps/dashboard/src/lib/utils/translations/hi.json b/apps/dashboard/src/lib/utils/translations/hi.json index 516fe075..deee37a1 100644 --- a/apps/dashboard/src/lib/utils/translations/hi.json +++ b/apps/dashboard/src/lib/utils/translations/hi.json @@ -949,7 +949,8 @@ "refresh": "ताज़ा करना", "remove": "निकालना", "custom_favicon": "कस्टम फ़ेविकॉन", - "custom_code": "कस्टम कोड" + "custom_code": "कस्टम कोड", + "custom_domain_not_classroomio": "डोमेन में 'classroomio' नहीं हो सकता" }, "customize-lms": { "title": "एलएमएस को अनुकूलित करें", diff --git a/apps/dashboard/src/lib/utils/translations/pt.json b/apps/dashboard/src/lib/utils/translations/pt.json index 584f9d9e..4a1269c6 100644 --- a/apps/dashboard/src/lib/utils/translations/pt.json +++ b/apps/dashboard/src/lib/utils/translations/pt.json @@ -949,7 +949,8 @@ "refresh": "Atualizar", "remove": "Remover", "custom_favicon": "Favicon personalizado", - "custom_code": "Código personalizado" + "custom_code": "Código personalizado", + "custom_domain_not_classroomio": "O domínio não pode conter 'classroomio'" }, "customize-lms": { "title": "Personalizar LMS", diff --git a/apps/dashboard/src/lib/utils/translations/ru.json b/apps/dashboard/src/lib/utils/translations/ru.json index 3de8470a..1218b008 100644 --- a/apps/dashboard/src/lib/utils/translations/ru.json +++ b/apps/dashboard/src/lib/utils/translations/ru.json @@ -949,7 +949,8 @@ "refresh": "Обновить", "remove": "Удалять", "custom_favicon": "Пользовательский значок", - "custom_code": "Пользовательский код" + "custom_code": "Пользовательский код", + "custom_domain_not_classroomio": "Домен не может содержать classio." }, "customize-lms": { "title": "Настроить систему управления обучением", diff --git a/apps/dashboard/src/lib/utils/translations/vi.json b/apps/dashboard/src/lib/utils/translations/vi.json index 3ae67086..4652311f 100644 --- a/apps/dashboard/src/lib/utils/translations/vi.json +++ b/apps/dashboard/src/lib/utils/translations/vi.json @@ -949,7 +949,8 @@ "refresh": "Làm cho khỏe lại", "remove": "Di dời", "custom_favicon": "Biểu tượng yêu thích tùy chỉnh", - "custom_code": "Mã tùy chỉnh" + "custom_code": "Mã tùy chỉnh", + "custom_domain_not_classroomio": "Tên miền không thể chứa 'classroomio'" }, "customize-lms": { "title": "Tùy chỉnh LMS", diff --git a/apps/dashboard/src/routes/api/domain/+server.ts b/apps/dashboard/src/routes/api/domain/+server.ts index e178100e..0142488e 100644 --- a/apps/dashboard/src/routes/api/domain/+server.ts +++ b/apps/dashboard/src/routes/api/domain/+server.ts @@ -4,7 +4,7 @@ import { getConfigResponse, addDomainToVercel, removeDomainFromVercelProject -} from '$lib/utils/functions/domains'; +} from '$lib/utils/services/org/domain'; const supabase = getSupabase();