mirror of
https://github.com/logto-io/logto.git
synced 2025-01-06 20:40:08 -05:00
feat(core): koa-log middleware (#590)
This commit is contained in:
parent
a0af0584f7
commit
4491eab5b4
6 changed files with 179 additions and 2 deletions
|
@ -2,6 +2,7 @@ import Koa from 'koa';
|
|||
|
||||
import * as koaErrorHandler from '@/middleware/koa-error-handler';
|
||||
import * as koaI18next from '@/middleware/koa-i18next';
|
||||
import * as koaLog from '@/middleware/koa-log';
|
||||
import * as koaOIDCErrorHandler from '@/middleware/koa-oidc-error-handler';
|
||||
import * as koaSlonikErrorHandler from '@/middleware/koa-slonik-error-handler';
|
||||
import * as koaSpaProxy from '@/middleware/koa-spa-proxy';
|
||||
|
@ -18,6 +19,7 @@ describe('App Init', () => {
|
|||
const middlewareList = [
|
||||
koaErrorHandler,
|
||||
koaI18next,
|
||||
koaLog,
|
||||
koaOIDCErrorHandler,
|
||||
koaSlonikErrorHandler,
|
||||
koaSpaProxy,
|
||||
|
|
|
@ -9,6 +9,7 @@ import envSet, { MountedApps } from '@/env-set';
|
|||
import koaConnectorErrorHandler from '@/middleware/koa-connector-error-handle';
|
||||
import koaErrorHandler from '@/middleware/koa-error-handler';
|
||||
import koaI18next from '@/middleware/koa-i18next';
|
||||
import koaLog from '@/middleware/koa-log';
|
||||
import koaOIDCErrorHandler from '@/middleware/koa-oidc-error-handler';
|
||||
import koaSlonikErrorHandler from '@/middleware/koa-slonik-error-handler';
|
||||
import koaSpaProxy from '@/middleware/koa-spa-proxy';
|
||||
|
@ -22,8 +23,8 @@ export default async function initApp(app: Koa): Promise<void> {
|
|||
app.use(koaSlonikErrorHandler());
|
||||
app.use(koaConnectorErrorHandler());
|
||||
|
||||
// TODO move to specific router (LOG-454)
|
||||
app.use(koaUserLog());
|
||||
app.use(koaLog());
|
||||
app.use(koaLogger());
|
||||
app.use(koaI18next());
|
||||
|
||||
|
|
110
packages/core/src/middleware/koa-log.test.ts
Normal file
110
packages/core/src/middleware/koa-log.test.ts
Normal file
|
@ -0,0 +1,110 @@
|
|||
import { LogType, LogResult } from '@logto/schemas';
|
||||
|
||||
import { insertLog } from '@/queries/log';
|
||||
import { createContextWithRouteParameters } from '@/utils/test-utils';
|
||||
|
||||
import koaLog, { WithLogContext, LogContext } from './koa-log';
|
||||
|
||||
const nanoIdMock = 'mockId';
|
||||
|
||||
jest.mock('@/queries/log', () => ({
|
||||
insertLog: jest.fn(async () => Promise.resolve()),
|
||||
}));
|
||||
|
||||
jest.mock('nanoid', () => ({
|
||||
nanoid: jest.fn(() => nanoIdMock),
|
||||
}));
|
||||
|
||||
describe('koaLog middleware', () => {
|
||||
const insertLogMock = insertLog as jest.Mock;
|
||||
const next = jest.fn();
|
||||
|
||||
const logMock: Partial<LogContext> = {
|
||||
type: LogType.SignInUsernamePassword,
|
||||
applicationId: 'foo',
|
||||
userId: 'foo',
|
||||
username: 'Foo Bar',
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('insert log with success response', async () => {
|
||||
const ctx: WithLogContext<ReturnType<typeof createContextWithRouteParameters>> = {
|
||||
...createContextWithRouteParameters(),
|
||||
log: {}, // Bypass middleware context type assert
|
||||
};
|
||||
|
||||
next.mockImplementationOnce(async () => {
|
||||
ctx.log = logMock;
|
||||
});
|
||||
|
||||
await koaLog()(ctx, next);
|
||||
|
||||
const { type, ...rest } = logMock;
|
||||
expect(insertLogMock).toBeCalledWith({
|
||||
id: nanoIdMock,
|
||||
type,
|
||||
payload: {
|
||||
...rest,
|
||||
result: LogResult.Success,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should not block request if insertLog throws error', async () => {
|
||||
const ctx: WithLogContext<ReturnType<typeof createContextWithRouteParameters>> = {
|
||||
...createContextWithRouteParameters(),
|
||||
log: {}, // Bypass middleware context type assert
|
||||
};
|
||||
|
||||
const error = new Error('Failed to insert log');
|
||||
insertLogMock.mockImplementationOnce(async () => {
|
||||
throw error;
|
||||
});
|
||||
|
||||
next.mockImplementationOnce(async () => {
|
||||
ctx.log = logMock;
|
||||
});
|
||||
|
||||
await koaLog()(ctx, next);
|
||||
|
||||
const { type, ...rest } = logMock;
|
||||
expect(insertLogMock).toBeCalledWith({
|
||||
id: nanoIdMock,
|
||||
type,
|
||||
payload: {
|
||||
...rest,
|
||||
result: LogResult.Success,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should insert log with failed result if next throws error', async () => {
|
||||
const ctx: WithLogContext<ReturnType<typeof createContextWithRouteParameters>> = {
|
||||
...createContextWithRouteParameters(),
|
||||
log: {}, // Bypass middleware context type assert
|
||||
};
|
||||
|
||||
const error = new Error('next error');
|
||||
|
||||
next.mockImplementationOnce(async () => {
|
||||
ctx.log = logMock;
|
||||
throw error;
|
||||
});
|
||||
|
||||
await expect(koaLog()(ctx, next)).rejects.toMatchError(error);
|
||||
|
||||
const { type, ...rest } = logMock;
|
||||
expect(insertLogMock).toBeCalledWith({
|
||||
id: nanoIdMock,
|
||||
type,
|
||||
payload: {
|
||||
...rest,
|
||||
result: LogResult.Error,
|
||||
error: String(error),
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
55
packages/core/src/middleware/koa-log.ts
Normal file
55
packages/core/src/middleware/koa-log.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
import { LogResult, LogType } from '@logto/schemas';
|
||||
import { Context, MiddlewareType } from 'koa';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { insertLog } from '@/queries/log';
|
||||
|
||||
export type WithLogContext<ContextT> = ContextT & {
|
||||
log: LogContext;
|
||||
};
|
||||
|
||||
export interface LogContext {
|
||||
[key: string]: unknown;
|
||||
type?: LogType;
|
||||
}
|
||||
|
||||
const log = async (ctx: WithLogContext<Context>, result: LogResult) => {
|
||||
const { type, ...rest } = ctx.log;
|
||||
|
||||
if (!type) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await insertLog({
|
||||
id: nanoid(),
|
||||
type,
|
||||
payload: {
|
||||
...rest,
|
||||
result,
|
||||
},
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
console.error('An error occurred while inserting log');
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
export default function koaLog<StateT, ContextT, ResponseBodyT>(): MiddlewareType<
|
||||
StateT,
|
||||
WithLogContext<ContextT>,
|
||||
ResponseBodyT
|
||||
> {
|
||||
return async (ctx, next) => {
|
||||
ctx.log = {};
|
||||
|
||||
try {
|
||||
await next();
|
||||
await log(ctx, LogResult.Success);
|
||||
} catch (error: unknown) {
|
||||
ctx.log.error = String(error);
|
||||
await log(ctx, LogResult.Error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
export * from './user';
|
||||
export * from './connector';
|
||||
export * from './log';
|
||||
export * from './oidc-config';
|
||||
export * from './user';
|
||||
|
|
8
packages/schemas/src/types/log.ts
Normal file
8
packages/schemas/src/types/log.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
export enum LogType {
|
||||
SignInUsernamePassword = 'SignInUsernamePassword',
|
||||
}
|
||||
|
||||
export enum LogResult {
|
||||
Success = 'Success',
|
||||
Error = 'Error',
|
||||
}
|
Loading…
Reference in a new issue