Skip to content

Commit ed849ca

Browse files
authored
feat(core): update other profile data (#6651)
1 parent 1a93881 commit ed849ca

File tree

4 files changed

+148
-10
lines changed

4 files changed

+148
-10
lines changed

packages/core/src/routes/profile/index.openapi.json

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,64 @@
5656
}
5757
}
5858
},
59+
"/api/profile/profile": {
60+
"patch": {
61+
"operationId": "UpdateOtherProfile",
62+
"summary": "Update other profile",
63+
"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.",
64+
"requestBody": {
65+
"content": {
66+
"application/json": {
67+
"schema": {
68+
"properties": {
69+
"familyName": {
70+
"description": "The new family name for the user."
71+
},
72+
"givenName": {
73+
"description": "The new given name for the user."
74+
},
75+
"middleName": {
76+
"description": "The new middle name for the user."
77+
},
78+
"nickname": {
79+
"description": "The new nickname for the user."
80+
},
81+
"preferredUsername": {
82+
"description": "The new preferred username for the user."
83+
},
84+
"profile": {
85+
"description": "The new profile for the user."
86+
},
87+
"website": {
88+
"description": "The new website for the user."
89+
},
90+
"gender": {
91+
"description": "The new gender for the user."
92+
},
93+
"birthdate": {
94+
"description": "The new birthdate for the user."
95+
},
96+
"zoneinfo": {
97+
"description": "The new zoneinfo for the user."
98+
},
99+
"locale": {
100+
"description": "The new locale for the user."
101+
},
102+
"address": {
103+
"description": "The new address for the user."
104+
}
105+
}
106+
}
107+
}
108+
}
109+
},
110+
"responses": {
111+
"200": {
112+
"description": "The profile was updated successfully."
113+
}
114+
}
115+
}
116+
},
59117
"/api/profile/password": {
60118
"post": {
61119
"operationId": "UpdatePassword",

packages/core/src/routes/profile/index.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { emailRegEx, usernameRegEx, UserScope } from '@logto/core-kit';
2-
import { VerificationType, userProfileResponseGuard } from '@logto/schemas';
2+
import { VerificationType, userProfileResponseGuard, userProfileGuard } from '@logto/schemas';
33
import { z } from 'zod';
44

55
import koaGuard from '#src/middleware/koa-guard.js';
@@ -67,7 +67,11 @@ export default function profileRoutes<T extends UserRouter>(
6767
await checkIdentifierCollision({ username }, userId);
6868
}
6969

70-
const updatedUser = await updateUserById(userId, { name, avatar, username });
70+
const updatedUser = await updateUserById(userId, {
71+
name,
72+
avatar,
73+
username,
74+
});
7175

7276
ctx.appendDataHookContext('User.Data.Updated', { user: updatedUser });
7377

@@ -77,11 +81,41 @@ export default function profileRoutes<T extends UserRouter>(
7781
}
7882
);
7983

84+
router.patch(
85+
'/profile/profile',
86+
koaGuard({
87+
body: userProfileGuard,
88+
response: userProfileGuard,
89+
status: [200, 400],
90+
}),
91+
async (ctx, next) => {
92+
const { id: userId, scopes } = ctx.auth;
93+
const { body } = ctx.guard;
94+
95+
assertThat(scopes.has(UserScope.Profile), 'auth.unauthorized');
96+
97+
if (body.address !== undefined) {
98+
assertThat(scopes.has(UserScope.Address), 'auth.unauthorized');
99+
}
100+
101+
const updatedUser = await updateUserById(userId, {
102+
profile: body,
103+
});
104+
105+
ctx.appendDataHookContext('User.Data.Updated', { user: updatedUser });
106+
107+
const profile = await getScopedProfile(queries, libraries, scopes, userId);
108+
ctx.body = profile.profile;
109+
110+
return next();
111+
}
112+
);
113+
80114
router.post(
81115
'/profile/password',
82116
koaGuard({
83117
body: z.object({ password: z.string().min(1), verificationRecordId: z.string() }),
84-
status: [204, 400, 403],
118+
status: [204, 401, 422],
85119
}),
86120
async (ctx, next) => {
87121
const { id: userId } = ctx.auth;

packages/integration-tests/src/api/profile.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,11 @@ export const updatePrimaryEmail = async (
1717
json: { email, verificationRecordId, newIdentifierVerificationRecordId },
1818
});
1919

20-
export const updateUser = async (api: KyInstance, body: Record<string, string>) =>
21-
api.patch('api/profile', { json: body }).json<{
22-
name?: string;
23-
avatar?: string;
24-
username?: string;
25-
}>();
20+
export const updateUser = async (api: KyInstance, body: Record<string, unknown>) =>
21+
api.patch('api/profile', { json: body }).json<Partial<UserProfileResponse>>();
22+
23+
export const updateOtherProfile = async (api: KyInstance, body: Record<string, unknown>) =>
24+
api.patch('api/profile/profile', { json: body }).json<Partial<UserProfileResponse['profile']>>();
2625

2726
export const getUserInfo = async (api: KyInstance) =>
2827
api.get('api/profile').json<Partial<UserProfileResponse>>();

packages/integration-tests/src/tests/api/profile/index.test.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { UserScope } from '@logto/core-kit';
22
import { hookEvents } from '@logto/schemas';
33

4-
import { getUserInfo, updatePassword, updateUser } from '#src/api/profile.js';
4+
import { getUserInfo, updateOtherProfile, updatePassword, updateUser } from '#src/api/profile.js';
55
import { createVerificationRecordByPassword } from '#src/api/verification-record.js';
66
import { WebHookApiTest } from '#src/helpers/hook.js';
77
import { expectRejects } from '#src/helpers/index.js';
@@ -177,6 +177,53 @@ describe('profile', () => {
177177
});
178178
});
179179

180+
describe('PATCH /profile/profile', () => {
181+
it('should be able to update other profile', async () => {
182+
const { user, username, password } = await createDefaultTenantUserWithPassword();
183+
const api = await signInAndGetUserApi(username, password);
184+
const newProfile = {
185+
profile: 'HI',
186+
middleName: 'middleName',
187+
};
188+
189+
const response = await updateOtherProfile(api, newProfile);
190+
expect(response).toMatchObject(newProfile);
191+
192+
await deleteDefaultTenantUser(user.id);
193+
});
194+
195+
it('should be able to update profile address', async () => {
196+
const { user, username, password } = await createDefaultTenantUserWithPassword();
197+
const api = await signInAndGetUserApi(username, password, {
198+
scopes: [UserScope.Address, UserScope.Profile],
199+
});
200+
const newProfile = {
201+
address: {
202+
country: 'USA',
203+
},
204+
};
205+
206+
const response = await updateOtherProfile(api, newProfile);
207+
expect(response).toMatchObject(newProfile);
208+
209+
await deleteDefaultTenantUser(user.id);
210+
});
211+
212+
it('should fail if user does not have the address scope', async () => {
213+
const { user, username, password } = await createDefaultTenantUserWithPassword();
214+
const api = await signInAndGetUserApi(username, password, {
215+
scopes: [UserScope.Profile],
216+
});
217+
218+
await expectRejects(updateOtherProfile(api, { address: { country: 'USA' } }), {
219+
code: 'auth.unauthorized',
220+
status: 400,
221+
});
222+
223+
await deleteDefaultTenantUser(user.id);
224+
});
225+
});
226+
180227
describe('POST /profile/password', () => {
181228
it('should fail if verification record is invalid', async () => {
182229
const { user, username, password } = await createDefaultTenantUserWithPassword();

0 commit comments

Comments
 (0)