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:
parent
3131802c6a
commit
26b9a38a4f
4 changed files with 51 additions and 6 deletions
|
@ -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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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>();
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
Loading…
Reference in a new issue