0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-02-03 21:48:55 -05:00
logto/packages/core/src/libraries/domain.ts

121 lines
3.8 KiB
TypeScript

import { type CloudflareData, type Domain, DomainStatus } from '@logto/schemas';
import { generateStandardId } from '@logto/shared';
import RequestError from '#src/errors/RequestError/index.js';
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,
deleteCustomHostname,
getFallbackOrigin,
getDomainStatusFromCloudflareData,
} from '#src/utils/cloudflare/index.js';
import { isSubdomainOf } from '#src/utils/domain.js';
import { clearCustomDomainCache } from '#src/utils/tenant.js';
export type DomainLibrary = ReturnType<typeof createDomainLibrary>;
export const createDomainLibrary = (queries: Queries) => {
const {
domains: { updateDomainById, insertDomain, findDomainById, deleteDomainById },
} = queries;
const syncDomainStatusFromCloudflareData = async (
domain: Domain,
cloudflareData: CloudflareData
): Promise<Domain> => {
const status = getDomainStatusFromCloudflareData(cloudflareData);
const {
verification_errors: verificationErrors,
ssl: { validation_errors: sslVerificationErrors },
} = cloudflareData;
const errorMessage: string = [
...(verificationErrors ?? []),
...(sslVerificationErrors ?? []).map(({ message }) => message),
]
.filter(Boolean)
.join('\n');
return updateDomainById(domain.id, { cloudflareData, errorMessage, status }, 'replace');
};
const syncDomainStatus = async (domain: Domain): Promise<Domain> => {
const { hostnameProviderConfig } = SystemContext.shared;
assertThat(hostnameProviderConfig, 'domain.not_configured');
assertThat(domain.cloudflareData, 'domain.cloudflare_data_missing');
const cloudflareData = await getCustomHostname(
hostnameProviderConfig,
domain.cloudflareData.id
);
const updatedDomain = await syncDomainStatusFromCloudflareData(domain, cloudflareData);
await clearCustomDomainCache(domain.domain);
return updatedDomain;
};
const addDomain = async (hostname: string): Promise<Domain> => {
const { hostnameProviderConfig } = SystemContext.shared;
assertThat(hostnameProviderConfig, 'domain.not_configured');
const { blockedDomains } = hostnameProviderConfig;
assertThat(
!(blockedDomains ?? []).some(
(domain) => hostname === domain || isSubdomainOf(hostname, domain)
),
'domain.domain_is_not_allowed'
);
const [fallbackOrigin, cloudflareData] = await Promise.all([
getFallbackOrigin(hostnameProviderConfig),
createCustomHostname(hostnameProviderConfig, hostname),
]);
const insertedDomain = await insertDomain({
domain: hostname,
id: generateStandardId(),
cloudflareData,
status: DomainStatus.PendingVerification,
dnsRecords: [
{
type: 'CNAME',
name: hostname,
value: fallbackOrigin,
},
],
});
await clearCustomDomainCache(hostname);
return insertedDomain;
};
const deleteDomain = async (id: string) => {
const { hostnameProviderConfig } = SystemContext.shared;
assertThat(hostnameProviderConfig, 'domain.not_configured');
const domain = await findDomainById(id);
if (domain.cloudflareData?.id) {
try {
await deleteCustomHostname(hostnameProviderConfig, domain.cloudflareData.id);
} catch (error: unknown) {
// Ignore not found error, since we are deleting the domain anyway
if (!(error instanceof RequestError) || error.code !== 'domain.cloudflare_not_found') {
throw error;
}
}
}
await deleteDomainById(id);
await clearCustomDomainCache(domain.domain);
};
return {
syncDomainStatus,
addDomain,
deleteDomain,
};
};