0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-10 22:22:45 -05:00

feat(core): create user with avatar and custom data (#5476)

This commit is contained in:
wangsijie 2024-03-08 11:29:39 +08:00 committed by GitHub
parent f44ba31275
commit 172411946a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 59 additions and 40 deletions

View file

@ -0,0 +1,5 @@
---
"@logto/core": minor
---
Add avatar and customData fields to create user API (POST /api/users)

View file

@ -119,6 +119,8 @@ export default function adminUserBasicsRoutes<T extends AuthedRouter>(...args: R
passwordDigest: string(), passwordDigest: string(),
passwordAlgorithm: nativeEnum(UsersPasswordEncryptionMethod), passwordAlgorithm: nativeEnum(UsersPasswordEncryptionMethod),
name: string(), name: string(),
avatar: string().url().or(literal('')).nullable(),
customData: jsonObjectGuard,
}).partial(), }).partial(),
response: userProfileResponseGuard, response: userProfileResponseGuard,
status: [200, 404, 422], status: [200, 404, 422],
@ -132,6 +134,8 @@ export default function adminUserBasicsRoutes<T extends AuthedRouter>(...args: R
name, name,
passwordDigest, passwordDigest,
passwordAlgorithm, passwordAlgorithm,
avatar,
customData,
} = ctx.guard.body; } = ctx.guard.body;
assertThat(!(password && passwordDigest), new RequestError('user.password_and_digest')); assertThat(!(password && passwordDigest), new RequestError('user.password_and_digest'));
@ -165,6 +169,8 @@ export default function adminUserBasicsRoutes<T extends AuthedRouter>(...args: R
primaryPhone, primaryPhone,
username, username,
name, name,
avatar,
...conditional(customData && { customData }),
...conditional(password && (await encryptUserPassword(password))), ...conditional(password && (await encryptUserPassword(password))),
...conditional( ...conditional(
passwordDigest && { passwordDigest && {

View file

@ -2,29 +2,30 @@ import fs from 'node:fs/promises';
import { createServer, type RequestListener } from 'node:http'; import { createServer, type RequestListener } from 'node:http';
import { mockConnectorFilePaths, type SendMessagePayload } from '@logto/connector-kit'; import { mockConnectorFilePaths, type SendMessagePayload } from '@logto/connector-kit';
import { type UsersPasswordEncryptionMethod } from '@logto/schemas'; import { type JsonObject, type UsersPasswordEncryptionMethod } from '@logto/schemas';
import { RequestError } from 'got'; import { RequestError } from 'got';
import { createUser } from '#src/api/index.js'; import { createUser } from '#src/api/index.js';
import { generateUsername } from '#src/utils.js'; import { generateUsername } from '#src/utils.js';
export const createUserByAdmin = async ( export const createUserByAdmin = async (
username?: string, payload: {
password?: string, username?: string;
primaryEmail?: string, password?: string;
primaryPhone?: string, primaryEmail?: string;
name?: string, primaryPhone?: string;
passwordDigest?: string, name?: string;
passwordAlgorithm?: UsersPasswordEncryptionMethod passwordDigest?: string;
passwordAlgorithm?: UsersPasswordEncryptionMethod;
customData?: JsonObject;
} = {}
) => { ) => {
const { username, name, ...rest } = payload;
return createUser({ return createUser({
...rest,
username: username ?? generateUsername(), username: username ?? generateUsername(),
password,
name: name ?? username ?? 'John', name: name ?? username ?? 'John',
primaryEmail,
primaryPhone,
passwordDigest,
passwordAlgorithm,
}); });
}; };

View file

@ -52,7 +52,7 @@ describe('admin console user search params', () => {
const primaryPhone = const primaryPhone =
phonePrefix[index % phonePrefix.length]! + index.toString().padStart(5, '0'); phonePrefix[index % phonePrefix.length]! + index.toString().padStart(5, '0');
return createUserByAdmin(prefix + username, undefined, primaryEmail, primaryPhone, name); return createUserByAdmin({ username: prefix + username, primaryEmail, primaryPhone, name });
}) })
); );
}); });

View file

