0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-30 20:33:54 -05:00

feat(core): update username (#6640)

This commit is contained in:
wangsijie 2024-09-29 14:07:14 +08:00 committed by GitHub
parent 3131802c6a
commit 26b9a38a4f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 51 additions and 6 deletions

View file

@ -24,6 +24,9 @@
}, },
"avatar": { "avatar": {
"description": "The new avatar for the user, must be a URL." "description": "The new avatar for the user, must be a URL."
},
"username": {
"description": "The new username for the user, must be a valid username and unique."
} }
} }
} }
@ -36,6 +39,9 @@
}, },
"400": { "400": {
"description": "The request body is invalid." "description": "The request body is invalid."
},
"422": {
"description": "The username is already in use."
} }
} }
} }

View file

@ -1,4 +1,4 @@
import { UserScope } from '@logto/core-kit'; import { usernameRegEx, UserScope } from '@logto/core-kit';
import { conditional } from '@silverhand/essentials'; import { conditional } from '@silverhand/essentials';
import { z } from 'zod'; import { z } from 'zod';
@ -18,6 +18,10 @@ export default function profileRoutes<T extends UserRouter>(
users: { updateUserById }, users: { updateUserById },
} = queries; } = queries;
const {
users: { checkIdentifierCollision },
} = libraries;
router.use(koaOidcAuth(provider)); router.use(koaOidcAuth(provider));
if (!EnvSet.values.isDevFeaturesEnabled) { if (!EnvSet.values.isDevFeaturesEnabled) {
@ -30,22 +34,27 @@ export default function profileRoutes<T extends UserRouter>(
body: z.object({ body: z.object({
name: z.string().nullable().optional(), name: z.string().nullable().optional(),
avatar: z.string().url().nullable().optional(), avatar: z.string().url().nullable().optional(),
username: z.string().regex(usernameRegEx).optional(),
}), }),
response: z.object({ response: z.object({
name: z.string().nullable().optional(), name: z.string().nullable().optional(),
avatar: z.string().nullable().optional(), avatar: z.string().nullable().optional(),
username: z.string().optional(),
}), }),
status: [200, 400], status: [200, 400, 422],
}), }),
async (ctx, next) => { async (ctx, next) => {
const { id: userId, scopes } = ctx.auth; const { id: userId, scopes } = ctx.auth;
const { const { body } = ctx.guard;
body: { name, avatar }, const { name, avatar, username } = body;
} = ctx.guard;
assertThat(scopes.has(UserScope.Profile), 'auth.unauthorized'); assertThat(scopes.has(UserScope.Profile), 'auth.unauthorized');
const updatedUser = await updateUserById(userId, { name, avatar }); if (username !== undefined) {
await checkIdentifierCollision({ username }, userId);
}
const updatedUser = await updateUserById(userId, { name, avatar, username });
// TODO(LOG-10005): trigger user updated webhook // TODO(LOG-10005): trigger user updated webhook
@ -53,6 +62,7 @@ export default function profileRoutes<T extends UserRouter>(
ctx.body = { ctx.body = {
...conditional(name !== undefined && { name: updatedUser.name }), ...conditional(name !== undefined && { name: updatedUser.name }),
...conditional(avatar !== undefined && { avatar: updatedUser.avatar }), ...conditional(avatar !== undefined && { avatar: updatedUser.avatar }),
...conditional(username !== undefined && { username: updatedUser.username }),
}; };
return next(); return next();

View file

@ -11,6 +11,7 @@ export const updateUser = async (api: KyInstance, body: Record<string, string>)
api.patch('api/profile', { json: body }).json<{ api.patch('api/profile', { json: body }).json<{
name?: string; name?: string;
avatar?: string; avatar?: string;
username?: string;
}>(); }>();
export const getUserInfo = async (api: KyInstance) => api.get('oidc/me').json<UserInfoResponse>(); export const getUserInfo = async (api: KyInstance) => api.get('oidc/me').json<UserInfoResponse>();

View file

@ -46,6 +46,34 @@ describe('profile', () => {
await deleteUser(user.id); await deleteUser(user.id);
}); });
it('should be able to update username', async () => {
const { user, username, password } = await createUserWithPassword();
const api = await signInAndGetUserApi(username, password);
const newUsername = generateUsername();
const response = await updateUser(api, { username: newUsername });
expect(response).toMatchObject({ username: newUsername });
// Sign in with new username
await initClientAndSignIn(newUsername, password);
await deleteUser(user.id);
});
it('should fail if username is already in use', async () => {
const { user, username, password } = await createUserWithPassword();
const { user: user2, username: username2 } = await createUserWithPassword();
const api = await signInAndGetUserApi(username, password);
await expectRejects(updateUser(api, { username: username2 }), {
code: 'user.username_already_in_use',
status: 422,
});
await deleteUser(user.id);
await deleteUser(user2.id);
});
}); });
describe('POST /profile/password', () => { describe('POST /profile/password', () => {