0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

feat(core,schemas): get fallback origin from cf api (#4005)

This commit is contained in:
wangsijie 2023-06-09 11:35:28 +09:00 committed by GitHub
parent 9b0e13e473
commit 81f053dfd8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 83 additions and 56 deletions

View file

@ -118,7 +118,7 @@ jobs:
- name: Setup mock Cloudflare Hostname Provider config
working-directory: tests
run: npm run cli db system set cloudflareHostnameProvider '{"zoneId":"mock-zone-id","apiToken":"","fallbackOrigin":"mock.logto.dev"}'
run: npm run cli db system set cloudflareHostnameProvider '{"zoneId":"mock-zone-id","apiToken":""}'
env:
DB_URL: postgres://postgres:postgres@localhost:5432/postgres

View file

@ -7,5 +7,4 @@ export const mockStorageProviderData = {
export const mockHostnameProviderData = {
apiToken: 'api-token',
zoneId: 'zone-id',
fallbackOrigin: 'logto.com',
};

View file

@ -10,6 +10,7 @@ import {
} from '#src/__mocks__/domain.js';
import RequestError from '#src/errors/RequestError/index.js';
import SystemContext from '#src/tenants/SystemContext.js';
import { mockFallbackOrigin } from '#src/utils/cloudflare/mock.js';
const { jest } = import.meta;
const { mockEsm } = createMockUtils(jest);
@ -20,6 +21,7 @@ const { getCustomHostname, createCustomHostname, deleteCustomHostname } = mockEs
createCustomHostname: jest.fn(async () => mockCloudflareData),
getCustomHostname: jest.fn(async () => mockCloudflareData),
deleteCustomHostname: jest.fn(),
getFallbackOrigin: jest.fn(async () => mockFallbackOrigin),
})
);
@ -34,13 +36,11 @@ const { syncDomainStatus, addDomain, deleteDomain } = createDomainLibrary(
new MockQueries({ domains: { updateDomainById, insertDomain, findDomainById, deleteDomainById } })
);
const fallbackOrigin = 'fake_origin';
beforeAll(() => {
// eslint-disable-next-line @silverhand/fp/no-mutation
SystemContext.shared.hostnameProviderConfig = {
zoneId: 'fake_zone_id',
apiToken: '',
fallbackOrigin,
};
});
@ -58,7 +58,7 @@ describe('addDomain()', () => {
expect(response.dnsRecords).toContainEqual({
type: 'CNAME',
name: mockDomainWithCloudflareData.domain,
value: fallbackOrigin,
value: mockFallbackOrigin,
});
});
});

View file

