0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-02-17 22:04:19 -05:00

feat(phone passwordless): add passwordless sign-in with phone (#217)

* feat(phone passwordless): add passwordless sign-in with phone

* feat(phone passwordless): rename phoneReg
This commit is contained in:
Darcy Ye 2022-02-09 16:14:42 +08:00 committed by GitHub
parent e7458f8a2b
commit 750ef0c3bf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 64 additions and 4 deletions

View file

@ -4,9 +4,14 @@ import { Provider } from 'oidc-provider';
import RequestError from '@/errors/RequestError';
import { WithUserLogContext } from '@/middleware/koa-user-log';
import { findUserByEmail, hasUserWithEmail } from '@/queries/user';
import {
findUserByEmail,
findUserByPhone,
hasUserWithEmail,
hasUserWithPhone,
} from '@/queries/user';
import assertThat from '@/utils/assert-that';
import { emailRegEx } from '@/utils/regex';
import { emailRegEx, phoneRegEx } from '@/utils/regex';
import { createPasscode, sendPasscode, verifyPasscode } from './passcode';
import { findUserByUsernameAndPassword } from './user';
@ -37,6 +42,20 @@ export const sendSignInWithEmailPasscode = async (ctx: Context, jti: string, ema
ctx.state = 204;
};
export const sendSignInWithPhonePasscode = 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_not_exists',
status: 422,
})
);
const passcode = await createPasscode(jti, PasscodeType.SignIn, { phone });
await sendPasscode(passcode);
ctx.state = 204;
};
export const signInWithUsernameAndPassword = async (
ctx: WithUserLogContext<Context>,
provider: Provider,
@ -65,3 +84,17 @@ export const signInWithEmailAndPasscode = async (
await assignSignInResult(ctx, provider, id);
};
export const signInWithPhoneAndPasscode = async (
ctx: WithUserLogContext<Context>,
provider: Provider,
{ 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 });
const { id } = await findUserByPhone(phone);
await assignSignInResult(ctx, provider, id);
};

View file

@ -23,6 +23,13 @@ export const findUserByEmail = async (email: string) =>
where ${fields.primaryEmail}=${email}
`);
export const findUserByPhone = async (phone: string) =>
pool.one<User>(sql`
select ${sql.join(Object.values(fields), sql`,`)}
from ${table}
where ${fields.primaryPhone}=${phone}
`);
export const findUserById = async (id: string) =>
pool.one<User>(sql`
select ${sql.join(Object.values(fields), sql`,`)}
@ -51,6 +58,13 @@ export const hasUserWithEmail = async (email: string) =>
where ${fields.primaryEmail}=${email}
`);
export const hasUserWithPhone = async (phone: string) =>
pool.exists(sql`
select ${fields.primaryPhone}
from ${table}
where ${fields.primaryPhone}=${phone}
`);
export const insertUser = buildInsertInto<CreateUser, User>(pool, Users, { returning: true });
export const findAllUsers = async () =>

View file

@ -11,7 +11,9 @@ import {
} from '@/lib/register';
import {
sendSignInWithEmailPasscode,
sendSignInWithPhonePasscode,
signInWithEmailAndPasscode,
signInWithPhoneAndPasscode,
signInWithUsernameAndPassword,
} from '@/lib/sign-in';
import koaGuard from '@/middleware/koa-guard';
@ -27,6 +29,7 @@ export default function sessionRoutes<T extends AnonymousRouter>(router: T, prov
username: string().optional(),
password: string().optional(),
email: string().optional(),
phone: string().optional(),
code: string().optional(),
}),
}),
@ -47,12 +50,16 @@ export default function sessionRoutes<T extends AnonymousRouter>(router: T, prov
}
if (name === 'login') {
const { username, password, email, code } = ctx.guard.body;
const { username, password, email, phone, code } = ctx.guard.body;
if (email && !code) {
await sendSignInWithEmailPasscode(ctx, jti, email);
} else if (email && code) {
await signInWithEmailAndPasscode(ctx, provider, { jti, email, code });
} else if (phone && !code) {
await sendSignInWithPhonePasscode(ctx, jti, phone);
} else if (phone && code) {
await signInWithPhoneAndPasscode(ctx, provider, { jti, phone, code });
} else if (username && password) {
await signInWithUsernameAndPassword(ctx, provider, username, password);
} else {

View file

@ -1 +1,2 @@
export const emailRegEx = /^\S+@\S+\.\S+$/;
export const phoneRegEx = /^[1-9]\d{10}$/;

View file

@ -47,7 +47,9 @@ const errors = {
username_exists: 'The username already exists.',
email_exists: 'The email already exists.',
invalid_email: 'Invalid email address.',
invalid_phone: 'Invalid phone number.',
email_not_exists: 'The email address has not been registered yet.',
phone_not_exists: 'The phone number has not been registered yet.',
},
password: {
unsupported_encryption_method: 'The encryption method {{name}} is not supported.',

View file

@ -48,7 +48,9 @@ const errors = {
username_exists: '用户名已存在。',
email_exists: '邮箱地址已存在。',
invalid_email: '邮箱地址不正确。',
invalid_phone: '手机号码不正确。',
email_not_exists: '邮箱地址尚未注册。',
phone_not_exists: '手机号码尚未注册。',
},
password: {
unsupported_encryption_method: '不支持的加密方法 {{name}}。',

View file

@ -13,6 +13,7 @@ export enum PasscodeType {
export enum UserLogType {
SignInUsernameAndPassword = 'SignInUsernameAndPassword',
SignInEmail = 'SignInEmail',
SignInSms = 'SignInSms',
ExchangeAccessToken = 'ExchangeAccessToken',
}
export enum UserLogResult {

View file

@ -1,4 +1,4 @@
create type user_log_type as enum ('SignInUsernameAndPassword', 'SignInEmail', 'ExchangeAccessToken');
create type user_log_type as enum ('SignInUsernameAndPassword', 'SignInEmail', 'SignInSms', 'ExchangeAccessToken');
create type user_log_result as enum ('Success', 'Failed');