mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
Merge pull request #2456 from logto-io/charles-log-4076-log-4077-log-4078-phone-session-apis
feat: phone related profile session APIs
This commit is contained in:
commit
f128d90f13
2 changed files with 127 additions and 6 deletions
|
@ -180,11 +180,11 @@ describe('session -> profileRoutes', () => {
|
|||
loginTs: getUnixTime(new Date()) - 601,
|
||||
}));
|
||||
|
||||
const updateResponse = await sessionRequest
|
||||
const response = await sessionRequest
|
||||
.patch(`${profileRoute}/email`)
|
||||
.send({ primaryEmail: 'test@logto.io' });
|
||||
|
||||
expect(updateResponse.statusCode).toEqual(422);
|
||||
expect(response.statusCode).toEqual(422);
|
||||
expect(mockUpdateUserById).not.toBeCalled();
|
||||
});
|
||||
|
||||
|
@ -231,9 +231,9 @@ describe('session -> profileRoutes', () => {
|
|||
loginTs: getUnixTime(new Date()) - 601,
|
||||
}));
|
||||
|
||||
const deleteResponse = await sessionRequest.delete(`${profileRoute}/email`);
|
||||
const response = await sessionRequest.delete(`${profileRoute}/email`);
|
||||
|
||||
expect(deleteResponse.statusCode).toEqual(422);
|
||||
expect(response.statusCode).toEqual(422);
|
||||
expect(mockUpdateUserById).not.toBeCalled();
|
||||
});
|
||||
|
||||
|
@ -248,7 +248,7 @@ describe('session -> profileRoutes', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should throw when no email address found in user', async () => {
|
||||
it('should throw when no email address found in user on unlinking email', async () => {
|
||||
mockFindUserById.mockImplementationOnce(async () => ({ ...mockUser, primaryEmail: null }));
|
||||
const response = await sessionRequest.delete(`${profileRoute}/email`);
|
||||
|
||||
|
@ -256,4 +256,88 @@ describe('session -> profileRoutes', () => {
|
|||
expect(mockUpdateUserById).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('phone related APIs', () => {
|
||||
it('should throw if last authentication time is over 10 mins ago on linking phone number', async () => {
|
||||
mockGetSession.mockImplementationOnce(async () => ({
|
||||
accountId: 'id',
|
||||
loginTs: getUnixTime(new Date()) - 601,
|
||||
}));
|
||||
|
||||
const updateResponse = await sessionRequest
|
||||
.patch(`${profileRoute}/phone`)
|
||||
.send({ primaryPhone: '6533333333' });
|
||||
|
||||
expect(updateResponse.statusCode).toEqual(422);
|
||||
expect(mockUpdateUserById).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should link phone number to the user profile', async () => {
|
||||
const mockPhoneNumber = '6533333333';
|
||||
const response = await sessionRequest
|
||||
.patch(`${profileRoute}/phone`)
|
||||
.send({ primaryPhone: mockPhoneNumber });
|
||||
|
||||
expect(mockUpdateUserById).toBeCalledWith(
|
||||
'id',
|
||||
expect.objectContaining({
|
||||
primaryPhone: mockPhoneNumber,
|
||||
})
|
||||
);
|
||||
expect(response.statusCode).toEqual(204);
|
||||
});
|
||||
|
||||
it('should throw when phone number already exists on linking phone number', async () => {
|
||||
mockHasUserWithPhone.mockImplementationOnce(async () => true);
|
||||
|
||||
const response = await sessionRequest
|
||||
.patch(`${profileRoute}/phone`)
|
||||
.send({ primaryPhone: mockUser.primaryPhone });
|
||||
|
||||
expect(response.statusCode).toEqual(422);
|
||||
expect(mockUpdateUserById).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should throw when phone number is invalid', async () => {
|
||||
mockHasUserWithPhone.mockImplementationOnce(async () => true);
|
||||
|
||||
const response = await sessionRequest
|
||||
.patch(`${profileRoute}/phone`)
|
||||
.send({ primaryPhone: 'invalid' });
|
||||
|
||||
expect(response.statusCode).toEqual(400);
|
||||
expect(mockUpdateUserById).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should throw if last authentication time is over 10 mins ago on unlinking phone number', async () => {
|
||||
mockGetSession.mockImplementationOnce(async () => ({
|
||||
accountId: 'id',
|
||||
loginTs: getUnixTime(new Date()) - 601,
|
||||
}));
|
||||
|
||||
const response = await sessionRequest.delete(`${profileRoute}/phone`);
|
||||
|
||||
expect(response.statusCode).toEqual(422);
|
||||
expect(mockUpdateUserById).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should unlink phone number from user', async () => {
|
||||
const response = await sessionRequest.delete(`${profileRoute}/phone`);
|
||||
expect(response.statusCode).toEqual(204);
|
||||
expect(mockUpdateUserById).toBeCalledWith(
|
||||
'id',
|
||||
expect.objectContaining({
|
||||
primaryPhone: null,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw when no phone number found in user on unlinking phone number', async () => {
|
||||
mockFindUserById.mockImplementationOnce(async () => ({ ...mockUser, primaryPhone: null }));
|
||||
const response = await sessionRequest.delete(`${profileRoute}/phone`);
|
||||
|
||||
expect(response.statusCode).toEqual(422);
|
||||
expect(mockUpdateUserById).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { emailRegEx, passwordRegEx, usernameRegEx } from '@logto/core-kit';
|
||||
import { emailRegEx, passwordRegEx, phoneRegEx, usernameRegEx } from '@logto/core-kit';
|
||||
import { userInfoSelectFields } from '@logto/schemas';
|
||||
import { argon2Verify } from 'hash-wasm';
|
||||
import pick from 'lodash.pick';
|
||||
|
@ -119,4 +119,41 @@ export default function profileRoutes<T extends AnonymousRouter>(router: T, prov
|
|||
|
||||
return next();
|
||||
});
|
||||
|
||||
router.patch(
|
||||
`${profileRoute}/phone`,
|
||||
koaGuard({
|
||||
body: object({ primaryPhone: string().regex(phoneRegEx) }),
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const userId = await checkSessionHealth(ctx, provider, verificationTimeout);
|
||||
|
||||
assertThat(userId, new RequestError('auth.unauthorized'));
|
||||
|
||||
const { primaryPhone } = ctx.guard.body;
|
||||
|
||||
await checkSignUpIdentifierCollision({ primaryPhone });
|
||||
await updateUserById(userId, { primaryPhone });
|
||||
|
||||
ctx.status = 204;
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
router.delete(`${profileRoute}/phone`, async (ctx, next) => {
|
||||
const userId = await checkSessionHealth(ctx, provider, verificationTimeout);
|
||||
|
||||
assertThat(userId, new RequestError('auth.unauthorized'));
|
||||
|
||||
const { primaryPhone } = await findUserById(userId);
|
||||
|
||||
assertThat(primaryPhone, new RequestError({ code: 'user.phone_not_exists', status: 422 }));
|
||||
|
||||
await updateUserById(userId, { primaryPhone: null });
|
||||
|
||||
ctx.status = 204;
|
||||
|
||||
return next();
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue