From ed849ca716e042c19c6cd8ccc0013f061322d7b7 Mon Sep 17 00:00:00 2001 From: wangsijie Date: Tue, 15 Oct 2024 15:20:21 +0800 Subject: [PATCH] feat(core): update other profile data (#6651) --- .../src/routes/profile/index.openapi.json | 58 +++++++++++++++++++ packages/core/src/routes/profile/index.ts | 40 ++++++++++++- packages/integration-tests/src/api/profile.ts | 11 ++-- .../src/tests/api/profile/index.test.ts | 49 +++++++++++++++- 4 files changed, 148 insertions(+), 10 deletions(-) diff --git a/packages/core/src/routes/profile/index.openapi.json b/packages/core/src/routes/profile/index.openapi.json index de5c1b8c4..c8c3c2438 100644 --- a/packages/core/src/routes/profile/index.openapi.json +++ b/packages/core/src/routes/profile/index.openapi.json @@ -56,6 +56,64 @@ } } }, + "/api/profile/profile": { + "patch": { + "operationId": "UpdateOtherProfile", + "summary": "Update other profile", + "description": "Update other profile for the user, only the fields that are passed in will be updated, to update the address, the user must have the address scope.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "properties": { + "familyName": { + "description": "The new family name for the user." + }, + "givenName": { + "description": "The new given name for the user." + }, + "middleName": { + "description": "The new middle name for the user." + }, + "nickname": { + "description": "The new nickname for the user." + }, + "preferredUsername": { + "description": "The new preferred username for the user." + }, + "profile": { + "description": "The new profile for the user." + }, + "website": { + "description": "The new website for the user." + }, + "gender": { + "description": "The new gender for the user." + }, + "birthdate": { + "description": "The new birthdate for the user." + }, + "zoneinfo": { + "description": "The new zoneinfo for the user." + }, + "locale": { + "description": "The new locale for the user." + }, + "address": { + "description": "The new address for the user." + } + } + } + } + } + }, + "responses": { + "200": { + "description": "The profile was updated successfully." + } + } + } + }, "/api/profile/password": { "post": { "operationId": "UpdatePassword", diff --git a/packages/core/src/routes/profile/index.ts b/packages/core/src/routes/profile/index.ts index b934a13d1..6bd36c00d 100644 --- a/packages/core/src/routes/profile/index.ts +++ b/packages/core/src/routes/profile/index.ts @@ -1,5 +1,5 @@ import { emailRegEx, usernameRegEx, UserScope } from '@logto/core-kit'; -import { VerificationType, userProfileResponseGuard } from '@logto/schemas'; +import { VerificationType, userProfileResponseGuard, userProfileGuard } from '@logto/schemas'; import { z } from 'zod'; import koaGuard from '#src/middleware/koa-guard.js'; @@ -67,7 +67,11 @@ export default function profileRoutes( await checkIdentifierCollision({ username }, userId); } - const updatedUser = await updateUserById(userId, { name, avatar, username }); + const updatedUser = await updateUserById(userId, { + name, + avatar, + username, + }); ctx.appendDataHookContext('User.Data.Updated', { user: updatedUser }); @@ -77,11 +81,41 @@ export default function profileRoutes( } ); + router.patch( + '/profile/profile', + koaGuard({ + body: userProfileGuard, + response: userProfileGuard, + status: [200, 400], + }), + async (ctx, next) => { + const { id: userId, scopes } = ctx.auth; + const { body } = ctx.guard; + + assertThat(scopes.has(UserScope.Profile), 'auth.unauthorized'); + + if (body.address !== undefined) { + assertThat(scopes.has(UserScope.Address), 'auth.unauthorized'); + } + + const updatedUser = await updateUserById(userId, { + profile: body, + }); + + ctx.appendDataHookContext('User.Data.Updated', { user: updatedUser }); + + const profile = await getScopedProfile(queries, libraries, scopes, userId); + ctx.body = profile.profile; + + return next(); + } + ); + router.post( '/profile/password', koaGuard({ body: z.object({ password: z.string().min(1), verificationRecordId: z.string() }), - status: [204, 400, 403], + status: [204, 401, 422], }), async (ctx, next) => { const { id: userId } = ctx.auth; diff --git a/packages/integration-tests/src/api/profile.ts b/packages/integration-tests/src/api/profile.ts index e9b4f0a2a..fb83c2bcd 100644 --- a/packages/integration-tests/src/api/profile.ts +++ b/packages/integration-tests/src/api/profile.ts @@ -17,12 +17,11 @@ export const updatePrimaryEmail = async ( json: { email, verificationRecordId, newIdentifierVerificationRecordId }, }); -export const updateUser = async (api: KyInstance, body: Record) => - api.patch('api/profile', { json: body }).json<{ - name?: string; - avatar?: string; - username?: string; - }>(); +export const updateUser = async (api: KyInstance, body: Record) => + api.patch('api/profile', { json: body }).json>(); + +export const updateOtherProfile = async (api: KyInstance, body: Record) => + api.patch('api/profile/profile', { json: body }).json>(); export const getUserInfo = async (api: KyInstance) => api.get('api/profile').json>(); diff --git a/packages/integration-tests/src/tests/api/profile/index.test.ts b/packages/integration-tests/src/tests/api/profile/index.test.ts index 774ed438a..71a0e4fd8 100644 --- a/packages/integration-tests/src/tests/api/profile/index.test.ts +++ b/packages/integration-tests/src/tests/api/profile/index.test.ts @@ -1,7 +1,7 @@ import { UserScope } from '@logto/core-kit'; import { hookEvents } from '@logto/schemas'; -import { getUserInfo, updatePassword, updateUser } from '#src/api/profile.js'; +import { getUserInfo, updateOtherProfile, updatePassword, updateUser } from '#src/api/profile.js'; import { createVerificationRecordByPassword } from '#src/api/verification-record.js'; import { WebHookApiTest } from '#src/helpers/hook.js'; import { expectRejects } from '#src/helpers/index.js'; @@ -177,6 +177,53 @@ describe('profile', () => { }); }); + describe('PATCH /profile/profile', () => { + it('should be able to update other profile', async () => { + const { user, username, password } = await createDefaultTenantUserWithPassword(); + const api = await signInAndGetUserApi(username, password); + const newProfile = { + profile: 'HI', + middleName: 'middleName', + }; + + const response = await updateOtherProfile(api, newProfile); + expect(response).toMatchObject(newProfile); + + await deleteDefaultTenantUser(user.id); + }); + + it('should be able to update profile address', async () => { + const { user, username, password } = await createDefaultTenantUserWithPassword(); + const api = await signInAndGetUserApi(username, password, { + scopes: [UserScope.Address, UserScope.Profile], + }); + const newProfile = { + address: { + country: 'USA', + }, + }; + + const response = await updateOtherProfile(api, newProfile); + expect(response).toMatchObject(newProfile); + + await deleteDefaultTenantUser(user.id); + }); + + it('should fail if user does not have the address scope', async () => { + const { user, username, password } = await createDefaultTenantUserWithPassword(); + const api = await signInAndGetUserApi(username, password, { + scopes: [UserScope.Profile], + }); + + await expectRejects(updateOtherProfile(api, { address: { country: 'USA' } }), { + code: 'auth.unauthorized', + status: 400, + }); + + await deleteDefaultTenantUser(user.id); + }); + }); + describe('POST /profile/password', () => { it('should fail if verification record is invalid', async () => { const { user, username, password } = await createDefaultTenantUserWithPassword();