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:
parent
e7458f8a2b
commit
750ef0c3bf
8 changed files with 64 additions and 4 deletions
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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 () =>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
export const emailRegEx = /^\S+@\S+\.\S+$/;
|
||||
export const phoneRegEx = /^[1-9]\d{10}$/;
|
||||
|
|
|
@ -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.',
|
||||
|
|
|
@ -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}}。',
|
||||
|
|
|
@ -13,6 +13,7 @@ export enum PasscodeType {
|
|||
export enum UserLogType {
|
||||
SignInUsernameAndPassword = 'SignInUsernameAndPassword',
|
||||
SignInEmail = 'SignInEmail',
|
||||
SignInSms = 'SignInSms',
|
||||
ExchangeAccessToken = 'ExchangeAccessToken',
|
||||
}
|
||||
export enum UserLogResult {
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue