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:
parent
4521c3c8d1
commit
0ecb7e4d2f
3 changed files with 96 additions and 88 deletions
|
@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Reference in a new issue