mirror of
https://github.com/logto-io/logto.git
synced 2024-12-30 20:33:54 -05:00
fix(core): delete cloudflare custom domain (#3953)
* fix(core): delete cloudflare custom domain * fix(core,phrases): handle cloudflare api errors
This commit is contained in:
parent
6f6941b9ba
commit
0e8817f279
20 changed files with 133 additions and 17 deletions
|
@ -18,11 +18,12 @@ import SystemContext from '#src/tenants/SystemContext.js';
|
||||||
const { jest } = import.meta;
|
const { jest } = import.meta;
|
||||||
const { mockEsm } = createMockUtils(jest);
|
const { mockEsm } = createMockUtils(jest);
|
||||||
|
|
||||||
const { getCustomHostname, createCustomHostname } = mockEsm(
|
const { getCustomHostname, createCustomHostname, deleteCustomHostname } = mockEsm(
|
||||||
'#src/utils/cloudflare/index.js',
|
'#src/utils/cloudflare/index.js',
|
||||||
() => ({
|
() => ({
|
||||||
createCustomHostname: jest.fn(async () => mockCloudflareData),
|
createCustomHostname: jest.fn(async () => mockCloudflareData),
|
||||||
getCustomHostname: jest.fn(async () => mockCloudflareData),
|
getCustomHostname: jest.fn(async () => mockCloudflareData),
|
||||||
|
deleteCustomHostname: jest.fn(),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -31,8 +32,10 @@ const { createDomainLibrary } = await import('./domain.js');
|
||||||
|
|
||||||
const updateDomainById = jest.fn(async (_, data) => data);
|
const updateDomainById = jest.fn(async (_, data) => data);
|
||||||
const insertDomain = jest.fn(async (data) => data);
|
const insertDomain = jest.fn(async (data) => data);
|
||||||
const { syncDomainStatus, addDomain } = createDomainLibrary(
|
const findDomainById = jest.fn(async () => mockDomain);
|
||||||
new MockQueries({ domains: { updateDomainById, insertDomain } })
|
const deleteDomainById = jest.fn();
|
||||||
|
const { syncDomainStatus, addDomain, deleteDomain } = createDomainLibrary(
|
||||||
|
new MockQueries({ domains: { updateDomainById, insertDomain, findDomainById, deleteDomainById } })
|
||||||
);
|
);
|
||||||
|
|
||||||
const fallbackOrigin = 'fake_origin';
|
const fallbackOrigin = 'fake_origin';
|
||||||
|
@ -50,7 +53,7 @@ afterAll(() => {
|
||||||
SystemContext.shared.hostnameProviderConfig = undefined;
|
SystemContext.shared.hostnameProviderConfig = undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('addDomainToCloudflare()', () => {
|
describe('addDomain()', () => {
|
||||||
it('should call createCustomHostname and return cloudflare data', async () => {
|
it('should call createCustomHostname and return cloudflare data', async () => {
|
||||||
const response = await addDomain(mockDomain.domain);
|
const response = await addDomain(mockDomain.domain);
|
||||||
expect(createCustomHostname).toBeCalledTimes(1);
|
expect(createCustomHostname).toBeCalledTimes(1);
|
||||||
|
@ -133,3 +136,24 @@ describe('syncDomainStatus()', () => {
|
||||||
expect(response.errorMessage).toContain('fake_error');
|
expect(response.errorMessage).toContain('fake_error');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('deleteDomain()', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
deleteDomainById.mockClear();
|
||||||
|
deleteCustomHostname.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete from remote and then delete local record', async () => {
|
||||||
|
findDomainById.mockResolvedValueOnce(mockDomainWithCloudflareData);
|
||||||
|
await deleteDomain(mockDomain.id);
|
||||||
|
expect(deleteCustomHostname).toBeCalledTimes(1);
|
||||||
|
expect(deleteDomainById).toBeCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete local record for non-synced domain', async () => {
|
||||||
|
findDomainById.mockResolvedValueOnce(mockDomain);
|
||||||
|
await deleteDomain(mockDomain.id);
|
||||||
|
expect(deleteCustomHostname).not.toBeCalled();
|
||||||
|
expect(deleteDomainById).toBeCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -9,7 +9,11 @@ import { generateStandardId } from '@logto/shared';
|
||||||
import type Queries from '#src/tenants/Queries.js';
|
import type Queries from '#src/tenants/Queries.js';
|
||||||
import SystemContext from '#src/tenants/SystemContext.js';
|
import SystemContext from '#src/tenants/SystemContext.js';
|
||||||
import assertThat from '#src/utils/assert-that.js';
|
import assertThat from '#src/utils/assert-that.js';
|
||||||
import { getCustomHostname, createCustomHostname } from '#src/utils/cloudflare/index.js';
|
import {
|
||||||
|
getCustomHostname,
|
||||||
|
createCustomHostname,
|
||||||
|
deleteCustomHostname,
|
||||||
|
} from '#src/utils/cloudflare/index.js';
|
||||||
import { findSslTxtRecord, findVerificationTxtRecord } from '#src/utils/cloudflare/utils.js';
|
import { findSslTxtRecord, findVerificationTxtRecord } from '#src/utils/cloudflare/utils.js';
|
||||||
|
|
||||||
export type DomainLibrary = ReturnType<typeof createDomainLibrary>;
|
export type DomainLibrary = ReturnType<typeof createDomainLibrary>;
|
||||||
|
@ -27,7 +31,7 @@ const getDomainStatusFromCloudflareData = (data: CloudflareData): DomainStatus =
|
||||||
|
|
||||||
export const createDomainLibrary = (queries: Queries) => {
|
export const createDomainLibrary = (queries: Queries) => {
|
||||||
const {
|
const {
|
||||||
domains: { updateDomainById, insertDomain },
|
domains: { updateDomainById, insertDomain, findDomainById, deleteDomainById },
|
||||||
} = queries;
|
} = queries;
|
||||||
|
|
||||||
const syncDomainStatusFromCloudflareData = async (
|
const syncDomainStatusFromCloudflareData = async (
|
||||||
|
@ -103,8 +107,22 @@ export const createDomainLibrary = (queries: Queries) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deleteDomain = async (id: string) => {
|
||||||
|
const { hostnameProviderConfig } = SystemContext.shared;
|
||||||
|
assertThat(hostnameProviderConfig, 'domain.not_configured');
|
||||||
|
|
||||||
|
const domain = await findDomainById(id);
|
||||||
|
|
||||||
|
if (domain.cloudflareData?.id) {
|
||||||
|
await deleteCustomHostname(hostnameProviderConfig, domain.cloudflareData.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
await deleteDomainById(id);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
syncDomainStatus,
|
syncDomainStatus,
|
||||||
addDomain,
|
addDomain,
|
||||||
|
deleteDomain,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { type Domain, type CreateDomain } from '@logto/schemas';
|
import { type Domain } from '@logto/schemas';
|
||||||
import { pickDefault } from '@logto/shared/esm';
|
import { pickDefault } from '@logto/shared/esm';
|
||||||
|
|
||||||
import { mockDomain, mockDomainResponse } from '#src/__mocks__/domain.js';
|
import { mockDomain, mockDomainResponse } from '#src/__mocks__/domain.js';
|
||||||
|
@ -9,10 +9,6 @@ const { jest } = import.meta;
|
||||||
|
|
||||||
const domains = {
|
const domains = {
|
||||||
findAllDomains: jest.fn(async (): Promise<Domain[]> => [mockDomain]),
|
findAllDomains: jest.fn(async (): Promise<Domain[]> => [mockDomain]),
|
||||||
insertDomain: async (data: CreateDomain): Promise<Domain> => ({
|
|
||||||
...mockDomain,
|
|
||||||
...data,
|
|
||||||
}),
|
|
||||||
findDomainById: async (id: string): Promise<Domain> => {
|
findDomainById: async (id: string): Promise<Domain> => {
|
||||||
const domain = [mockDomain].find((domain) => domain.id === id);
|
const domain = [mockDomain].find((domain) => domain.id === id);
|
||||||
if (!domain) {
|
if (!domain) {
|
||||||
|
@ -20,7 +16,6 @@ const domains = {
|
||||||
}
|
}
|
||||||
return domain;
|
return domain;
|
||||||
},
|
},
|
||||||
deleteDomainById: jest.fn(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const syncDomainStatus = jest.fn(async (domain: Domain): Promise<Domain> => domain);
|
const syncDomainStatus = jest.fn(async (domain: Domain): Promise<Domain> => domain);
|
||||||
|
@ -30,11 +25,13 @@ const addDomain = jest.fn(
|
||||||
domain,
|
domain,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
const deleteDomain = jest.fn();
|
||||||
|
|
||||||
const mockLibraries = {
|
const mockLibraries = {
|
||||||
domains: {
|
domains: {
|
||||||
syncDomainStatus,
|
syncDomainStatus,
|
||||||
addDomain,
|
addDomain,
|
||||||
|
deleteDomain,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -64,6 +61,7 @@ describe('domain routes', () => {
|
||||||
it('POST /domains', async () => {
|
it('POST /domains', async () => {
|
||||||
domains.findAllDomains.mockResolvedValueOnce([]);
|
domains.findAllDomains.mockResolvedValueOnce([]);
|
||||||
const response = await domainRequest.post('/domains').send({ domain: 'test.com' });
|
const response = await domainRequest.post('/domains').send({ domain: 'test.com' });
|
||||||
|
expect(addDomain).toBeCalledWith('test.com');
|
||||||
expect(response.status).toEqual(201);
|
expect(response.status).toEqual(201);
|
||||||
expect(response.body.id).toBeTruthy();
|
expect(response.body.id).toBeTruthy();
|
||||||
expect(response.body.domain).toEqual('test.com');
|
expect(response.body.domain).toEqual('test.com');
|
||||||
|
@ -80,5 +78,6 @@ describe('domain routes', () => {
|
||||||
'status',
|
'status',
|
||||||
204
|
204
|
||||||
);
|
);
|
||||||
|
expect(deleteDomain).toBeCalledWith(mockDomain.id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -12,10 +12,10 @@ export default function domainRoutes<T extends AuthedRouter>(
|
||||||
...[router, { queries, libraries }]: RouterInitArgs<T>
|
...[router, { queries, libraries }]: RouterInitArgs<T>
|
||||||
) {
|
) {
|
||||||
const {
|
const {
|
||||||
domains: { findAllDomains, findDomainById, deleteDomainById },
|
domains: { findAllDomains, findDomainById },
|
||||||
} = queries;
|
} = queries;
|
||||||
const {
|
const {
|
||||||
domains: { syncDomainStatus, addDomain },
|
domains: { syncDomainStatus, addDomain, deleteDomain },
|
||||||
} = libraries;
|
} = libraries;
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
|
@ -83,7 +83,7 @@ export default function domainRoutes<T extends AuthedRouter>(
|
||||||
koaGuard({ params: z.object({ id: z.string() }), status: [204, 404] }),
|
koaGuard({ params: z.object({ id: z.string() }), status: [204, 404] }),
|
||||||
async (ctx, next) => {
|
async (ctx, next) => {
|
||||||
const { id } = ctx.guard.params;
|
const { id } = ctx.guard.params;
|
||||||
await deleteDomainById(id);
|
await deleteDomain(id);
|
||||||
ctx.status = 204;
|
ctx.status = 204;
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
|
|
|
@ -3,6 +3,8 @@ import path from 'node:path';
|
||||||
import { type HostnameProviderData, cloudflareDataGuard } from '@logto/schemas';
|
import { type HostnameProviderData, cloudflareDataGuard } from '@logto/schemas';
|
||||||
import { got } from 'got';
|
import { got } from 'got';
|
||||||
|
|
||||||
|
import RequestError from '#src/errors/RequestError/index.js';
|
||||||
|
|
||||||
import assertThat from '../assert-that.js';
|
import assertThat from '../assert-that.js';
|
||||||
|
|
||||||
import { baseUrl } from './consts.js';
|
import { baseUrl } from './consts.js';
|
||||||
|
@ -29,10 +31,23 @@ export const createCustomHostname = async (auth: HostnameProviderData, hostname:
|
||||||
hostname,
|
hostname,
|
||||||
ssl: { method: 'txt', type: 'dv', settings: { min_tls_version: '1.0' } },
|
ssl: { method: 'txt', type: 'dv', settings: { min_tls_version: '1.0' } },
|
||||||
},
|
},
|
||||||
|
throwHttpErrors: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
assertThat(response.ok, 'domain.cloudflare_unknown_error');
|
if (!response.ok) {
|
||||||
|
if (response.statusCode === 409) {
|
||||||
|
throw new RequestError('domain.hostname_already_exists');
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new RequestError(
|
||||||
|
{
|
||||||
|
code: 'domain.cloudflare_unknown_error',
|
||||||
|
status: 500,
|
||||||
|
},
|
||||||
|
response.body
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const result = cloudflareDataGuard.safeParse(parseCloudflareResponse(response.body));
|
const result = cloudflareDataGuard.safeParse(parseCloudflareResponse(response.body));
|
||||||
|
|
||||||
|
@ -60,10 +75,20 @@ export const getCustomHostname = async (auth: HostnameProviderData, identifier:
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${auth.apiToken}`,
|
Authorization: `Bearer ${auth.apiToken}`,
|
||||||
},
|
},
|
||||||
|
throwHttpErrors: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
assertThat(response.ok, 'domain.cloudflare_unknown_error');
|
assertThat(
|
||||||
|
response.ok,
|
||||||
|
new RequestError(
|
||||||
|
{
|
||||||
|
code: 'domain.cloudflare_unknown_error',
|
||||||
|
status: 500,
|
||||||
|
},
|
||||||
|
response.body
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
const result = cloudflareDataGuard.safeParse(parseCloudflareResponse(response.body));
|
const result = cloudflareDataGuard.safeParse(parseCloudflareResponse(response.body));
|
||||||
|
|
||||||
|
@ -71,3 +96,38 @@ export const getCustomHostname = async (auth: HostnameProviderData, identifier:
|
||||||
|
|
||||||
return result.data;
|
return result.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const deleteCustomHostname = async (auth: HostnameProviderData, identifier: string) => {
|
||||||
|
const {
|
||||||
|
EnvSet: {
|
||||||
|
values: { isIntegrationTest },
|
||||||
|
},
|
||||||
|
} = await import('#src/env-set/index.js');
|
||||||
|
if (isIntegrationTest) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await got.delete(
|
||||||
|
new URL(
|
||||||
|
path.join(baseUrl.pathname, `/zones/${auth.zoneId}/custom_hostnames/${identifier}`),
|
||||||
|
baseUrl
|
||||||
|
),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${auth.apiToken}`,
|
||||||
|
},
|
||||||
|
throwHttpErrors: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(
|
||||||
|
response.ok,
|
||||||
|
new RequestError(
|
||||||
|
{
|
||||||
|
code: 'domain.cloudflare_unknown_error',
|
||||||
|
status: 500,
|
||||||
|
},
|
||||||
|
response.body
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -5,6 +5,7 @@ const domain = {
|
||||||
'Beim Anfordern der Cloudflare-API ist ein unbekannter Fehler aufgetreten',
|
'Beim Anfordern der Cloudflare-API ist ein unbekannter Fehler aufgetreten',
|
||||||
cloudflare_response_error: 'Vom Cloudflare wurde eine unerwartete Antwort erhalten.',
|
cloudflare_response_error: 'Vom Cloudflare wurde eine unerwartete Antwort erhalten.',
|
||||||
limit_to_one_domain: 'Sie können nur eine benutzerdefinierte Domain haben.',
|
limit_to_one_domain: 'Sie können nur eine benutzerdefinierte Domain haben.',
|
||||||
|
hostname_already_exists: 'Diese Domain existiert bereits auf unserem Server.',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default domain;
|
export default domain;
|
||||||
|
|
|
@ -4,6 +4,7 @@ const domain = {
|
||||||
cloudflare_unknown_error: 'Got unknown error when requesting Cloudflare API',
|
cloudflare_unknown_error: 'Got unknown error when requesting Cloudflare API',
|
||||||
cloudflare_response_error: 'Got unexpected response from Cloudflare.',
|
cloudflare_response_error: 'Got unexpected response from Cloudflare.',
|
||||||
limit_to_one_domain: 'You can only have one custom domain.',
|
limit_to_one_domain: 'You can only have one custom domain.',
|
||||||
|
hostname_already_exists: 'This domain already exists in our server.',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default domain;
|
export default domain;
|
||||||
|
|
|
@ -4,6 +4,7 @@ const domain = {
|
||||||
cloudflare_unknown_error: 'Se produjo un error desconocido al solicitar la API de Cloudflare',
|
cloudflare_unknown_error: 'Se produjo un error desconocido al solicitar la API de Cloudflare',
|
||||||
cloudflare_response_error: 'Recibió una respuesta inesperada de Cloudflare.',
|
cloudflare_response_error: 'Recibió una respuesta inesperada de Cloudflare.',
|
||||||
limit_to_one_domain: 'Solo puedes tener un dominio personalizado.',
|
limit_to_one_domain: 'Solo puedes tener un dominio personalizado.',
|
||||||
|
hostname_already_exists: 'Este dominio ya existe en nuestro servidor.',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default domain;
|
export default domain;
|
||||||
|
|
|
@ -4,6 +4,7 @@ const domain = {
|
||||||
cloudflare_unknown_error: "Erreur inconnue lors de la requête de l'API Cloudflare",
|
cloudflare_unknown_error: "Erreur inconnue lors de la requête de l'API Cloudflare",
|
||||||
cloudflare_response_error: 'Réponse inattendue de Cloudflare',
|
cloudflare_response_error: 'Réponse inattendue de Cloudflare',
|
||||||
limit_to_one_domain: "Vous ne pouvez avoir qu'un seul domaine personnalisé",
|
limit_to_one_domain: "Vous ne pouvez avoir qu'un seul domaine personnalisé",
|
||||||
|
hostname_already_exists: 'Ce domaine existe déjà sur notre serveur.',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default domain;
|
export default domain;
|
||||||
|
|
|
@ -4,6 +4,7 @@ const domain = {
|
||||||
cloudflare_unknown_error: 'Errore sconosciuto durante la richiesta di API Cloudflare',
|
cloudflare_unknown_error: 'Errore sconosciuto durante la richiesta di API Cloudflare',
|
||||||
cloudflare_response_error: 'Ricevuta una risposta non prevista da Cloudflare.',
|
cloudflare_response_error: 'Ricevuta una risposta non prevista da Cloudflare.',
|
||||||
limit_to_one_domain: 'Puoi avere solo un dominio personalizzato.',
|
limit_to_one_domain: 'Puoi avere solo un dominio personalizzato.',
|
||||||
|
hostname_already_exists: 'Questo dominio esiste già nel nostro server.',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default domain;
|
export default domain;
|
||||||
|
|
|
@ -4,6 +4,7 @@ const domain = {
|
||||||
cloudflare_unknown_error: 'Cloudflare API のリクエスト中に未知のエラーが発生しました。',
|
cloudflare_unknown_error: 'Cloudflare API のリクエスト中に未知のエラーが発生しました。',
|
||||||
cloudflare_response_error: 'Cloudflare から予期しない応答がありました。',
|
cloudflare_response_error: 'Cloudflare から予期しない応答がありました。',
|
||||||
limit_to_one_domain: 'カスタムドメインは1つしか持てません。',
|
limit_to_one_domain: 'カスタムドメインは1つしか持てません。',
|
||||||
|
hostname_already_exists: 'サーバーには既にこのドメインが存在しています。',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default domain;
|
export default domain;
|
||||||
|
|
|
@ -4,6 +4,7 @@ const domain = {
|
||||||
cloudflare_unknown_error: 'Cloudflare API 요청시 알 수 없는 오류 발생',
|
cloudflare_unknown_error: 'Cloudflare API 요청시 알 수 없는 오류 발생',
|
||||||
cloudflare_response_error: 'Cloudflare 로부터 예상치 못한 응답을 받았습니다.',
|
cloudflare_response_error: 'Cloudflare 로부터 예상치 못한 응답을 받았습니다.',
|
||||||
limit_to_one_domain: '하나의 맞춤 도메인만 사용할 수 있습니다.',
|
limit_to_one_domain: '하나의 맞춤 도메인만 사용할 수 있습니다.',
|
||||||
|
hostname_already_exists: '이 도메인은 이미 서버에 존재합니다.',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default domain;
|
export default domain;
|
||||||
|
|
|
@ -4,6 +4,7 @@ const domain = {
|
||||||
cloudflare_unknown_error: 'Otrzymano nieznany błąd podczas żądania API Cloudflare',
|
cloudflare_unknown_error: 'Otrzymano nieznany błąd podczas żądania API Cloudflare',
|
||||||
cloudflare_response_error: 'Otrzymano nieoczekiwaną odpowiedź od Cloudflare.',
|
cloudflare_response_error: 'Otrzymano nieoczekiwaną odpowiedź od Cloudflare.',
|
||||||
limit_to_one_domain: 'Możesz mieć tylko jedną niestandardową domenę.',
|
limit_to_one_domain: 'Możesz mieć tylko jedną niestandardową domenę.',
|
||||||
|
hostname_already_exists: 'Ta domena już istnieje na naszym serwerze.',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default domain;
|
export default domain;
|
||||||
|
|
|
@ -4,6 +4,7 @@ const domain = {
|
||||||
cloudflare_unknown_error: 'Recebido erro desconhecido ao solicitar API do Cloudflare',
|
cloudflare_unknown_error: 'Recebido erro desconhecido ao solicitar API do Cloudflare',
|
||||||
cloudflare_response_error: 'Recebido resposta inesperada do Cloudflare.',
|
cloudflare_response_error: 'Recebido resposta inesperada do Cloudflare.',
|
||||||
limit_to_one_domain: 'Você só pode ter um domínio personalizado.',
|
limit_to_one_domain: 'Você só pode ter um domínio personalizado.',
|
||||||
|
hostname_already_exists: 'Este domínio já existe em nosso servidor.',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default domain;
|
export default domain;
|
||||||
|
|
|
@ -4,6 +4,7 @@ const domain = {
|
||||||
cloudflare_unknown_error: 'Obteve um erro desconhecido ao solicitar a API Cloudflare',
|
cloudflare_unknown_error: 'Obteve um erro desconhecido ao solicitar a API Cloudflare',
|
||||||
cloudflare_response_error: 'Obteve uma resposta inesperada da Cloudflare.',
|
cloudflare_response_error: 'Obteve uma resposta inesperada da Cloudflare.',
|
||||||
limit_to_one_domain: 'Você só pode ter um domínio personalizado.',
|
limit_to_one_domain: 'Você só pode ter um domínio personalizado.',
|
||||||
|
hostname_already_exists: 'Este domínio já existe em nosso servidor.',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default domain;
|
export default domain;
|
||||||
|
|
|
@ -4,6 +4,7 @@ const domain = {
|
||||||
cloudflare_unknown_error: 'Получена неизвестная ошибка при запросе к API Cloudflare',
|
cloudflare_unknown_error: 'Получена неизвестная ошибка при запросе к API Cloudflare',
|
||||||
cloudflare_response_error: 'Получен неожиданный ответ от Cloudflare.',
|
cloudflare_response_error: 'Получен неожиданный ответ от Cloudflare.',
|
||||||
limit_to_one_domain: 'Вы можете использовать только один пользовательский домен.',
|
limit_to_one_domain: 'Вы можете использовать только один пользовательский домен.',
|
||||||
|
hostname_already_exists: 'Этот домен уже существует на нашем сервере.',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default domain;
|
export default domain;
|
||||||
|
|
|
@ -4,6 +4,7 @@ const domain = {
|
||||||
cloudflare_unknown_error: 'Cloudflare API isteği yapılırken bilinmeyen bir hata oluştu.',
|
cloudflare_unknown_error: 'Cloudflare API isteği yapılırken bilinmeyen bir hata oluştu.',
|
||||||
cloudflare_response_error: 'Cloudflare’dan beklenmeyen bir yanıt alındı.',
|
cloudflare_response_error: 'Cloudflare’dan beklenmeyen bir yanıt alındı.',
|
||||||
limit_to_one_domain: 'Sadece bir özel alan adınız olabilir.',
|
limit_to_one_domain: 'Sadece bir özel alan adınız olabilir.',
|
||||||
|
hostname_already_exists: 'Bu alan adı sunucumuzda zaten mevcut.',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default domain;
|
export default domain;
|
||||||
|
|
|
@ -4,6 +4,7 @@ const domain = {
|
||||||
cloudflare_unknown_error: '请求 Cloudflare API 时出现未知错误。',
|
cloudflare_unknown_error: '请求 Cloudflare API 时出现未知错误。',
|
||||||
cloudflare_response_error: '从 Cloudflare 得到意外的响应。',
|
cloudflare_response_error: '从 Cloudflare 得到意外的响应。',
|
||||||
limit_to_one_domain: '仅限一个自定义域名。',
|
limit_to_one_domain: '仅限一个自定义域名。',
|
||||||
|
hostname_already_exists: '该域名在我们的服务器中已存在。',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default domain;
|
export default domain;
|
||||||
|
|
|
@ -4,6 +4,7 @@ const domain = {
|
||||||
cloudflare_unknown_error: '獲取 Cloudflare API 時發生未知錯誤',
|
cloudflare_unknown_error: '獲取 Cloudflare API 時發生未知錯誤',
|
||||||
cloudflare_response_error: '從 Cloudflare 獲取到意外的響應',
|
cloudflare_response_error: '從 Cloudflare 獲取到意外的響應',
|
||||||
limit_to_one_domain: '您只能擁有一個自定義域名。',
|
limit_to_one_domain: '您只能擁有一個自定義域名。',
|
||||||
|
hostname_already_exists: '此域名已存在於我們的伺服器中。',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default domain;
|
export default domain;
|
||||||
|
|
|
@ -4,6 +4,7 @@ const domain = {
|
||||||
cloudflare_unknown_error: '在請求 Cloudflare API 時出現未知錯誤',
|
cloudflare_unknown_error: '在請求 Cloudflare API 時出現未知錯誤',
|
||||||
cloudflare_response_error: '從 Cloudflare 收到意外回應',
|
cloudflare_response_error: '從 Cloudflare 收到意外回應',
|
||||||
limit_to_one_domain: '您只能擁有一個自訂網域。',
|
limit_to_one_domain: '您只能擁有一個自訂網域。',
|
||||||
|
hostname_already_exists: '此網域名稱已經存在我們的伺服器中。',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default domain;
|
export default domain;
|
||||||
|
|
Loading…
Reference in a new issue