diff --git a/packages/core/src/app/init.test.ts b/packages/core/src/app/init.test.ts index 934c1fb25..ebdcae0b8 100644 --- a/packages/core/src/app/init.test.ts +++ b/packages/core/src/app/init.test.ts @@ -6,7 +6,6 @@ import * as koaLog from '@/middleware/koa-log'; import * as koaOIDCErrorHandler from '@/middleware/koa-oidc-error-handler'; import * as koaSlonikErrorHandler from '@/middleware/koa-slonik-error-handler'; import * as koaSpaProxy from '@/middleware/koa-spa-proxy'; -import * as koaUserLog from '@/middleware/koa-user-log'; import * as initOidc from '@/oidc/init'; import * as initRouter from '@/routes/init'; @@ -23,7 +22,6 @@ describe('App Init', () => { koaOIDCErrorHandler, koaSlonikErrorHandler, koaSpaProxy, - koaUserLog, ]; const initMethods = [initRouter, initOidc]; diff --git a/packages/core/src/app/init.ts b/packages/core/src/app/init.ts index 138c832ea..afc21f814 100644 --- a/packages/core/src/app/init.ts +++ b/packages/core/src/app/init.ts @@ -13,7 +13,6 @@ import koaLog from '@/middleware/koa-log'; import koaOIDCErrorHandler from '@/middleware/koa-oidc-error-handler'; import koaSlonikErrorHandler from '@/middleware/koa-slonik-error-handler'; import koaSpaProxy from '@/middleware/koa-spa-proxy'; -import koaUserLog from '@/middleware/koa-user-log'; import initOidc from '@/oidc/init'; import initRouter from '@/routes/init'; @@ -23,7 +22,6 @@ export default async function initApp(app: Koa): Promise { app.use(koaSlonikErrorHandler()); app.use(koaConnectorErrorHandler()); - app.use(koaUserLog()); app.use(koaLog()); app.use(koaLogger()); app.use(koaI18next()); diff --git a/packages/core/src/middleware/koa-user-log.test.ts b/packages/core/src/middleware/koa-user-log.test.ts deleted file mode 100644 index 31057c294..000000000 --- a/packages/core/src/middleware/koa-user-log.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { UserLogType, UserLogResult } from '@logto/schemas'; - -import { insertUserLog } from '@/queries/user-log'; -import { createContextWithRouteParameters } from '@/utils/test-utils'; - -import koaUserLog, { WithUserLogContext, UserLogContext } from './koa-user-log'; - -const nanoIdMock = 'mockId'; - -jest.mock('@/queries/user-log', () => ({ - insertUserLog: jest.fn(async () => Promise.resolve()), -})); - -jest.mock('nanoid', () => ({ - nanoid: jest.fn(() => nanoIdMock), -})); - -describe('koaUserLog middleware', () => { - const insertUserLogMock = insertUserLog as jest.Mock; - const next = jest.fn(); - - const userLogMock: Partial = { - userId: 'foo', - type: UserLogType.SignInEmail, - email: 'foo@logto.io', - payload: { applicationId: 'foo' }, - }; - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('insert userLog with success response', async () => { - const ctx: WithUserLogContext> = { - ...createContextWithRouteParameters(), - userLog: { - payload: {}, - createdAt: 0, - }, // Bypass middleware context type assert - }; - - next.mockImplementationOnce(async () => { - ctx.userLog = { - ...ctx.userLog, - ...userLogMock, - }; - }); - - await koaUserLog()(ctx, next); - expect(ctx.userLog).toHaveProperty('userId', userLogMock.userId); - expect(ctx.userLog).toHaveProperty('type', userLogMock.type); - expect(ctx.userLog).toHaveProperty('email', userLogMock.email); - expect(ctx.userLog).toHaveProperty('payload', userLogMock.payload); - expect(ctx.userLog.createdAt).not.toBeFalsy(); - expect(insertUserLogMock).toBeCalledWith({ - id: nanoIdMock, - userId: ctx.userLog.userId, - type: ctx.userLog.type, - result: UserLogResult.Success, - payload: ctx.userLog.payload, - }); - }); - - it('should not block request if insertLog throws error', async () => { - const ctx: WithUserLogContext> = { - ...createContextWithRouteParameters(), - userLog: { - payload: {}, - createdAt: 0, - }, // Bypass middleware context type assert - }; - - insertUserLogMock.mockRejectedValue(new Error(' ')); - - next.mockImplementationOnce(async () => { - ctx.userLog = { - ...ctx.userLog, - ...userLogMock, - }; - }); - - await koaUserLog()(ctx, next); - expect(ctx.userLog).toHaveProperty('userId', userLogMock.userId); - expect(ctx.userLog).toHaveProperty('type', userLogMock.type); - expect(ctx.userLog).toHaveProperty('email', userLogMock.email); - expect(ctx.userLog).toHaveProperty('payload', userLogMock.payload); - expect(ctx.userLog.createdAt).not.toBeFalsy(); - expect(insertUserLogMock).toBeCalledWith({ - id: nanoIdMock, - userId: ctx.userLog.userId, - type: ctx.userLog.type, - result: UserLogResult.Success, - payload: ctx.userLog.payload, - }); - }); - - it('should insert userLog with failed result if next throws error', async () => { - const ctx: WithUserLogContext> = { - ...createContextWithRouteParameters(), - userLog: { - payload: {}, - createdAt: 0, - }, // Bypass middleware context type assert - }; - - const error = new Error('next error'); - - next.mockImplementationOnce(async () => { - ctx.userLog = { - ...ctx.userLog, - ...userLogMock, - }; - throw error; - }); - - await expect(koaUserLog()(ctx, next)).rejects.toMatchError(error); - - expect(ctx.userLog.createdAt).not.toBeFalsy(); - expect(insertUserLogMock).toBeCalledWith({ - id: nanoIdMock, - userId: ctx.userLog.userId, - type: ctx.userLog.type, - result: UserLogResult.Failed, - payload: ctx.userLog.payload, - }); - }); -}); diff --git a/packages/core/src/middleware/koa-user-log.ts b/packages/core/src/middleware/koa-user-log.ts deleted file mode 100644 index f59824492..000000000 --- a/packages/core/src/middleware/koa-user-log.ts +++ /dev/null @@ -1,61 +0,0 @@ -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: UserLogContext; -}; - -export interface UserLogContext { - type?: UserLogType; - userId?: string; - username?: string; - email?: string; - phone?: string; - connectorId?: 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.test.ts b/packages/core/src/queries/user-log.test.ts deleted file mode 100644 index 4abe8ad08..000000000 --- a/packages/core/src/queries/user-log.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { UserLogs } from '@logto/schemas'; -import { createMockPool, createMockQueryResult, sql } from 'slonik'; -import { snakeCase } from 'snake-case'; - -import { mockUserLog } from '@/__mocks__'; -import { - convertToIdentifiers, - excludeAutoSetFields, - convertToPrimitiveOrSql, -} from '@/database/utils'; -import { expectSqlAssert, QueryType } from '@/utils/test-utils'; - -import { insertUserLog, findLogsByUserId } from './user-log'; - -const mockQuery: jest.MockedFunction = jest.fn(); - -jest.mock('@/database/pool', () => - createMockPool({ - query: async (sql, values) => { - return mockQuery(sql, values); - }, - }) -); - -describe('user-log query', () => { - const { table, fields } = convertToIdentifiers(UserLogs); - const dbvalue = { ...mockUserLog, payload: JSON.stringify(mockUserLog.payload) }; - - it('findLogsByUserId', async () => { - const userId = 'foo'; - const expectSql = sql` - select ${sql.join(Object.values(fields), sql`,`)} - from ${table} - where ${fields.userId}=${userId} - order by created_at desc - `; - - mockQuery.mockImplementationOnce(async (sql, values) => { - expectSqlAssert(sql, expectSql.sql); - expect(values).toEqual([userId]); - - return createMockQueryResult([dbvalue]); - }); - - await expect(findLogsByUserId(userId)).resolves.toEqual([dbvalue]); - }); - - it('insertUserLog', async () => { - const keys = excludeAutoSetFields(UserLogs.fieldKeys); - - // eslint-disable-next-line sql/no-unsafe-query - const expectSql = ` - insert into "user_logs" (${keys.map((k) => `"${snakeCase(k)}"`).join(', ')}) - values (${keys.map((_, index) => `$${index + 1}`).join(', ')}) - `; - - mockQuery.mockImplementationOnce(async (sql, values) => { - expectSqlAssert(sql, expectSql); - expect(values).toEqual(keys.map((k) => convertToPrimitiveOrSql(k, mockUserLog[k]))); - - return createMockQueryResult([]); - }); - - await insertUserLog(mockUserLog); - }); -}); diff --git a/packages/core/src/queries/user-log.ts b/packages/core/src/queries/user-log.ts deleted file mode 100644 index e4b8b7e97..000000000 --- a/packages/core/src/queries/user-log.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { CreateUserLog, 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 5497e735c..3a22e3f89 100644 --- a/packages/core/src/routes/session.ts +++ b/packages/core/src/routes/session.ts @@ -2,7 +2,7 @@ import path from 'path'; import { LogtoErrorCode } from '@logto/phrases'; -import { PasscodeType, UserLogType, userInfoSelectFields } from '@logto/schemas'; +import { PasscodeType, userInfoSelectFields } from '@logto/schemas'; import { conditional } from '@silverhand/essentials'; import pick from 'lodash.pick'; import { Provider } from 'oidc-provider'; @@ -68,14 +68,9 @@ export default function sessionRoutes(router: T, prov }), async (ctx, next) => { const { username, password } = ctx.guard.body; - ctx.userLog.username = username; - ctx.userLog.type = UserLogType.SignInUsernameAndPassword; - assertThat(password, 'session.insufficient_info'); const { id } = await findUserByUsernameAndPassword(username, password); - ctx.userLog.userId = id; - await assignInteractionResults(ctx, provider, { login: { accountId: id } }); return next(); @@ -88,8 +83,6 @@ export default function sessionRoutes(router: T, prov async (ctx, next) => { const { jti } = await provider.interactionDetails(ctx.req, ctx.res); const { phone } = ctx.guard.body; - ctx.userLog.phone = phone; - ctx.userLog.type = UserLogType.SignInPhone; assertThat( await hasUserWithPhone(phone), @@ -110,8 +103,6 @@ export default function sessionRoutes(router: T, prov async (ctx, next) => { const { jti } = await provider.interactionDetails(ctx.req, ctx.res); const { phone, code } = ctx.guard.body; - ctx.userLog.phone = phone; - ctx.userLog.type = UserLogType.SignInPhone; assertThat( await hasUserWithPhone(phone), @@ -120,7 +111,6 @@ export default function sessionRoutes(router: T, prov await verifyPasscode(jti, PasscodeType.SignIn, code, { phone }); const { id } = await findUserByPhone(phone); - ctx.userLog.userId = id; await assignInteractionResults(ctx, provider, { login: { accountId: id } }); @@ -134,8 +124,6 @@ export default function sessionRoutes(router: T, prov async (ctx, next) => { const { jti } = await provider.interactionDetails(ctx.req, ctx.res); const { email } = ctx.guard.body; - ctx.userLog.email = email; - ctx.userLog.type = UserLogType.SignInEmail; assertThat( await hasUserWithEmail(email), @@ -156,8 +144,6 @@ export default function sessionRoutes(router: T, prov async (ctx, next) => { const { jti } = await provider.interactionDetails(ctx.req, ctx.res); const { email, code } = ctx.guard.body; - ctx.userLog.email = email; - ctx.userLog.type = UserLogType.SignInEmail; assertThat( await hasUserWithEmail(email), @@ -166,7 +152,6 @@ export default function sessionRoutes(router: T, prov await verifyPasscode(jti, PasscodeType.SignIn, code, { email }); const { id } = await findUserByEmail(email); - ctx.userLog.userId = id; await assignInteractionResults(ctx, provider, { login: { accountId: id } }); @@ -186,8 +171,6 @@ export default function sessionRoutes(router: T, prov }), async (ctx, next) => { const { connectorId, code, state, redirectUri } = ctx.guard.body; - ctx.userLog.connectorId = connectorId; - ctx.userLog.type = UserLogType.SignInSocial; if (!code) { assertThat(state && redirectUri, 'session.insufficient_info'); @@ -214,7 +197,6 @@ export default function sessionRoutes(router: T, prov } const { id, identities } = await findUserByIdentity(connectorId, userInfo.id); - ctx.userLog.userId = id; // Update social connector's user info await updateUserById(id, { @@ -232,22 +214,15 @@ export default function sessionRoutes(router: T, prov body: object({ connectorId: string() }), }), async (ctx, next) => { - ctx.userLog.type = UserLogType.SignInSocial; const { connectorId } = ctx.guard.body; const { result } = await provider.interactionDetails(ctx.req, ctx.res); - assertThat(result, 'session.connector_session_not_found'); - ctx.userLog.connectorId = connectorId; - const userInfo = await getUserInfoFromInteractionResult(connectorId, result); const relatedInfo = await findSocialRelatedUser(userInfo); - assertThat(relatedInfo, 'session.connector_session_not_found'); const { id, identities } = relatedInfo[1]; - ctx.userLog.userId = id; - await updateUserById(id, { identities: { ...identities, [connectorId]: { userId: userInfo.id, details: userInfo } }, }); @@ -302,8 +277,6 @@ export default function sessionRoutes(router: T, prov }), async (ctx, next) => { const { username, password } = ctx.guard.body; - ctx.userLog.username = username; - ctx.userLog.type = UserLogType.RegisterUsernameAndPassword; assertThat( password, @@ -321,8 +294,6 @@ export default function sessionRoutes(router: T, prov ); const id = await generateUserId(); - ctx.userLog.userId = id; - const { passwordEncryptionSalt, passwordEncrypted, passwordEncryptionMethod } = encryptUserPassword(id, password); @@ -355,10 +326,8 @@ export default function sessionRoutes(router: T, prov '/session/register/passwordless/sms/send-passcode', koaGuard({ body: object({ phone: string().regex(phoneRegEx) }) }), async (ctx, next) => { - ctx.userLog.type = UserLogType.RegisterPhone; const { jti } = await provider.interactionDetails(ctx.req, ctx.res); const { phone } = ctx.guard.body; - ctx.userLog.phone = phone; assertThat( !(await hasUserWithPhone(phone)), @@ -379,8 +348,6 @@ export default function sessionRoutes(router: T, prov async (ctx, next) => { const { jti } = await provider.interactionDetails(ctx.req, ctx.res); const { phone, code } = ctx.guard.body; - ctx.userLog.phone = phone; - ctx.userLog.type = UserLogType.RegisterPhone; assertThat( !(await hasUserWithPhone(phone)), @@ -389,7 +356,6 @@ export default function sessionRoutes(router: T, prov await verifyPasscode(jti, PasscodeType.Register, code, { phone }); const id = await generateUserId(); - ctx.userLog.userId = id; await insertUser({ id, primaryPhone: phone }); await assignInteractionResults(ctx, provider, { login: { accountId: id } }); @@ -404,8 +370,6 @@ export default function sessionRoutes(router: T, prov async (ctx, next) => { const { jti } = await provider.interactionDetails(ctx.req, ctx.res); const { email } = ctx.guard.body; - ctx.userLog.email = email; - ctx.userLog.type = UserLogType.RegisterEmail; assertThat( !(await hasUserWithEmail(email)), @@ -426,8 +390,6 @@ export default function sessionRoutes(router: T, prov async (ctx, next) => { const { jti } = await provider.interactionDetails(ctx.req, ctx.res); const { email, code } = ctx.guard.body; - ctx.userLog.email = email; - ctx.userLog.type = UserLogType.RegisterEmail; assertThat( !(await hasUserWithEmail(email)), @@ -436,7 +398,6 @@ export default function sessionRoutes(router: T, prov await verifyPasscode(jti, PasscodeType.Register, code, { email }); const id = await generateUserId(); - ctx.userLog.userId = id; await insertUser({ id, primaryEmail: email }); await assignInteractionResults(ctx, provider, { login: { accountId: id } }); diff --git a/packages/core/src/routes/types.ts b/packages/core/src/routes/types.ts index c65a51122..886b246a4 100644 --- a/packages/core/src/routes/types.ts +++ b/packages/core/src/routes/types.ts @@ -2,11 +2,11 @@ import Router from 'koa-router'; import { WithAuthContext } from '@/middleware/koa-auth'; import { WithI18nContext } from '@/middleware/koa-i18next'; +import { WithLogContext } from '@/middleware/koa-log'; import { WithUserInfoContext } from '@/middleware/koa-user-info'; -import { WithUserLogContext } from '@/middleware/koa-user-log'; -export type AnonymousRouter = Router>; +export type AnonymousRouter = Router>; export type AuthedRouter = Router< unknown, - WithUserInfoContext>> + WithUserInfoContext>> >;