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