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:
parent
9b0e13e473
commit
81f053dfd8
8 changed files with 83 additions and 56 deletions
2
.github/workflows/integration-test.yml
vendored
2
.github/workflows/integration-test.yml
vendored
|
@ -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
|
||||
|
||||
|
|
|
@ -7,5 +7,4 @@ export const mockStorageProviderData = {
|
|||
export const mockHostnameProviderData = {
|
||||
apiToken: 'api-token',
|
||||
zoneId: 'zone-id',
|
||||
fallbackOrigin: 'logto.com',
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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>;
|
||||
|
|
Loading…
Reference in a new issue