0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-27 21:39:16 -05:00

feat(core,schemas): log IP and user agent (#682)

* feat(core,schemas): log IP and user agent

* refactor(core): extract initLog from koaLog

* refactor(core): log koa

log koa

* fix(core): rename

rename

* refactor(core): initLogger

Co-authored-by: simeng-li <simeng@silverhand.io>
This commit is contained in:
IceHe.xyz 2022-05-05 15:38:59 +08:00 committed by GitHub
parent 4521c3c8d1
commit 0ecb7e4d2f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 96 additions and 88 deletions

View file

@ -19,55 +19,30 @@ jest.mock('nanoid', () => ({
describe('koaLog middleware', () => {
const insertLogMock = insertLog as jest.Mock;
const next = jest.fn();
const type = 'SignInUsernamePassword';
const payload: LogPayload = {
userId: 'foo',
username: 'Bar',
};
const ip = '192.168.0.1';
const userAgent =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36';
afterEach(() => {
jest.clearAllMocks();
});
it('insert log with success response', async () => {
const ctx: WithLogContext<ReturnType<typeof createContextWithRouteParameters>> = {
...createContextWithRouteParameters(),
...createContextWithRouteParameters({ headers: { 'user-agent': userAgent } }),
log, // Bypass middleware context type assert
};
ctx.request.ip = ip;
next.mockImplementationOnce(async () => {
const next = async () => {
ctx.log(type, payload);
});
await koaLog()(ctx, next);
expect(insertLogMock).toBeCalledWith({
id: nanoIdMock,
type,
payload: {
...payload,
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(type, payload);
});
};
await koaLog()(ctx, next);
expect(insertLogMock).toBeCalledWith({
@ -76,23 +51,25 @@ describe('koaLog middleware', () => {
payload: {
...payload,
result: LogResult.Success,
ip,
userAgent,
},
});
});
it('should insert log with failed result if next throws error', async () => {
const ctx: WithLogContext<ReturnType<typeof createContextWithRouteParameters>> = {
...createContextWithRouteParameters(),
...createContextWithRouteParameters({ headers: { 'user-agent': userAgent } }),
log, // Bypass middleware context type assert
};
ctx.request.ip = ip;
const error = new Error('next error');
next.mockImplementationOnce(async () => {
const next = async () => {
ctx.log(type, payload);
throw error;
});
};
await expect(koaLog()(ctx, next)).rejects.toMatchError(error);
expect(insertLogMock).toBeCalledWith({
@ -102,6 +79,8 @@ describe('koaLog middleware', () => {
...payload,
result: LogResult.Error,
error: String(error),
ip,
userAgent,
},
});
});

View file

@ -1,27 +1,64 @@
import { LogPayload, LogPayloads, LogResult, LogType } from '@logto/schemas';
import { Optional } from '@silverhand/essentials';
import { BaseLogPayload, LogPayload, LogPayloads, LogResult, LogType } from '@logto/schemas';
import deepmerge from 'deepmerge';
import { MiddlewareType } from 'koa';
import { nanoid } from 'nanoid';
import { insertLog } from '@/queries/log';
export type WithLogContext<ContextT> = ContextT & {
log: <T extends LogType>(type: T, payload: LogPayloads[T]) => void;
type MergeLog = <T extends LogType>(type: T, payload: LogPayloads[T]) => void;
export type WithLogContext<ContextT> = ContextT & { log: MergeLog };
type Logger = {
type?: LogType;
basePayload?: BaseLogPayload;
payload: LogPayload;
set: (basePayload: BaseLogPayload) => void;
log: MergeLog;
save: () => Promise<void>;
};
const saveLog = async (type: LogType, payload: LogPayload) => {
try {
await insertLog({
id: nanoid(),
type,
payload,
});
} catch (error: unknown) {
console.error('An error occurred while inserting log');
console.error(error);
}
/* eslint-disable @silverhand/fp/no-mutation */
const initLogger = (basePayload?: Readonly<BaseLogPayload>) => {
const logger: Logger = {
type: undefined,
basePayload,
payload: {},
set: (basePayload) => {
logger.basePayload = {
...logger.basePayload,
...basePayload,
};
},
log: (type, payload) => {
if (type !== logger.type) {
logger.type = type;
logger.payload = payload;
return;
}
logger.payload = deepmerge(logger.payload, payload);
},
save: async () => {
if (!logger.type) {
return;
}
await insertLog({
id: nanoid(),
type: logger.type,
payload: {
...logger.basePayload,
...logger.payload,
},
});
},
};
return logger;
};
/* eslint-enable @silverhand/fp/no-mutation */
export default function koaLog<StateT, ContextT, ResponseBodyT>(): MiddlewareType<
StateT,
@ -29,34 +66,21 @@ export default function koaLog<StateT, ContextT, ResponseBodyT>(): MiddlewareTyp
ResponseBodyT
> {
return async (ctx, next) => {
// eslint-disable-next-line @silverhand/fp/no-let
let logType: Optional<LogType>;
// eslint-disable-next-line @silverhand/fp/no-let
let logPayload: LogPayload = {};
const {
ip,
headers: { 'user-agent': userAgent },
} = ctx.request;
ctx.log = (type, payload) => {
if (logType !== type) {
// eslint-disable-next-line @silverhand/fp/no-mutation
logPayload = {}; // Reset payload when type changes
}
// eslint-disable-next-line @silverhand/fp/no-mutation
logType = type; // Use first initialized log type
// eslint-disable-next-line @silverhand/fp/no-mutation
logPayload = deepmerge(logPayload, payload);
};
const logger = initLogger({ result: LogResult.Success, ip, userAgent });
ctx.log = logger.log;
try {
await next();
if (logType) {
await saveLog(logType, { ...logPayload, result: LogResult.Success });
}
} catch (error: unknown) {
if (logType) {
await saveLog(logType, { ...logPayload, result: LogResult.Error, error: String(error) });
}
logger.set({ result: LogResult.Error, error: String(error) });
throw error;
} finally {
await logger.save();
}
};
}

View file

@ -5,40 +5,45 @@ export enum LogResult {
Error = 'Error',
}
interface BaseLogPayload {
sessionId?: string;
export interface BaseLogPayload {
result?: LogResult;
error?: string;
ip?: string;
userAgent?: string;
}
interface RegisterUsernamePasswordLogPayload extends BaseLogPayload {
interface CommonFields {
sessionId?: string;
}
interface RegisterUsernamePasswordLogPayload extends CommonFields {
userId?: string;
username?: string;
}
interface RegisterEmailSendPasscodeLogPayload extends BaseLogPayload {
interface RegisterEmailSendPasscodeLogPayload extends CommonFields {
email?: string;
passcode?: Passcode;
}
interface RegisterEmailLogPayload extends BaseLogPayload {
interface RegisterEmailLogPayload extends CommonFields {
email?: string;
code?: string;
userId?: string;
}
interface RegisterSmsSendPasscodeLogPayload extends BaseLogPayload {
interface RegisterSmsSendPasscodeLogPayload extends CommonFields {
phone?: string;
passcode?: Passcode;
}
interface RegisterSmsLogPayload extends BaseLogPayload {
interface RegisterSmsLogPayload extends CommonFields {
phone?: string;
code?: string;
userId?: string;
}
interface RegisterSocialBindLogPayload extends BaseLogPayload {
interface RegisterSocialBindLogPayload extends CommonFields {
connectorId?: string;
userInfo?: object;
userId?: string;
@ -51,34 +56,34 @@ interface RegisterSocialLogPayload extends RegisterSocialBindLogPayload {
redirectTo?: string;
}
interface SignInUsernamePasswordLogPayload extends BaseLogPayload {
interface SignInUsernamePasswordLogPayload extends CommonFields {
userId?: string;
username?: string;
}
interface SignInEmailSendPasscodeLogPayload extends BaseLogPayload {
interface SignInEmailSendPasscodeLogPayload extends CommonFields {
email?: string;
passcode?: Passcode;
}
interface SignInEmailLogPayload extends BaseLogPayload {
interface SignInEmailLogPayload extends CommonFields {
email?: string;
code?: string;
userId?: string;
}
interface SignInSmsSendPasscodeLogPayload extends BaseLogPayload {
interface SignInSmsSendPasscodeLogPayload extends CommonFields {
phone?: string;
passcode?: Passcode;
}
interface SignInSmsLogPayload extends BaseLogPayload {
interface SignInSmsLogPayload extends CommonFields {
phone?: string;
code?: string;
userId?: string;
}
interface SignInSocialBindLogPayload extends BaseLogPayload {
interface SignInSocialBindLogPayload extends CommonFields {
connectorId?: string;
userInfo?: object;
userId?: string;