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 koaErrorHandler from '@/middleware/koa-error-handler';
|
||||||
import * as koaI18next from '@/middleware/koa-i18next';
|
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 koaOIDCErrorHandler from '@/middleware/koa-oidc-error-handler';
|
||||||
import * as koaSlonikErrorHandler from '@/middleware/koa-slonik-error-handler';
|
import * as koaSlonikErrorHandler from '@/middleware/koa-slonik-error-handler';
|
||||||
import * as koaSpaProxy from '@/middleware/koa-spa-proxy';
|
import * as koaSpaProxy from '@/middleware/koa-spa-proxy';
|
||||||
|
@ -18,6 +19,7 @@ describe('App Init', () => {
|
||||||
const middlewareList = [
|
const middlewareList = [
|
||||||
koaErrorHandler,
|
koaErrorHandler,
|
||||||
koaI18next,
|
koaI18next,
|
||||||
|
koaLog,
|
||||||
koaOIDCErrorHandler,
|
koaOIDCErrorHandler,
|
||||||
koaSlonikErrorHandler,
|
koaSlonikErrorHandler,
|
||||||
koaSpaProxy,
|
koaSpaProxy,
|
||||||
|
|
|
@ -9,6 +9,7 @@ import envSet, { MountedApps } from '@/env-set';
|
||||||
import koaConnectorErrorHandler from '@/middleware/koa-connector-error-handle';
|
import koaConnectorErrorHandler from '@/middleware/koa-connector-error-handle';
|
||||||
import koaErrorHandler from '@/middleware/koa-error-handler';
|
import koaErrorHandler from '@/middleware/koa-error-handler';
|
||||||
import koaI18next from '@/middleware/koa-i18next';
|
import koaI18next from '@/middleware/koa-i18next';
|
||||||
|
import koaLog from '@/middleware/koa-log';
|
||||||
import koaOIDCErrorHandler from '@/middleware/koa-oidc-error-handler';
|
import koaOIDCErrorHandler from '@/middleware/koa-oidc-error-handler';
|
||||||
import koaSlonikErrorHandler from '@/middleware/koa-slonik-error-handler';
|
import koaSlonikErrorHandler from '@/middleware/koa-slonik-error-handler';
|
||||||
import koaSpaProxy from '@/middleware/koa-spa-proxy';
|
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(koaSlonikErrorHandler());
|
||||||
app.use(koaConnectorErrorHandler());
|
app.use(koaConnectorErrorHandler());
|
||||||
|
|
||||||
// TODO move to specific router (LOG-454)
|
|
||||||
app.use(koaUserLog());
|
app.use(koaUserLog());
|
||||||
|
app.use(koaLog());
|
||||||
app.use(koaLogger());
|
app.use(koaLogger());
|
||||||
app.use(koaI18next());
|
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 './connector';
|
||||||
|
export * from './log';
|
||||||
export * from './oidc-config';
|
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