fix: validate domain isn't classroomio
This commit is contained in:
@@ -25,7 +25,7 @@
|
|||||||
import IconButton from '$lib/components/IconButton/index.svelte';
|
import IconButton from '$lib/components/IconButton/index.svelte';
|
||||||
import { updateOrgSiteNameValidation } from '$lib/utils/functions/validator';
|
import { updateOrgSiteNameValidation } from '$lib/utils/functions/validator';
|
||||||
import { t } from '$lib/utils/functions/translations';
|
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 siteName = '';
|
||||||
let customDomain = '';
|
let customDomain = '';
|
||||||
@@ -81,16 +81,23 @@
|
|||||||
async function handleSaveCustomDomain() {
|
async function handleSaveCustomDomain() {
|
||||||
if (!isDomainValid) return;
|
if (!isDomainValid) return;
|
||||||
|
|
||||||
const details = parse(customDomain);
|
const sanitizedDomain = sanitizeDomain(customDomain);
|
||||||
|
|
||||||
|
const details = parse(sanitizedDomain);
|
||||||
if (!details.subdomain) {
|
if (!details.subdomain) {
|
||||||
errors.customDomain = $t('components.settings.domains.custom_domain_error');
|
errors.customDomain = $t('components.settings.domains.custom_domain_error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
isCustomDomainLoading = true;
|
|
||||||
|
|
||||||
|
if (sanitizedDomain.includes('classroomio')) {
|
||||||
|
errors.customDomain = $t('components.settings.domains.custom_domain_not_classroomio');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isCustomDomainLoading = true;
|
||||||
const { error } = await supabase
|
const { error } = await supabase
|
||||||
.from('organization')
|
.from('organization')
|
||||||
.update({ customDomain })
|
.update({ customDomain: sanitizedDomain })
|
||||||
.match({ id: $currentOrg.id });
|
.match({ id: $currentOrg.id });
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -101,7 +108,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await sendDomainRequest('add_domain', customDomain);
|
const response = await sendDomainRequest('add_domain', sanitizedDomain);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log('added domain to vercel', data);
|
console.log('added domain to vercel', data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -111,7 +118,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
snackbar.success('components.settings.domains.custom_domain_success');
|
snackbar.success('components.settings.domains.custom_domain_success');
|
||||||
$currentOrg.customDomain = customDomain;
|
$currentOrg.customDomain = sanitizedDomain;
|
||||||
|
|
||||||
isCustomDomainLoading = false;
|
isCustomDomainLoading = false;
|
||||||
}
|
}
|
||||||
@@ -200,7 +207,7 @@
|
|||||||
$: setDefaults($currentOrg);
|
$: setDefaults($currentOrg);
|
||||||
$: resetErrors(siteName, customDomain);
|
$: resetErrors(siteName, customDomain);
|
||||||
|
|
||||||
$: isDomainValid = isValidDomain(customDomain, { subdomain: true });
|
$: isDomainValid = isValidDomain(sanitizeDomain(customDomain), { subdomain: true });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Grid class="border rounded border-gray-200 dark:border-neutral-600 w-full mt-5">
|
<Grid class="border rounded border-gray-200 dark:border-neutral-600 w-full mt-5">
|
||||||
|
|||||||
@@ -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<Response> {
|
||||||
|
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}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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<DomainResponse & { error: { code: string; message: 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}` : ''
|
|
||||||
}`,
|
|
||||||
{
|
|
||||||
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<DomainConfigResponse> => {
|
|
||||||
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<DomainVerificationResponse> => {
|
|
||||||
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,}$/
|
|
||||||
);
|
|
||||||
@@ -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<Response> {
|
import {
|
||||||
const { data } = await supabase.auth.getSession();
|
DomainResponse,
|
||||||
const accessToken = data.session?.access_token || '';
|
DomainConfigResponse,
|
||||||
|
DomainVerificationResponse
|
||||||
|
} from '$lib/utils/types/org';
|
||||||
|
import { env } from '$env/dynamic/private';
|
||||||
|
|
||||||
return fetch('/api/domain', {
|
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',
|
method: 'POST',
|
||||||
body: JSON.stringify({ params: { key, domain } }),
|
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
Authorization: `Bearer ${env.AUTH_BEARER_TOKEN}`,
|
||||||
Authorization: `${accessToken}`
|
'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<DomainResponse & { error: { code: string; message: 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}` : ''
|
||||||
|
}`,
|
||||||
|
{
|
||||||
|
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<DomainConfigResponse> => {
|
||||||
|
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<DomainVerificationResponse> => {
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -949,7 +949,8 @@
|
|||||||
"refresh": "Aktualisieren",
|
"refresh": "Aktualisieren",
|
||||||
"remove": "Entfernen",
|
"remove": "Entfernen",
|
||||||
"custom_favicon": "Benutzerdefiniertes Favicon",
|
"custom_favicon": "Benutzerdefiniertes Favicon",
|
||||||
"custom_code": "Benutzerdefinierter Code"
|
"custom_code": "Benutzerdefinierter Code",
|
||||||
|
"custom_domain_not_classroomio": "Domain darf „classroomio“ nicht enthalten"
|
||||||
},
|
},
|
||||||
"customize-lms": {
|
"customize-lms": {
|
||||||
"title": "LMS anpassen",
|
"title": "LMS anpassen",
|
||||||
|
|||||||
@@ -364,6 +364,7 @@
|
|||||||
"save": "Save",
|
"save": "Save",
|
||||||
"your_domain": "Your Domain",
|
"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_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",
|
"custom_domain_success": "Custom domain added successfully",
|
||||||
"dns_description": "Set the following record on your DNS provider to continue:",
|
"dns_description": "Set the following record on your DNS provider to continue:",
|
||||||
"dns_type": "Type",
|
"dns_type": "Type",
|
||||||
|
|||||||
@@ -949,7 +949,8 @@
|
|||||||
"refresh": "Refrescar",
|
"refresh": "Refrescar",
|
||||||
"remove": "Eliminar",
|
"remove": "Eliminar",
|
||||||
"custom_favicon": "favicon personalizado",
|
"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": {
|
"customize-lms": {
|
||||||
"title": "Personalizar LMS",
|
"title": "Personalizar LMS",
|
||||||
|
|||||||
@@ -949,7 +949,8 @@
|
|||||||
"refresh": "Rafraîchir",
|
"refresh": "Rafraîchir",
|
||||||
"remove": "Retirer",
|
"remove": "Retirer",
|
||||||
"custom_favicon": "Icône de favori personnalisée",
|
"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": {
|
"customize-lms": {
|
||||||
"title": "Personnaliser le LMS",
|
"title": "Personnaliser le LMS",
|
||||||
|
|||||||
@@ -949,7 +949,8 @@
|
|||||||
"refresh": "ताज़ा करना",
|
"refresh": "ताज़ा करना",
|
||||||
"remove": "निकालना",
|
"remove": "निकालना",
|
||||||
"custom_favicon": "कस्टम फ़ेविकॉन",
|
"custom_favicon": "कस्टम फ़ेविकॉन",
|
||||||
"custom_code": "कस्टम कोड"
|
"custom_code": "कस्टम कोड",
|
||||||
|
"custom_domain_not_classroomio": "डोमेन में 'classroomio' नहीं हो सकता"
|
||||||
},
|
},
|
||||||
"customize-lms": {
|
"customize-lms": {
|
||||||
"title": "एलएमएस को अनुकूलित करें",
|
"title": "एलएमएस को अनुकूलित करें",
|
||||||
|
|||||||
@@ -949,7 +949,8 @@
|
|||||||
"refresh": "Atualizar",
|
"refresh": "Atualizar",
|
||||||
"remove": "Remover",
|
"remove": "Remover",
|
||||||
"custom_favicon": "Favicon personalizado",
|
"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": {
|
"customize-lms": {
|
||||||
"title": "Personalizar LMS",
|
"title": "Personalizar LMS",
|
||||||
|
|||||||
@@ -949,7 +949,8 @@
|
|||||||
"refresh": "Обновить",
|
"refresh": "Обновить",
|
||||||
"remove": "Удалять",
|
"remove": "Удалять",
|
||||||
"custom_favicon": "Пользовательский значок",
|
"custom_favicon": "Пользовательский значок",
|
||||||
"custom_code": "Пользовательский код"
|
"custom_code": "Пользовательский код",
|
||||||
|
"custom_domain_not_classroomio": "Домен не может содержать classio."
|
||||||
},
|
},
|
||||||
"customize-lms": {
|
"customize-lms": {
|
||||||
"title": "Настроить систему управления обучением",
|
"title": "Настроить систему управления обучением",
|
||||||
|
|||||||
@@ -949,7 +949,8 @@
|
|||||||
"refresh": "Làm cho khỏe lại",
|
"refresh": "Làm cho khỏe lại",
|
||||||
"remove": "Di dời",
|
"remove": "Di dời",
|
||||||
"custom_favicon": "Biểu tượng yêu thích tùy chỉnh",
|
"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": {
|
"customize-lms": {
|
||||||
"title": "Tùy chỉnh LMS",
|
"title": "Tùy chỉnh LMS",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
getConfigResponse,
|
getConfigResponse,
|
||||||
addDomainToVercel,
|
addDomainToVercel,
|
||||||
removeDomainFromVercelProject
|
removeDomainFromVercelProject
|
||||||
} from '$lib/utils/functions/domains';
|
} from '$lib/utils/services/org/domain';
|
||||||
|
|
||||||
const supabase = getSupabase();
|
const supabase = getSupabase();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user