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:
parent
0960afc97d
commit
d3d189aa77
15 changed files with 673 additions and 547 deletions
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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>();
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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.',
|
||||
|
|
|
@ -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é.",
|
||||
|
|
|
@ -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 이름이예요.',
|
||||
|
|
|
@ -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.',
|
||||
|
|
|
@ -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ı.',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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>();
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Reference in a new issue