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": { "/api/profile/password": {
"post": { "post": {
"operationId": "UpdatePassword", "operationId": "UpdatePassword",

View file

@ -1,5 +1,5 @@
import { emailRegEx, usernameRegEx, UserScope } from '@logto/core-kit'; 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 { z } from 'zod';
import koaGuard from '#src/middleware/koa-guard.js'; import koaGuard from '#src/middleware/koa-guard.js';
@ -67,7 +67,11 @@ export default function profileRoutes<T extends UserRouter>(
await checkIdentifierCollision({ username }, userId); 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 }); 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( router.post(
'/profile/password', '/profile/password',
koaGuard({ koaGuard({
body: z.object({ password: z.string().min(1), verificationRecordId: z.string() }), body: z.object({ password: z.string().min(1), verificationRecordId: z.string() }),
status: [204, 400, 403], status: [204, 401, 422],
}), }),
async (ctx, next) => { async (ctx, next) => {
const { id: userId } = ctx.auth; const { id: userId } = ctx.auth;

View file

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

View file

@ -1,7 +1,7 @@
import { UserScope } from '@logto/core-kit'; import { UserScope } from '@logto/core-kit';
import { hookEvents } from '@logto/schemas'; 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 { createVerificationRecordByPassword } from '#src/api/verification-record.js';
import { WebHookApiTest } from '#src/helpers/hook.js'; import { WebHookApiTest } from '#src/helpers/hook.js';
import { expectRejects } from '#src/helpers/index.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', () => { describe('POST /profile/password', () => {
it('should fail if verification record is invalid', async () => { it('should fail if verification record is invalid', async () => {
const { user, username, password } = await createDefaultTenantUserWithPassword(); const { user, username, password } = await createDefaultTenantUserWithPassword();