0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

feat(core): update other profile data (#6651)

This commit is contained in:
wangsijie 2024-10-15 15:20:21 +08:00 committed by GitHub
parent 1a938816b0
commit ed849ca716
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 148 additions and 10 deletions

View file

@ -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",

View file

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

View file

@ -17,12 +17,11 @@ export const updatePrimaryEmail = async (
json: { email, verificationRecordId, newIdentifierVerificationRecordId },
});
export const updateUser = async (api: KyInstance, body: Record<string, string>) =>
api.patch('api/profile', { json: body }).json<{
name?: string;
avatar?: string;
username?: string;
}>();
export const updateUser = async (api: KyInstance, body: Record<string, unknown>) =>
api.patch('api/profile', { json: body }).json<Partial<UserProfileResponse>>();
export const updateOtherProfile = async (api: KyInstance, body: Record<string, unknown>) =>
api.patch('api/profile/profile', { json: body }).json<Partial<UserProfileResponse['profile']>>();
export const getUserInfo = async (api: KyInstance) =>
api.get('api/profile').json<Partial<UserProfileResponse>>();

View file

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