0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-06 20:40:08 -05:00

fix(console): fix m2m app log not accessible bug (#5307)

This commit is contained in:
Darcy Ye 2024-01-26 11:15:11 +08:00 committed by GitHub
parent a4a02e2c66
commit c2e6a610bc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 90 additions and 6 deletions

View file

@ -98,6 +98,10 @@ function ConsoleContent() {
<Route index element={<Navigate replace to={ApplicationDetailsTabs.Settings} />} />
<Route path=":tab" element={<ApplicationDetails />} />
</Route>
<Route
path={`:appId/${ApplicationDetailsTabs.Logs}/:logId`}
element={<AuditLogDetails />}
/>
</Route>
<Route path="api-resources">
<Route index element={<ApiResources />} />

View file

@ -1,5 +1,5 @@
import { withAppInsights } from '@logto/app-insights/react';
import type { User, Log, Hook } from '@logto/schemas';
import type { Application, User, Log, Hook } from '@logto/schemas';
import { demoAppApplicationId } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import { useTranslation } from 'react-i18next';
@ -33,10 +33,11 @@ const isWebhookEventLog = (key?: string) =>
key && Object.values<string>(hookEventLogKey).includes(key);
function AuditLogDetails() {
const { userId, hookId, logId } = useParams();
const { appId, userId, hookId, logId } = useParams();
const { pathname } = useLocation();
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { data, error, mutate } = useSWR<Log, RequestError>(logId && `api/logs/${logId}`);
const { data: appData } = useSWR<Application, RequestError>(appId && `api/applications/${appId}`);
const { data: userData } = useSWR<User, RequestError>(userId && `api/users/${userId}`);
const { data: hookData } = useSWR<Hook, RequestError>(hookId && `api/hooks/${hookId}`);
@ -54,7 +55,14 @@ function AuditLogDetails() {
hookId &&
t('log_details.back_to', {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
name: hookData?.name || t('users.unnamed'),
name: hookData?.name || t('general.unnamed'),
})
) ??
conditional(
appId &&
t('log_details.back_to', {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
name: appData?.name || t('general.unnamed'),
})
) ??
t('log_details.back_to_logs');

View file

@ -29,3 +29,7 @@ export const authedAdminTenantApi = adminTenantApi.extend({
export const cloudApi = got.extend({
prefixUrl: new URL('/api', logtoCloudUrl),
});
export const oidcApi = got.extend({
prefixUrl: new URL('/oidc', logtoUrl),
});

View file

@ -1,14 +1,14 @@
import {
ApplicationType,
type Application,
type CreateApplication,
type ApplicationType,
type OidcClientMetadata,
type Role,
type ProtectedAppMetadata,
} from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import { authedAdminApi } from './api.js';
import { authedAdminApi, oidcApi } from './api.js';
export const createApplication = async (
name: string,
@ -89,3 +89,20 @@ export const putRolesToApplication = async (applicationId: string, roleIds: stri
export const deleteRoleFromApplication = async (applicationId: string, roleId: string) =>
authedAdminApi.delete(`applications/${applicationId}/roles/${roleId}`);
export const generateM2mLog = async (applicationId: string) => {
const { id, secret, type, isThirdParty } = await getApplication(applicationId);
if (type !== ApplicationType.MachineToMachine || isThirdParty) {
return;
}
// This is a token request with insufficient parameters and should fail. We make the request to generate a log for the current machine to machine app.
return oidcApi.post('token', {
form: {
client_id: id,
client_secret: secret,
grant_type: 'client_credentials',
},
});
};

View file

@ -1,5 +1,6 @@
import { ApplicationType } from '@logto/schemas';
import { generateM2mLog } from '#src/api/application.js';
import { logtoConsoleUrl as logtoConsoleUrlString } from '#src/constants.js';
import {
expectConfirmModalAndAct,
@ -12,7 +13,7 @@ import {
goToAdminConsole,
waitForToast,
} from '#src/ui-helpers/index.js';
import { expectNavigation, appendPathname } from '#src/utils.js';
import { expectNavigation, appendPathname, dcls } from '#src/utils.js';
import {
type ApplicationMetadata,
@ -255,6 +256,56 @@ describe('applications', () => {
text: app.name,
});
// Make sure the machine log details page can be accessed.
// Machine logs only available for m2m apps.
if (app.type === ApplicationType.MachineToMachine) {
// Get the app id from the page.
const appId = await page.$eval(
[dcls('main'), dcls('header'), dcls('row'), dcls('copyId'), dcls('content')].join(' '),
(element) => element.textContent
);
expect(appId).toBeTruthy();
await Promise.all([
expect(generateM2mLog(appId!)).rejects.toThrow(),
expect(page).toClick('nav div[class$=item] div[class$=link] a', {
text: 'Machine logs',
}),
]);
expect(page.url().endsWith('logs')).toBeTruthy();
// Logs were preloaded, so we need to reload the page to get the latest list of logs.
await page.reload({ waitUntil: 'networkidle0' });
// Go to the details page of the log.
await expect(page).toClick(
'table tbody tr td div[class*=eventName]:has(div[class*=title])',
{
text: 'Exchange token by Client Credentials',
}
);
await expect(page).toMatchElement([dcls('main'), dcls('header'), dcls('label')].join(' '), {
text: 'Failed',
});
await expect(page).toMatchElement(
[dcls('main'), dcls('header'), dcls('content'), dcls('basicInfo'), 'a[class*=link]'].join(
' '
),
{
text: app.name,
}
);
// Go back to machine logs tab of the m2m app details page.
await expect(page).toClick(
[dcls('main'), dcls('container'), 'a[class*=backLink]', 'span'].join(' '),
{
text: `Back to ${app.name}`,
}
);
}
await expectToProceedAppDeletion(page, app.name);
expect(page.url()).toBe(new URL('/console/applications', logtoConsoleUrl).href);