mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
feat(core): add update name and avatar endpoint (#6636)
This commit is contained in:
parent
fc6f94f111
commit
3131802c6a
8 changed files with 139 additions and 17 deletions
|
@ -9,6 +9,37 @@
|
|||
}
|
||||
],
|
||||
"paths": {
|
||||
"/api/profile": {
|
||||
"patch": {
|
||||
"operationId": "UpdateProfile",
|
||||
"summary": "Update profile",
|
||||
"description": "Update profile for the user, only the fields that are passed in will be updated.",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "The new name for the user."
|
||||
},
|
||||
"avatar": {
|
||||
"description": "The new avatar for the user, must be a URL."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The profile was updated successfully."
|
||||
},
|
||||
"400": {
|
||||
"description": "The request body is invalid."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/profile/password": {
|
||||
"post": {
|
||||
"operationId": "UpdatePassword",
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { UserScope } from '@logto/core-kit';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import { z } from 'zod';
|
||||
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
|
@ -22,6 +24,41 @@ export default function profileRoutes<T extends UserRouter>(
|
|||
return;
|
||||
}
|
||||
|
||||
router.patch(
|
||||
'/profile',
|
||||
koaGuard({
|
||||
body: z.object({
|
||||
name: z.string().nullable().optional(),
|
||||
avatar: z.string().url().nullable().optional(),
|
||||
}),
|
||||
response: z.object({
|
||||
name: z.string().nullable().optional(),
|
||||
avatar: z.string().nullable().optional(),
|
||||
}),
|
||||
status: [200, 400],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { id: userId, scopes } = ctx.auth;
|
||||
const {
|
||||
body: { name, avatar },
|
||||
} = ctx.guard;
|
||||
|
||||
assertThat(scopes.has(UserScope.Profile), 'auth.unauthorized');
|
||||
|
||||
const updatedUser = await updateUserById(userId, { name, avatar });
|
||||
|
||||
// TODO(LOG-10005): trigger user updated webhook
|
||||
|
||||
// Only return the fields that were actually updated
|
||||
ctx.body = {
|
||||
...conditional(name !== undefined && { name: updatedUser.name }),
|
||||
...conditional(avatar !== undefined && { avatar: updatedUser.avatar }),
|
||||
};
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/profile/password',
|
||||
koaGuard({
|
||||
|
|
|
@ -10,6 +10,10 @@ const api = ky.extend({
|
|||
|
||||
export default api;
|
||||
|
||||
export const baseAdminTenantApi = ky.extend({
|
||||
prefixUrl: new URL(logtoConsoleUrl),
|
||||
});
|
||||
|
||||
// TODO: @gao rename
|
||||
export const authedAdminApi = api.extend({
|
||||
headers: {
|
||||
|
|
|
@ -1,7 +1,16 @@
|
|||
import { type UserInfoResponse } from '@logto/js';
|
||||
import { type KyInstance } from 'ky';
|
||||
|
||||
export const updatePassword = async (
|
||||
api: KyInstance,
|
||||
verificationRecordId: string,
|
||||
password: string
|
||||
) => api.post('profile/password', { json: { password, verificationRecordId } });
|
||||
) => api.post('api/profile/password', { json: { password, verificationRecordId } });
|
||||
|
||||
export const updateUser = async (api: KyInstance, body: Record<string, string>) =>
|
||||
api.patch('api/profile', { json: body }).json<{
|
||||
name?: string;
|
||||
avatar?: string;
|
||||
}>();
|
||||
|
||||
export const getUserInfo = async (api: KyInstance) => api.get('oidc/me').json<UserInfoResponse>();
|
||||
|
|
|
@ -2,7 +2,7 @@ import { type KyInstance } from 'ky';
|
|||
|
||||
export const createVerificationRecordByPassword = async (api: KyInstance, password: string) => {
|
||||
const { verificationRecordId } = await api
|
||||
.post('verifications/password', {
|
||||
.post('api/verifications/password', {
|
||||
json: {
|
||||
password,
|
||||
},
|
||||
|
|
|
@ -114,16 +114,6 @@ export const initClientAndSignIn = async (
|
|||
return client;
|
||||
};
|
||||
|
||||
export const signInAndGetProfileApi = async (username: string, password: string) => {
|
||||
const client = await initClientAndSignIn(username, password);
|
||||
const accessToken = await client.getAccessToken();
|
||||
return api.extend({
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const createUserWithAllRolesAndSignInToClient = async () => {
|
||||
const [{ id }, { username, password }] = await createUserWithAllRoles();
|
||||
const client = await initClientAndSignIn(username, password, {
|
||||
|
|
20
packages/integration-tests/src/helpers/profile.ts
Normal file
20
packages/integration-tests/src/helpers/profile.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { type LogtoConfig } from '@logto/node';
|
||||
|
||||
import { baseAdminTenantApi } from '../api/api.js';
|
||||
|
||||
import { initClientAndSignIn } from './admin-tenant.js';
|
||||
|
||||
export const signInAndGetUserApi = async (
|
||||
username: string,
|
||||
password: string,
|
||||
config?: Partial<LogtoConfig>
|
||||
) => {
|
||||
const client = await initClientAndSignIn(username, password, config);
|
||||
const accessToken = await client.getAccessToken();
|
||||
|
||||
return baseAdminTenantApi.extend({
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
};
|
|
@ -1,14 +1,14 @@
|
|||
import { updatePassword } from '#src/api/profile.js';
|
||||
import { getUserInfo, updatePassword, updateUser } from '#src/api/profile.js';
|
||||
import { createVerificationRecordByPassword } from '#src/api/verification-record.js';
|
||||
import {
|
||||
createUserWithPassword,
|
||||
deleteUser,
|
||||
initClientAndSignIn,
|
||||
signInAndGetProfileApi,
|
||||
} from '#src/helpers/admin-tenant.js';
|
||||
import { expectRejects } from '#src/helpers/index.js';
|
||||
import { signInAndGetUserApi } from '#src/helpers/profile.js';
|
||||
import { enableAllPasswordSignInMethods } from '#src/helpers/sign-in-experience.js';
|
||||
import { devFeatureTest, generatePassword } from '#src/utils.js';
|
||||
import { devFeatureTest, generatePassword, generateUsername } from '#src/utils.js';
|
||||
|
||||
const { describe, it } = devFeatureTest;
|
||||
|
||||
|
@ -17,10 +17,41 @@ describe('profile', () => {
|
|||
await enableAllPasswordSignInMethods();
|
||||
});
|
||||
|
||||
describe('PATCH /profile', () => {
|
||||
it('should be able to update name', async () => {
|
||||
const { user, username, password } = await createUserWithPassword();
|
||||
const api = await signInAndGetUserApi(username, password);
|
||||
const newName = generateUsername();
|
||||
|
||||
const response = await updateUser(api, { name: newName });
|
||||
expect(response).toMatchObject({ name: newName });
|
||||
|
||||
const userInfo = await getUserInfo(api);
|
||||
expect(userInfo).toHaveProperty('name', newName);
|
||||
|
||||
await deleteUser(user.id);
|
||||
});
|
||||
|
||||
it('should be able to update picture', async () => {
|
||||
const { user, username, password } = await createUserWithPassword();
|
||||
const api = await signInAndGetUserApi(username, password);
|
||||
const newAvatar = 'https://example.com/avatar.png';
|
||||
|
||||
const response = await updateUser(api, { avatar: newAvatar });
|
||||
expect(response).toMatchObject({ avatar: newAvatar });
|
||||
|
||||
const userInfo = await getUserInfo(api);
|
||||
// In OIDC, the avatar is mapped to the `picture` field
|
||||
expect(userInfo).toHaveProperty('picture', newAvatar);
|
||||
|
||||
await deleteUser(user.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /profile/password', () => {
|
||||
it('should fail if verification record is invalid', async () => {
|
||||
const { user, username, password } = await createUserWithPassword();
|
||||
const api = await signInAndGetProfileApi(username, password);
|
||||
const api = await signInAndGetUserApi(username, password);
|
||||
const newPassword = generatePassword();
|
||||
|
||||
await expectRejects(updatePassword(api, 'invalid-varification-record-id', newPassword), {
|
||||
|
@ -33,7 +64,7 @@ describe('profile', () => {
|
|||
|
||||
it('should be able to update password', async () => {
|
||||
const { user, username, password } = await createUserWithPassword();
|
||||
const api = await signInAndGetProfileApi(username, password);
|
||||
const api = await signInAndGetUserApi(username, password);
|
||||
const verificationRecordId = await createVerificationRecordByPassword(api, password);
|
||||
const newPassword = generatePassword();
|
||||
|
||||
|
|
Loading…
Reference in a new issue