From 1fc73030e8bd26af01f97e465096ec97a94c2912 Mon Sep 17 00:00:00 2001 From: Wang Sijie Date: Wed, 8 Dec 2021 15:56:57 +0800 Subject: [PATCH] feat(core): sign in logs (#139) * fix(core): add log result type * fix: comments in sql * feat(core): sign in log * refactor: insert error log in middleware * fix: pr fix * feat: userLog middleware * refactor: auto capture log * fix: pr fix * fix: pr fix --- packages/core/src/app/init.ts | 3 ++ packages/core/src/middleware/koa-user-log.ts | 56 ++++++++++++++++++++ packages/core/src/queries/user-log.ts | 18 +++++++ packages/core/src/routes/session.ts | 5 ++ packages/core/src/routes/types.ts | 8 ++- 5 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 packages/core/src/middleware/koa-user-log.ts create mode 100644 packages/core/src/queries/user-log.ts diff --git a/packages/core/src/app/init.ts b/packages/core/src/app/init.ts index 1134cee76..85cafa96f 100644 --- a/packages/core/src/app/init.ts +++ b/packages/core/src/app/init.ts @@ -8,11 +8,14 @@ import { port } from '@/env/consts'; import koaErrorHandler from '@/middleware/koa-error-handler'; import koaI18next from '@/middleware/koa-i18next'; import koaUIProxy from '@/middleware/koa-ui-proxy'; +import koaUserLog from '@/middleware/koa-user-log'; import initOidc from '@/oidc/init'; import initRouter from '@/routes/init'; export default async function initApp(app: Koa): Promise { app.use(koaErrorHandler()); + // TODO move to specific router (LOG-454) + app.use(koaUserLog()); app.use(koaLogger()); app.use(koaI18next()); diff --git a/packages/core/src/middleware/koa-user-log.ts b/packages/core/src/middleware/koa-user-log.ts new file mode 100644 index 000000000..dd289d496 --- /dev/null +++ b/packages/core/src/middleware/koa-user-log.ts @@ -0,0 +1,56 @@ +import { UserLogPayload, UserLogResult, UserLogType } from '@logto/schemas'; +import { Context, MiddlewareType } from 'koa'; +import { nanoid } from 'nanoid'; + +import { insertUserLog } from '@/queries/user-log'; + +export type WithUserLogContext = ContextT & { + userLog: LogContext; +}; + +export interface LogContext { + type?: UserLogType; + userId?: string; + payload: UserLogPayload; + createdAt: number; +} + +const insertLog = async (ctx: WithUserLogContext, result: UserLogResult) => { + // Insert log if log context is set properly. + if (ctx.userLog.userId && ctx.userLog.type) { + try { + await insertUserLog({ + id: nanoid(), + userId: ctx.userLog.userId, + type: ctx.userLog.type, + result, + payload: ctx.userLog.payload, + }); + } catch (error: unknown) { + console.error('An error occured while inserting user log'); + console.error(error); + } + } +}; + +export default function koaUserLog(): MiddlewareType< + StateT, + WithUserLogContext, + ResponseBodyT +> { + return async (ctx, next) => { + ctx.userLog = { + createdAt: Date.now(), + payload: {}, + }; + + try { + await next(); + await insertLog(ctx, UserLogResult.Success); + return; + } catch (error: unknown) { + await insertLog(ctx, UserLogResult.Failed); + throw error; + } + }; +} diff --git a/packages/core/src/queries/user-log.ts b/packages/core/src/queries/user-log.ts new file mode 100644 index 000000000..80f99c426 --- /dev/null +++ b/packages/core/src/queries/user-log.ts @@ -0,0 +1,18 @@ +import { UserLogDBEntry, UserLogs } from '@logto/schemas'; +import { sql } from 'slonik'; + +import { buildInsertInto } from '@/database/insert-into'; +import pool from '@/database/pool'; +import { convertToIdentifiers } from '@/database/utils'; + +const { table, fields } = convertToIdentifiers(UserLogs); + +export const insertUserLog = buildInsertInto(pool, UserLogs); + +export const findLogsByUserId = async (userId: string) => + pool.many(sql` + select ${sql.join(Object.values(fields), sql`,`)} + from ${table} + where ${fields.userId}=${userId} + order by created_at desc + `); diff --git a/packages/core/src/routes/session.ts b/packages/core/src/routes/session.ts index 37661e689..c8059f8c6 100644 --- a/packages/core/src/routes/session.ts +++ b/packages/core/src/routes/session.ts @@ -1,4 +1,5 @@ import { LogtoErrorCode } from '@logto/phrases'; +import { UserLogType } from '@logto/schemas'; import { conditional } from '@silverhand/essentials'; import { Provider } from 'oidc-provider'; import { object, string } from 'zod'; @@ -30,10 +31,14 @@ export default function sessionRoutes(router: T, prov const { id, passwordEncrypted, passwordEncryptionMethod, passwordEncryptionSalt } = await findUserByUsername(username); + ctx.userLog.userId = id; + ctx.userLog.type = UserLogType.SignInUsernameAndPassword; + assertThat( passwordEncrypted && passwordEncryptionMethod && passwordEncryptionSalt, 'session.invalid_sign_in_method' ); + assertThat( encryptPassword(id, password, passwordEncryptionSalt, passwordEncryptionMethod) === passwordEncrypted, diff --git a/packages/core/src/routes/types.ts b/packages/core/src/routes/types.ts index 793f370b1..c65a51122 100644 --- a/packages/core/src/routes/types.ts +++ b/packages/core/src/routes/types.ts @@ -3,6 +3,10 @@ import Router from 'koa-router'; import { WithAuthContext } from '@/middleware/koa-auth'; import { WithI18nContext } from '@/middleware/koa-i18next'; import { WithUserInfoContext } from '@/middleware/koa-user-info'; +import { WithUserLogContext } from '@/middleware/koa-user-log'; -export type AnonymousRouter = Router; -export type AuthedRouter = Router>>; +export type AnonymousRouter = Router>; +export type AuthedRouter = Router< + unknown, + WithUserInfoContext>> +>;