0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-13 21:30:30 -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:
wangsijie 2023-06-02 16:50:36 +08:00 committed by GitHub
parent 6f6941b9ba
commit 0e8817f279
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 133 additions and 17 deletions

View file

@ -18,11 +18,12 @@ import SystemContext from '#src/tenants/SystemContext.js';
const { jest } = import.meta;
const { mockEsm } = createMockUtils(jest);
const { getCustomHostname, createCustomHostname } = mockEsm(
const { getCustomHostname, createCustomHostname, deleteCustomHostname } = mockEsm(
'#src/utils/cloudflare/index.js',
() => ({
createCustomHostname: 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 insertDomain = jest.fn(async (data) => data);
const { syncDomainStatus, addDomain } = createDomainLibrary(
new MockQueries({ domains: { updateDomainById, insertDomain } })
const findDomainById = jest.fn(async () => mockDomain);
const deleteDomainById = jest.fn();
const { syncDomainStatus, addDomain, deleteDomain } = createDomainLibrary(
new MockQueries({ domains: { updateDomainById, insertDomain, findDomainById, deleteDomainById } })
);
const fallbackOrigin = 'fake_origin';
@ -50,7 +53,7 @@ afterAll(() => {
SystemContext.shared.hostnameProviderConfig = undefined;
});
describe('addDomainToCloudflare()', () => {
describe('addDomain()', () => {
it('should call createCustomHostname and return cloudflare data', async () => {
const response = await addDomain(mockDomain.domain);
expect(createCustomHostname).toBeCalledTimes(1);
@ -133,3 +136,24 @@ describe('syncDomainStatus()', () => {
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);
});
});

View file

@ -9,7 +9,11 @@ import { generateStandardId } from '@logto/shared';
import type Queries from '#src/tenants/Queries.js';
import SystemContext from '#src/tenants/SystemContext.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';
export type DomainLibrary = ReturnType<typeof createDomainLibrary>;
@ -27,7 +31,7 @@ const getDomainStatusFromCloudflareData = (data: CloudflareData): DomainStatus =
export const createDomainLibrary = (queries: Queries) => {
const {
domains: { updateDomainById, insertDomain },
domains: { updateDomainById, insertDomain, findDomainById, deleteDomainById },
} = queries;
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 {
syncDomainStatus,
addDomain,
deleteDomain,
};
};

View file

@ -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 { mockDomain, mockDomainResponse } from '#src/__mocks__/domain.js';
@ -9,10 +9,6 @@ const { jest } = import.meta;
const domains = {
findAllDomains: jest.fn(async (): Promise<Domain[]> => [mockDomain]),
insertDomain: async (data: CreateDomain): Promise<Domain> => ({
...mockDomain,
...data,
}),
findDomainById: async (id: string): Promise<Domain> => {
const domain = [mockDomain].find((domain) => domain.id === id);
if (!domain) {
@ -20,7 +16,6 @@ const domains = {
}
return domain;
},
deleteDomainById: jest.fn(),
};
const syncDomainStatus = jest.fn(async (domain: Domain): Promise<Domain> => domain);
@ -30,11 +25,13 @@ const addDomain = jest.fn(
domain,
})
);
const deleteDomain = jest.fn();
const mockLibraries = {
domains: {
syncDomainStatus,
addDomain,
deleteDomain,
},
};
@ -64,6 +61,7 @@ describe('domain routes', () => {
it('POST /domains', async () => {
domains.findAllDomains.mockResolvedValueOnce([]);
const response = await domainRequest.post('/domains').send({ domain: 'test.com' });
expect(addDomain).toBeCalledWith('test.com');
expect(response.status).toEqual(201);
expect(response.body.id).toBeTruthy();
expect(response.body.domain).toEqual('test.com');
@ -80,5 +78,6 @@ describe('domain routes', () => {
'status',
204
);
expect(deleteDomain).toBeCalledWith(mockDomain.id);
});
});

View file

@ -12,10 +12,10 @@ export default function domainRoutes<T extends AuthedRouter>(
...[router, { queries, libraries }]: RouterInitArgs<T>
) {
const {
domains: { findAllDomains, findDomainById, deleteDomainById },
domains: { findAllDomains, findDomainById },
} = queries;
const {
domains: { syncDomainStatus, addDomain },
domains: { syncDomainStatus, addDomain, deleteDomain },
} = libraries;
router.get(
@ -83,7 +83,7 @@ export default function domainRoutes<T extends AuthedRouter>(
koaGuard({ params: z.object({ id: z.string() }), status: [204, 404] }),
async (ctx, next) => {
const { id } = ctx.guard.params;
await deleteDomainById(id);
await deleteDomain(id);
ctx.status = 204;
return next();

View file

@ -3,6 +3,8 @@ import path from 'node:path';
import { type HostnameProviderData, cloudflareDataGuard } from '@logto/schemas';
import { got } from 'got';
import RequestError from '#src/errors/RequestError/index.js';
import assertThat from '../assert-that.js';
import { baseUrl } from './consts.js';
@ -29,10 +31,23 @@ export const createCustomHostname = async (auth: HostnameProviderData, hostname:
hostname,
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));
@ -60,10 +75,20 @@ export const getCustomHostname = async (auth: HostnameProviderData, identifier:
headers: {
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));
@ -71,3 +96,38 @@ export const getCustomHostname = async (auth: HostnameProviderData, identifier:
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
)
);
};

View file

@ -5,6 +5,7 @@ const domain = {
'Beim Anfordern der Cloudflare-API ist ein unbekannter Fehler aufgetreten',
cloudflare_response_error: 'Vom Cloudflare wurde eine unerwartete Antwort erhalten.',
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;

View file

@ -4,6 +4,7 @@ const domain = {
cloudflare_unknown_error: 'Got unknown error when requesting Cloudflare API',
cloudflare_response_error: 'Got unexpected response from Cloudflare.',
limit_to_one_domain: 'You can only have one custom domain.',
hostname_already_exists: 'This domain already exists in our server.',
};
export default domain;

View file

@ -4,6 +4,7 @@ const domain = {
cloudflare_unknown_error: 'Se produjo un error desconocido al solicitar la API de Cloudflare',
cloudflare_response_error: 'Recibió una respuesta inesperada de Cloudflare.',
limit_to_one_domain: 'Solo puedes tener un dominio personalizado.',
hostname_already_exists: 'Este dominio ya existe en nuestro servidor.',
};
export default domain;

View file

@ -4,6 +4,7 @@ const domain = {
cloudflare_unknown_error: "Erreur inconnue lors de la requête de l'API Cloudflare",
cloudflare_response_error: 'Réponse inattendue de Cloudflare',
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;

View file

@ -4,6 +4,7 @@ const domain = {
cloudflare_unknown_error: 'Errore sconosciuto durante la richiesta di API Cloudflare',
cloudflare_response_error: 'Ricevuta una risposta non prevista da Cloudflare.',
limit_to_one_domain: 'Puoi avere solo un dominio personalizzato.',
hostname_already_exists: 'Questo dominio esiste già nel nostro server.',
};
export default domain;

View file

@ -4,6 +4,7 @@ const domain = {
cloudflare_unknown_error: 'Cloudflare API のリクエスト中に未知のエラーが発生しました。',
cloudflare_response_error: 'Cloudflare から予期しない応答がありました。',
limit_to_one_domain: 'カスタムドメインは1つしか持てません。',
hostname_already_exists: 'サーバーには既にこのドメインが存在しています。',
};
export default domain;

View file

@ -4,6 +4,7 @@ const domain = {
cloudflare_unknown_error: 'Cloudflare API 요청시 알 수 없는 오류 발생',
cloudflare_response_error: 'Cloudflare 로부터 예상치 못한 응답을 받았습니다.',
limit_to_one_domain: '하나의 맞춤 도메인만 사용할 수 있습니다.',
hostname_already_exists: '이 도메인은 이미 서버에 존재합니다.',
};
export default domain;

View file

@ -4,6 +4,7 @@ const domain = {
cloudflare_unknown_error: 'Otrzymano nieznany błąd podczas żądania API Cloudflare',
cloudflare_response_error: 'Otrzymano nieoczekiwaną odpowiedź od Cloudflare.',
limit_to_one_domain: 'Możesz mieć tylko jedną niestandardową domenę.',
hostname_already_exists: 'Ta domena już istnieje na naszym serwerze.',
};
export default domain;

View file

@ -4,6 +4,7 @@ const domain = {
cloudflare_unknown_error: 'Recebido erro desconhecido ao solicitar API do Cloudflare',
cloudflare_response_error: 'Recebido resposta inesperada do Cloudflare.',
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;

View file

@ -4,6 +4,7 @@ const domain = {
cloudflare_unknown_error: 'Obteve um erro desconhecido ao solicitar a API Cloudflare',
cloudflare_response_error: 'Obteve uma resposta inesperada da Cloudflare.',
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;

View file

@ -4,6 +4,7 @@ const domain = {
cloudflare_unknown_error: 'Получена неизвестная ошибка при запросе к API Cloudflare',
cloudflare_response_error: 'Получен неожиданный ответ от Cloudflare.',
limit_to_one_domain: 'Вы можете использовать только один пользовательский домен.',
hostname_already_exists: 'Этот домен уже существует на нашем сервере.',
};
export default domain;

View file

@ -4,6 +4,7 @@ const domain = {
cloudflare_unknown_error: 'Cloudflare API isteği yapılırken bilinmeyen bir hata oluştu.',
cloudflare_response_error: 'Cloudflaredan beklenmeyen bir yanıt alındı.',
limit_to_one_domain: 'Sadece bir özel alan adınız olabilir.',
hostname_already_exists: 'Bu alan adı sunucumuzda zaten mevcut.',
};
export default domain;

View file

@ -4,6 +4,7 @@ const domain = {
cloudflare_unknown_error: '请求 Cloudflare API 时出现未知错误。',
cloudflare_response_error: '从 Cloudflare 得到意外的响应。',
limit_to_one_domain: '仅限一个自定义域名。',
hostname_already_exists: '该域名在我们的服务器中已存在。',
};
export default domain;

View file

@ -4,6 +4,7 @@ const domain = {
cloudflare_unknown_error: '獲取 Cloudflare API 時發生未知錯誤',
cloudflare_response_error: '從 Cloudflare 獲取到意外的響應',
limit_to_one_domain: '您只能擁有一個自定義域名。',
hostname_already_exists: '此域名已存在於我們的伺服器中。',
};
export default domain;

View file

@ -4,6 +4,7 @@ const domain = {
cloudflare_unknown_error: '在請求 Cloudflare API 時出現未知錯誤',
cloudflare_response_error: '從 Cloudflare 收到意外回應',
limit_to_one_domain: '您只能擁有一個自訂網域。',
hostname_already_exists: '此網域名稱已經存在我們的伺服器中。',
};
export default domain;