0
Fork 0
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:
Charles Zhao 2022-11-16 16:40:43 +08:00 committed by GitHub
commit f128d90f13
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 127 additions and 6 deletions

View file

@ -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();
});
});
});

View file

@ -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();
});
}