0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-02-03 21:48:55 -05:00

feat(core): add passwordless register flow of phone (#221)

* feat(phone passwordless): add passwordless register flow of phone

* feat(phone passwordless): add userId to both sign-in and register user logs

* feat(phone passwordless): add register error descriptions

* feat(core): update error descriptions and leave redundancy error descriptions for later changes
This commit is contained in:
Darcy Ye 2022-02-14 10:44:46 +08:00 committed by GitHub
parent 66808d6d02
commit 8249493c40
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 82 additions and 22 deletions

View file

@ -1,12 +1,12 @@
import { PasscodeType } from '@logto/schemas'; import { PasscodeType, UserLogType } from '@logto/schemas';
import { Context } from 'koa'; import { Context } from 'koa';
import { Provider } from 'oidc-provider'; import { Provider } from 'oidc-provider';
import RequestError from '@/errors/RequestError'; import RequestError from '@/errors/RequestError';
import { WithUserLogContext } from '@/middleware/koa-user-log'; import { WithUserLogContext } from '@/middleware/koa-user-log';
import { hasUser, hasUserWithEmail, insertUser } from '@/queries/user'; import { hasUser, hasUserWithEmail, hasUserWithPhone, insertUser } from '@/queries/user';
import assertThat from '@/utils/assert-that'; import assertThat from '@/utils/assert-that';
import { emailRegEx } from '@/utils/regex'; import { emailRegEx, phoneRegEx } from '@/utils/regex';
import { createPasscode, sendPasscode, verifyPasscode } from './passcode'; import { createPasscode, sendPasscode, verifyPasscode } from './passcode';
import { encryptUserPassword, generateUserId } from './user'; import { encryptUserPassword, generateUserId } from './user';
@ -39,7 +39,7 @@ export const registerWithUsernameAndPassword = async (
assertThat( assertThat(
!(await hasUser(username)), !(await hasUser(username)),
new RequestError({ new RequestError({
code: 'user.username_exists', code: 'user.username_exists_register',
status: 422, status: 422,
}) })
); );
@ -58,6 +58,9 @@ export const registerWithUsernameAndPassword = async (
}); });
await assignRegistrationResult(ctx, provider, id); 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) => { export const sendPasscodeToEmail = async (ctx: Context, jti: string, email: string) => {
@ -65,7 +68,7 @@ export const sendPasscodeToEmail = async (ctx: Context, jti: string, email: stri
assertThat( assertThat(
!(await hasUserWithEmail(email)), !(await hasUserWithEmail(email)),
new RequestError({ new RequestError({
code: 'user.email_exists', code: 'user.email_exists_register',
status: 422, status: 422,
}) })
); );
@ -75,6 +78,21 @@ export const sendPasscodeToEmail = async (ctx: Context, jti: string, email: stri
ctx.state = 204; 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 ( export const registerWithEmailAndPasscode = async (
ctx: WithUserLogContext<Context>, ctx: WithUserLogContext<Context>,
provider: Provider, provider: Provider,
@ -83,7 +101,7 @@ export const registerWithEmailAndPasscode = async (
assertThat( assertThat(
!(await hasUserWithEmail(email)), !(await hasUserWithEmail(email)),
new RequestError({ new RequestError({
code: 'user.email_exists', code: 'user.email_exists_register',
status: 422, status: 422,
}) })
); );
@ -96,4 +114,33 @@ export const registerWithEmailAndPasscode = async (
}); });
await assignRegistrationResult(ctx, provider, id); 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;
}; };

View file

@ -67,11 +67,11 @@ export const signInWithUsernameAndPassword = async (
) => { ) => {
assertThat(username && password, 'session.insufficient_info'); assertThat(username && password, 'session.insufficient_info');
ctx.userLog.username = username;
ctx.userLog.type = UserLogType.SignInUsernameAndPassword;
const { id } = await findUserByUsernameAndPassword(username, password); const { id } = await findUserByUsernameAndPassword(username, password);
await assignSignInResult(ctx, provider, id); await assignSignInResult(ctx, provider, id);
ctx.userLog.userId = id;
ctx.userLog.username = username;
ctx.userLog.type = UserLogType.SignInUsernameAndPassword;
}; };
export const signInWithEmailAndPasscode = async ( export const signInWithEmailAndPasscode = async (
@ -79,13 +79,13 @@ export const signInWithEmailAndPasscode = async (
provider: Provider, provider: Provider,
{ jti, email, code }: { jti: string; email: string; code: string } { jti, email, code }: { jti: string; email: string; code: string }
) => { ) => {
ctx.userLog.email = email;
ctx.userLog.type = UserLogType.SignInEmail;
await verifyPasscode(jti, PasscodeType.SignIn, code, { email }); await verifyPasscode(jti, PasscodeType.SignIn, code, { email });
const { id } = await findUserByEmail(email); const { id } = await findUserByEmail(email);
await assignSignInResult(ctx, provider, id); await assignSignInResult(ctx, provider, id);
ctx.userLog.userId = id;
ctx.userLog.email = email;
ctx.userLog.type = UserLogType.SignInEmail;
}; };
export const signInWithPhoneAndPasscode = async ( export const signInWithPhoneAndPasscode = async (
@ -93,13 +93,13 @@ export const signInWithPhoneAndPasscode = async (
provider: Provider, provider: Provider,
{ jti, phone, code }: { jti: string; phone: string; code: string } { jti, phone, code }: { jti: string; phone: string; code: string }
) => { ) => {
ctx.userLog.phone = phone;
ctx.userLog.type = UserLogType.SignInSms;
await verifyPasscode(jti, PasscodeType.SignIn, code, { phone }); await verifyPasscode(jti, PasscodeType.SignIn, code, { phone });
const { id } = await findUserByPhone(phone); const { id } = await findUserByPhone(phone);
await assignSignInResult(ctx, provider, id); await assignSignInResult(ctx, provider, id);
ctx.userLog.userId = id;
ctx.userLog.phone = phone;
ctx.userLog.type = UserLogType.SignInPhone;
}; };
// TODO: change this after frontend is ready. // TODO: change this after frontend is ready.

View file

@ -6,8 +6,10 @@ import { object, string } from 'zod';
import RequestError from '@/errors/RequestError'; import RequestError from '@/errors/RequestError';
import { import {
registerWithEmailAndPasscode, registerWithEmailAndPasscode,
registerWithPhoneAndPasscode,
registerWithUsernameAndPassword, registerWithUsernameAndPassword,
sendPasscodeToEmail, sendPasscodeToEmail,
sendPasscodeToPhone,
} from '@/lib/register'; } from '@/lib/register';
import { import {
assignRedirectUrlForSocial, assignRedirectUrlForSocial,
@ -129,18 +131,23 @@ export default function sessionRoutes<T extends AnonymousRouter>(router: T, prov
username: string().min(3).optional(), username: string().min(3).optional(),
password: string().min(6).optional(), password: string().min(6).optional(),
email: string().optional(), email: string().optional(),
phone: string().optional(),
code: string().optional(), code: string().optional(),
}), }),
}), }),
async (ctx, next) => { async (ctx, next) => {
const interaction = await provider.interactionDetails(ctx.req, ctx.res); const interaction = await provider.interactionDetails(ctx.req, ctx.res);
const { jti } = interaction; const { jti } = interaction;
const { username, password, email, code } = ctx.guard.body; const { username, password, email, phone, code } = ctx.guard.body;
if (email && !code) { if (email && !code) {
await sendPasscodeToEmail(ctx, jti, email); await sendPasscodeToEmail(ctx, jti, email);
} else if (email && code) { } else if (email && code) {
await registerWithEmailAndPasscode(ctx, provider, { jti, email, code }); await registerWithEmailAndPasscode(ctx, provider, { jti, email, code });
} else if (phone && !code) {
await sendPasscodeToPhone(ctx, jti, phone);
} else if (phone && code) {
await registerWithPhoneAndPasscode(ctx, provider, { jti, phone, code });
} else if (username && password) { } else if (username && password) {
await registerWithUsernameAndPassword(ctx, provider, username, password); await registerWithUsernameAndPassword(ctx, provider, username, password);
} else { } else {

View file

@ -44,8 +44,9 @@ const errors = {
provider_error: 'OIDC Internal Error: {{message}}.', provider_error: 'OIDC Internal Error: {{message}}.',
}, },
user: { user: {
username_exists: 'The username already exists.', username_exists_register: 'The username has been registered.',
email_exists: 'The email already exists.', email_exists_register: 'The email address has been registered.',
phone_exists_register: 'The phone number has been registered.',
invalid_email: 'Invalid email address.', invalid_email: 'Invalid email address.',
invalid_phone: 'Invalid phone number.', invalid_phone: 'Invalid phone number.',
email_not_exists: 'The email address has not been registered yet.', email_not_exists: 'The email address has not been registered yet.',

View file

@ -45,8 +45,9 @@ const errors = {
provider_error: 'OIDC 错误: {{message}}。', provider_error: 'OIDC 错误: {{message}}。',
}, },
user: { user: {
username_exists: '用户名已存在。', username_exists_register: '用户名已被注册。',
email_exists: '邮箱地址已存在。', email_exists_register: '邮箱地址已被注册。',
phone_exists_register: '手机号码已被注册。',
invalid_email: '邮箱地址不正确。', invalid_email: '邮箱地址不正确。',
invalid_phone: '手机号码不正确。', invalid_phone: '手机号码不正确。',
email_not_exists: '邮箱地址尚未注册。', email_not_exists: '邮箱地址尚未注册。',

View file

@ -13,8 +13,12 @@ export enum PasscodeType {
export enum UserLogType { export enum UserLogType {
SignInUsernameAndPassword = 'SignInUsernameAndPassword', SignInUsernameAndPassword = 'SignInUsernameAndPassword',
SignInEmail = 'SignInEmail', SignInEmail = 'SignInEmail',
SignInSms = 'SignInSms', SignInPhone = 'SignInPhone',
SignInSocial = 'SignInSocial', SignInSocial = 'SignInSocial',
RegisterUsernameAndPassword = 'RegisterUsernameAndPassword',
RegisterEmail = 'RegisterEmail',
RegisterPhone = 'RegisterPhone',
RegisterSocial = 'RegisterSocial',
ExchangeAccessToken = 'ExchangeAccessToken', ExchangeAccessToken = 'ExchangeAccessToken',
} }
export enum UserLogResult { export enum UserLogResult {

View file

@ -1,4 +1,4 @@
create type user_log_type as enum ('SignInUsernameAndPassword', 'SignInEmail', 'SignInSms', 'SignInSocial', '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'); create type user_log_result as enum ('Success', 'Failed');