@ -8,6 +8,7 @@ import {
getCustomHostname,
createCustomHostname,
deleteCustomHostname,
getFallbackOrigin,
} from '#src/utils/cloudflare/index.js';
export type DomainLibrary = ReturnType<typeof createDomainLibrary>;
@ -66,7 +67,10 @@ export const createDomainLibrary = (queries: Queries) => {
const { hostnameProviderConfig } = SystemContext.shared;
assertThat(hostnameProviderConfig, 'domain.not_configured');
const cloudflareData = await createCustomHostname(hostnameProviderConfig, hostname);
const [fallbackOrigin, cloudflareData] = await Promise.all([
getFallbackOrigin(hostnameProviderConfig),
createCustomHostname(hostnameProviderConfig, hostname),
]);
return insertDomain({
domain: hostname,
@ -74,11 +78,10 @@ export const createDomainLibrary = (queries: Queries) => {
cloudflareData,
status: DomainStatus.PendingVerification,
dnsRecords: [
// Verification CNAME, fixed value, generated by us
{
type: 'CNAME',
name: hostname,
value: hostnameProviderConfig.fallbackOrigin,
value: fallbackOrigin,
},
],
});

View file

@ -1,16 +1,76 @@
import path from 'node:path';
import { type HostnameProviderData, cloudflareDataGuard } from '@logto/schemas';
import { got } from 'got';
import { type Response, got } from 'got';
import { type ZodType } from 'zod';
import RequestError from '#src/errors/RequestError/index.js';
import assertThat from '../assert-that.js';
import { baseUrl } from './consts.js';
import { mockCustomHostnameResponse } from './mock.js';
import { mockCustomHostnameResponse, mockFallbackOrigin } from './mock.js';
import { cloudflareHostnameResponseGuard } from './types.js';
import { parseCloudflareResponse } from './utils.js';
type HandleResponse = {
<T>(response: Response<string>, guard: ZodType<T>): T;
(response: Response<string>): void;
};
const handleResponse: HandleResponse = <T>(response: Response<string>, guard?: ZodType<T>) => {
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
);
}
if (!guard) {
return;
}
const result = guard.safeParse(parseCloudflareResponse(response.body));
assertThat(result.success, 'domain.cloudflare_response_error');
return result.data;
};
export const getFallbackOrigin = async (auth: HostnameProviderData): Promise<string> => {
const {
EnvSet: {
values: { isIntegrationTest },
},
} = await import('#src/env-set/index.js');
if (isIntegrationTest) {
return mockFallbackOrigin;
}
const response = await got.get(
new URL(
path.join(baseUrl.pathname, `/zones/${auth.zoneId}/custom_hostnames/fallback_origin`),
baseUrl
),
{
headers: {
Authorization: `Bearer ${auth.apiToken}`,
},
throwHttpErrors: false,
}
);
const result = handleResponse(response, cloudflareHostnameResponseGuard);
return result.origin;
};
export const createCustomHostname = async (auth: HostnameProviderData, hostname: string) => {
const {
EnvSet: {
@ -35,25 +95,7 @@ export const createCustomHostname = async (auth: HostnameProviderData, hostname:
}
);
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));
assertThat(result.success, 'domain.cloudflare_response_error');
return result.data;
return handleResponse(response, cloudflareDataGuard);
};
export const getCustomHostname = async (auth: HostnameProviderData, identifier: string) => {
@ -79,22 +121,7 @@ export const getCustomHostname = async (auth: HostnameProviderData, identifier:
}
);
assertThat(
response.ok,
new RequestError(
{
code: 'domain.cloudflare_unknown_error',
status: 500,
},
response.body
)
);
const result = cloudflareDataGuard.safeParse(parseCloudflareResponse(response.body));
assertThat(result.success, 'domain.cloudflare_response_error');
return result.data;
return handleResponse(response, cloudflareDataGuard);
};
export const deleteCustomHostname = async (auth: HostnameProviderData, identifier: string) => {
@ -120,14 +147,5 @@ export const deleteCustomHostname = async (auth: HostnameProviderData, identifie
}
);
assertThat(
response.ok,
new RequestError(
{
code: 'domain.cloudflare_unknown_error',
status: 500,
},
response.body
)
);
handleResponse(response);
};

View file

@ -3,3 +3,5 @@ import { mockCloudflareData } from '#src/__mocks__/domain.js';
export const mockCustomHostnameResponse = async (identifier?: string) => {
return mockCloudflareData;
};
export const mockFallbackOrigin = 'mock.logto.dev';

View file

@ -4,3 +4,9 @@ export const cloudflareResponseGuard = z.object({
success: z.boolean(),
result: z.unknown(),
});
export const cloudflareHostnameResponseGuard = z
.object({
origin: z.string(),
})
.catchall(z.unknown());

View file

@ -101,7 +101,6 @@ export const demoSocialGuard: Readonly<{
export const hostnameProviderDataGuard = z.object({
zoneId: z.string(),
apiToken: z.string(), // Requires zone permission for "SSL and Certificates Edit"
fallbackOrigin: z.string(), // A domain name
});
export type HostnameProviderData = z.infer<typeof hostnameProviderDataGuard>;