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:
parent
1a938816b0
commit
ed849ca716
4 changed files with 148 additions and 10 deletions
|
@ -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",
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>>();
|
||||||
|
|
|
@ -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();
|
||||||
|
|
Loading…
Reference in a new issue