0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-31 22:51:25 -05:00

refactor(core): remove deprecated koa-user-log middleware (#591)

* refactor(core): remove koa-user-log middleware

* refactor(core): remove user-log queries
This commit is contained in:
IceHe.xyz 2022-04-20 15:11:43 +08:00 committed by GitHub
parent 93a93b4c8f
commit d4e241b661
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 4 additions and 319 deletions

View file

@ -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];

View file

@ -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<void> {
app.use(koaSlonikErrorHandler());
app.use(koaConnectorErrorHandler());
app.use(koaUserLog());
app.use(koaLog());
app.use(koaLogger());
app.use(koaI18next());

View file

@ -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<UserLogContext> = {
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<ReturnType<typeof createContextWithRouteParameters>> = {
...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<ReturnType<typeof createContextWithRouteParameters>> = {
...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<ReturnType<typeof createContextWithRouteParameters>> = {
...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,
});
});
});

View file

@ -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> = 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<Context>, 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<StateT, ContextT, ResponseBodyT>(): MiddlewareType<
StateT,
WithUserLogContext<ContextT>,
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;
}
};
}

View file

@ -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<QueryType> = 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);
});
});

View file

@ -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<CreateUserLog>(pool, UserLogs);
export const findLogsByUserId = async (userId: string) =>
pool.many<CreateUserLog>(sql`
select ${sql.join(Object.values(fields), sql`,`)}
from ${table}
where ${fields.userId}=${userId}
order by created_at desc
`);

View file

@ -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<T extends AnonymousRouter>(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<T extends AnonymousRouter>(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<T extends AnonymousRouter>(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<T extends AnonymousRouter>(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<T extends AnonymousRouter>(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<T extends AnonymousRouter>(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<T extends AnonymousRouter>(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<T extends AnonymousRouter>(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<T extends AnonymousRouter>(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<T extends AnonymousRouter>(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<T extends AnonymousRouter>(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<T extends AnonymousRouter>(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<T extends AnonymousRouter>(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<T extends AnonymousRouter>(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<T extends AnonymousRouter>(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<T extends AnonymousRouter>(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<T extends AnonymousRouter>(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<T extends AnonymousRouter>(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 } });

View file

@ -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<unknown, WithUserLogContext<WithI18nContext>>;
export type AnonymousRouter = Router<unknown, WithLogContext<WithI18nContext>>;
export type AuthedRouter = Router<
unknown,
WithUserInfoContext<WithAuthContext<WithUserLogContext<WithI18nContext>>>
WithUserInfoContext<WithAuthContext<WithLogContext<WithI18nContext>>>
>;