diff --git a/lib/management/helpers.ts b/lib/management/helpers.ts index ee80a7b5e..fa7ecde06 100644 --- a/lib/management/helpers.ts +++ b/lib/management/helpers.ts @@ -5,8 +5,9 @@ import { User } from './types'; * Transforms user objects by converting roles to roleNames */ export function transformUsersForBatch(users: User[]): any[] { - return users.map(({ roles, ...user }) => ({ + return users.map(({ loginIdOrUserId, loginId, roles, ...user }) => ({ ...user, + loginId: loginIdOrUserId ?? loginId, roleNames: roles, })); } diff --git a/lib/management/types.ts b/lib/management/types.ts index 8a493f131..26a4094af 100644 --- a/lib/management/types.ts +++ b/lib/management/types.ts @@ -371,7 +371,6 @@ export type AttributesTypes = string | boolean | number | string[] | null; export type TemplateOptions = Record; // for providing messaging template options (templates that are being sent via email / text message) export type User = { - loginId: string; email?: string; phone?: string; displayName?: string; @@ -388,7 +387,21 @@ export type User = { seed?: string; // a TOTP seed to set for the user in case of batch invite status?: UserStatus; // the status of the user (enabled, disabled, invited, expired) createdTime?: number; // the time the user was created in seconds since epoch -}; +} & ( + | { + /** The login ID or user ID of the user. When a userId is provided, the user must + * already exist — no new user is created, and the invite is sent to the existing + * user (useful for re-inviting). */ + loginIdOrUserId: string; + /** @deprecated Use loginIdOrUserId instead */ + loginId?: string; + } + | { + /** @deprecated Use loginIdOrUserId instead */ + loginId: string; + loginIdOrUserId?: string; + } +); // The kind of prehashed password to set for a user (only one should be set) export type UserPasswordHashed = { diff --git a/lib/management/user.test.ts b/lib/management/user.test.ts index 85e113067..77cde9b2c 100644 --- a/lib/management/user.test.ts +++ b/lib/management/user.test.ts @@ -346,6 +346,29 @@ describe('Management User', () => { response: httpResponse, }); }); + + it('should send the correct request when passing a userId', async () => { + const httpResponse = { + ok: true, + json: () => mockMgmtUserResponse, + clone: () => ({ + json: () => Promise.resolve(mockMgmtUserResponse), + }), + status: 200, + }; + mockHttpClient.post.mockResolvedValue(httpResponse); + + const userId = 'U2abc1234567890123456789'; + await management.user.invite(userId, { email: 'a@b.c', sendMail: true }); + + expect(mockHttpClient.post).toHaveBeenCalledWith(apiPaths.user.create, { + loginId: userId, + email: 'a@b.c', + roleNames: undefined, + invite: true, + sendMail: true, + }); + }); }); describe('invite batch', () => { @@ -373,8 +396,14 @@ describe('Management User', () => { const resp: SdkResponse = await management.user.inviteBatch( [ - { loginId: 'one', roles: ['r1'], email: 'one@one', password: 'clear', seed: 'aaa' }, - { loginId: 'two', roles: ['r1'], email: 'two@two', hashedPassword: hashed }, + { + loginIdOrUserId: 'one', + roles: ['r1'], + email: 'one@one', + password: 'clear', + seed: 'aaa', + }, + { loginIdOrUserId: 'two', roles: ['r1'], email: 'two@two', hashedPassword: hashed }, ], 'https://invite.me', true, @@ -417,6 +446,50 @@ describe('Management User', () => { response: httpResponse, }); }); + + it('should support deprecated loginId field for backwards compatibility (createBatch)', async () => { + const httpResponse = { + ok: true, + json: () => mockMgmtInviteBatchResponse, + clone: () => ({ + json: () => Promise.resolve(mockMgmtInviteBatchResponse), + }), + status: 200, + }; + mockHttpClient.post.mockResolvedValue(httpResponse); + + await management.user.createBatch([ + { loginId: 'legacy@user.com', roles: ['r1'], email: 'legacy@user.com' }, + ]); + + expect(mockHttpClient.post).toHaveBeenCalledWith(apiPaths.user.createBatch, { + users: [{ loginId: 'legacy@user.com', roleNames: ['r1'], email: 'legacy@user.com' }], + }); + }); + + it('should support deprecated loginId field for backwards compatibility', async () => { + const httpResponse = { + ok: true, + json: () => mockMgmtInviteBatchResponse, + clone: () => ({ + json: () => Promise.resolve(mockMgmtInviteBatchResponse), + }), + status: 200, + }; + mockHttpClient.post.mockResolvedValue(httpResponse); + + await management.user.inviteBatch( + [{ loginId: 'legacy@user.com', roles: ['r1'], email: 'legacy@user.com' }], + 'https://invite.me', + ); + + expect(mockHttpClient.post).toHaveBeenCalledWith(apiPaths.user.createBatch, { + users: [{ loginId: 'legacy@user.com', roleNames: ['r1'], email: 'legacy@user.com' }], + invite: true, + inviteUrl: 'https://invite.me', + sendMail: undefined, + }); + }); }); describe('create batch', () => { @@ -443,8 +516,14 @@ describe('Management User', () => { }; const resp: SdkResponse = await management.user.createBatch([ - { loginId: 'one', roles: ['r1'], email: 'one@one', password: 'clear', seed: 'aaa' }, - { loginId: 'two', roles: ['r1'], email: 'two@two', hashedPassword: hashed }, + { + loginIdOrUserId: 'one', + roles: ['r1'], + email: 'one@one', + password: 'clear', + seed: 'aaa', + }, + { loginIdOrUserId: 'two', roles: ['r1'], email: 'two@two', hashedPassword: hashed }, ]); expect(mockHttpClient.post).toHaveBeenCalledWith(apiPaths.user.createBatch, { diff --git a/lib/management/user.ts b/lib/management/user.ts index 9867c55bb..9a152c0c3 100644 --- a/lib/management/user.ts +++ b/lib/management/user.ts @@ -216,8 +216,14 @@ const withUser = (httpClient: HttpClient) => { /* Create Test User End */ /* Invite User */ + /** + * Create a new user and invite them to set up their credentials. + * When loginIdOrUserId is a loginId, a new user is created if one doesn't already exist. + * When loginIdOrUserId is a userId, the user must already exist — no new user is created, + * and the invite is sent to the existing user (useful for re-inviting). + */ function invite( - loginId: string, + loginIdOrUserId: string, options?: UserOptions & { inviteUrl?: string; sendMail?: boolean; // send invite via mail, default is according to project settings @@ -227,7 +233,7 @@ const withUser = (httpClient: HttpClient) => { }, ): Promise>; function invite( - loginId: string, + loginIdOrUserId: string, email?: string, phone?: string, displayName?: string, @@ -248,7 +254,7 @@ const withUser = (httpClient: HttpClient) => { ): Promise>; function invite( - loginId: string, + loginIdOrUserId: string, emailOrOptions?: string | UserOptions, phone?: string, displayName?: string, @@ -268,12 +274,12 @@ const withUser = (httpClient: HttpClient) => { templateId?: string, ): Promise> { // We support both the old and new parameters forms of invite user - // 1. The new form - invite(loginId, { email, phone, ... }}) - // 2. The old form - invite(loginId, email, phone, ...) + // 1. The new form - invite(loginIdOrUserId, { email, phone, ... }}) + // 2. The old form - invite(loginIdOrUserId, email, phone, ...) const body = typeof emailOrOptions === 'string' ? { - loginId, + loginId: loginIdOrUserId, email: emailOrOptions, phone, displayName, @@ -294,7 +300,7 @@ const withUser = (httpClient: HttpClient) => { templateId, } : { - loginId, + loginId: loginIdOrUserId, ...emailOrOptions, roleNames: emailOrOptions?.roles, roles: undefined,