0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-02-03 21:48:55 -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', () => { describe('koaLog middleware', () => {
const insertLogMock = insertLog as jest.Mock; const insertLogMock = insertLog as jest.Mock;
const next = jest.fn();
const type = 'SignInUsernamePassword'; const type = 'SignInUsernamePassword';
const payload: LogPayload = { const payload: LogPayload = {
userId: 'foo', userId: 'foo',
username: 'Bar', 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(() => { afterEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
}); });
it('insert log with success response', async () => { it('insert log with success response', async () => {
const ctx: WithLogContext<ReturnType<typeof createContextWithRouteParameters>> = { const ctx: WithLogContext<ReturnType<typeof createContextWithRouteParameters>> = {
...createContextWithRouteParameters(), ...createContextWithRouteParameters({ headers: { 'user-agent': userAgent } }),
log, // Bypass middleware context type assert log, // Bypass middleware context type assert
}; };
ctx.request.ip = ip;
next.mockImplementationOnce(async () => { const next = async () => {
ctx.log(type, payload); 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); await koaLog()(ctx, next);
expect(insertLogMock).toBeCalledWith({ expect(insertLogMock).toBeCalledWith({
@ -76,23 +51,25 @@ describe('koaLog middleware', () => {
payload: { payload: {
...payload, ...payload,
result: LogResult.Success, result: LogResult.Success,
ip,
userAgent,
}, },
}); });
}); });
it('should insert log with failed result if next throws error', async () => { it('should insert log with failed result if next throws error', async () => {
const ctx: WithLogContext<ReturnType<typeof createContextWithRouteParameters>> = { const ctx: WithLogContext<ReturnType<typeof createContextWithRouteParameters>> = {
...createContextWithRouteParameters(), ...createContextWithRouteParameters({ headers: { 'user-agent': userAgent } }),
log, // Bypass middleware context type assert log, // Bypass middleware context type assert
}; };
ctx.request.ip = ip;
const error = new Error('next error'); const error = new Error('next error');
next.mockImplementationOnce(async () => { const next = async () => {
ctx.log(type, payload); ctx.log(type, payload);
throw error; throw error;
}); };
await expect(koaLog()(ctx, next)).rejects.toMatchError(error); await expect(koaLog()(ctx, next)).rejects.toMatchError(error);
expect(insertLogMock).toBeCalledWith({ expect(insertLogMock).toBeCalledWith({
@ -102,6 +79,8 @@ describe('koaLog middleware', () => {
...payload, ...payload,
result: LogResult.Error, result: LogResult.Error,
error: String(error), error: String(error),
ip,
userAgent,
}, },
}); });
}); });

View file

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

View file

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