@ -38,36 +38,39 @@ describe('admin console user management', () => {
}); });
it('should create user with password digest successfully', async () => { it('should create user with password digest successfully', async () => {
const user = await createUserByAdmin( const user = await createUserByAdmin({
undefined, passwordDigest: '5f4dcc3b5aa765d61d8327deb882cf99',
undefined, passwordAlgorithm: UsersPasswordEncryptionMethod.MD5,
undefined, });
undefined,
undefined,
'5f4dcc3b5aa765d61d8327deb882cf99',
UsersPasswordEncryptionMethod.MD5
);
await expect(verifyUserPassword(user.id, 'password')).resolves.not.toThrow(); await expect(verifyUserPassword(user.id, 'password')).resolves.not.toThrow();
}); });
it('should create user with custom data successfully', async () => {
const user = await createUserByAdmin({
customData: { foo: 'bar' },
});
const { customData } = await getUser(user.id);
expect(customData).toStrictEqual({ foo: 'bar' });
});
it('should fail when create user with conflict identifiers', async () => { it('should fail when create user with conflict identifiers', async () => {
const [username, password, email, phone] = [ const [username, password, primaryEmail, primaryPhone] = [
generateUsername(), generateUsername(),
generatePassword(), generatePassword(),
generateEmail(), generateEmail(),
generatePhone(), generatePhone(),
]; ];
await createUserByAdmin(username, password, email, phone); await createUserByAdmin({ username, password, primaryEmail, primaryPhone });
await expectRejects(createUserByAdmin(username, password), { await expectRejects(createUserByAdmin({ username, password }), {
code: 'user.username_already_in_use', code: 'user.username_already_in_use',
statusCode: 422, statusCode: 422,
}); });
await expectRejects(createUserByAdmin(undefined, undefined, email), { await expectRejects(createUserByAdmin({ primaryEmail }), {
code: 'user.email_already_in_use', code: 'user.email_already_in_use',
statusCode: 422, statusCode: 422,
}); });
await expectRejects(createUserByAdmin(undefined, undefined, undefined, phone), { await expectRejects(createUserByAdmin({ primaryPhone }), {
code: 'user.phone_already_in_use', code: 'user.phone_already_in_use',
statusCode: 422, statusCode: 422,
}); });
@ -108,8 +111,12 @@ describe('admin console user management', () => {
}); });
it('should fail when update userinfo with conflict identifiers', async () => { it('should fail when update userinfo with conflict identifiers', async () => {
const [username, email, phone] = [generateUsername(), generateEmail(), generatePhone()]; const [username, primaryEmail, primaryPhone] = [
await createUserByAdmin(username, undefined, email, phone); generateUsername(),
generateEmail(),
generatePhone(),
];
await createUserByAdmin({ username, primaryEmail, primaryPhone });
const anotherUser = await createUserByAdmin(); const anotherUser = await createUserByAdmin();
await expectRejects(updateUser(anotherUser.id, { username }), { await expectRejects(updateUser(anotherUser.id, { username }), {
@ -117,12 +124,12 @@ describe('admin console user management', () => {
statusCode: 422, statusCode: 422,
}); });
await expectRejects(updateUser(anotherUser.id, { primaryEmail: email }), { await expectRejects(updateUser(anotherUser.id, { primaryEmail }), {
code: 'user.email_already_in_use', code: 'user.email_already_in_use',
statusCode: 422, statusCode: 422,
}); });
await expectRejects(updateUser(anotherUser.id, { primaryPhone: phone }), { await expectRejects(updateUser(anotherUser.id, { primaryPhone }), {
code: 'user.phone_already_in_use', code: 'user.phone_already_in_use',
statusCode: 422, statusCode: 422,
}); });
@ -229,13 +236,13 @@ describe('admin console user management', () => {
}); });
it('should return 204 if password is correct', async () => { it('should return 204 if password is correct', async () => {
const user = await createUserByAdmin(undefined, 'new_password'); const user = await createUserByAdmin({ password: 'new_password' });
expect(await verifyUserPassword(user.id, 'new_password')).toHaveProperty('statusCode', 204); expect(await verifyUserPassword(user.id, 'new_password')).toHaveProperty('statusCode', 204);
await deleteUser(user.id); await deleteUser(user.id);
}); });
it('should return 422 if password is incorrect', async () => { it('should return 422 if password is incorrect', async () => {
const user = await createUserByAdmin(undefined, 'new_password'); const user = await createUserByAdmin({ password: 'new_password' });
await expectRejects(verifyUserPassword(user.id, 'wrong_password'), { await expectRejects(verifyUserPassword(user.id, 'wrong_password'), {
code: 'session.invalid_credentials', code: 'session.invalid_credentials',
statusCode: 422, statusCode: 422,

View file

@ -36,7 +36,7 @@ describe('admin console dashboard', () => {
const password = generatePassword(); const password = generatePassword();
const username = generateUsername(); const username = generateUsername();
await createUserByAdmin(username, password); await createUserByAdmin({ username, password });
const { totalUserCount } = await getTotalUsersCount(); const { totalUserCount } = await getTotalUsersCount();
@ -63,7 +63,7 @@ describe('admin console dashboard', () => {
const password = generatePassword(); const password = generatePassword();
const username = generateUsername(); const username = generateUsername();
await createUserByAdmin(username, password); await createUserByAdmin({ username, password });
await signInWithPassword({ username, password }); await signInWithPassword({ username, password });

View file

@ -34,7 +34,7 @@ describe('always issue Refresh Token config', () => {
}; };
beforeAll(async () => { beforeAll(async () => {
await createUserByAdmin(username, password); await createUserByAdmin({ username, password });
await enableAllPasswordSignInMethods(); await enableAllPasswordSignInMethods();
}); });

View file

@ -26,8 +26,8 @@ describe('get access token', () => {
const testApiScopeNames = ['read', 'write', 'delete', 'update']; const testApiScopeNames = ['read', 'write', 'delete', 'update'];
beforeAll(async () => { beforeAll(async () => {
await createUserByAdmin(guestUsername, password); await createUserByAdmin({ username: guestUsername, password });
const user = await createUserByAdmin(username, password); const user = await createUserByAdmin({ username, password });
const testApiResource = await createResource( const testApiResource = await createResource(
testApiResourceInfo.name, testApiResourceInfo.name,
testApiResourceInfo.indicator testApiResourceInfo.indicator

View file

@ -39,7 +39,7 @@ describe('OpenID Connect ID token', () => {
}; };
beforeAll(async () => { beforeAll(async () => {
const { id } = await createUserByAdmin(username, password); const { id } = await createUserByAdmin({ username, password });
// eslint-disable-next-line @silverhand/fp/no-mutation // eslint-disable-next-line @silverhand/fp/no-mutation
userId = id; userId = id;
await enableAllPasswordSignInMethods(); await enableAllPasswordSignInMethods();