diff --git a/packages/core/src/lib/user.test.ts b/packages/core/src/lib/user.test.ts index 898e96236..34723dd5a 100644 --- a/packages/core/src/lib/user.test.ts +++ b/packages/core/src/lib/user.test.ts @@ -1,22 +1,20 @@ import { UsersPasswordEncryptionMethod } from '@logto/schemas'; -import { hasUserWithId, findUserById } from '@/queries/user'; +import { hasUserWithId } from '@/queries/user'; -import { encryptUserPassword, generateUserId, findUserSignInMethodsById } from './user'; +import { encryptUserPassword, generateUserId } from './user'; -jest.mock('@/queries/user', () => ({ - findUserById: jest.fn(), - hasUserWithId: jest.fn(), -})); +jest.mock('@/queries/user'); describe('generateUserId()', () => { afterEach(() => { - jest.clearAllMocks(); + (hasUserWithId as jest.MockedFunction).mockClear(); }); it('generates user ID with correct length when no conflict found', async () => { - const mockedHasUserWithId = hasUserWithId as jest.Mock; - mockedHasUserWithId.mockImplementationOnce(async () => false); + const mockedHasUserWithId = ( + hasUserWithId as jest.MockedFunction + ).mockImplementationOnce(async () => false); await expect(generateUserId()).resolves.toHaveLength(12); expect(mockedHasUserWithId).toBeCalledTimes(1); @@ -25,8 +23,9 @@ describe('generateUserId()', () => { it('generates user ID with correct length when retry limit is not reached', async () => { // eslint-disable-next-line @silverhand/fp/no-let let tried = 0; - const mockedHasUserWithId = hasUserWithId as jest.Mock; - mockedHasUserWithId.mockImplementation(async () => { + const mockedHasUserWithId = ( + hasUserWithId as jest.MockedFunction + ).mockImplementation(async () => { if (tried) { return false; } @@ -42,8 +41,9 @@ describe('generateUserId()', () => { }); it('rejects with correct error message when retry limit is reached', async () => { - const mockedHasUserWithId = hasUserWithId as jest.Mock; - mockedHasUserWithId.mockImplementation(async () => true); + const mockedHasUserWithId = ( + hasUserWithId as jest.MockedFunction + ).mockImplementation(async () => true); await expect(generateUserId(10)).rejects.toThrow( 'Cannot generate user ID in reasonable retries' @@ -61,78 +61,3 @@ describe('encryptUserPassword()', () => { expect(passwordEncryptionSalt).toHaveLength(21); }); }); - -describe('findUserSignInMethodsById()', () => { - afterEach(() => { - jest.clearAllMocks(); - }); - - it('generate and test user with username and password sign-in method', async () => { - const mockFindUserById = findUserById as jest.Mock; - mockFindUserById.mockResolvedValue({ - username: 'abcd', - passwordEncrypted: '1234567890', - passwordEncryptionMethod: UsersPasswordEncryptionMethod.SaltAndPepper, - passwordEncryptionSalt: '123456790', - }); - const { usernameAndPassword, emailPasswordless, phonePasswordless, social } = - await findUserSignInMethodsById(''); - expect(usernameAndPassword).toEqual(true); - expect(emailPasswordless).toBeFalsy(); - expect(phonePasswordless).toBeFalsy(); - expect(social).toBeFalsy(); - }); - - it('generate and test user with email passwordless sign-in method', async () => { - const mockFindUserById = findUserById as jest.Mock; - mockFindUserById.mockResolvedValue({ - primaryEmail: 'b@a.com', - identities: {}, - }); - const { usernameAndPassword, emailPasswordless, phonePasswordless, social } = - await findUserSignInMethodsById(''); - expect(usernameAndPassword).toBeFalsy(); - expect(emailPasswordless).toEqual(true); - expect(phonePasswordless).toBeFalsy(); - expect(social).toBeFalsy(); - }); - - it('generate and test user with phone passwordless sign-in method', async () => { - const mockFindUserById = findUserById as jest.Mock; - mockFindUserById.mockResolvedValue({ - primaryPhone: '13000000000', - }); - const { usernameAndPassword, emailPasswordless, phonePasswordless, social } = - await findUserSignInMethodsById(''); - expect(usernameAndPassword).toBeFalsy(); - expect(emailPasswordless).toBeFalsy(); - expect(phonePasswordless).toEqual(true); - expect(social).toBeFalsy(); - }); - - it('generate and test user with social sign-in method (single social connector information in record)', async () => { - const mockFindUserById = findUserById as jest.Mock; - mockFindUserById.mockResolvedValue({ - identities: { connector1: { userId: 'foo1' } }, - }); - const { usernameAndPassword, emailPasswordless, phonePasswordless, social } = - await findUserSignInMethodsById(''); - expect(usernameAndPassword).toBeFalsy(); - expect(emailPasswordless).toBeFalsy(); - expect(phonePasswordless).toBeFalsy(); - expect(social).toEqual(true); - }); - - it('generate and test user with social sign-in method (multiple social connectors information in record)', async () => { - const mockFindUserById = findUserById as jest.Mock; - mockFindUserById.mockResolvedValue({ - identities: { connector1: { userId: 'foo1' }, connector2: { userId: 'foo2' } }, - }); - const { usernameAndPassword, emailPasswordless, phonePasswordless, social } = - await findUserSignInMethodsById(''); - expect(usernameAndPassword).toBeFalsy(); - expect(emailPasswordless).toBeFalsy(); - expect(phonePasswordless).toBeFalsy(); - expect(social).toEqual(true); - }); -}); diff --git a/packages/core/src/lib/user.ts b/packages/core/src/lib/user.ts index fad38bdcc..3b5349378 100644 --- a/packages/core/src/lib/user.ts +++ b/packages/core/src/lib/user.ts @@ -2,7 +2,7 @@ import { UsersPasswordEncryptionMethod, User } from '@logto/schemas'; import { nanoid } from 'nanoid'; import pRetry from 'p-retry'; -import { findUserById, findUserByUsername, hasUserWithId } from '@/queries/user'; +import { findUserByUsername, hasUserWithId } from '@/queries/user'; import assertThat from '@/utils/assert-that'; import { buildIdGenerator } from '@/utils/id'; import { encryptPassword } from '@/utils/password'; @@ -43,36 +43,6 @@ export const encryptUserPassword = ( return { passwordEncrypted, passwordEncryptionMethod, passwordEncryptionSalt }; }; -export const findUserSignInMethodsById = async ( - id: string -): Promise<{ - usernameAndPassword: boolean; - emailPasswordless: boolean; - phonePasswordless: boolean; - social: boolean; -}> => { - const user = await findUserById(id); - const { - username, - passwordEncrypted, - passwordEncryptionMethod, - passwordEncryptionSalt, - primaryEmail, - primaryPhone, - identities, - } = user; - - const usernameAndPassword = Boolean( - username && passwordEncrypted && passwordEncryptionMethod && passwordEncryptionSalt - ); - const emailPasswordless = Boolean(primaryEmail); - const phonePasswordless = Boolean(primaryPhone); - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - const social = identities && Object.keys(identities).length > 0; - - return { usernameAndPassword, emailPasswordless, phonePasswordless, social }; -}; - export const findUserByUsernameAndPassword = async ( username: string, password: string diff --git a/packages/core/src/routes/session.test.ts b/packages/core/src/routes/session.test.ts index 5d365afb5..840ddcdf5 100644 --- a/packages/core/src/routes/session.test.ts +++ b/packages/core/src/routes/session.test.ts @@ -729,132 +729,6 @@ describe('sessionRoutes', () => { }); }); - describe('POST /session/forgot-password/phone/send-passcode', () => { - beforeAll(() => { - interactionDetails.mockResolvedValueOnce({ - jti: 'jti', - }); - }); - - it('throw if no user can be found with phone', async () => { - const response = await sessionRequest - .post('/session/forgot-password/phone/send-passcode') - .send({ phone: '13000000001' }); - expect(response).toHaveProperty('statusCode', 422); - }); - - it('create and send passcode', async () => { - const response = await sessionRequest - .post('/session/forgot-password/phone/send-passcode') - .send({ phone: '13000000000' }); - expect(response.statusCode).toEqual(204); - expect(sendPasscode).toHaveBeenCalled(); - }); - }); - - describe('POST /session/forgot-password/phone/verify-passcode-and-reset-password', () => { - beforeAll(() => { - interactionDetails.mockResolvedValueOnce({ - jti: 'jti', - }); - }); - - it('throw if no user can be found with phone', async () => { - const response = await sessionRequest - .post('/session/forgot-password/phone/verify-passcode-and-reset-password') - .send({ phone: '13000000001', code: '1234', password: '123456' }); - expect(response).toHaveProperty('statusCode', 422); - }); - - it('fail to verify passcode', async () => { - const response = await sessionRequest - .post('/session/forgot-password/phone/verify-passcode-and-reset-password') - .send({ phone: '13000000000', code: '1231', password: '123456' }); - expect(response).toHaveProperty('statusCode', 400); - }); - - it('verify passcode, reset password and assign result', async () => { - const response = await sessionRequest - .post('/session/forgot-password/phone/verify-passcode-and-reset-password') - .send({ phone: '13000000000', code: '1234', password: '123456' }); - expect(response).toHaveProperty('statusCode', 200); - expect(updateUserById).toHaveBeenCalledWith('id', { - passwordEncryptionSalt: 'user1', - passwordEncrypted: 'id_123456_user1', - passwordEncryptionMethod: 'SaltAndPepper', - }); - expect(interactionResult).toHaveBeenCalledWith( - expect.anything(), - expect.anything(), - expect.objectContaining({ login: { accountId: 'id' } }), - expect.anything() - ); - }); - }); - - describe('POST /session/forgot-password/email/send-passcode', () => { - beforeAll(() => { - interactionDetails.mockResolvedValueOnce({ - jti: 'jti', - }); - }); - - it('throw if no user can be found with email', async () => { - const response = await sessionRequest - .post('/session/forgot-password/email/send-passcode') - .send({ email: 'b@a.com' }); - expect(response).toHaveProperty('statusCode', 422); - }); - - it('create and send passcode', async () => { - const response = await sessionRequest - .post('/session/forgot-password/email/send-passcode') - .send({ email: 'a@a.com' }); - expect(response.statusCode).toEqual(204); - expect(sendPasscode).toHaveBeenCalled(); - }); - }); - - describe('POST /session/forgot-password/email/verify-passcode-and-reset-password', () => { - beforeAll(() => { - interactionDetails.mockResolvedValueOnce({ - jti: 'jti', - }); - }); - - it('throw if no user can be found with email', async () => { - const response = await sessionRequest - .post('/session/forgot-password/email/verify-passcode-and-reset-password') - .send({ email: 'b@a.com', code: '1234', password: '123456' }); - expect(response).toHaveProperty('statusCode', 422); - }); - - it('fail to verify passcode', async () => { - const response = await sessionRequest - .post('/session/forgot-password/email/verify-passcode-and-reset-password') - .send({ email: 'a@a.com', code: '1231', password: '123456' }); - expect(response).toHaveProperty('statusCode', 400); - }); - - it('verify passcode, reset password and assign result', async () => { - const response = await sessionRequest - .post('/session/forgot-password/email/verify-passcode-and-reset-password') - .send({ email: 'a@a.com', code: '1234', password: '123456' }); - expect(response).toHaveProperty('statusCode', 200); - expect(updateUserById).toHaveBeenCalledWith('id', { - passwordEncryptionSalt: 'user1', - passwordEncrypted: 'id_123456_user1', - passwordEncryptionMethod: 'SaltAndPepper', - }); - expect(interactionResult).toHaveBeenCalledWith( - expect.anything(), - expect.anything(), - expect.objectContaining({ login: { accountId: 'id' } }), - expect.anything() - ); - }); - }); - describe('POST /session/bind-social', () => { it('throw if session is not authorized', async () => { interactionDetails.mockResolvedValueOnce({}); diff --git a/packages/core/src/routes/session.ts b/packages/core/src/routes/session.ts index b9937a05a..bead1ed94 100644 --- a/packages/core/src/routes/session.ts +++ b/packages/core/src/routes/session.ts @@ -16,7 +16,7 @@ import { getUserInfoByAuthCode, getUserInfoFromInteractionResult, } from '@/lib/social'; -import { generateUserId, encryptUserPassword, findUserByUsernameAndPassword } from '@/lib/user'; +import { encryptUserPassword, generateUserId, findUserByUsernameAndPassword } from '@/lib/user'; import koaGuard from '@/middleware/koa-guard'; import { hasUserWithEmail, @@ -475,128 +475,6 @@ export default function sessionRoutes(router: T, prov } ); - router.post( - '/session/forgot-password/phone/send-passcode', - koaGuard({ body: object({ phone: string().regex(phoneRegEx) }) }), - 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.ForgotPasswordPhone; - - assertThat( - await hasUserWithPhone(phone), - new RequestError({ code: 'user.phone_not_exists', status: 422 }) - ); - const { id } = await findUserByPhone(phone); - ctx.userLog.userId = id; - - const passcode = await createPasscode(jti, PasscodeType.ForgotPassword, { phone }); - await sendPasscode(passcode); - ctx.status = 204; - - return next(); - } - ); - - router.post( - '/session/forgot-password/phone/verify-passcode-and-reset-password', - koaGuard({ - body: object({ - phone: string().regex(phoneRegEx), - code: string(), - password: string().regex(passwordRegEx), - }), - }), - async (ctx, next) => { - const { jti } = await provider.interactionDetails(ctx.req, ctx.res); - const { phone, code, password } = ctx.guard.body; - ctx.userLog.phone = phone; - ctx.userLog.type = UserLogType.ForgotPasswordPhone; - - assertThat( - await hasUserWithPhone(phone), - new RequestError({ code: 'user.phone_not_exists', status: 422 }) - ); - - await verifyPasscode(jti, PasscodeType.ForgotPassword, code, { phone }); - const { id } = await findUserByPhone(phone); - ctx.userLog.userId = id; - - const { passwordEncryptionSalt, passwordEncrypted, passwordEncryptionMethod } = - encryptUserPassword(id, password); - await updateUserById(id, { - passwordEncryptionSalt, - passwordEncrypted, - passwordEncryptionMethod, - }); - await assignInteractionResults(ctx, provider, { login: { accountId: id } }); - - return next(); - } - ); - - router.post( - '/session/forgot-password/email/send-passcode', - koaGuard({ body: object({ email: string().regex(emailRegEx) }) }), - 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.ForgotPasswordEmail; - - assertThat( - await hasUserWithEmail(email), - new RequestError({ code: 'user.email_not_exists', status: 422 }) - ); - const { id } = await findUserByEmail(email); - ctx.userLog.userId = id; - - const passcode = await createPasscode(jti, PasscodeType.ForgotPassword, { email }); - await sendPasscode(passcode); - ctx.status = 204; - - return next(); - } - ); - - router.post( - '/session/forgot-password/email/verify-passcode-and-reset-password', - koaGuard({ - body: object({ - email: string().regex(emailRegEx), - code: string(), - password: string().regex(passwordRegEx), - }), - }), - async (ctx, next) => { - const { jti } = await provider.interactionDetails(ctx.req, ctx.res); - const { email, code, password } = ctx.guard.body; - ctx.userLog.email = email; - ctx.userLog.type = UserLogType.ForgotPasswordEmail; - - assertThat( - await hasUserWithEmail(email), - new RequestError({ code: 'user.email_not_exists', status: 422 }) - ); - - await verifyPasscode(jti, PasscodeType.ForgotPassword, code, { email }); - const { id } = await findUserByEmail(email); - ctx.userLog.userId = id; - - const { passwordEncryptionSalt, passwordEncrypted, passwordEncryptionMethod } = - encryptUserPassword(id, password); - await updateUserById(id, { - passwordEncryptionSalt, - passwordEncrypted, - passwordEncryptionMethod, - }); - await assignInteractionResults(ctx, provider, { login: { accountId: id } }); - - return next(); - } - ); - router.post( '/session/bind-social', koaGuard({ diff --git a/packages/phrases/src/locales/en.ts b/packages/phrases/src/locales/en.ts index a4a28e33e..4d860d32e 100644 --- a/packages/phrases/src/locales/en.ts +++ b/packages/phrases/src/locales/en.ts @@ -161,8 +161,6 @@ const errors = { phone_not_exists: 'The phone number has not been registered yet.', identity_not_exists: 'The social account has not been registered yet.', identity_exists: 'The social account has been registered.', - username_password_signin_not_exists: - 'Signing in with username and password has not been enabled for this user.', }, password: { unsupported_encryption_method: 'The encryption method {{name}} is not supported.', diff --git a/packages/phrases/src/locales/zh-cn.ts b/packages/phrases/src/locales/zh-cn.ts index 476162586..d8d73cf7d 100644 --- a/packages/phrases/src/locales/zh-cn.ts +++ b/packages/phrases/src/locales/zh-cn.ts @@ -162,7 +162,6 @@ const errors = { phone_not_exists: '手机号码尚未注册。', identity_not_exists: '该社交账号尚未注册。', identity_exists: '该社交账号已被注册。', - username_password_signin_not_exists: '该账号暂未开通账号密码登录方式。', }, password: { unsupported_encryption_method: '不支持的加密方法 {{name}}。', diff --git a/packages/schemas/src/db-entries/custom-types.ts b/packages/schemas/src/db-entries/custom-types.ts index 2091defc6..a87dbc9f9 100644 --- a/packages/schemas/src/db-entries/custom-types.ts +++ b/packages/schemas/src/db-entries/custom-types.ts @@ -19,8 +19,6 @@ export enum UserLogType { RegisterEmail = 'RegisterEmail', RegisterPhone = 'RegisterPhone', RegisterSocial = 'RegisterSocial', - ForgotPasswordEmail = 'ForgotPasswordEmail', - ForgotPasswordPhone = 'ForgotPasswordPhone', ExchangeAccessToken = 'ExchangeAccessToken', } export enum UserLogResult { diff --git a/packages/schemas/tables/user_logs.sql b/packages/schemas/tables/user_logs.sql index 4b21c3d2b..d8a578fc2 100644 --- a/packages/schemas/tables/user_logs.sql +++ b/packages/schemas/tables/user_logs.sql @@ -1,4 +1,4 @@ -create type user_log_type as enum ('SignInUsernameAndPassword', 'SignInEmail', 'SignInPhone', 'SignInSocial', 'RegisterUsernameAndPassword', 'RegisterEmail', 'RegisterPhone', 'RegisterSocial', 'ForgotPasswordEmail', 'ForgotPasswordPhone', 'ExchangeAccessToken'); +create type user_log_type as enum ('SignInUsernameAndPassword', 'SignInEmail', 'SignInPhone', 'SignInSocial', 'RegisterUsernameAndPassword', 'RegisterEmail', 'RegisterPhone', 'RegisterSocial', 'ExchangeAccessToken'); create type user_log_result as enum ('Success', 'Failed');