From ab4beadd705625149b5e93054337568c214bfa13 Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Tue, 22 Feb 2022 22:12:38 +0800 Subject: [PATCH] chore(core): refactor register codes (#271) * chore(core): refactor register routes * chore(core): remove unnecessary lib/register * chore(core): fix --- packages/core/src/lib/register.ts | 167 ---------------------------- packages/core/src/routes/session.ts | 141 ++++++++++++++++++++--- 2 files changed, 125 insertions(+), 183 deletions(-) delete mode 100644 packages/core/src/lib/register.ts diff --git a/packages/core/src/lib/register.ts b/packages/core/src/lib/register.ts deleted file mode 100644 index 243ccd5a4..000000000 --- a/packages/core/src/lib/register.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { PasscodeType, UserLogType } from '@logto/schemas'; -import { Context } from 'koa'; -import { Provider } from 'oidc-provider'; - -import { SocialUserInfo } from '@/connectors/types'; -import RequestError from '@/errors/RequestError'; -import { WithUserLogContext } from '@/middleware/koa-user-log'; -import { hasUser, hasUserWithEmail, hasUserWithPhone, insertUser } from '@/queries/user'; -import assertThat from '@/utils/assert-that'; -import { emailRegEx, phoneRegEx } from '@/utils/regex'; - -import { createPasscode, sendPasscode, verifyPasscode } from './passcode'; -import { encryptUserPassword, generateUserId } from './user'; - -const assignRegistrationResult = async (ctx: Context, provider: Provider, userId: string) => { - const redirectTo = await provider.interactionResult( - ctx.req, - ctx.res, - { login: { accountId: userId } }, - { mergeWithLastSubmission: false } - ); - ctx.body = { redirectTo }; -}; - -export const registerWithUsernameAndPassword = async ( - ctx: WithUserLogContext, - provider: Provider, - username: string, - password: string -) => { - assertThat( - username && password, - new RequestError({ - code: 'session.insufficient_info', - status: 400, - }) - ); - assertThat( - !(await hasUser(username)), - new RequestError({ - code: 'user.username_exists_register', - status: 422, - }) - ); - - const id = await generateUserId(); - - const { passwordEncryptionSalt, passwordEncrypted, passwordEncryptionMethod } = - encryptUserPassword(id, password); - - await insertUser({ - id, - username, - passwordEncrypted, - passwordEncryptionMethod, - passwordEncryptionSalt, - }); - - await assignRegistrationResult(ctx, provider, id); - ctx.userLog.userId = id; - ctx.userLog.username = username; - ctx.userLog.type = UserLogType.RegisterUsernameAndPassword; -}; - -export const sendPasscodeToEmail = async (ctx: Context, jti: string, email: string) => { - assertThat(emailRegEx.test(email), new RequestError('user.invalid_email')); - assertThat( - !(await hasUserWithEmail(email)), - new RequestError({ - code: 'user.email_exists_register', - status: 422, - }) - ); - - const passcode = await createPasscode(jti, PasscodeType.Register, { email }); - await sendPasscode(passcode); - ctx.state = 204; -}; - -export const sendPasscodeToPhone = async (ctx: Context, jti: string, phone: string) => { - assertThat(phoneRegEx.test(phone), new RequestError('user.invalid_phone')); - assertThat( - !(await hasUserWithPhone(phone)), - new RequestError({ - code: 'user.phone_exists_register', - status: 422, - }) - ); - - const passcode = await createPasscode(jti, PasscodeType.Register, { phone }); - await sendPasscode(passcode); - ctx.state = 204; -}; - -export const registerWithEmailAndPasscode = async ( - ctx: WithUserLogContext, - provider: Provider, - { jti, email, code }: { jti: string; email: string; code: string } -) => { - assertThat( - !(await hasUserWithEmail(email)), - new RequestError({ - code: 'user.email_exists_register', - status: 422, - }) - ); - await verifyPasscode(jti, PasscodeType.Register, code, { email }); - - const id = await generateUserId(); - await insertUser({ - id, - primaryEmail: email, - }); - - await assignRegistrationResult(ctx, provider, id); - ctx.userLog.userId = id; - ctx.userLog.email = email; - ctx.userLog.type = UserLogType.RegisterEmail; -}; - -export const registerWithPhoneAndPasscode = async ( - ctx: WithUserLogContext, - provider: Provider, - { jti, phone, code }: { jti: string; phone: string; code: string } -) => { - assertThat( - !(await hasUserWithPhone(phone)), - new RequestError({ - code: 'user.phone_exists_register', - status: 422, - }) - ); - await verifyPasscode(jti, PasscodeType.Register, code, { phone }); - - const id = await generateUserId(); - await insertUser({ - id, - primaryPhone: phone, - }); - - await assignRegistrationResult(ctx, provider, id); - ctx.userLog.userId = id; - ctx.userLog.phone = phone; - ctx.userLog.type = UserLogType.RegisterPhone; -}; - -export const registerWithSocial = async ( - ctx: WithUserLogContext, - provider: Provider, - connectorId: string, - userInfo: SocialUserInfo -) => { - const id = await generateUserId(); - await insertUser({ - id, - name: userInfo.name ?? null, - avatar: userInfo.avatar ?? null, - identities: { - [connectorId]: { - userId: userInfo.id, - details: userInfo, - }, - }, - }); - - await assignRegistrationResult(ctx, provider, id); -}; diff --git a/packages/core/src/routes/session.ts b/packages/core/src/routes/session.ts index 0459b80c5..e9b47689a 100644 --- a/packages/core/src/routes/session.ts +++ b/packages/core/src/routes/session.ts @@ -1,20 +1,14 @@ import path from 'path'; import { LogtoErrorCode } from '@logto/phrases'; -import { userInfoSelectFields } from '@logto/schemas'; +import { PasscodeType, UserLogType, userInfoSelectFields } from '@logto/schemas'; import { conditional } from '@silverhand/essentials'; import pick from 'lodash.pick'; import { Provider } from 'oidc-provider'; import { object, string } from 'zod'; -import { - registerWithSocial, - registerWithEmailAndPasscode, - registerWithPhoneAndPasscode, - registerWithUsernameAndPassword, - sendPasscodeToEmail, - sendPasscodeToPhone, -} from '@/lib/register'; +import RequestError from '@/errors/RequestError'; +import { createPasscode, sendPasscode, verifyPasscode } from '@/lib/passcode'; import { assignRedirectUrlForSocial, sendSignInWithEmailPasscode, @@ -26,9 +20,19 @@ import { signInWithSocialRelatedUser, } from '@/lib/sign-in'; import { getUserInfoByAuthCode, getUserInfoFromInteractionResult } from '@/lib/social'; +import { encryptUserPassword, generateUserId } from '@/lib/user'; import koaGuard from '@/middleware/koa-guard'; -import { findUserById, hasUserWithIdentity, updateUserById } from '@/queries/user'; +import { + hasUser, + hasUserWithEmail, + hasUserWithIdentity, + hasUserWithPhone, + insertUser, + findUserById, + updateUserById, +} from '@/queries/user'; import assertThat from '@/utils/assert-that'; +import { emailRegEx, phoneRegEx } from '@/utils/regex'; import { AnonymousRouter } from './types'; @@ -179,7 +183,47 @@ export default function sessionRoutes(router: T, prov koaGuard({ body: object({ username: string(), password: string() }) }), async (ctx, next) => { const { username, password } = ctx.guard.body; - await registerWithUsernameAndPassword(ctx, provider, username, password); + assertThat( + username && password, + new RequestError({ + code: 'session.insufficient_info', + status: 400, + }) + ); + assertThat( + !(await hasUser(username)), + new RequestError({ + code: 'user.username_exists_register', + status: 422, + }) + ); + + const id = await generateUserId(); + + const { passwordEncryptionSalt, passwordEncrypted, passwordEncryptionMethod } = + encryptUserPassword(id, password); + + await insertUser({ + id, + username, + passwordEncrypted, + passwordEncryptionMethod, + passwordEncryptionSalt, + }); + + ctx.userLog.userId = id; + ctx.userLog.username = username; + ctx.userLog.type = UserLogType.RegisterUsernameAndPassword; + + const redirectTo = await provider.interactionResult( + ctx.req, + ctx.res, + { login: { accountId: id } }, + { + mergeWithLastSubmission: false, + } + ); + ctx.body = { redirectTo }; return next(); } @@ -192,13 +236,36 @@ export default function sessionRoutes(router: T, prov const { jti } = await provider.interactionDetails(ctx.req, ctx.res); const { phone, code } = ctx.guard.body; + assertThat(phoneRegEx.test(phone), 'user.invalid_phone'); + assertThat( + !(await hasUserWithPhone(phone)), + new RequestError({ code: 'user.phone_exists_register', status: 422 }) + ); + if (!code) { - await sendPasscodeToPhone(ctx, jti, phone); + const passcode = await createPasscode(jti, PasscodeType.Register, { phone }); + await sendPasscode(passcode); + ctx.state = 204; return next(); } - await registerWithPhoneAndPasscode(ctx, provider, { jti, phone, code }); + await verifyPasscode(jti, PasscodeType.Register, code, { phone }); + const id = await generateUserId(); + await insertUser({ id, primaryPhone: phone }); + const redirectTo = await provider.interactionResult( + ctx.req, + ctx.res, + { login: { accountId: id } }, + { mergeWithLastSubmission: false } + ); + ctx.body = { redirectTo }; + ctx.userLog = { + ...ctx.userLog, + type: UserLogType.RegisterPhone, + userId: id, + phone, + }; return next(); } @@ -211,13 +278,36 @@ export default function sessionRoutes(router: T, prov const { jti } = await provider.interactionDetails(ctx.req, ctx.res); const { email, code } = ctx.guard.body; + assertThat(emailRegEx.test(email), 'user.invalid_email'); + assertThat( + !(await hasUserWithEmail(email)), + new RequestError({ code: 'user.email_exists_register', status: 422 }) + ); + if (!code) { - await sendPasscodeToEmail(ctx, jti, email); + const passcode = await createPasscode(jti, PasscodeType.Register, { email }); + await sendPasscode(passcode); + ctx.state = 204; return next(); } - await registerWithEmailAndPasscode(ctx, provider, { jti, email, code }); + await verifyPasscode(jti, PasscodeType.Register, code, { email }); + const id = await generateUserId(); + await insertUser({ id, primaryEmail: email }); + const redirectTo = await provider.interactionResult( + ctx.req, + ctx.res, + { login: { accountId: id } }, + { mergeWithLastSubmission: false } + ); + ctx.body = { redirectTo }; + ctx.userLog = { + ...ctx.userLog, + type: UserLogType.RegisterPhone, + userId: id, + email, + }; return next(); } @@ -242,7 +332,26 @@ export default function sessionRoutes(router: T, prov const userInfo = await getUserInfoFromInteractionResult(connectorId, result); assertThat(!(await hasUserWithIdentity(connectorId, userInfo.id)), 'user.identity_exists'); - await registerWithSocial(ctx, provider, connectorId, userInfo); + const id = await generateUserId(); + await insertUser({ + id, + name: userInfo.name ?? null, + avatar: userInfo.avatar ?? null, + identities: { + [connectorId]: { + userId: userInfo.id, + details: userInfo, + }, + }, + }); + + const redirectTo = await provider.interactionResult( + ctx.req, + ctx.res, + { login: { accountId: id } }, + { mergeWithLastSubmission: false } + ); + ctx.body = { redirectTo }; return next(); }