0
Fork 0
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:
Charles Zhao 2022-11-16 14:57:41 +08:00
parent 98e99c4cc0
commit 3e9694bd69
No known key found for this signature in database
GPG key ID: 4858774754C92DF2
2 changed files with 84 additions and 20 deletions

View file

@ -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', () => {

View file

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