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

feat(core): separate passwordless flow routes (#280)

* feat(core): separate passwordless flow routes

* feat(core): rename routes

* feat(core): wrap frequently called methods and rerank log assignment order

* feat(core): rerank log assignment order
This commit is contained in:
Darcy Ye 2022-02-24 18:21:59 +08:00 committed by GitHub
parent 861833c01a
commit 75d2bb3e9b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 135 additions and 73 deletions

View file

@ -1,6 +1,11 @@
import { Context } from 'koa'; import { Context } from 'koa';
import { InteractionResults, Provider } from 'oidc-provider'; import { InteractionResults, Provider } from 'oidc-provider';
import RequestError from '@/errors/RequestError';
import { hasUserWithEmail, hasUserWithPhone } from '@/queries/user';
import assertThat from '@/utils/assert-that';
import { emailRegEx, phoneRegEx } from '@/utils/regex';
// TODO: change this after frontend is ready. // TODO: change this after frontend is ready.
// Should combine baseUrl(domain) from database with a 'callback' endpoint. // Should combine baseUrl(domain) from database with a 'callback' endpoint.
export const connectorRedirectUrl = 'https://logto.dev/callback'; export const connectorRedirectUrl = 'https://logto.dev/callback';
@ -16,3 +21,35 @@ export const assignInteractionResults = async (
}); });
ctx.body = { redirectTo }; ctx.body = { redirectTo };
}; };
export const checkEmailValidityAndAvailability = async (email: string) => {
assertThat(emailRegEx.test(email), new RequestError('user.invalid_email'));
assertThat(
!(await hasUserWithEmail(email)),
new RequestError({ code: 'user.email_exists_register', status: 422 })
);
};
export const checkEmailValidityAndExistence = async (email: string) => {
assertThat(emailRegEx.test(email), new RequestError('user.invalid_email'));
assertThat(
await hasUserWithEmail(email),
new RequestError({ code: 'user.email_not_exists', status: 422 })
);
};
export const checkPhoneNumberValidityAndAvailability = async (phone: string) => {
assertThat(phoneRegEx.test(phone), new RequestError('user.invalid_phone'));
assertThat(
!(await hasUserWithPhone(phone)),
new RequestError({ code: 'user.phone_exists_register', status: 422 })
);
};
export const checkPhoneNumberValidityAndExistence = async (phone: string) => {
assertThat(phoneRegEx.test(phone), new RequestError('user.invalid_phone'));
assertThat(
await hasUserWithPhone(phone),
new RequestError({ code: 'user.phone_not_exists', status: 422 })
);
};

View file

