Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/clever-buttons-behave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@logto/core": minor
---

Add avatar and customData fields to create user API (POST /api/users)
6 changes: 6 additions & 0 deletions packages/core/src/routes/admin-user/basics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ export default function adminUserBasicsRoutes<T extends AuthedRouter>(...args: R
passwordDigest: string(),
passwordAlgorithm: nativeEnum(UsersPasswordEncryptionMethod),
name: string(),
avatar: string().url().or(literal('')).nullable(),
customData: jsonObjectGuard,
}).partial(),
response: userProfileResponseGuard,
status: [200, 404, 422],
Expand All @@ -132,6 +134,8 @@ export default function adminUserBasicsRoutes<T extends AuthedRouter>(...args: R
name,
passwordDigest,
passwordAlgorithm,
avatar,
customData,
} = ctx.guard.body;

assertThat(!(password && passwordDigest), new RequestError('user.password_and_digest'));
Expand Down Expand Up @@ -165,6 +169,8 @@ export default function adminUserBasicsRoutes<T extends AuthedRouter>(...args: R
primaryPhone,
username,
name,
avatar,
...conditional(customData && { customData }),
...conditional(password && (await encryptUserPassword(password))),
...conditional(
passwordDigest && {
Expand Down
27 changes: 14 additions & 13 deletions packages/integration-tests/src/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,30 @@ import fs from 'node:fs/promises';
import { createServer, type RequestListener } from 'node:http';

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 { createUser } from '#src/api/index.js';
import { generateUsername } from '#src/utils.js';

export const createUserByAdmin = async (
username?: string,
password?: string,
primaryEmail?: string,
primaryPhone?: string,
name?: string,
passwordDigest?: string,
passwordAlgorithm?: UsersPasswordEncryptionMethod
payload: {
username?: string;
password?: string;
primaryEmail?: string;
primaryPhone?: string;
name?: string;
passwordDigest?: string;
passwordAlgorithm?: UsersPasswordEncryptionMethod;
customData?: JsonObject;
} = {}
) => {
const { username, name, ...rest } = payload;

return createUser({
...rest,
username: username ?? generateUsername(),
password,
name: name ?? username ?? 'John',
primaryEmail,
primaryPhone,
passwordDigest,
passwordAlgorithm,
});
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ describe('admin console user search params', () => {
const primaryPhone =
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 });
})
);
});
Expand Down
47 changes: 27 additions & 20 deletions packages/integration-tests/src/tests/api/admin-user.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,36 +38,39 @@ describe('admin console user management', () => {
});

it('should create user with password digest successfully', async () => {
const user = await createUserByAdmin(
undefined,
undefined,
undefined,
undefined,
undefined,
'5f4dcc3b5aa765d61d8327deb882cf99',
UsersPasswordEncryptionMethod.MD5
);
const user = await createUserByAdmin({
passwordDigest: '5f4dcc3b5aa765d61d8327deb882cf99',
passwordAlgorithm: UsersPasswordEncryptionMethod.MD5,
});

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 () => {
const [username, password, email, phone] = [
const [username, password, primaryEmail, primaryPhone] = [
generateUsername(),
generatePassword(),
generateEmail(),
generatePhone(),
];
await createUserByAdmin(username, password, email, phone);
await expectRejects(createUserByAdmin(username, password), {
await createUserByAdmin({ username, password, primaryEmail, primaryPhone });
await expectRejects(createUserByAdmin({ username, password }), {
code: 'user.username_already_in_use',
statusCode: 422,
});
await expectRejects(createUserByAdmin(undefined, undefined, email), {
await expectRejects(createUserByAdmin({ primaryEmail }), {
code: 'user.email_already_in_use',
statusCode: 422,
});
await expectRejects(createUserByAdmin(undefined, undefined, undefined, phone), {
await expectRejects(createUserByAdmin({ primaryPhone }), {
code: 'user.phone_already_in_use',
statusCode: 422,
});
Expand Down Expand Up @@ -108,21 +111,25 @@ describe('admin console user management', () => {
});

it('should fail when update userinfo with conflict identifiers', async () => {
const [username, email, phone] = [generateUsername(), generateEmail(), generatePhone()];
await createUserByAdmin(username, undefined, email, phone);
const [username, primaryEmail, primaryPhone] = [
generateUsername(),
generateEmail(),
generatePhone(),
];
await createUserByAdmin({ username, primaryEmail, primaryPhone });
const anotherUser = await createUserByAdmin();

await expectRejects(updateUser(anotherUser.id, { username }), {
code: 'user.username_already_in_use',
statusCode: 422,
});

await expectRejects(updateUser(anotherUser.id, { primaryEmail: email }), {
await expectRejects(updateUser(anotherUser.id, { primaryEmail }), {
code: 'user.email_already_in_use',
statusCode: 422,
});

await expectRejects(updateUser(anotherUser.id, { primaryPhone: phone }), {
await expectRejects(updateUser(anotherUser.id, { primaryPhone }), {
code: 'user.phone_already_in_use',
statusCode: 422,
});
Expand Down Expand Up @@ -229,13 +236,13 @@ describe('admin console user management', () => {
});

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);
await deleteUser(user.id);
});

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'), {
code: 'session.invalid_credentials',
statusCode: 422,
Expand Down
4 changes: 2 additions & 2 deletions packages/integration-tests/src/tests/api/dashboard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ describe('admin console dashboard', () => {

const password = generatePassword();
const username = generateUsername();
await createUserByAdmin(username, password);
await createUserByAdmin({ username, password });

const { totalUserCount } = await getTotalUsersCount();

Expand All @@ -63,7 +63,7 @@ describe('admin console dashboard', () => {

const password = generatePassword();
const username = generateUsername();
await createUserByAdmin(username, password);
await createUserByAdmin({ username, password });

await signInWithPassword({ username, password });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe('always issue Refresh Token config', () => {
};

beforeAll(async () => {
await createUserByAdmin(username, password);
await createUserByAdmin({ username, password });
await enableAllPasswordSignInMethods();
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ describe('get access token', () => {
const testApiScopeNames = ['read', 'write', 'delete', 'update'];

beforeAll(async () => {
await createUserByAdmin(guestUsername, password);
const user = await createUserByAdmin(username, password);
await createUserByAdmin({ username: guestUsername, password });
const user = await createUserByAdmin({ username, password });
const testApiResource = await createResource(
testApiResourceInfo.name,
testApiResourceInfo.indicator
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ describe('OpenID Connect ID token', () => {
};

beforeAll(async () => {
const { id } = await createUserByAdmin(username, password);
const { id } = await createUserByAdmin({ username, password });
// eslint-disable-next-line @silverhand/fp/no-mutation
userId = id;
await enableAllPasswordSignInMethods();
Expand Down
Loading