mirror of
https://github.com/logto-io/logto.git
synced 2025-01-06 20:40:08 -05:00
feat(core,phrases): update protected app (#5189)
This commit is contained in:
parent
1ee1efc532
commit
5bc649e10d
22 changed files with 195 additions and 16 deletions
|
@ -43,6 +43,7 @@ import MachineToMachineApplicationRoles from './components/MachineToMachineAppli
|
|||
import RefreshTokenSettings from './components/RefreshTokenSettings';
|
||||
import Settings from './components/Settings';
|
||||
import * as styles from './index.module.scss';
|
||||
import { type ApplicationForm, applicationFormDataParser } from './utils';
|
||||
|
||||
const mapToUriFormatArrays = (value?: string[]) =>
|
||||
value?.filter(Boolean).map((uri) => decodeURIComponent(uri));
|
||||
|
@ -73,7 +74,7 @@ function ApplicationDetails() {
|
|||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [isDeleted, setIsDeleted] = useState(false);
|
||||
const api = useApi();
|
||||
const formMethods = useForm<Application & { isAdmin: boolean }>({
|
||||
const formMethods = useForm<ApplicationForm>({
|
||||
defaultValues: { customClientMetadata: customClientMetadataDefault, isAdmin: false },
|
||||
});
|
||||
|
||||
|
@ -92,7 +93,7 @@ function ApplicationDetails() {
|
|||
return;
|
||||
}
|
||||
|
||||
reset(data);
|
||||
reset(applicationFormDataParser.fromResponse(data));
|
||||
}, [data, isDirty, reset]);
|
||||
|
||||
const onSubmit = handleSubmit(
|
||||
|
|
20
packages/console/src/pages/ApplicationDetails/utils.ts
Normal file
20
packages/console/src/pages/ApplicationDetails/utils.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { type ApplicationResponse } from '@logto/schemas';
|
||||
|
||||
export type ApplicationForm = Pick<
|
||||
ApplicationResponse,
|
||||
'name' | 'description' | 'oidcClientMetadata' | 'customClientMetadata' | 'isAdmin'
|
||||
>;
|
||||
|
||||
export const applicationFormDataParser = {
|
||||
fromResponse: (data: ApplicationResponse): ApplicationForm => {
|
||||
const { name, description, oidcClientMetadata, customClientMetadata, isAdmin } = data;
|
||||
|
||||
return {
|
||||
name,
|
||||
description,
|
||||
oidcClientMetadata,
|
||||
customClientMetadata,
|
||||
isAdmin,
|
||||
};
|
||||
},
|
||||
};
|
|
@ -12,6 +12,12 @@ const { jest } = import.meta;
|
|||
const findApplicationById = jest.fn(async () => mockApplication);
|
||||
const deleteApplicationById = jest.fn();
|
||||
const syncAppConfigsToRemote = jest.fn();
|
||||
const updateApplicationById = jest.fn(
|
||||
async (_, data: Partial<CreateApplication>): Promise<Application> => ({
|
||||
...mockApplication,
|
||||
...data,
|
||||
})
|
||||
);
|
||||
|
||||
await mockIdGenerators();
|
||||
|
||||
|
@ -33,12 +39,7 @@ const tenantContext = new MockTenant(
|
|||
},
|
||||
})
|
||||
),
|
||||
updateApplicationById: jest.fn(
|
||||
async (_, data: Partial<CreateApplication>): Promise<Application> => ({
|
||||
...mockApplication,
|
||||
...data,
|
||||
})
|
||||
),
|
||||
updateApplicationById,
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
|
@ -60,6 +61,11 @@ const customClientMetadata = {
|
|||
};
|
||||
|
||||
describe('application route', () => {
|
||||
afterEach(() => {
|
||||
updateApplicationById.mockClear();
|
||||
syncAppConfigsToRemote.mockClear();
|
||||
});
|
||||
|
||||
const applicationRequest = createRequester({ authedRoutes: applicationRoutes, tenantContext });
|
||||
|
||||
it('GET /applications', async () => {
|
||||
|
@ -197,6 +203,23 @@ describe('application route', () => {
|
|||
expect(response.body).toEqual({ ...mockApplication, name, description, customClientMetadata });
|
||||
});
|
||||
|
||||
it('PATCH /applications/:applicationId for protected app', async () => {
|
||||
findApplicationById.mockResolvedValueOnce(mockProtectedApplication);
|
||||
const name = 'FooApplication';
|
||||
const description = 'FooDescription';
|
||||
const origin = 'https://example.com';
|
||||
|
||||
const response = await applicationRequest
|
||||
.patch('/applications/foo')
|
||||
.send({ name, description, protectedAppMetadata: { origin } });
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual({ ...mockApplication, name, description });
|
||||
expect(syncAppConfigsToRemote).toHaveBeenCalledWith('foo');
|
||||
expect(updateApplicationById).toHaveBeenNthCalledWith(1, 'foo', {
|
||||
protectedAppMetadata: { ...mockProtectedApplication.protectedAppMetadata, origin },
|
||||
});
|
||||
});
|
||||
|
||||
it('PATCH /applications/:applicationId expect to throw with invalid properties', async () => {
|
||||
await expect(
|
||||
applicationRequest.patch('/applications/doo').send({
|
||||
|
|
|
@ -217,7 +217,7 @@ export default function applicationRoutes<T extends AuthedRouter>(
|
|||
'/applications/:id',
|
||||
koaGuard({
|
||||
params: object({ id: string().min(1) }),
|
||||
body: applicationPatchGuard.deepPartial().merge(
|
||||
body: applicationPatchGuard.merge(
|
||||
object({
|
||||
isAdmin: boolean().optional(),
|
||||
})
|
||||
|
@ -231,7 +231,7 @@ export default function applicationRoutes<T extends AuthedRouter>(
|
|||
body,
|
||||
} = ctx.guard;
|
||||
|
||||
const { isAdmin, ...rest } = body;
|
||||
const { isAdmin, protectedAppMetadata, ...rest } = body;
|
||||
|
||||
// User can enable the admin access of Machine-to-Machine apps by switching on a toggle on Admin Console.
|
||||
// Since those apps sit in the user tenant, we provide an internal role to apply the necessary scopes.
|
||||
|
@ -261,6 +261,34 @@ export default function applicationRoutes<T extends AuthedRouter>(
|
|||
}
|
||||
}
|
||||
|
||||
if (protectedAppMetadata) {
|
||||
const { type, protectedAppMetadata: originProtectedAppMetadata } =
|
||||
await findApplicationById(id);
|
||||
assertThat(type === ApplicationType.Protected, 'application.protected_application_only');
|
||||
assertThat(
|
||||
originProtectedAppMetadata,
|
||||
new RequestError({
|
||||
code: 'application.protected_application_misconfigured',
|
||||
status: 422,
|
||||
})
|
||||
);
|
||||
await updateApplicationById(id, {
|
||||
protectedAppMetadata: {
|
||||
...originProtectedAppMetadata,
|
||||
...protectedAppMetadata,
|
||||
},
|
||||
});
|
||||
try {
|
||||
await protectedApps.syncAppConfigsToRemote(id);
|
||||
} catch (error: unknown) {
|
||||
// Revert changes on sync failure
|
||||
await updateApplicationById(id, {
|
||||
protectedAppMetadata: originProtectedAppMetadata,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
ctx.body = await (Object.keys(rest).length > 0
|
||||
? updateApplicationById(id, rest)
|
||||
: findApplicationById(id));
|
||||
|
|
|
@ -37,6 +37,28 @@ const applicationCreateGuardWithProtectedAppMetadata = originalApplicationCreate
|
|||
.optional(),
|
||||
});
|
||||
|
||||
const applicationPatchGuardWithProtectedAppMetadata = originalApplicationPatchGuard
|
||||
.deepPartial()
|
||||
.omit({
|
||||
protectedAppMetadata: true,
|
||||
})
|
||||
.extend({
|
||||
protectedAppMetadata: z
|
||||
.object({
|
||||
origin: z.string().optional(),
|
||||
sessionDuration: z.number().optional(),
|
||||
pageRules: z
|
||||
.array(
|
||||
z.object({
|
||||
/* The path pattern (regex) to match */
|
||||
path: z.string(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
// FIXME: @wangsijie Remove this guard once protected app is ready
|
||||
// @ts-expect-error -- hide the dev feature field from the guard type, but always return the full type to make the api logic simpler
|
||||
export const applicationCreateGuard: typeof applicationCreateGuardWithProtectedAppMetadata = EnvSet
|
||||
|
@ -48,7 +70,7 @@ export const applicationCreateGuard: typeof applicationCreateGuardWithProtectedA
|
|||
|
||||
// FIXME: @wangsijie Remove this guard once protected app is ready
|
||||
// @ts-expect-error -- hide the dev feature field from the guard type, but always return the full type to make the api logic simpler
|
||||
export const applicationPatchGuard: typeof originalApplicationPatchGuard = EnvSet.values
|
||||
.isDevFeaturesEnabled
|
||||
? originalApplicationPatchGuard
|
||||
: originalApplicationPatchGuard.omit({ protectedAppMetadata: true });
|
||||
export const applicationPatchGuard: typeof applicationPatchGuardWithProtectedAppMetadata = EnvSet
|
||||
.values.isDevFeaturesEnabled
|
||||
? applicationPatchGuardWithProtectedAppMetadata
|
||||
: applicationPatchGuardWithProtectedAppMetadata.omit({ protectedAppMetadata: true });
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
type ApplicationType,
|
||||
type OidcClientMetadata,
|
||||
type Role,
|
||||
type ProtectedAppMetadata,
|
||||
} from '@logto/schemas';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
|
||||
|
@ -48,9 +49,9 @@ export const getApplication = async (applicationId: string) =>
|
|||
export const updateApplication = async (
|
||||
applicationId: string,
|
||||
payload: Partial<
|
||||
Omit<CreateApplication, 'id' | 'created_at' | 'oidcClientMetadata'> & {
|
||||
Omit<CreateApplication, 'id' | 'created_at' | 'oidcClientMetadata' | 'protectedAppMetadata'> & {
|
||||
oidcClientMetadata: Partial<OidcClientMetadata>;
|
||||
}
|
||||
} & { protectedAppMetadata: Partial<ProtectedAppMetadata> }
|
||||
> & { isAdmin?: boolean }
|
||||
) =>
|
||||
authedAdminApi
|
||||
|
|
|
@ -102,6 +102,32 @@ describe('admin console application', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should update application details for protected app successfully', async () => {
|
||||
const metadata = {
|
||||
origin: 'https://example.com',
|
||||
host: 'example.protected.app',
|
||||
};
|
||||
|
||||
const application = await createApplication('test-update-app', ApplicationType.Protected, {
|
||||
// @ts-expect-error the create guard has been modified
|
||||
protectedAppMetadata: metadata,
|
||||
});
|
||||
|
||||
const newApplicationDescription = `new_${application.description ?? ''}`;
|
||||
|
||||
const newOrigin = 'https://example2.com';
|
||||
|
||||
await updateApplication(application.id, {
|
||||
description: newApplicationDescription,
|
||||
protectedAppMetadata: { origin: newOrigin },
|
||||
});
|
||||
|
||||
const updatedApplication = await getApplication(application.id);
|
||||
|
||||
expect(updatedApplication.description).toBe(newApplicationDescription);
|
||||
expect(updatedApplication.protectedAppMetadata?.origin).toEqual(newOrigin);
|
||||
});
|
||||
|
||||
it('should update application "admin" successfully', async () => {
|
||||
const application = await createApplication(
|
||||
'test-update-is-admin',
|
||||
|
|
|
@ -12,6 +12,10 @@ const application = {
|
|||
protected_app_not_configured: 'Protected app provider is not configured.',
|
||||
/** UNTRANSLATED */
|
||||
cloudflare_unknown_error: 'Got unknown error when requesting Cloudflare API',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_only: 'The feature is only available for protected applications.',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -9,6 +9,8 @@ const application = {
|
|||
protected_app_metadata_is_required: 'Protected app metadata is required.',
|
||||
protected_app_not_configured: 'Protected app provider is not configured.',
|
||||
cloudflare_unknown_error: 'Got unknown error when requesting Cloudflare API',
|
||||
protected_application_only: 'The feature is only available for protected applications.',
|
||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -12,6 +12,10 @@ const application = {
|
|||
protected_app_not_configured: 'Protected app provider is not configured.',
|
||||
/** UNTRANSLATED */
|
||||
cloudflare_unknown_error: 'Got unknown error when requesting Cloudflare API',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_only: 'The feature is only available for protected applications.',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -13,6 +13,10 @@ const application = {
|
|||
protected_app_not_configured: 'Protected app provider is not configured.',
|
||||
/** UNTRANSLATED */
|
||||
cloudflare_unknown_error: 'Got unknown error when requesting Cloudflare API',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_only: 'The feature is only available for protected applications.',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -13,6 +13,10 @@ const application = {
|
|||
protected_app_not_configured: 'Protected app provider is not configured.',
|
||||
/** UNTRANSLATED */
|
||||
cloudflare_unknown_error: 'Got unknown error when requesting Cloudflare API',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_only: 'The feature is only available for protected applications.',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -12,6 +12,10 @@ const application = {
|
|||
protected_app_not_configured: 'Protected app provider is not configured.',
|
||||
/** UNTRANSLATED */
|
||||
cloudflare_unknown_error: 'Got unknown error when requesting Cloudflare API',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_only: 'The feature is only available for protected applications.',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -11,6 +11,10 @@ const application = {
|
|||
protected_app_not_configured: 'Protected app provider is not configured.',
|
||||
/** UNTRANSLATED */
|
||||
cloudflare_unknown_error: 'Got unknown error when requesting Cloudflare API',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_only: 'The feature is only available for protected applications.',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -11,6 +11,10 @@ const application = {
|
|||
protected_app_not_configured: 'Protected app provider is not configured.',
|
||||
/** UNTRANSLATED */
|
||||
cloudflare_unknown_error: 'Got unknown error when requesting Cloudflare API',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_only: 'The feature is only available for protected applications.',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -12,6 +12,10 @@ const application = {
|
|||
protected_app_not_configured: 'Protected app provider is not configured.',
|
||||
/** UNTRANSLATED */
|
||||
cloudflare_unknown_error: 'Got unknown error when requesting Cloudflare API',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_only: 'The feature is only available for protected applications.',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -12,6 +12,10 @@ const application = {
|
|||
protected_app_not_configured: 'Protected app provider is not configured.',
|
||||
/** UNTRANSLATED */
|
||||
cloudflare_unknown_error: 'Got unknown error when requesting Cloudflare API',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_only: 'The feature is only available for protected applications.',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -13,6 +13,10 @@ const application = {
|
|||
protected_app_not_configured: 'Protected app provider is not configured.',
|
||||
/** UNTRANSLATED */
|
||||
cloudflare_unknown_error: 'Got unknown error when requesting Cloudflare API',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_only: 'The feature is only available for protected applications.',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -11,6 +11,10 @@ const application = {
|
|||
protected_app_not_configured: 'Protected app provider is not configured.',
|
||||
/** UNTRANSLATED */
|
||||
cloudflare_unknown_error: 'Got unknown error when requesting Cloudflare API',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_only: 'The feature is only available for protected applications.',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -10,6 +10,10 @@ const application = {
|
|||
protected_app_not_configured: 'Protected app provider is not configured.',
|
||||
/** UNTRANSLATED */
|
||||
cloudflare_unknown_error: 'Got unknown error when requesting Cloudflare API',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_only: 'The feature is only available for protected applications.',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -10,6 +10,10 @@ const application = {
|
|||
protected_app_not_configured: 'Protected app provider is not configured.',
|
||||
/** UNTRANSLATED */
|
||||
cloudflare_unknown_error: 'Got unknown error when requesting Cloudflare API',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_only: 'The feature is only available for protected applications.',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -10,6 +10,10 @@ const application = {
|
|||
protected_app_not_configured: 'Protected app provider is not configured.',
|
||||
/** UNTRANSLATED */
|
||||
cloudflare_unknown_error: 'Got unknown error when requesting Cloudflare API',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_only: 'The feature is only available for protected applications.',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
Loading…
Reference in a new issue