0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-31 22:51:25 -05:00

refactor(core,ui,test): sign-in and register flows mount on passwordless APIs (#2054)

This commit is contained in:
Darcy Ye 2022-10-10 14:21:39 +08:00 committed by GitHub
parent 0960afc97d
commit d3d189aa77
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 673 additions and 547 deletions

View file

@ -254,260 +254,342 @@ describe('session -> passwordlessRoutes', () => {
it('throw when code is wrong', async () => {
const response = await sessionRequest
.post('/session/passwordless/email/verify')
.send({ email: 'a@a.com', code: '1231', flow: 'sign-in' });
.send({ email: 'a@a.com', code: '1231', flow: PasscodeType.SignIn });
expect(response.statusCode).toEqual(400);
});
});
describe('POST /session/sign-in/passwordless/sms/send-passcode', () => {
beforeAll(() => {
describe('POST /session/sign-in/passwordless/sms', () => {
beforeEach(() => {
jest.resetAllMocks();
});
it('should call interactionResult', async () => {
interactionDetails.mockResolvedValueOnce({
jti: 'jti',
result: {
verification: {
phone: '13000000000',
flow: PasscodeType.SignIn,
expiresAt: dayjs().add(1, 'day').toISOString(),
},
},
});
});
it('should call sendPasscode', async () => {
const response = await sessionRequest
.post(`${signInRoute}/sms/send-passcode`)
.send({ phone: '13000000000' });
expect(response.statusCode).toEqual(204);
expect(sendPasscode).toHaveBeenCalled();
});
it('throw error if phone does not exist', async () => {
const response = await sessionRequest
.post(`${signInRoute}/sms/send-passcode`)
.send({ phone: '13000000001' });
expect(response.statusCode).toEqual(422);
});
});
describe('POST /session/sign-in/passwordless/sms/verify-passcode', () => {
it('assign result and redirect', async () => {
const response = await sessionRequest
.post(`${signInRoute}/sms/verify-passcode`)
.send({ phone: '13000000000', code: '1234' });
const response = await sessionRequest.post(`${signInRoute}/sms`);
expect(response.statusCode).toEqual(200);
expect(response.body).toHaveProperty('redirectTo');
expect(interactionResult).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.objectContaining({ login: { accountId: 'id' } }),
expect.objectContaining({
login: { accountId: 'id' },
}),
expect.anything()
);
});
it('throw error if phone does not exist', async () => {
const response = await sessionRequest
.post(`${signInRoute}/sms/verify-passcode`)
.send({ phone: '13000000001', code: '1234' });
expect(response.statusCode).toEqual(422);
});
it('throw error if verifyPasscode failed', async () => {
const response = await sessionRequest
.post(`${signInRoute}/sms/verify-passcode`)
.send({ phone: '13000000000', code: '1231' });
expect(response.statusCode).toEqual(400);
});
});
describe('POST /session/sign-in/passwordless/email/send-passcode', () => {
beforeAll(() => {
interactionDetails.mockResolvedValue({
jti: 'jti',
});
});
it('should call sendPasscode', async () => {
const response = await sessionRequest
.post(`${signInRoute}/email/send-passcode`)
.send({ email: 'a@a.com' });
expect(response.statusCode).toEqual(204);
expect(sendPasscode).toHaveBeenCalled();
});
it('throw error if email does not exist', async () => {
const response = await sessionRequest
.post(`${signInRoute}/email/send-passcode`)
.send({ email: 'b@a.com' });
expect(response.statusCode).toEqual(422);
});
});
describe('POST /session/sign-in/passwordless/email/verify-passcode', () => {
it('assign result and redirect', async () => {
const response = await sessionRequest
.post(`${signInRoute}/email/verify-passcode`)
.send({ email: 'a@a.com', code: '1234' });
expect(response.statusCode).toEqual(200);
expect(response.body).toHaveProperty('redirectTo');
expect(interactionResult).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.objectContaining({ login: { accountId: 'id' } }),
expect.anything()
);
});
it('throw error if email does not exist', async () => {
const response = await sessionRequest
.post(`${signInRoute}/email/send-passcode`)
.send({ email: 'b@a.com' });
expect(response.statusCode).toEqual(422);
});
it('throw error if verifyPasscode failed', async () => {
const response = await sessionRequest
.post(`${signInRoute}/email/verify-passcode`)
.send({ email: 'a@a.com', code: '1231' });
expect(response.statusCode).toEqual(400);
});
});
describe('POST /session/register/passwordless/sms/send-passcode', () => {
beforeAll(() => {
it('throw when verification session invalid', async () => {
interactionDetails.mockResolvedValueOnce({
jti: 'jti',
result: {
verification: {
phone: '13000000000',
expiresAt: dayjs().add(1, 'day').toISOString(),
},
},
});
const response = await sessionRequest.post(`${signInRoute}/sms`);
expect(response.statusCode).toEqual(404);
});
it('should call sendPasscode', async () => {
const response = await sessionRequest
.post(`${registerRoute}/sms/send-passcode`)
.send({ phone: '13000000001' });
expect(response.statusCode).toEqual(204);
expect(sendPasscode).toHaveBeenCalled();
});
it('throw error if phone not valid (charactors other than digits)', async () => {
const response = await sessionRequest
.post(`${registerRoute}/sms/send-passcode`)
.send({ phone: '1300000000a' });
expect(response.statusCode).toEqual(400);
});
it('throw error if phone not valid (without digits)', async () => {
const response = await sessionRequest
.post(`${registerRoute}/sms/send-passcode`)
.send({ phone: 'abcdefg' });
expect(response.statusCode).toEqual(400);
});
it('throw error if phone exists', async () => {
const response = await sessionRequest
.post(`${registerRoute}/sms/send-passcode`)
.send({ phone: '13000000000' });
expect(response.statusCode).toEqual(422);
});
});
describe('POST /session/register/passwordless/sms/verify-passcode', () => {
it('assign result and redirect', async () => {
const response = await sessionRequest
.post(`${registerRoute}/sms/verify-passcode`)
.send({ phone: '13000000001', code: '1234' });
expect(response.statusCode).toEqual(200);
expect(response.body).toHaveProperty('redirectTo');
expect(insertUser).toHaveBeenCalledWith(
expect.objectContaining({ id: 'user1', primaryPhone: '13000000001' })
);
expect(interactionResult).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.objectContaining({ login: { accountId: 'user1' } }),
expect.anything()
);
});
it('throw error if phone is invalid (characters other than digits)', async () => {
const response = await sessionRequest
.post(`${registerRoute}/sms/verify-passcode`)
.send({ phone: '1300000000a', code: '1234' });
expect(response.statusCode).toEqual(400);
});
it('throw error if phone not valid (without digits)', async () => {
const response = await sessionRequest
.post(`${registerRoute}/sms/verify-passcode`)
.send({ phone: 'abcdefg', code: '1234' });
expect(response.statusCode).toEqual(400);
});
it('throw error if phone exists', async () => {
const response = await sessionRequest
.post(`${registerRoute}/sms/verify-passcode`)
.send({ phone: '13000000000', code: '1234' });
expect(response.statusCode).toEqual(422);
});
it('throw error if verifyPasscode failed', async () => {
const response = await sessionRequest
.post(`${registerRoute}/sms/verify-passcode`)
.send({ phone: '13000000001', code: '1231' });
expect(response.statusCode).toEqual(400);
});
});
describe('POST /session/register/passwordless/email/send-passcode', () => {
beforeAll(() => {
it('throw when flow is not `sign-in`', async () => {
interactionDetails.mockResolvedValueOnce({
jti: 'jti',
result: {
verification: {
phone: '13000000000',
flow: PasscodeType.Register,
expiresAt: dayjs().add(1, 'day').toISOString(),
},
},
});
const response = await sessionRequest.post(`${signInRoute}/sms`);
expect(response.statusCode).toEqual(404);
});
it('should call sendPasscode', async () => {
const response = await sessionRequest
.post(`${registerRoute}/email/send-passcode`)
.send({ email: 'b@a.com' });
expect(response.statusCode).toEqual(204);
expect(sendPasscode).toHaveBeenCalled();
it('throw when expiresAt is not valid ISO date string', async () => {
interactionDetails.mockResolvedValueOnce({
result: {
verification: {
phone: '13000000000',
flow: PasscodeType.SignIn,
expiresAt: 'invalid date string',
},
},
});
const response = await sessionRequest.post(`${signInRoute}/sms`);
expect(response.statusCode).toEqual(401);
});
it('throw error if email not valid', async () => {
const response = await sessionRequest
.post(`${registerRoute}/email/send-passcode`)
.send({ email: 'aaa.com' });
expect(response.statusCode).toEqual(400);
it('throw when validation expired', async () => {
interactionDetails.mockResolvedValueOnce({
result: {
verification: {
phone: '13000000000',
flow: PasscodeType.SignIn,
expiresAt: dayjs().subtract(1, 'day').toISOString(),
},
},
});
const response = await sessionRequest.post(`${signInRoute}/sms`);
expect(response.statusCode).toEqual(401);
});
it('throw error if email exists', async () => {
const response = await sessionRequest
.post(`${registerRoute}/email/send-passcode`)
.send({ email: 'a@a.com' });
it('throw when phone not exist', async () => {
interactionDetails.mockResolvedValueOnce({
result: {
verification: {
flow: PasscodeType.SignIn,
expiresAt: dayjs().add(1, 'day').toISOString(),
},
},
});
const response = await sessionRequest.post(`${signInRoute}/sms`);
expect(response.statusCode).toEqual(404);
});
it("throw when phone not exist as user's primaryPhone", async () => {
interactionDetails.mockResolvedValueOnce({
result: {
verification: {
phone: '13000000001',
flow: PasscodeType.SignIn,
expiresAt: dayjs().add(1, 'day').toISOString(),
},
},
});
const response = await sessionRequest.post(`${signInRoute}/sms`);
expect(response.statusCode).toEqual(422);
});
});
describe('POST /session/register/passwordless/email/verify-passcode', () => {
it('assign result and redirect', async () => {
const response = await sessionRequest
.post(`${registerRoute}/email/verify-passcode`)
.send({ email: 'b@a.com', code: '1234' });
describe('POST /session/sign-in/passwordless/email', () => {
beforeEach(() => {
jest.resetAllMocks();
});
it('should call interactionResult', async () => {
interactionDetails.mockResolvedValueOnce({
result: {
verification: {
email: 'a@a.com',
flow: PasscodeType.SignIn,
expiresAt: dayjs().add(1, 'day').toISOString(),
},
},
});
const response = await sessionRequest.post(`${signInRoute}/email`);
expect(response.statusCode).toEqual(200);
expect(response.body).toHaveProperty('redirectTo');
expect(insertUser).toHaveBeenCalledWith(
expect.objectContaining({ id: 'user1', primaryEmail: 'b@a.com' })
);
expect(interactionResult).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.objectContaining({ login: { accountId: 'user1' } }),
expect.objectContaining({
login: { accountId: 'id' },
}),
expect.anything()
);
});
it('throw error if email not valid', async () => {
const response = await sessionRequest
.post(`${signInRoute}/email/send-passcode`)
.send({ email: 'aaa.com' });
expect(response.statusCode).toEqual(400);
it('throw when verification session invalid', async () => {
interactionDetails.mockResolvedValueOnce({
result: {
verification: {
email: 'a@a.com',
expiresAt: dayjs().add(1, 'day').toISOString(),
},
},
});
const response = await sessionRequest.post(`${signInRoute}/email`);
expect(response.statusCode).toEqual(404);
});
it('throw error if email exist', async () => {
const response = await sessionRequest
.post(`${signInRoute}/email/send-passcode`)
.send({ email: 'b@a.com' });
it('throw when flow is not `sign-in`', async () => {
interactionDetails.mockResolvedValueOnce({
result: {
verification: {
email: 'a@a.com',
flow: PasscodeType.Register,
expiresAt: dayjs().add(1, 'day').toISOString(),
},
},
});
const response = await sessionRequest.post(`${signInRoute}/email`);
expect(response.statusCode).toEqual(404);
});
it('throw when email not exist', async () => {
interactionDetails.mockResolvedValueOnce({
result: {
verification: {
flow: PasscodeType.SignIn,
expiresAt: dayjs().add(1, 'day').toISOString(),
},
},
});
const response = await sessionRequest.post(`${signInRoute}/email`);
expect(response.statusCode).toEqual(404);
});
it("throw when email not exist as user's primaryEmail", async () => {
interactionDetails.mockResolvedValueOnce({
result: {
verification: {
email: 'b@a.com',
flow: PasscodeType.SignIn,
expiresAt: dayjs().add(1, 'day').toISOString(),
},
},
});
const response = await sessionRequest.post(`${signInRoute}/email`);
expect(response.statusCode).toEqual(422);
});
});
it('throw error if verifyPasscode failed', async () => {
const response = await sessionRequest
.post(`${signInRoute}/email/verify-passcode`)
.send({ email: 'a@a.com', code: '1231' });
expect(response.statusCode).toEqual(400);
describe('POST /session/register/passwordless/sms', () => {
beforeEach(() => {
jest.resetAllMocks();
});
it('should call interactionResult', async () => {
interactionDetails.mockResolvedValueOnce({
result: {
verification: {
phone: '13000000001',
flow: PasscodeType.Register,
expiresAt: dayjs().add(1, 'day').toISOString(),
},
},
});
const response = await sessionRequest.post(`${registerRoute}/sms`);
expect(response.statusCode).toEqual(200);
expect(interactionResult).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.objectContaining({
login: { accountId: 'user1' },
}),
expect.anything()
);
});
it('throw when verification session invalid', async () => {
interactionDetails.mockResolvedValueOnce({
result: {
verification: {
phone: '13000000001',
expiresAt: dayjs().add(1, 'day').toISOString(),
},
},
});
const response = await sessionRequest.post(`${registerRoute}/sms`);
expect(response.statusCode).toEqual(404);
});
it('throw when flow is not `register`', async () => {
interactionDetails.mockResolvedValueOnce({
result: {
verification: {
phone: '13000000001',
flow: PasscodeType.SignIn,
expiresAt: dayjs().add(1, 'day').toISOString(),
},
},
});
const response = await sessionRequest.post(`${registerRoute}/sms`);
expect(response.statusCode).toEqual(404);
});
it('throw when phone not exist', async () => {
interactionDetails.mockResolvedValueOnce({
result: {
verification: {
flow: PasscodeType.Register,
expiresAt: dayjs().add(1, 'day').toISOString(),
},
},
});
const response = await sessionRequest.post(`${registerRoute}/sms`);
expect(response.statusCode).toEqual(404);
});
it("throw when phone already exist as user's primaryPhone", async () => {
interactionDetails.mockResolvedValueOnce({
result: {
verification: {
phone: '13000000000',
flow: PasscodeType.Register,
expiresAt: dayjs().add(1, 'day').toISOString(),
},
},
});
const response = await sessionRequest.post(`${registerRoute}/sms`);
expect(response.statusCode).toEqual(422);
});
});
describe('POST /session/register/passwordless/email', () => {
beforeEach(() => {
jest.resetAllMocks();
});
it('should call interactionResult', async () => {
interactionDetails.mockResolvedValueOnce({
result: {
verification: {
email: 'b@a.com',
flow: PasscodeType.Register,
expiresAt: dayjs().add(1, 'day').toISOString(),
},
},
});
const response = await sessionRequest.post(`${registerRoute}/email`);
expect(response.statusCode).toEqual(200);
expect(interactionResult).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.objectContaining({
login: { accountId: 'user1' },
}),
expect.anything()
);
});
it('throw when verification session invalid', async () => {
interactionDetails.mockResolvedValueOnce({
result: {
verification: {
email: 'b@a.com',
expiresAt: dayjs().add(1, 'day').toISOString(),
},
},
});
const response = await sessionRequest.post(`${registerRoute}/email`);
expect(response.statusCode).toEqual(404);
});
it('throw when flow is not `register`', async () => {
interactionDetails.mockResolvedValueOnce({
result: {
verification: {
email: 'b@a.com',
flow: PasscodeType.SignIn,
expiresAt: dayjs().add(1, 'day').toISOString(),
},
},
});
const response = await sessionRequest.post(`${registerRoute}/email`);
expect(response.statusCode).toEqual(404);
});
it('throw when email not exist', async () => {
interactionDetails.mockResolvedValueOnce({
result: {
verification: {
flow: PasscodeType.Register,
expiresAt: dayjs().add(1, 'day').toISOString(),
},
},
});
const response = await sessionRequest.post(`${registerRoute}/email`);
expect(response.statusCode).toEqual(404);
});
it("throw when email already exist as user's primaryEmail", async () => {
interactionDetails.mockResolvedValueOnce({
result: {
verification: {
email: 'a@a.com',
flow: PasscodeType.Register,
expiresAt: dayjs().add(1, 'day').toISOString(),
},
},
});
const response = await sessionRequest.post(`${registerRoute}/email`);
expect(response.statusCode).toEqual(422);
});
});
});

View file

@ -1,6 +1,5 @@
import { emailRegEx, phoneRegEx } from '@logto/core-kit';
import { PasscodeType } from '@logto/schemas';
import dayjs from 'dayjs';
import { Provider } from 'oidc-provider';
import { object, string } from 'zod';
@ -10,18 +9,29 @@ import { assignInteractionResults } from '@/lib/session';
import { generateUserId, insertUser } from '@/lib/user';
import koaGuard from '@/middleware/koa-guard';
import {
updateUserById,
hasUserWithEmail,
hasUserWithPhone,
findUserByEmail,
findUserByPhone,
hasUserWithEmail,
hasUserWithPhone,
updateUserById,
} from '@/queries/user';
import { passcodeTypeGuard } from '@/routes/session/types';
import {
emailRegisterSessionResultGuard,
emailSignInSessionResultGuard,
passcodeTypeGuard,
smsRegisterSessionResultGuard,
smsSignInSessionResultGuard,
} from '@/routes/session/types';
import assertThat from '@/utils/assert-that';
import { AnonymousRouter } from '../types';
import { verificationTimeout } from './consts';
import { getPasswordlessRelatedLogType, getRoutePrefix } from './utils';
import {
assignVerificationResult,
getPasswordlessRelatedLogType,
getRoutePrefix,
getVerificationStorageFromInteraction,
validateAndCheckWhetherVerificationExpires,
} from './utils';
export const registerRoute = getRoutePrefix('register', 'passwordless');
export const signInRoute = getRoutePrefix('sign-in', 'passwordless');
@ -102,13 +112,7 @@ export default function passwordlessRoutes<T extends AnonymousRouter>(
await verifyPasscode(jti, flow, code, { phone });
await provider.interactionResult(ctx.req, ctx.res, {
verification: {
flow,
expiresAt: dayjs().add(verificationTimeout, 'second').toISOString(),
phone,
},
});
await assignVerificationResult(ctx, provider, flow, { phone });
ctx.status = 204;
return next();
@ -135,208 +139,118 @@ export default function passwordlessRoutes<T extends AnonymousRouter>(
await verifyPasscode(jti, flow, code, { email });
await provider.interactionResult(ctx.req, ctx.res, {
verification: {
flow,
expiresAt: dayjs().add(verificationTimeout, 'second').toISOString(),
email,
},
});
await assignVerificationResult(ctx, provider, flow, { email });
ctx.status = 204;
return next();
}
);
router.post(
`${signInRoute}/sms/send-passcode`,
koaGuard({ body: object({ phone: string().regex(phoneRegEx) }) }),
async (ctx, next) => {
const { jti } = await provider.interactionDetails(ctx.req, ctx.res);
const { phone } = ctx.guard.body;
const type = 'SignInSmsSendPasscode';
ctx.log(type, { phone });
router.post(`${signInRoute}/sms`, async (ctx, next) => {
const verificationStorage = await getVerificationStorageFromInteraction(
ctx,
provider,
smsSignInSessionResultGuard
);
assertThat(
await hasUserWithPhone(phone),
new RequestError({ code: 'user.phone_not_exists', status: 422 })
);
const type = getPasswordlessRelatedLogType(PasscodeType.SignIn, 'sms');
ctx.log(type, verificationStorage);
const passcode = await createPasscode(jti, PasscodeType.SignIn, { phone });
const { dbEntry } = await sendPasscode(passcode);
ctx.log(type, { connectorId: dbEntry.id });
ctx.status = 204;
const { phone, expiresAt } = verificationStorage;
return next();
}
);
validateAndCheckWhetherVerificationExpires(expiresAt);
router.post(
`${signInRoute}/sms/verify-passcode`,
koaGuard({ body: object({ phone: string().regex(phoneRegEx), code: string() }) }),
async (ctx, next) => {
const { jti } = await provider.interactionDetails(ctx.req, ctx.res);
const { phone, code } = ctx.guard.body;
const type = 'SignInSms';
ctx.log(type, { phone, code });
assertThat(
await hasUserWithPhone(phone),
new RequestError({ code: 'user.phone_not_exists', status: 422 })
);
const { id } = await findUserByPhone(phone);
ctx.log(type, { userId: id });
assertThat(
await hasUserWithPhone(phone),
new RequestError({ code: 'user.phone_not_exists', status: 422 })
);
await updateUserById(id, { lastSignInAt: Date.now() });
await assignInteractionResults(ctx, provider, { login: { accountId: id } });
await verifyPasscode(jti, PasscodeType.SignIn, code, { phone });
const { id } = await findUserByPhone(phone);
ctx.log(type, { userId: id });
return next();
});
await updateUserById(id, { lastSignInAt: Date.now() });
await assignInteractionResults(ctx, provider, { login: { accountId: id } }, true);
router.post(`${signInRoute}/email`, async (ctx, next) => {
const verificationStorage = await getVerificationStorageFromInteraction(
ctx,
provider,
emailSignInSessionResultGuard
);
return next();
}
);
const type = getPasswordlessRelatedLogType(PasscodeType.SignIn, 'email');
ctx.log(type, verificationStorage);
router.post(
`${signInRoute}/email/send-passcode`,
koaGuard({ body: object({ email: string().regex(emailRegEx) }) }),
async (ctx, next) => {
const { jti } = await provider.interactionDetails(ctx.req, ctx.res);
const { email } = ctx.guard.body;
const type = 'SignInEmailSendPasscode';
ctx.log(type, { email });
const { email, expiresAt } = verificationStorage;
assertThat(
await hasUserWithEmail(email),
new RequestError({ code: 'user.email_not_exists', status: 422 })
);
validateAndCheckWhetherVerificationExpires(expiresAt);
const passcode = await createPasscode(jti, PasscodeType.SignIn, { email });
const { dbEntry } = await sendPasscode(passcode);
ctx.log(type, { connectorId: dbEntry.id });
ctx.status = 204;
assertThat(
await hasUserWithEmail(email),
new RequestError({ code: 'user.email_not_exists', status: 422 })
);
const { id } = await findUserByEmail(email);
ctx.log(type, { userId: id });
return next();
}
);
await updateUserById(id, { lastSignInAt: Date.now() });
await assignInteractionResults(ctx, provider, { login: { accountId: id } });
router.post(
`${signInRoute}/email/verify-passcode`,
koaGuard({ body: object({ email: string().regex(emailRegEx), code: string() }) }),
async (ctx, next) => {
const { jti } = await provider.interactionDetails(ctx.req, ctx.res);
const { email, code } = ctx.guard.body;
const type = 'SignInEmail';
ctx.log(type, { email, code });
return next();
});
assertThat(
await hasUserWithEmail(email),
new RequestError({ code: 'user.email_not_exists', status: 422 })
);
router.post(`${registerRoute}/sms`, async (ctx, next) => {
const verificationStorage = await getVerificationStorageFromInteraction(
ctx,
provider,
smsRegisterSessionResultGuard
);
await verifyPasscode(jti, PasscodeType.SignIn, code, { email });
const { id } = await findUserByEmail(email);
ctx.log(type, { userId: id });
const type = getPasswordlessRelatedLogType(PasscodeType.Register, 'sms');
ctx.log(type, verificationStorage);
await updateUserById(id, { lastSignInAt: Date.now() });
await assignInteractionResults(ctx, provider, { login: { accountId: id } }, true);
const { phone, expiresAt } = verificationStorage;
return next();
}
);
validateAndCheckWhetherVerificationExpires(expiresAt);
router.post(
`${registerRoute}/sms/send-passcode`,
koaGuard({ body: object({ phone: string().regex(phoneRegEx) }) }),
async (ctx, next) => {
const { jti } = await provider.interactionDetails(ctx.req, ctx.res);
const { phone } = ctx.guard.body;
const type = 'RegisterSmsSendPasscode';
ctx.log(type, { phone });
assertThat(
!(await hasUserWithPhone(phone)),
new RequestError({ code: 'user.phone_exists_register', status: 422 })
);
const id = await generateUserId();
ctx.log(type, { userId: id });
assertThat(
!(await hasUserWithPhone(phone)),
new RequestError({ code: 'user.phone_exists_register', status: 422 })
);
await insertUser({ id, primaryPhone: phone, lastSignInAt: Date.now() });
await assignInteractionResults(ctx, provider, { login: { accountId: id } });
const passcode = await createPasscode(jti, PasscodeType.Register, { phone });
const { dbEntry } = await sendPasscode(passcode);
ctx.log(type, { connectorId: dbEntry.id });
ctx.status = 204;
return next();
});
return next();
}
);
router.post(`${registerRoute}/email`, async (ctx, next) => {
const verificationStorage = await getVerificationStorageFromInteraction(
ctx,
provider,
emailRegisterSessionResultGuard
);
router.post(
`${registerRoute}/sms/verify-passcode`,
koaGuard({ body: object({ phone: string().regex(phoneRegEx), code: string() }) }),
async (ctx, next) => {
const { jti } = await provider.interactionDetails(ctx.req, ctx.res);
const { phone, code } = ctx.guard.body;
const type = 'RegisterSms';
ctx.log(type, { phone, code });
const type = getPasswordlessRelatedLogType(PasscodeType.Register, 'email');
ctx.log(type, verificationStorage);
assertThat(
!(await hasUserWithPhone(phone)),
new RequestError({ code: 'user.phone_exists_register', status: 422 })
);
const { email, expiresAt } = verificationStorage;
await verifyPasscode(jti, PasscodeType.Register, code, { phone });
const id = await generateUserId();
ctx.log(type, { userId: id });
validateAndCheckWhetherVerificationExpires(expiresAt);
await insertUser({ id, primaryPhone: phone, lastSignInAt: Date.now() });
await assignInteractionResults(ctx, provider, { login: { accountId: id } });
assertThat(
!(await hasUserWithEmail(email)),
new RequestError({ code: 'user.email_exists_register', status: 422 })
);
const id = await generateUserId();
ctx.log(type, { userId: id });
return next();
}
);
await insertUser({ id, primaryEmail: email, lastSignInAt: Date.now() });
await assignInteractionResults(ctx, provider, { login: { accountId: id } });
router.post(
`${registerRoute}/email/send-passcode`,
koaGuard({ body: object({ email: string().regex(emailRegEx) }) }),
async (ctx, next) => {
const { jti } = await provider.interactionDetails(ctx.req, ctx.res);
const { email } = ctx.guard.body;
const type = 'RegisterEmailSendPasscode';
ctx.log(type, { email });
assertThat(
!(await hasUserWithEmail(email)),
new RequestError({ code: 'user.email_exists_register', status: 422 })
);
const passcode = await createPasscode(jti, PasscodeType.Register, { email });
const { dbEntry } = await sendPasscode(passcode);
ctx.log(type, { connectorId: dbEntry.id });
ctx.status = 204;
return next();
}
);
router.post(
`${registerRoute}/email/verify-passcode`,
koaGuard({ body: object({ email: string().regex(emailRegEx), code: string() }) }),
async (ctx, next) => {
const { jti } = await provider.interactionDetails(ctx.req, ctx.res);
const { email, code } = ctx.guard.body;
const type = 'RegisterEmail';
ctx.log(type, { email, code });
assertThat(
!(await hasUserWithEmail(email)),
new RequestError({ code: 'user.email_exists_register', status: 422 })
);
await verifyPasscode(jti, PasscodeType.Register, code, { email });
const id = await generateUserId();
ctx.log(type, { userId: id });
await insertUser({ id, primaryEmail: email, lastSignInAt: Date.now() });
await assignInteractionResults(ctx, provider, { login: { accountId: id } });
return next();
}
);
return next();
});
}

View file

@ -3,15 +3,15 @@ import { z } from 'zod';
export const passcodeTypeGuard = z.nativeEnum(PasscodeType);
export const viaGuard = z.enum(['email', 'sms']);
export const methodGuard = z.enum(['email', 'sms']);
export type Via = z.infer<typeof viaGuard>;
export type Method = z.infer<typeof methodGuard>;
export const operationGuard = z.enum(['send', 'verify']);
export type Operation = z.infer<typeof operationGuard>;
export type PasscodePayload = { email: string } | { phone: string };
export type VerifiedIdentity = { email: string } | { phone: string };
export const verificationStorageGuard = z.object({
email: z.string().optional(),
@ -21,3 +21,51 @@ export const verificationStorageGuard = z.object({
});
export type VerificationStorage = z.infer<typeof verificationStorageGuard>;
export type VerificationResult<T = VerificationStorage> = { verification: T };
const smsSignInSessionStorageGuard = z.object({
flow: z.literal(PasscodeType.SignIn),
expiresAt: z.string(),
phone: z.string(),
});
export type SmsSignInSessionStorage = z.infer<typeof smsSignInSessionStorageGuard>;
export const smsSignInSessionResultGuard = z.object({ verification: smsSignInSessionStorageGuard });
const emailSignInSessionStorageGuard = z.object({
flow: z.literal(PasscodeType.SignIn),
expiresAt: z.string(),
email: z.string(),
});
export type EmailSignInSessionStorage = z.infer<typeof emailSignInSessionStorageGuard>;
export const emailSignInSessionResultGuard = z.object({
verification: emailSignInSessionStorageGuard,
});
const smsRegisterSessionStorageGuard = z.object({
flow: z.literal(PasscodeType.Register),
expiresAt: z.string(),
phone: z.string(),
});
export type SmsRegisterSessionStorage = z.infer<typeof smsRegisterSessionStorageGuard>;
export const smsRegisterSessionResultGuard = z.object({
verification: smsRegisterSessionStorageGuard,
});
const emailRegisterSessionStorageGuard = z.object({
flow: z.literal(PasscodeType.Register),
expiresAt: z.string(),
email: z.string(),
});
export type EmailRegisterSessionStorage = z.infer<typeof emailRegisterSessionStorageGuard>;
export const emailRegisterSessionResultGuard = z.object({
verification: emailRegisterSessionStorageGuard,
});

View file

@ -1,12 +1,15 @@
import { logTypeGuard, LogType, PasscodeType } from '@logto/schemas';
import { Truthy } from '@silverhand/essentials';
import dayjs from 'dayjs';
import { z } from 'zod';
import { Context } from 'koa';
import { Provider } from 'oidc-provider';
import { ZodType, ZodTypeDef } from 'zod';
import RequestError from '@/errors/RequestError';
import assertThat from '@/utils/assert-that';
import { verificationStorageGuard, Operation, VerificationStorage, Via } from './types';
import { verificationTimeout } from './consts';
import { Method, Operation, VerificationResult, VerifiedIdentity } from './types';
export const getRoutePrefix = (
type: 'sign-in' | 'register' | 'forgot-password',
@ -20,10 +23,10 @@ export const getRoutePrefix = (
export const getPasswordlessRelatedLogType = (
flow: PasscodeType,
via: Via,
method: Method,
operation?: Operation
): LogType => {
const body = via === 'email' ? 'Email' : 'Sms';
const body = method === 'email' ? 'Email' : 'Sms';
const suffix = operation === 'send' ? 'SendPasscode' : '';
const result = logTypeGuard.safeParse(flow + body + suffix);
@ -32,37 +35,54 @@ export const getPasswordlessRelatedLogType = (
return result.data;
};
export const parseVerificationStorage = (data: unknown): VerificationStorage => {
const verificationResult = z
.object({
verification: verificationStorageGuard,
})
.safeParse(data);
const parseVerificationStorage = <T = unknown>(
data: unknown,
resultGuard: ZodType<VerificationResult<T>, ZodTypeDef, unknown>
): T => {
const verificationResult = resultGuard.safeParse(data);
assertThat(
verificationResult.success,
new RequestError({
code: 'session.verification_session_not_found',
status: 404,
})
);
if (!verificationResult.success) {
throw new RequestError(
{
code: 'session.verification_session_not_found',
status: 404,
},
verificationResult.error
);
}
return verificationResult.data.verification;
};
export const verificationSessionCheckByFlow = (
currentFlow: PasscodeType,
payload: Pick<VerificationStorage, 'flow' | 'expiresAt'>
) => {
const { flow, expiresAt } = payload;
assertThat(
flow === currentFlow,
new RequestError({ code: 'session.passwordless_not_verified', status: 401 })
);
export const validateAndCheckWhetherVerificationExpires = (expiresAt: string) => {
assertThat(
dayjs(expiresAt).isValid() && dayjs(expiresAt).isAfter(dayjs()),
new RequestError({ code: 'session.verification_expired', status: 401 })
);
};
export const getVerificationStorageFromInteraction = async <T = unknown>(
ctx: Context,
provider: Provider,
resultGuard: ZodType<VerificationResult<T>, ZodTypeDef, unknown>
): Promise<T> => {
const { result } = await provider.interactionDetails(ctx.req, ctx.res);
return parseVerificationStorage<T>(result, resultGuard);
};
export const assignVerificationResult = async (
ctx: Context,
provider: Provider,
flow: PasscodeType,
identity: VerifiedIdentity
) => {
const verificationStorage: VerificationResult = {
verification: {
flow,
expiresAt: dayjs().add(verificationTimeout, 'second').toISOString(),
...identity,
},
};
await provider.interactionResult(ctx.req, ctx.res, verificationStorage);
};

View file

@ -1,3 +1,5 @@
import { PasscodeType } from '@logto/schemas';
import api from './api';
type RedirectResponse = {
@ -51,12 +53,13 @@ export const consent = async (interactionCookie: string) =>
.json<RedirectResponse>();
export const sendRegisterUserWithEmailPasscode = (email: string, interactionCookie: string) =>
api.post('session/register/passwordless/email/send-passcode', {
api.post('session/passwordless/email/send', {
headers: {
cookie: interactionCookie,
},
json: {
email,
flow: PasscodeType.Register,
},
});
@ -65,25 +68,34 @@ export const verifyRegisterUserWithEmailPasscode = (
code: string,
interactionCookie: string
) =>
api
.post('session/register/passwordless/email/verify-passcode', {
headers: {
cookie: interactionCookie,
},
json: {
email,
code,
},
})
.json<RedirectResponse>();
export const sendSignInUserWithEmailPasscode = (email: string, interactionCookie: string) =>
api.post('session/sign-in/passwordless/email/send-passcode', {
api.post('session/passwordless/email/verify', {
headers: {
cookie: interactionCookie,
},
json: {
email,
code,
flow: PasscodeType.Register,
},
});
export const checkVerificationSessionAndRegisterWithEmail = (interactionCookie: string) =>
api
.post('session/register/passwordless/email', {
headers: {
cookie: interactionCookie,
},
})
.json<RedirectResponse>();
export const sendSignInUserWithEmailPasscode = (email: string, interactionCookie: string) =>
api.post('session/passwordless/email/send', {
headers: {
cookie: interactionCookie,
},
json: {
email,
flow: PasscodeType.SignIn,
},
});
@ -92,25 +104,34 @@ export const verifySignInUserWithEmailPasscode = (
code: string,
interactionCookie: string
) =>
api.post('session/passwordless/email/verify', {
headers: {
cookie: interactionCookie,
},
json: {
email,
code,
flow: PasscodeType.SignIn,
},
});
export const checkVerificationSessionAndSignInWithEmail = (interactionCookie: string) =>
api
.post('session/sign-in/passwordless/email/verify-passcode', {
.post('session/sign-in/passwordless/email', {
headers: {
cookie: interactionCookie,
},
json: {
email,
code,
},
})
.json<RedirectResponse>();
export const sendRegisterUserWithSmsPasscode = (phone: string, interactionCookie: string) =>
api.post('session/register/passwordless/sms/send-passcode', {
api.post('session/passwordless/sms/send', {
headers: {
cookie: interactionCookie,
},
json: {
phone,
flow: PasscodeType.Register,
},
});
@ -119,25 +140,34 @@ export const verifyRegisterUserWithSmsPasscode = (
code: string,
interactionCookie: string
) =>
api
.post('session/register/passwordless/sms/verify-passcode', {
headers: {
cookie: interactionCookie,
},
json: {
phone,
code,
},
})
.json<RedirectResponse>();
export const sendSignInUserWithSmsPasscode = (phone: string, interactionCookie: string) =>
api.post('session/sign-in/passwordless/sms/send-passcode', {
api.post('session/passwordless/sms/verify', {
headers: {
cookie: interactionCookie,
},
json: {
phone,
code,
flow: PasscodeType.Register,
},
});
export const checkVerificationSessionAndRegisterWithSms = (interactionCookie: string) =>
api
.post('session/register/passwordless/sms', {
headers: {
cookie: interactionCookie,
},
})
.json<RedirectResponse>();
export const sendSignInUserWithSmsPasscode = (phone: string, interactionCookie: string) =>
api.post('session/passwordless/sms/send', {
headers: {
cookie: interactionCookie,
},
json: {
phone,
flow: PasscodeType.SignIn,
},
});
@ -146,15 +176,23 @@ export const verifySignInUserWithSmsPasscode = (
code: string,
interactionCookie: string
) =>
api.post('session/passwordless/sms/verify', {
headers: {
cookie: interactionCookie,
},
json: {
phone,
code,
flow: PasscodeType.SignIn,
},
});
export const checkVerificationSessionAndSignInWithSms = (interactionCookie: string) =>
api
.post('session/sign-in/passwordless/sms/verify-passcode', {
.post('session/sign-in/passwordless/sms', {
headers: {
cookie: interactionCookie,
},
json: {
phone,
code,
},
})
.json<RedirectResponse>();

View file

@ -10,12 +10,16 @@ import {
import {
sendRegisterUserWithEmailPasscode,
verifyRegisterUserWithEmailPasscode,
checkVerificationSessionAndRegisterWithEmail,
sendSignInUserWithEmailPasscode,
verifySignInUserWithEmailPasscode,
checkVerificationSessionAndSignInWithEmail,
sendRegisterUserWithSmsPasscode,
verifyRegisterUserWithSmsPasscode,
checkVerificationSessionAndRegisterWithSms,
sendSignInUserWithSmsPasscode,
verifySignInUserWithSmsPasscode,
checkVerificationSessionAndSignInWithSms,
disableConnector,
signInWithUsernameAndPassword,
} from '@/api';
@ -69,9 +73,11 @@ describe('email passwordless flow', () => {
const { code } = passcodeRecord;
const { redirectTo } = await verifyRegisterUserWithEmailPasscode(
email,
code,
await expect(
verifyRegisterUserWithEmailPasscode(email, code, client.interactionCookie)
).resolves.not.toThrow();
const { redirectTo } = await checkVerificationSessionAndRegisterWithEmail(
client.interactionCookie
);
@ -99,9 +105,11 @@ describe('email passwordless flow', () => {
const { code } = passcodeRecord;
const { redirectTo } = await verifySignInUserWithEmailPasscode(
email,
code,
await expect(
verifySignInUserWithEmailPasscode(email, code, client.interactionCookie)
).resolves.not.toThrow();
const { redirectTo } = await checkVerificationSessionAndSignInWithEmail(
client.interactionCookie
);
@ -142,9 +150,11 @@ describe('sms passwordless flow', () => {
const { code } = passcodeRecord;
const { redirectTo } = await verifyRegisterUserWithSmsPasscode(
phone,
code,
await expect(
verifyRegisterUserWithSmsPasscode(phone, code, client.interactionCookie)
).resolves.not.toThrow();
const { redirectTo } = await checkVerificationSessionAndRegisterWithSms(
client.interactionCookie
);
@ -172,11 +182,11 @@ describe('sms passwordless flow', () => {
const { code } = passcodeRecord;
const { redirectTo } = await verifySignInUserWithSmsPasscode(
phone,
code,
client.interactionCookie
);
await expect(
verifySignInUserWithSmsPasscode(phone, code, client.interactionCookie)
).resolves.not.toThrow();
const { redirectTo } = await checkVerificationSessionAndSignInWithSms(client.interactionCookie);
await client.processSession(redirectTo);

View file

@ -62,8 +62,6 @@ const errors = {
'Forgot password verification has expired. Please go back and verify again.',
verification_session_not_found:
'Passwordless verification session not found. Please go back and retry.',
passwordless_not_verified:
'Passwordless of {{flow}} flow is not verified. Please go back and verify.',
verification_expired: 'Passwordless verification has expired. Please go back and verify again.',
unauthorized: 'Please sign in first.',
unsupported_prompt_name: 'Unsupported prompt name.',

View file

@ -67,8 +67,6 @@ const errors = {
'Forgot password verification has expired. Please go back and verify again.', // UNTRANSLATED
verification_session_not_found:
'Passwordless verification session not found. Please go back and retry.', // UNTRANSLATED
passwordless_not_verified:
'Passwordless of {{flow}} flow is not verified. Please go back and verify.', // UNTRANSLATED
verification_expired: 'Passwordless verification has expired. Please go back and verify again.', // UNTRANSLATED
unauthorized: "Veuillez vous enregistrer d'abord.",
unsupported_prompt_name: "Nom d'invite non supporté.",

View file

@ -61,8 +61,6 @@ const errors = {
'Forgot password verification has expired. Please go back and verify again.', // UNTRANSLATED
verification_session_not_found:
'Passwordless verification session not found. Please go back and retry.', // UNTRANSLATED
passwordless_not_verified:
'Passwordless of {{flow}} flow is not verified. Please go back and verify.', // UNTRANSLATED
verification_expired: 'Passwordless verification has expired. Please go back and verify again.', // UNTRANSLATED
unauthorized: '로그인을 먼저 해주세요.',
unsupported_prompt_name: '지원하지 않는 Prompt 이름이예요.',

View file

@ -63,8 +63,6 @@ const errors = {
'Forgot password verification has expired. Please go back and verify again.', // UNTRANSLATED
verification_session_not_found:
'Passwordless verification session not found. Please go back and retry.', // UNTRANSLATED
passwordless_not_verified:
'Passwordless of {{flow}} flow is not verified. Please go back and verify.', // UNTRANSLATED
verification_expired: 'Passwordless verification has expired. Please go back and verify again.', // UNTRANSLATED
unauthorized: 'Faça login primeiro.',
unsupported_prompt_name: 'Nome de prompt não suportado.',

View file

@ -63,8 +63,6 @@ const errors = {
'Forgot password verification has expired. Please go back and verify again.', // UNTRANSLATED
verification_session_not_found:
'Passwordless verification session not found. Please go back and retry.', // UNTRANSLATED
passwordless_not_verified:
'Passwordless of {{flow}} flow is not verified. Please go back and verify.', // UNTRANSLATED
verification_expired: 'Passwordless verification has expired. Please go back and verify again.', // UNTRANSLATED
unauthorized: 'Lütfen önce oturum açın.',
unsupported_prompt_name: 'Desteklenmeyen prompt adı.',

View file

@ -58,7 +58,6 @@ const errors = {
forgot_password_session_not_found: '无法找到忘记密码验证信息,请尝试重新验证。',
forgot_password_verification_expired: '忘记密码验证已过期,请尝试重新验证。',
verification_session_not_found: '无法找到无密码流程验证信息,请尝试重新验证。',
passwordless_not_verified: '无密码验证 {{flow}} 流程没找到。请返回并验证。',
verification_expired: '无密码验证已过期。请返回重新验证。',
unauthorized: '请先登录',
unsupported_prompt_name: '不支持的 prompt name',

View file

@ -1,3 +1,4 @@
import { PasscodeType } from '@logto/schemas';
import ky from 'ky';
import { consent } from './consent';
@ -87,50 +88,56 @@ describe('api', () => {
it('sendSignInSmsPasscode', async () => {
await sendSignInSmsPasscode(phone);
expect(ky.post).toBeCalledWith('/api/session/sign-in/passwordless/sms/send-passcode', {
expect(ky.post).toBeCalledWith('/api/session/passwordless/sms/send', {
json: {
phone,
flow: PasscodeType.SignIn,
},
});
});
it('verifySignInSmsPasscode', async () => {
mockKyPost.mockReturnValueOnce({
mockKyPost.mockReturnValueOnce({}).mockReturnValueOnce({
json: () => ({
redirectTo: '/',
}),
});
await verifySignInSmsPasscode(phone, code);
expect(ky.post).toBeCalledWith('/api/session/sign-in/passwordless/sms/verify-passcode', {
expect(ky.post).toBeCalledWith('/api/session/passwordless/sms/verify', {
json: {
phone,
code,
flow: PasscodeType.SignIn,
},
});
expect(ky.post).toBeCalledWith('/api/session/sign-in/passwordless/sms');
});
it('sendSignInEmailPasscode', async () => {
await sendSignInEmailPasscode(email);
expect(ky.post).toBeCalledWith('/api/session/sign-in/passwordless/email/send-passcode', {
expect(ky.post).toBeCalledWith('/api/session/passwordless/email/send', {
json: {
email,
flow: PasscodeType.SignIn,
},
});
});
it('verifySignInEmailPasscode', async () => {
mockKyPost.mockReturnValueOnce({
mockKyPost.mockReturnValueOnce({}).mockReturnValueOnce({
json: () => ({
redirectTo: '/',
}),
});
await verifySignInEmailPasscode(email, code);
expect(ky.post).toBeCalledWith('/api/session/sign-in/passwordless/email/verify-passcode', {
expect(ky.post).toBeCalledWith('/api/session/passwordless/email/verify', {
json: {
email,
code,
flow: PasscodeType.SignIn,
},
});
expect(ky.post).toBeCalledWith('/api/session/sign-in/passwordless/email');
});
it('consent', async () => {
@ -150,40 +157,46 @@ describe('api', () => {
it('sendRegisterSmsPasscode', async () => {
await sendRegisterSmsPasscode(phone);
expect(ky.post).toBeCalledWith('/api/session/register/passwordless/sms/send-passcode', {
expect(ky.post).toBeCalledWith('/api/session/passwordless/sms/send', {
json: {
phone,
flow: PasscodeType.Register,
},
});
});
it('verifyRegisterSmsPasscode', async () => {
await verifyRegisterSmsPasscode(phone, code);
expect(ky.post).toBeCalledWith('/api/session/register/passwordless/sms/verify-passcode', {
expect(ky.post).toBeCalledWith('/api/session/passwordless/sms/verify', {
json: {
phone,
code,
flow: PasscodeType.Register,
},
});
expect(ky.post).toBeCalledWith('/api/session/register/passwordless/sms');
});
it('sendRegisterEmailPasscode', async () => {
await sendRegisterEmailPasscode(email);
expect(ky.post).toBeCalledWith('/api/session/register/passwordless/email/send-passcode', {
expect(ky.post).toBeCalledWith('/api/session/passwordless/email/send', {
json: {
email,
flow: PasscodeType.Register,
},
});
});
it('verifyRegisterEmailPasscode', async () => {
await verifyRegisterEmailPasscode(email, code);
expect(ky.post).toBeCalledWith('/api/session/register/passwordless/email/verify-passcode', {
expect(ky.post).toBeCalledWith('/api/session/passwordless/email/verify', {
json: {
email,
code,
flow: PasscodeType.Register,
},
});
expect(ky.post).toBeCalledWith('/api/session/register/passwordless/email');
});
it('sendForgotPasswordSmsPasscode', async () => {

View file

@ -1,6 +1,8 @@
import { PasscodeType } from '@logto/schemas';
import api from './api';
const registerApiPrefix = '/api/session/register';
const apiPrefix = '/api/session';
export const register = async (username: string, password: string) => {
type Response = {
@ -8,7 +10,7 @@ export const register = async (username: string, password: string) => {
};
return api
.post(`${registerApiPrefix}/username-password`, {
.post(`${apiPrefix}/register/username-password`, {
json: {
username,
password,
@ -19,9 +21,10 @@ export const register = async (username: string, password: string) => {
export const sendRegisterSmsPasscode = async (phone: string) => {
await api
.post(`${registerApiPrefix}/passwordless/sms/send-passcode`, {
.post(`${apiPrefix}/passwordless/sms/send`, {
json: {
phone,
flow: PasscodeType.Register,
},
})
.json();
@ -34,21 +37,23 @@ export const verifyRegisterSmsPasscode = async (phone: string, code: string) =>
redirectTo: string;
};
return api
.post(`${registerApiPrefix}/passwordless/sms/verify-passcode`, {
json: {
phone,
code,
},
})
.json<Response>();
await api.post(`${apiPrefix}/passwordless/sms/verify`, {
json: {
phone,
code,
flow: PasscodeType.Register,
},
});
return api.post(`${apiPrefix}/register/passwordless/sms`).json<Response>();
};
export const sendRegisterEmailPasscode = async (email: string) => {
await api
.post(`${registerApiPrefix}/passwordless/email/send-passcode`, {
.post(`${apiPrefix}/passwordless/email/send`, {
json: {
email,
flow: PasscodeType.Register,
},
})
.json();
@ -61,12 +66,13 @@ export const verifyRegisterEmailPasscode = async (email: string, code: string) =
redirectTo: string;
};
return api
.post(`${registerApiPrefix}/passwordless/email/verify-passcode`, {
json: {
email,
code,
},
})
.json<Response>();
await api.post(`${apiPrefix}/passwordless/email/verify`, {
json: {
email,
code,
flow: PasscodeType.Register,
},
});
return api.post(`${apiPrefix}/register/passwordless/email`).json<Response>();
};

View file

@ -1,3 +1,5 @@
import { PasscodeType } from '@logto/schemas';
import api from './api';
import { bindSocialAccount } from './social';
@ -24,9 +26,10 @@ export const signInBasic = async (username: string, password: string, socialToBi
export const sendSignInSmsPasscode = async (phone: string) => {
await api
.post('/api/session/sign-in/passwordless/sms/send-passcode', {
.post('/api/session/passwordless/sms/send', {
json: {
phone,
flow: PasscodeType.SignIn,
},
})
.json();
@ -43,14 +46,15 @@ export const verifySignInSmsPasscode = async (
redirectTo: string;
};
const result = await api
.post('/api/session/sign-in/passwordless/sms/verify-passcode', {
json: {
phone,
code,
},
})
.json<Response>();
await api.post('/api/session/passwordless/sms/verify', {
json: {
phone,
code,
flow: PasscodeType.SignIn,
},
});
const result = await api.post('/api/session/sign-in/passwordless/sms').json<Response>();
if (result.redirectTo && socialToBind) {
await bindSocialAccount(socialToBind);
@ -61,9 +65,10 @@ export const verifySignInSmsPasscode = async (
export const sendSignInEmailPasscode = async (email: string) => {
await api
.post('/api/session/sign-in/passwordless/email/send-passcode', {
.post('/api/session/passwordless/email/send', {
json: {
email,
flow: PasscodeType.SignIn,
},
})
.json();
@ -80,14 +85,15 @@ export const verifySignInEmailPasscode = async (
redirectTo: string;
};
const result = await api
.post('/api/session/sign-in/passwordless/email/verify-passcode', {
json: {
email,
code,
},
})
.json<Response>();
await api.post('/api/session/passwordless/email/verify', {
json: {
email,
code,
flow: PasscodeType.SignIn,
},
});
const result = await api.post('/api/session/sign-in/passwordless/email').json<Response>();
if (result.redirectTo && socialToBind) {
await bindSocialAccount(socialToBind);