mirror of
https://github.com/logto-io/logto.git
synced 2025-04-07 23:01:25 -05:00
feat(core,phrases): delete custom domain (#5261)
This commit is contained in:
parent
fa89d33252
commit
12fc67a039
20 changed files with 189 additions and 5 deletions
|
@ -18,12 +18,13 @@ import { mockFallbackOrigin } from '#src/utils/cloudflare/mock.js';
|
|||
const { jest } = import.meta;
|
||||
const { mockEsmWithActual } = createMockUtils(jest);
|
||||
|
||||
const { updateProtectedAppSiteConfigs } = await mockEsmWithActual(
|
||||
const { updateProtectedAppSiteConfigs, deleteCustomHostname } = await mockEsmWithActual(
|
||||
'#src/utils/cloudflare/index.js',
|
||||
() => ({
|
||||
updateProtectedAppSiteConfigs: jest.fn(),
|
||||
getCustomHostname: jest.fn(async () => mockCloudflareData),
|
||||
getFallbackOrigin: jest.fn(async () => mockFallbackOrigin),
|
||||
deleteCustomHostname: jest.fn(),
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -36,12 +37,12 @@ const updateApplicationById = jest.fn(async (id: string, data: Partial<Applicati
|
|||
...mockProtectedApplication,
|
||||
...data,
|
||||
}));
|
||||
|
||||
const {
|
||||
syncAppConfigsToRemote,
|
||||
checkAndBuildProtectedAppData,
|
||||
syncAppCustomDomainStatus,
|
||||
getDefaultDomain,
|
||||
deleteDomainFromRemote,
|
||||
} = createProtectedAppLibrary(
|
||||
new MockQueries({
|
||||
applications: {
|
||||
|
@ -222,3 +223,10 @@ describe('syncAppCustomDomainStatus()', () => {
|
|||
expect(updateApplicationById).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteDomainFromRemote()', () => {
|
||||
it('should call deleteCustomHostname', async () => {
|
||||
await deleteDomainFromRemote('id');
|
||||
expect(deleteCustomHostname).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -17,6 +17,7 @@ import SystemContext from '#src/tenants/SystemContext.js';
|
|||
import assertThat from '#src/utils/assert-that.js';
|
||||
import {
|
||||
createCustomHostname,
|
||||
deleteCustomHostname,
|
||||
deleteProtectedAppSiteConfigs,
|
||||
getCustomHostname,
|
||||
getDomainStatusFromCloudflareData,
|
||||
|
@ -95,6 +96,14 @@ const addDomainToRemote = async (
|
|||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Call Cloudflare API to delete the domain (custom hostname)
|
||||
*/
|
||||
const deleteDomainFromRemote = async (id: string) => {
|
||||
const hostnameProviderConfig = await getHostnameProviderConfig();
|
||||
await deleteCustomHostname(hostnameProviderConfig, id);
|
||||
};
|
||||
|
||||
export const createProtectedAppLibrary = (queries: Queries) => {
|
||||
const {
|
||||
applications: { findApplicationById, findApplicationByProtectedAppHost, updateApplicationById },
|
||||
|
@ -274,5 +283,6 @@ export const createProtectedAppLibrary = (queries: Queries) => {
|
|||
getDefaultDomain,
|
||||
addDomainToRemote,
|
||||
syncAppCustomDomainStatus,
|
||||
deleteDomainFromRemote,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -41,6 +41,20 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/applications/{id}/protected-app-metadata/custom-domains/{domain}": {
|
||||
"delete": {
|
||||
"summary": "Delete a custom domain.",
|
||||
"description": "Add a custom domain.",
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "The domain has been deleted."
|
||||
},
|
||||
"404": {
|
||||
"description": "Can not find the domain."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import { type Application, DomainStatus } from '@logto/schemas';
|
|||
import { pickDefault } from '@logto/shared/esm';
|
||||
import { type Nullable } from '@silverhand/essentials';
|
||||
|
||||
import { mockProtectedApplication } from '#src/__mocks__/index.js';
|
||||
import { mockCloudflareData, mockProtectedApplication } from '#src/__mocks__/index.js';
|
||||
import { mockIdGenerators } from '#src/test-utils/nanoid.js';
|
||||
import { MockTenant } from '#src/test-utils/tenant.js';
|
||||
|
||||
|
@ -38,6 +38,8 @@ const syncAppCustomDomainStatus = jest.fn(async () => ({
|
|||
},
|
||||
}));
|
||||
const syncAppConfigsToRemote = jest.fn();
|
||||
const deleteDomainFromRemote = jest.fn();
|
||||
const deleteRemoteAppConfigs = jest.fn();
|
||||
|
||||
await mockIdGenerators();
|
||||
|
||||
|
@ -52,7 +54,13 @@ const tenantContext = new MockTenant(
|
|||
},
|
||||
undefined,
|
||||
{
|
||||
protectedApps: { addDomainToRemote, syncAppCustomDomainStatus, syncAppConfigsToRemote },
|
||||
protectedApps: {
|
||||
addDomainToRemote,
|
||||
syncAppCustomDomainStatus,
|
||||
syncAppConfigsToRemote,
|
||||
deleteDomainFromRemote,
|
||||
deleteRemoteAppConfigs,
|
||||
},
|
||||
applications: { validateProtectedApplicationById: jest.fn() },
|
||||
}
|
||||
);
|
||||
|
@ -63,6 +71,10 @@ const applicationProtectedAppMetadataRoutes = await pickDefault(
|
|||
);
|
||||
|
||||
describe('application protected app metadata routes', () => {
|
||||
afterEach(() => {
|
||||
updateApplicationById.mockClear();
|
||||
});
|
||||
|
||||
const requester = createRequester({
|
||||
authedRoutes: applicationProtectedAppMetadataRoutes,
|
||||
tenantContext,
|
||||
|
@ -131,4 +143,51 @@ describe('application protected app metadata routes', () => {
|
|||
expect(response.status).toEqual(422);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /applications/:applicationId/protected-app-metadata/custom-domains/:domain', () => {
|
||||
it('should update application, delete remote domain, and delete site configs', async () => {
|
||||
findApplicationById.mockResolvedValueOnce({
|
||||
...mockProtectedApplication,
|
||||
protectedAppMetadata: {
|
||||
...mockProtectedApplication.protectedAppMetadata,
|
||||
customDomains: [
|
||||
{
|
||||
...mockDomainResponse,
|
||||
cloudflareData: mockCloudflareData,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const response = await requester.delete(
|
||||
`/applications/${mockProtectedApplication.id}/protected-app-metadata/custom-domains/${mockDomainResponse.domain}`
|
||||
);
|
||||
expect(response.status).toEqual(204);
|
||||
expect(updateApplicationById).toHaveBeenCalledWith(mockProtectedApplication.id, {
|
||||
protectedAppMetadata: {
|
||||
...mockProtectedApplication.protectedAppMetadata,
|
||||
customDomains: [],
|
||||
},
|
||||
oidcClientMetadata: {
|
||||
postLogoutRedirectUris: [`https://${mockProtectedApplication.protectedAppMetadata.host}`],
|
||||
redirectUris: [`https://${mockProtectedApplication.protectedAppMetadata.host}/callback`],
|
||||
},
|
||||
});
|
||||
expect(deleteDomainFromRemote).toHaveBeenCalledWith(mockCloudflareData.id);
|
||||
expect(deleteRemoteAppConfigs).toHaveBeenCalledWith(mockDomainResponse.domain);
|
||||
});
|
||||
|
||||
it('throw when domain exists', async () => {
|
||||
findApplicationById.mockResolvedValueOnce({
|
||||
...mockProtectedApplication,
|
||||
protectedAppMetadata: {
|
||||
...mockProtectedApplication.protectedAppMetadata,
|
||||
customDomains: [mockDomainResponse],
|
||||
},
|
||||
});
|
||||
const response = await requester.delete(
|
||||
`/applications/${mockProtectedApplication.id}/protected-app-metadata/custom-domains/unexists.com`
|
||||
);
|
||||
expect(response.status).toEqual(404);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -20,7 +20,13 @@ export default function applicationProtectedAppMetadataRoutes<T extends AuthedRo
|
|||
},
|
||||
libraries: {
|
||||
applications: { validateProtectedApplicationById },
|
||||
protectedApps: { addDomainToRemote, syncAppCustomDomainStatus, syncAppConfigsToRemote },
|
||||
protectedApps: {
|
||||
addDomainToRemote,
|
||||
syncAppCustomDomainStatus,
|
||||
syncAppConfigsToRemote,
|
||||
deleteDomainFromRemote,
|
||||
deleteRemoteAppConfigs,
|
||||
},
|
||||
},
|
||||
},
|
||||
]: RouterInitArgs<T>
|
||||
|
@ -108,4 +114,62 @@ export default function applicationProtectedAppMetadataRoutes<T extends AuthedRo
|
|||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
router.delete(
|
||||
`${customDomainsPathname}/:domain`,
|
||||
koaGuard({
|
||||
params: z.object({
|
||||
...params,
|
||||
domain: z.string(),
|
||||
}),
|
||||
status: [204, 404],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { id, domain } = ctx.guard.params;
|
||||
|
||||
const { protectedAppMetadata, oidcClientMetadata } = await findApplicationById(id);
|
||||
|
||||
const domainObject = protectedAppMetadata?.customDomains?.find(
|
||||
({ domain: domainName }) => domainName === domain
|
||||
);
|
||||
|
||||
assertThat(
|
||||
protectedAppMetadata && domainObject,
|
||||
new RequestError({
|
||||
code: 'application.custom_domain_not_found',
|
||||
status: 404,
|
||||
})
|
||||
);
|
||||
|
||||
// Remove domain from Cloudflare
|
||||
if (domainObject.cloudflareData?.id) {
|
||||
await deleteDomainFromRemote(domainObject.cloudflareData.id);
|
||||
}
|
||||
|
||||
// Remove site configs
|
||||
await deleteRemoteAppConfigs(domain);
|
||||
|
||||
await updateApplicationById(id, {
|
||||
oidcClientMetadata: {
|
||||
...oidcClientMetadata,
|
||||
redirectUris: oidcClientMetadata.redirectUris.filter(
|
||||
(uri) => uri !== `https://${domain}/callback`
|
||||
),
|
||||
postLogoutRedirectUris: oidcClientMetadata.postLogoutRedirectUris.filter(
|
||||
(uri) => uri !== `https://${domain}`
|
||||
),
|
||||
},
|
||||
protectedAppMetadata: {
|
||||
...protectedAppMetadata,
|
||||
customDomains: protectedAppMetadata.customDomains?.filter(
|
||||
({ domain: domainName }) => domainName !== domain
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
ctx.status = 204;
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -23,6 +23,8 @@ const application = {
|
|||
'The subdomain of Protected application is already in use.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_subdomain: 'Invalid subdomain.',
|
||||
/** UNTRANSLATED */
|
||||
custom_domain_not_found: 'Custom domain not found.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -15,6 +15,7 @@ const application = {
|
|||
protected_application_subdomain_exists:
|
||||
'The subdomain of Protected application is already in use.',
|
||||
invalid_subdomain: 'Invalid subdomain.',
|
||||
custom_domain_not_found: 'Custom domain not found.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -23,6 +23,8 @@ const application = {
|
|||
'The subdomain of Protected application is already in use.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_subdomain: 'Invalid subdomain.',
|
||||
/** UNTRANSLATED */
|
||||
custom_domain_not_found: 'Custom domain not found.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -24,6 +24,8 @@ const application = {
|
|||
'The subdomain of Protected application is already in use.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_subdomain: 'Invalid subdomain.',
|
||||
/** UNTRANSLATED */
|
||||
custom_domain_not_found: 'Custom domain not found.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -24,6 +24,8 @@ const application = {
|
|||
'The subdomain of Protected application is already in use.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_subdomain: 'Invalid subdomain.',
|
||||
/** UNTRANSLATED */
|
||||
custom_domain_not_found: 'Custom domain not found.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -23,6 +23,8 @@ const application = {
|
|||
'The subdomain of Protected application is already in use.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_subdomain: 'Invalid subdomain.',
|
||||
/** UNTRANSLATED */
|
||||
custom_domain_not_found: 'Custom domain not found.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -22,6 +22,8 @@ const application = {
|
|||
'The subdomain of Protected application is already in use.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_subdomain: 'Invalid subdomain.',
|
||||
/** UNTRANSLATED */
|
||||
custom_domain_not_found: 'Custom domain not found.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -22,6 +22,8 @@ const application = {
|
|||
'The subdomain of Protected application is already in use.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_subdomain: 'Invalid subdomain.',
|
||||
/** UNTRANSLATED */
|
||||
custom_domain_not_found: 'Custom domain not found.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -23,6 +23,8 @@ const application = {
|
|||
'The subdomain of Protected application is already in use.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_subdomain: 'Invalid subdomain.',
|
||||
/** UNTRANSLATED */
|
||||
custom_domain_not_found: 'Custom domain not found.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -23,6 +23,8 @@ const application = {
|
|||
'The subdomain of Protected application is already in use.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_subdomain: 'Invalid subdomain.',
|
||||
/** UNTRANSLATED */
|
||||
custom_domain_not_found: 'Custom domain not found.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -24,6 +24,8 @@ const application = {
|
|||
'The subdomain of Protected application is already in use.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_subdomain: 'Invalid subdomain.',
|
||||
/** UNTRANSLATED */
|
||||
custom_domain_not_found: 'Custom domain not found.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -22,6 +22,8 @@ const application = {
|
|||
'The subdomain of Protected application is already in use.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_subdomain: 'Invalid subdomain.',
|
||||
/** UNTRANSLATED */
|
||||
custom_domain_not_found: 'Custom domain not found.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -21,6 +21,8 @@ const application = {
|
|||
'The subdomain of Protected application is already in use.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_subdomain: 'Invalid subdomain.',
|
||||
/** UNTRANSLATED */
|
||||
custom_domain_not_found: 'Custom domain not found.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -21,6 +21,8 @@ const application = {
|
|||
'The subdomain of Protected application is already in use.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_subdomain: 'Invalid subdomain.',
|
||||
/** UNTRANSLATED */
|
||||
custom_domain_not_found: 'Custom domain not found.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -21,6 +21,8 @@ const application = {
|
|||
'The subdomain of Protected application is already in use.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_subdomain: 'Invalid subdomain.',
|
||||
/** UNTRANSLATED */
|
||||
custom_domain_not_found: 'Custom domain not found.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
Loading…
Add table
Reference in a new issue