From 750ef0c3bf2774b59df5a6d484ab989bcda1212d Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Wed, 9 Feb 2022 16:14:42 +0800 Subject: [PATCH] feat(phone passwordless): add passwordless sign-in with phone (#217) * feat(phone passwordless): add passwordless sign-in with phone * feat(phone passwordless): rename phoneReg --- packages/core/src/lib/sign-in.ts | 37 ++++++++++++++++++- packages/core/src/queries/user.ts | 14 +++++++ packages/core/src/routes/session.ts | 9 ++++- packages/core/src/utils/regex.ts | 1 + packages/phrases/src/locales/en.ts | 2 + packages/phrases/src/locales/zh-cn.ts | 2 + .../schemas/src/db-entries/custom-types.ts | 1 + packages/schemas/tables/user_logs.sql | 2 +- 8 files changed, 64 insertions(+), 4 deletions(-) diff --git a/packages/core/src/lib/sign-in.ts b/packages/core/src/lib/sign-in.ts index 27ef1f709..c31ec4796 100644 --- a/packages/core/src/lib/sign-in.ts +++ b/packages/core/src/lib/sign-in.ts @@ -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, provider: Provider, @@ -65,3 +84,17 @@ export const signInWithEmailAndPasscode = async ( await assignSignInResult(ctx, provider, id); }; + +export const signInWithPhoneAndPasscode = async ( + ctx: WithUserLogContext, + 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); +}; diff --git a/packages/core/src/queries/user.ts b/packages/core/src/queries/user.ts index d28d2c468..8b913fe5a 100644 --- a/packages/core/src/queries/user.ts +++ b/packages/core/src/queries/user.ts @@ -23,6 +23,13 @@ export const findUserByEmail = async (email: string) => where ${fields.primaryEmail}=${email} `); +export const findUserByPhone = async (phone: string) => + pool.one(sql` + select ${sql.join(Object.values(fields), sql`,`)} + from ${table} + where ${fields.primaryPhone}=${phone} + `); + export const findUserById = async (id: string) => pool.one(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(pool, Users, { returning: true }); export const findAllUsers = async () => diff --git a/packages/core/src/routes/session.ts b/packages/core/src/routes/session.ts index 1b5e97b8d..225a476c4 100644 --- a/packages/core/src/routes/session.ts +++ b/packages/core/src/routes/session.ts @@ -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(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(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 { diff --git a/packages/core/src/utils/regex.ts b/packages/core/src/utils/regex.ts index 163a1386f..99edee4d0 100644 --- a/packages/core/src/utils/regex.ts +++ b/packages/core/src/utils/regex.ts @@ -1 +1,2 @@ export const emailRegEx = /^\S+@\S+\.\S+$/; +export const phoneRegEx = /^[1-9]\d{10}$/; diff --git a/packages/phrases/src/locales/en.ts b/packages/phrases/src/locales/en.ts index dbc4e4695..fe85b1a4c 100644 --- a/packages/phrases/src/locales/en.ts +++ b/packages/phrases/src/locales/en.ts @@ -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.', diff --git a/packages/phrases/src/locales/zh-cn.ts b/packages/phrases/src/locales/zh-cn.ts index f806849f4..ba9b4fea0 100644 --- a/packages/phrases/src/locales/zh-cn.ts +++ b/packages/phrases/src/locales/zh-cn.ts @@ -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}}。', diff --git a/packages/schemas/src/db-entries/custom-types.ts b/packages/schemas/src/db-entries/custom-types.ts index f941faa6a..e81386c82 100644 --- a/packages/schemas/src/db-entries/custom-types.ts +++ b/packages/schemas/src/db-entries/custom-types.ts @@ -13,6 +13,7 @@ export enum PasscodeType { export enum UserLogType { SignInUsernameAndPassword = 'SignInUsernameAndPassword', SignInEmail = 'SignInEmail', + SignInSms = 'SignInSms', ExchangeAccessToken = 'ExchangeAccessToken', } export enum UserLogResult { diff --git a/packages/schemas/tables/user_logs.sql b/packages/schemas/tables/user_logs.sql index 34dac6dfd..ff4c003e4 100644 --- a/packages/schemas/tables/user_logs.sql +++ b/packages/schemas/tables/user_logs.sql @@ -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');