@ -10,7 +10,14 @@ import { object, string } from 'zod';
import { getSocialConnectorInstanceById } from '@/connectors'; import { getSocialConnectorInstanceById } from '@/connectors';
import RequestError from '@/errors/RequestError'; import RequestError from '@/errors/RequestError';
import { createPasscode, sendPasscode, verifyPasscode } from '@/lib/passcode'; import { createPasscode, sendPasscode, verifyPasscode } from '@/lib/passcode';
import { assignInteractionResults, connectorRedirectUrl } from '@/lib/session'; import {
assignInteractionResults,
connectorRedirectUrl,
checkEmailValidityAndAvailability,
checkEmailValidityAndExistence,
checkPhoneNumberValidityAndAvailability,
checkPhoneNumberValidityAndExistence,
} from '@/lib/session';
import { import {
findSocialRelatedUser, findSocialRelatedUser,
getUserInfoByAuthCode, getUserInfoByAuthCode,
@ -20,9 +27,7 @@ import { encryptUserPassword, generateUserId, findUserByUsernameAndPassword } fr
import koaGuard from '@/middleware/koa-guard'; import koaGuard from '@/middleware/koa-guard';
import { import {
hasUser, hasUser,
hasUserWithEmail,
hasUserWithIdentity, hasUserWithIdentity,
hasUserWithPhone,
insertUser, insertUser,
findUserById, findUserById,
updateUserById, updateUserById,
@ -31,7 +36,6 @@ import {
findUserByIdentity, findUserByIdentity,
} from '@/queries/user'; } from '@/queries/user';
import assertThat from '@/utils/assert-that'; import assertThat from '@/utils/assert-that';
import { emailRegEx, phoneRegEx } from '@/utils/regex';
import { AnonymousRouter } from './types'; import { AnonymousRouter } from './types';
@ -70,30 +74,34 @@ export default function sessionRoutes<T extends AnonymousRouter>(router: T, prov
); );
router.post( router.post(
'/session/sign-in/passwordless/phone', '/session/sign-in/passwordless/phone/send-passcode',
koaGuard({ body: object({ phone: string(), code: string().optional() }) }), koaGuard({ body: object({ phone: string() }) }),
async (ctx, next) => { 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; ctx.userLog.type = UserLogType.SignInPhone;
await checkPhoneNumberValidityAndExistence(phone);
const passcode = await createPasscode(jti, PasscodeType.SignIn, { phone });
await sendPasscode(passcode);
ctx.state = 204;
return next();
}
);
router.post(
'/session/sign-in/passwordless/phone/verify-passcode',
koaGuard({ body: object({ phone: string(), code: string() }) }),
async (ctx, next) => {
const { jti } = await provider.interactionDetails(ctx.req, ctx.res); const { jti } = await provider.interactionDetails(ctx.req, ctx.res);
const { phone, code } = ctx.guard.body; const { phone, code } = ctx.guard.body;
assertThat(phoneRegEx.test(phone), new RequestError('user.invalid_phone'));
assertThat(
await hasUserWithPhone(phone),
new RequestError({
code: 'user.phone_not_exists',
status: 422,
})
);
ctx.userLog.phone = phone; ctx.userLog.phone = phone;
ctx.userLog.type = UserLogType.SignInPhone;
if (!code) { await checkPhoneNumberValidityAndExistence(phone);
const passcode = await createPasscode(jti, PasscodeType.SignIn, { phone });
await sendPasscode(passcode);
ctx.state = 204;
return next();
}
await verifyPasscode(jti, PasscodeType.SignIn, code, { phone }); await verifyPasscode(jti, PasscodeType.SignIn, code, { phone });
const { id } = await findUserByPhone(phone); const { id } = await findUserByPhone(phone);
@ -106,30 +114,34 @@ export default function sessionRoutes<T extends AnonymousRouter>(router: T, prov
); );
router.post( router.post(
'/session/sign-in/passwordless/email', '/session/sign-in/passwordless/email/send-passcode',
koaGuard({ body: object({ email: string(), code: string().optional() }) }), koaGuard({ body: object({ email: string() }) }),
async (ctx, next) => { 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; ctx.userLog.type = UserLogType.SignInEmail;
await checkEmailValidityAndExistence(email);
const passcode = await createPasscode(jti, PasscodeType.SignIn, { email });
await sendPasscode(passcode);
ctx.state = 204;
return next();
}
);
router.post(
'/session/sign-in/passwordless/email/verify-passcode',
koaGuard({ body: object({ email: string(), code: string() }) }),
async (ctx, next) => {
const { jti } = await provider.interactionDetails(ctx.req, ctx.res); const { jti } = await provider.interactionDetails(ctx.req, ctx.res);
const { email, code } = ctx.guard.body; const { email, code } = ctx.guard.body;
assertThat(emailRegEx.test(email), new RequestError('user.invalid_email'));
assertThat(
await hasUserWithEmail(email),
new RequestError({
code: 'user.email_not_exists',
status: 422,
})
);
ctx.userLog.email = email; ctx.userLog.email = email;
ctx.userLog.type = UserLogType.SignInEmail;
if (!code) { await checkEmailValidityAndExistence(email);
const passcode = await createPasscode(jti, PasscodeType.SignIn, { email });
await sendPasscode(passcode);
ctx.state = 204;
return next();
}
await verifyPasscode(jti, PasscodeType.SignIn, code, { email }); await verifyPasscode(jti, PasscodeType.SignIn, code, { email });
const { id } = await findUserByEmail(email); const { id } = await findUserByEmail(email);
@ -147,10 +159,9 @@ export default function sessionRoutes<T extends AnonymousRouter>(router: T, prov
body: object({ connectorId: string(), code: string().optional(), state: string() }), body: object({ connectorId: string(), code: string().optional(), state: string() }),
}), }),
async (ctx, next) => { async (ctx, next) => {
ctx.userLog.type = UserLogType.SignInSocial;
const { connectorId, code, state } = ctx.guard.body; const { connectorId, code, state } = ctx.guard.body;
ctx.userLog.connectorId = connectorId; ctx.userLog.connectorId = connectorId;
ctx.userLog.type = UserLogType.SignInSocial;
if (!code) { if (!code) {
assertThat(state, 'session.insufficient_info'); assertThat(state, 'session.insufficient_info');
@ -298,27 +309,34 @@ export default function sessionRoutes<T extends AnonymousRouter>(router: T, prov
); );
router.post( router.post(
'/session/register/passwordless/phone', '/session/register/passwordless/phone/send-passcode',
koaGuard({ body: object({ phone: string(), code: string().optional() }) }), koaGuard({ body: object({ phone: string() }) }),
async (ctx, next) => { async (ctx, next) => {
ctx.userLog.type = UserLogType.RegisterPhone; ctx.userLog.type = UserLogType.RegisterPhone;
const { jti } = await provider.interactionDetails(ctx.req, ctx.res); const { jti } = await provider.interactionDetails(ctx.req, ctx.res);
const { phone, code } = ctx.guard.body; const { phone } = ctx.guard.body;
assertThat(phoneRegEx.test(phone), 'user.invalid_phone');
assertThat(
!(await hasUserWithPhone(phone)),
new RequestError({ code: 'user.phone_exists_register', status: 422 })
);
ctx.userLog.phone = phone; ctx.userLog.phone = phone;
if (!code) { await checkPhoneNumberValidityAndAvailability(phone);
const passcode = await createPasscode(jti, PasscodeType.Register, { phone });
await sendPasscode(passcode);
ctx.state = 204;
return next(); const passcode = await createPasscode(jti, PasscodeType.Register, { phone });
} await sendPasscode(passcode);
ctx.state = 204;
return next();
}
);
router.post(
'/session/register/passwordless/phone/verify-passcode',
koaGuard({ body: object({ phone: string(), code: string() }) }),
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;
await checkPhoneNumberValidityAndAvailability(phone);
await verifyPasscode(jti, PasscodeType.Register, code, { phone }); await verifyPasscode(jti, PasscodeType.Register, code, { phone });
const id = await generateUserId(); const id = await generateUserId();
@ -332,27 +350,34 @@ export default function sessionRoutes<T extends AnonymousRouter>(router: T, prov
); );
router.post( router.post(
'/session/register/passwordless/email', '/session/register/passwordless/email/send-passcode',
koaGuard({ body: object({ email: string(), code: string().optional() }) }), koaGuard({ body: object({ email: string() }) }),
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;
await checkEmailValidityAndAvailability(email);
const passcode = await createPasscode(jti, PasscodeType.Register, { email });
await sendPasscode(passcode);
ctx.state = 204;
return next();
}
);
router.post(
'/session/register/passwordless/email/verify-passcode',
koaGuard({ body: object({ email: string(), code: string() }) }),
async (ctx, next) => { async (ctx, next) => {
ctx.userLog.type = UserLogType.RegisterPhone;
const { jti } = await provider.interactionDetails(ctx.req, ctx.res); const { jti } = await provider.interactionDetails(ctx.req, ctx.res);
const { email, code } = ctx.guard.body; 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 })
);
ctx.userLog.email = email; ctx.userLog.email = email;
ctx.userLog.type = UserLogType.RegisterEmail;
if (!code) { await checkEmailValidityAndAvailability(email);
const passcode = await createPasscode(jti, PasscodeType.Register, { email });
await sendPasscode(passcode);
ctx.state = 204;
return next();
}
await verifyPasscode(jti, PasscodeType.Register, code, { email }); await verifyPasscode(jti, PasscodeType.Register, code, { email });
const id = await generateUserId(); const id = await generateUserId();