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:
parent
66808d6d02
commit
8249493c40
7 changed files with 82 additions and 22 deletions
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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.',
|
||||||
|
|
|
@ -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: '邮箱地址尚未注册。',
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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');
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue