mirror of
https://github.com/logto-io/logto.git
synced 2025-01-06 20:40:08 -05:00
chore(core): refactor register codes (#271)
* chore(core): refactor register routes * chore(core): remove unnecessary lib/register * chore(core): fix
This commit is contained in:
parent
b31e90dab2
commit
ab4beadd70
2 changed files with 125 additions and 183 deletions
|
@ -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<Context>,
|
||||
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<Context>,
|
||||
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<Context>,
|
||||
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<Context>,
|
||||
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);
|
||||
};
|
|
@ -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<T extends AnonymousRouter>(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<T extends AnonymousRouter>(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<T extends AnonymousRouter>(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<T extends AnonymousRouter>(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();
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue