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 RequestError from '@/errors/RequestError';
|
||||||
import { WithUserLogContext } from '@/middleware/koa-user-log';
|
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 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 { findUserByUsernameAndPassword } from './user';
|
import { findUserByUsernameAndPassword } from './user';
|
||||||
|
@ -37,6 +42,20 @@ export const sendSignInWithEmailPasscode = async (ctx: Context, jti: string, ema
|
||||||
ctx.state = 204;
|
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 (
|
export const signInWithUsernameAndPassword = async (
|
||||||
ctx: WithUserLogContext<Context>,
|
ctx: WithUserLogContext<Context>,
|
||||||
provider: Provider,
|
provider: Provider,
|
||||||
|
@ -65,3 +84,17 @@ export const signInWithEmailAndPasscode = async (
|
||||||
|
|
||||||
await assignSignInResult(ctx, provider, id);
|
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}
|
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) =>
|
export const findUserById = async (id: string) =>
|
||||||
pool.one<User>(sql`
|
pool.one<User>(sql`
|
||||||
select ${sql.join(Object.values(fields), sql`,`)}
|
select ${sql.join(Object.values(fields), sql`,`)}
|
||||||
|
@ -51,6 +58,13 @@ export const hasUserWithEmail = async (email: string) =>
|
||||||
where ${fields.primaryEmail}=${email}
|
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 insertUser = buildInsertInto<CreateUser, User>(pool, Users, { returning: true });
|
||||||
|
|
||||||
export const findAllUsers = async () =>
|
export const findAllUsers = async () =>
|
||||||
|
|
|
@ -11,7 +11,9 @@ import {
|
||||||
} from '@/lib/register';
|
} from '@/lib/register';
|
||||||
import {
|
import {
|
||||||
sendSignInWithEmailPasscode,
|
sendSignInWithEmailPasscode,
|
||||||
|
sendSignInWithPhonePasscode,
|
||||||
signInWithEmailAndPasscode,
|
signInWithEmailAndPasscode,
|
||||||
|
signInWithPhoneAndPasscode,
|
||||||
signInWithUsernameAndPassword,
|
signInWithUsernameAndPassword,
|
||||||
} from '@/lib/sign-in';
|
} from '@/lib/sign-in';
|
||||||
import koaGuard from '@/middleware/koa-guard';
|
import koaGuard from '@/middleware/koa-guard';
|
||||||
|
@ -27,6 +29,7 @@ export default function sessionRoutes<T extends AnonymousRouter>(router: T, prov
|
||||||
username: string().optional(),
|
username: string().optional(),
|
||||||
password: string().optional(),
|
password: string().optional(),
|
||||||
email: string().optional(),
|
email: string().optional(),
|
||||||
|
phone: string().optional(),
|
||||||
code: string().optional(),
|
code: string().optional(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
@ -47,12 +50,16 @@ export default function sessionRoutes<T extends AnonymousRouter>(router: T, prov
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'login') {
|
if (name === 'login') {
|
||||||
const { username, password, email, code } = ctx.guard.body;
|
const { username, password, email, phone, code } = ctx.guard.body;
|
||||||
|
|
||||||
if (email && !code) {
|
if (email && !code) {
|
||||||
await sendSignInWithEmailPasscode(ctx, jti, email);
|
await sendSignInWithEmailPasscode(ctx, jti, email);
|
||||||
} else if (email && code) {
|
} else if (email && code) {
|
||||||
await signInWithEmailAndPasscode(ctx, provider, { jti, 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) {
|
} else if (username && password) {
|
||||||
await signInWithUsernameAndPassword(ctx, provider, username, password);
|
await signInWithUsernameAndPassword(ctx, provider, username, password);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
export const emailRegEx = /^\S+@\S+\.\S+$/;
|
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.',
|
username_exists: 'The username already exists.',
|
||||||
email_exists: 'The email already exists.',
|
email_exists: 'The email already exists.',
|
||||||
invalid_email: 'Invalid email address.',
|
invalid_email: 'Invalid email address.',
|
||||||
|
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.',
|
||||||
|
phone_not_exists: 'The phone number has not been registered yet.',
|
||||||
},
|
},
|
||||||
password: {
|
password: {
|
||||||
unsupported_encryption_method: 'The encryption method {{name}} is not supported.',
|
unsupported_encryption_method: 'The encryption method {{name}} is not supported.',
|
||||||
|
|
|
@ -48,7 +48,9 @@ const errors = {
|
||||||
username_exists: '用户名已存在。',
|
username_exists: '用户名已存在。',
|
||||||
email_exists: '邮箱地址已存在。',
|
email_exists: '邮箱地址已存在。',
|
||||||
invalid_email: '邮箱地址不正确。',
|
invalid_email: '邮箱地址不正确。',
|
||||||
|
invalid_phone: '手机号码不正确。',
|
||||||
email_not_exists: '邮箱地址尚未注册。',
|
email_not_exists: '邮箱地址尚未注册。',
|
||||||
|
phone_not_exists: '手机号码尚未注册。',
|
||||||
},
|
},
|
||||||
password: {
|
password: {
|
||||||
unsupported_encryption_method: '不支持的加密方法 {{name}}。',
|
unsupported_encryption_method: '不支持的加密方法 {{name}}。',
|
||||||
|
|
|
@ -13,6 +13,7 @@ export enum PasscodeType {
|
||||||
export enum UserLogType {
|
export enum UserLogType {
|
||||||
SignInUsernameAndPassword = 'SignInUsernameAndPassword',
|
SignInUsernameAndPassword = 'SignInUsernameAndPassword',
|
||||||
SignInEmail = 'SignInEmail',
|
SignInEmail = 'SignInEmail',
|
||||||
|
SignInSms = 'SignInSms',
|
||||||
ExchangeAccessToken = 'ExchangeAccessToken',
|
ExchangeAccessToken = 'ExchangeAccessToken',
|
||||||
}
|
}
|
||||||
export enum UserLogResult {
|
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');
|
create type user_log_result as enum ('Success', 'Failed');
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue