mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
feat: profile API to change other insensitive user info
This commit is contained in:
parent
98e99c4cc0
commit
3e9694bd69
2 changed files with 84 additions and 20 deletions
|
@ -23,10 +23,7 @@ const encryptUserPassword = jest.fn(async (password: string) => ({
|
||||||
passwordEncryptionMethod: 'Argon2i',
|
passwordEncryptionMethod: 'Argon2i',
|
||||||
}));
|
}));
|
||||||
const mockArgon2Verify = jest.fn(async (password: string) => password === mockPasswordEncrypted);
|
const mockArgon2Verify = jest.fn(async (password: string) => password === mockPasswordEncrypted);
|
||||||
const mockGetSession = jest.fn(async () => ({
|
const mockGetSession = jest.fn();
|
||||||
accountId: 'id',
|
|
||||||
loginTs: getUnixTime(new Date()) - 60,
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('oidc-provider', () => ({
|
jest.mock('oidc-provider', () => ({
|
||||||
Provider: jest.fn(() => ({
|
Provider: jest.fn(() => ({
|
||||||
|
@ -69,6 +66,10 @@ jest.mock('hash-wasm', () => ({
|
||||||
describe('session -> profileRoutes', () => {
|
describe('session -> profileRoutes', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
|
mockGetSession.mockImplementation(async () => ({
|
||||||
|
accountId: 'id',
|
||||||
|
loginTs: getUnixTime(new Date()) - 60,
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
const sessionRequest = createRequester({
|
const sessionRequest = createRequester({
|
||||||
|
@ -84,10 +85,51 @@ describe('session -> profileRoutes', () => {
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GET /session/profile should return current user data', async () => {
|
describe('GET /session/profile', () => {
|
||||||
const response = await sessionRequest.get(profileRoute);
|
it('should return current user data', async () => {
|
||||||
expect(response.statusCode).toEqual(200);
|
const response = await sessionRequest.get(profileRoute);
|
||||||
expect(response.body).toEqual(mockUserResponse);
|
expect(response.statusCode).toEqual(200);
|
||||||
|
expect(response.body).toEqual(mockUserResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw when the user is not authenticated', async () => {
|
||||||
|
mockGetSession.mockImplementationOnce(
|
||||||
|
jest.fn(async () => ({
|
||||||
|
accountId: undefined,
|
||||||
|
loginTs: undefined,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await sessionRequest.get(profileRoute);
|
||||||
|
expect(response.statusCode).toEqual(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PATCH /session/profile', () => {
|
||||||
|
it('should update current user with display name, avatar and custom data', async () => {
|
||||||
|
const updatedUserInfo = {
|
||||||
|
name: 'John Doe',
|
||||||
|
avatar: 'https://new-avatar.cdn.com',
|
||||||
|
customData: { gender: 'male', age: '30' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await sessionRequest.patch(profileRoute).send(updatedUserInfo);
|
||||||
|
|
||||||
|
expect(mockUpdateUserById).toBeCalledWith('id', expect.objectContaining(updatedUserInfo));
|
||||||
|
expect(response.statusCode).toEqual(204);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw when the user is not authenticated', async () => {
|
||||||
|
mockGetSession.mockImplementationOnce(
|
||||||
|
jest.fn(async () => ({
|
||||||
|
accountId: undefined,
|
||||||
|
loginTs: undefined,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await sessionRequest.patch(profileRoute).send({ name: 'John Doe' });
|
||||||
|
expect(response.statusCode).toEqual(401);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('PATCH /session/profile/username', () => {
|
describe('PATCH /session/profile/username', () => {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { emailRegEx, passwordRegEx, phoneRegEx, usernameRegEx } from '@logto/core-kit';
|
import { emailRegEx, passwordRegEx, phoneRegEx, usernameRegEx } from '@logto/core-kit';
|
||||||
import { userInfoSelectFields } from '@logto/schemas';
|
import { arbitraryObjectGuard, userInfoSelectFields } from '@logto/schemas';
|
||||||
import { argon2Verify } from 'hash-wasm';
|
import { argon2Verify } from 'hash-wasm';
|
||||||
import pick from 'lodash.pick';
|
import pick from 'lodash.pick';
|
||||||
import type { Provider } from 'oidc-provider';
|
import type { Provider } from 'oidc-provider';
|
||||||
|
@ -20,19 +20,41 @@ export const profileRoute = '/session/profile';
|
||||||
|
|
||||||
export default function profileRoutes<T extends AnonymousRouter>(router: T, provider: Provider) {
|
export default function profileRoutes<T extends AnonymousRouter>(router: T, provider: Provider) {
|
||||||
router.get(profileRoute, async (ctx, next) => {
|
router.get(profileRoute, async (ctx, next) => {
|
||||||
const { accountId } = await provider.Session.get(ctx);
|
const { accountId: userId } = await provider.Session.get(ctx);
|
||||||
|
|
||||||
if (!accountId) {
|
assertThat(userId, new RequestError({ code: 'auth.unauthorized', status: 401 }));
|
||||||
throw new RequestError('auth.unauthorized');
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await findUserById(accountId);
|
const user = await findUserById(userId);
|
||||||
|
|
||||||
ctx.body = pick(user, ...userInfoSelectFields);
|
ctx.body = pick(user, ...userInfoSelectFields);
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.patch(
|
||||||
|
profileRoute,
|
||||||
|
koaGuard({
|
||||||
|
body: object({
|
||||||
|
name: string().nullable().optional(),
|
||||||
|
avatar: string().nullable().optional(),
|
||||||
|
customData: arbitraryObjectGuard.optional(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (ctx, next) => {
|
||||||
|
const { accountId: userId } = await provider.Session.get(ctx);
|
||||||
|
|
||||||
|
assertThat(userId, new RequestError({ code: 'auth.unauthorized', status: 401 }));
|
||||||
|
|
||||||
|
const { name, avatar, customData } = ctx.guard.body;
|
||||||
|
|
||||||
|
await updateUserById(userId, { name, avatar, customData });
|
||||||
|
|
||||||
|
ctx.status = 204;
|
||||||
|
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
router.patch(
|
router.patch(
|
||||||
`${profileRoute}/username`,
|
`${profileRoute}/username`,
|
||||||
koaGuard({
|
koaGuard({
|
||||||
|
@ -41,7 +63,7 @@ export default function profileRoutes<T extends AnonymousRouter>(router: T, prov
|
||||||
async (ctx, next) => {
|
async (ctx, next) => {
|
||||||
const userId = await checkSessionHealth(ctx, provider, verificationTimeout);
|
const userId = await checkSessionHealth(ctx, provider, verificationTimeout);
|
||||||
|
|
||||||
assertThat(userId, new RequestError('auth.unauthorized'));
|
assertThat(userId, new RequestError({ code: 'auth.unauthorized', status: 401 }));
|
||||||
|
|
||||||
const { username } = ctx.guard.body;
|
const { username } = ctx.guard.body;
|
||||||
|
|
||||||
|
@ -63,7 +85,7 @@ export default function profileRoutes<T extends AnonymousRouter>(router: T, prov
|
||||||
async (ctx, next) => {
|
async (ctx, next) => {
|
||||||
const userId = await checkSessionHealth(ctx, provider, verificationTimeout);
|
const userId = await checkSessionHealth(ctx, provider, verificationTimeout);
|
||||||
|
|
||||||
assertThat(userId, new RequestError('auth.unauthorized'));
|
assertThat(userId, new RequestError({ code: 'auth.unauthorized', status: 401 }));
|
||||||
|
|
||||||
const { password } = ctx.guard.body;
|
const { password } = ctx.guard.body;
|
||||||
const { passwordEncrypted: oldPasswordEncrypted } = await findUserById(userId);
|
const { passwordEncrypted: oldPasswordEncrypted } = await findUserById(userId);
|
||||||
|
@ -91,7 +113,7 @@ export default function profileRoutes<T extends AnonymousRouter>(router: T, prov
|
||||||
async (ctx, next) => {
|
async (ctx, next) => {
|
||||||
const userId = await checkSessionHealth(ctx, provider, verificationTimeout);
|
const userId = await checkSessionHealth(ctx, provider, verificationTimeout);
|
||||||
|
|
||||||
assertThat(userId, new RequestError('auth.unauthorized'));
|
assertThat(userId, new RequestError({ code: 'auth.unauthorized', status: 401 }));
|
||||||
|
|
||||||
const { primaryEmail } = ctx.guard.body;
|
const { primaryEmail } = ctx.guard.body;
|
||||||
|
|
||||||
|
@ -107,7 +129,7 @@ export default function profileRoutes<T extends AnonymousRouter>(router: T, prov
|
||||||
router.delete(`${profileRoute}/email`, async (ctx, next) => {
|
router.delete(`${profileRoute}/email`, async (ctx, next) => {
|
||||||
const userId = await checkSessionHealth(ctx, provider, verificationTimeout);
|
const userId = await checkSessionHealth(ctx, provider, verificationTimeout);
|
||||||
|
|
||||||
assertThat(userId, new RequestError('auth.unauthorized'));
|
assertThat(userId, new RequestError({ code: 'auth.unauthorized', status: 401 }));
|
||||||
|
|
||||||
const { primaryEmail } = await findUserById(userId);
|
const { primaryEmail } = await findUserById(userId);
|
||||||
|
|
||||||
|
@ -128,7 +150,7 @@ export default function profileRoutes<T extends AnonymousRouter>(router: T, prov
|
||||||
async (ctx, next) => {
|
async (ctx, next) => {
|
||||||
const userId = await checkSessionHealth(ctx, provider, verificationTimeout);
|
const userId = await checkSessionHealth(ctx, provider, verificationTimeout);
|
||||||
|
|
||||||
assertThat(userId, new RequestError('auth.unauthorized'));
|
assertThat(userId, new RequestError({ code: 'auth.unauthorized', status: 401 }));
|
||||||
|
|
||||||
const { primaryPhone } = ctx.guard.body;
|
const { primaryPhone } = ctx.guard.body;
|
||||||
|
|
||||||
|
@ -144,7 +166,7 @@ export default function profileRoutes<T extends AnonymousRouter>(router: T, prov
|
||||||
router.delete(`${profileRoute}/phone`, async (ctx, next) => {
|
router.delete(`${profileRoute}/phone`, async (ctx, next) => {
|
||||||
const userId = await checkSessionHealth(ctx, provider, verificationTimeout);
|
const userId = await checkSessionHealth(ctx, provider, verificationTimeout);
|
||||||
|
|
||||||
assertThat(userId, new RequestError('auth.unauthorized'));
|
assertThat(userId, new RequestError({ code: 'auth.unauthorized', status: 401 }));
|
||||||
|
|
||||||
const { primaryPhone } = await findUserById(userId);
|
const { primaryPhone } = await findUserById(userId);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue