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:
parent
f44ba31275
commit
172411946a
9 changed files with 59 additions and 40 deletions
5
.changeset/clever-buttons-behave.md
Normal file
5
.changeset/clever-buttons-behave.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
"@logto/core": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add avatar and customData fields to create user API (POST /api/users)
|
|
@ -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 && {
|
||||||
|
|
|
@ -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,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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 });
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 });
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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();
|
||||||
|
|
Loading…
Add table
Reference in a new issue