Skip to content

Commit fbfb6ef

Browse files
Sarath1018claude
andcommitted
feat(identity): add invite to Users service
Final slice of the Identity user management onboarding (follows the getById and CRUD PRs): - invite(users) POST /api/User/InviteUsers Each invited user receives an invitation email with an accept link. The API reports per-user outcomes — the request can succeed overall while individual invitations fail (e.g. disallowed redirect URL), surfaced via the renamed errorMessage field. Live-API findings encoded: redirectUrl is effectively required and must be a portal acceptInvite URL (the API gateway host is rejected per-user with "Redirect URL is not valid"); failed invitations return an all-zeros GUID. Adds the optional UIPATH_PORTAL_URL integration test config for environments where UIPATH_BASE_URL points at the API gateway host. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent c8571c5 commit fbfb6ef

14 files changed

Lines changed: 299 additions & 3 deletions

File tree

docs/oauth-scopes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,3 +280,4 @@ The `ConversationalAgents` scope is required for real-time WebSocket sessions (`
280280
| `updateById()` | `PM.Users` or `PM.Users.Write` |
281281
| `deleteById()` | `PM.Users` or `PM.Users.Write` |
282282
| `create()` | `PM.Users` or `PM.Users.Write` |
283+
| `invite()` | `PM.Users` or `PM.Users.Write` |

src/models/identity/users.constants.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ export const UserMap: { [key: string]: string } = {
1515
groupIDsToRemove: 'groupIdsToRemove',
1616
};
1717

18+
/**
19+
* Invite result field mappings (API field name → SDK field name).
20+
*/
21+
export const UserInviteResultMap: { [key: string]: string } = {
22+
errorMsg: 'errorMessage',
23+
};
24+
1825
/**
1926
* Maps numeric user type codes (from API) to {@link UserType} enum values.
2027
*/

src/models/identity/users.internal-types.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,22 @@ export interface RawUserCreateResponse {
5959
result: UserOperationResult;
6060
users: RawUserEntry[];
6161
}
62+
63+
/**
64+
* Raw per-user result entry of `POST /api/User/InviteUsers`.
65+
*/
66+
export interface RawUserInviteResult {
67+
email: string;
68+
id: string;
69+
/** Renamed to `errorMessage` in the SDK response. */
70+
errorMsg: string | null;
71+
success: boolean;
72+
}
73+
74+
/**
75+
* Raw response of `POST /api/User/InviteUsers`.
76+
*/
77+
export interface RawUserInviteResponse {
78+
result: UserOperationResult;
79+
users: RawUserInviteResult[];
80+
}

src/models/identity/users.models.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import type {
77
RawUserGetResponse,
88
UserCreateData,
99
UserCreateOptions,
10+
UserInviteData,
11+
UserInviteResponse,
1012
UserOperationResult,
1113
UserUpdateOptions,
1214
UserUpdateResponse,
@@ -148,6 +150,44 @@ export interface UserServiceModel {
148150
options?: UserCreateOptions
149151
): Promise<UserCreateResponse>;
150152

153+
/**
154+
* Invites users to the organization by email.
155+
*
156+
* Each invited user receives an invitation email with an accept link that
157+
* redirects to `redirectUrl`. Individual invitations can fail while the
158+
* request as a whole succeeds — check `success` on each entry in `users`.
159+
*
160+
* @param users - Users to invite
161+
* @returns Promise resolving to the overall outcome and per-user results
162+
* {@link UserInviteResponse}
163+
*
164+
* @example
165+
* ```typescript
166+
* const response = await users.invite([
167+
* {
168+
* email: 'jdoe@acme.com',
169+
* redirectUrl: 'https://cloud.uipath.com/portal_/acceptInvite?organizationId=<organizationId>',
170+
* },
171+
* ]);
172+
* for (const user of response.users) {
173+
* console.log(`${user.email}: ${user.success ? 'invited' : user.errorMessage}`);
174+
* }
175+
* ```
176+
*
177+
* @example Invite into groups
178+
* ```typescript
179+
* await users.invite([
180+
* {
181+
* email: 'jdoe@acme.com',
182+
* redirectUrl: 'https://cloud.uipath.com/portal_/acceptInvite?organizationId=<organizationId>',
183+
* name: 'Jane',
184+
* surname: 'Doe',
185+
* groupIds: ['<groupId>'],
186+
* },
187+
* ]);
188+
* ```
189+
*/
190+
invite(users: UserInviteData[]): Promise<UserInviteResponse>;
151191
}
152192

153193
/**

src/models/identity/users.types.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,3 +152,51 @@ export interface UserCreateOptions {
152152
groupIds?: string[];
153153
}
154154

155+
/**
156+
* A user to invite with `invite()`.
157+
*/
158+
export interface UserInviteData {
159+
/** Email address the invitation is sent to. */
160+
email: string;
161+
/**
162+
* URL the accept-invite link redirects to. Must be an allowed UiPath portal URL for
163+
* the organization, e.g. `https://cloud.uipath.com/portal_/acceptInvite?organizationId=...`
164+
* — other URLs are rejected per user with `Redirect URL is not valid`.
165+
*/
166+
redirectUrl: string;
167+
/** First name. */
168+
name?: string;
169+
/** Last name. */
170+
surname?: string;
171+
/** Language code for the invitation email (e.g. `en`). */
172+
language?: string;
173+
/** GUIDs of groups the invited user is added to. */
174+
groupIds?: string[];
175+
}
176+
177+
/**
178+
* Per-user outcome of `invite()`.
179+
*/
180+
export interface UserInviteResult {
181+
/** Email address the invitation was sent to. */
182+
email: string;
183+
/** GUID of the newly invited user. All-zeros GUID when the invitation failed. */
184+
id: string;
185+
/** Why the invitation failed. `null` on success. */
186+
errorMessage: string | null;
187+
/** Whether this user was successfully invited. */
188+
success: boolean;
189+
}
190+
191+
/**
192+
* Response from `invite()`.
193+
*
194+
* `result` reflects the request as a whole; individual invitations can still fail —
195+
* check `success` on each entry in `users`.
196+
*/
197+
export interface UserInviteResponse {
198+
/** Overall outcome of the invite request. */
199+
result: UserOperationResult;
200+
/** Per-user invitation outcomes. */
201+
users: UserInviteResult[];
202+
}

src/services/identity/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
* Identity Users Service Module
33
*
44
* Provides organization-level user administration via the UiPath Identity API:
5-
* - `Users` — retrieve, update and delete users, and create users in bulk
5+
* - `Users` — retrieve, update and delete users, create users in bulk, and
6+
* invite users to the organization by email
67
*
78
* @example
89
* ```typescript

src/services/identity/users.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import type {
99
RawUserGetResponse,
1010
UserCreateData,
1111
UserCreateOptions,
12+
UserInviteData,
13+
UserInviteResponse,
14+
UserInviteResult,
1215
UserUpdateOptions,
1316
UserUpdateResponse,
1417
} from '../../models/identity/users.types';
@@ -22,9 +25,11 @@ import {
2225
INTERNAL_USER_FIELDS,
2326
type RawUserCreateResponse,
2427
type RawUserEntry,
28+
type RawUserInviteResponse,
2529
} from '../../models/identity/users.internal-types';
2630
import {
2731
UserCategoryMap,
32+
UserInviteResultMap,
2833
UserMap,
2934
UserTypeMap,
3035
} from '../../models/identity/users.constants';
@@ -78,6 +83,18 @@ export class UserService extends BaseService implements UserServiceModel {
7883
};
7984
}
8085

86+
@track('Users.Invite')
87+
async invite(users: UserInviteData[]): Promise<UserInviteResponse> {
88+
const response = await this.post<RawUserInviteResponse>(
89+
IDENTITY_USER_ENDPOINTS.INVITE,
90+
users.map((user) => transformRequest(user, UserMap))
91+
);
92+
return {
93+
result: response.data.result,
94+
users: transformData(response.data.users, UserInviteResultMap) as unknown as UserInviteResult[],
95+
};
96+
}
97+
8198
/**
8299
* Strips internal fields from a raw user entry and applies the SDK field
83100
* renames and numeric → enum value mappings before returning it to the consumer.

src/utils/constants/endpoints/identity.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,5 @@ export const IDENTITY_USER_ENDPOINTS = {
2424
/** GET (retrieve), PUT (update) and DELETE share this URL — the HTTP method is chosen at the call site. */
2525
BY_ID: (userId: string) => `${USER_API_BASE}/${userId}`,
2626
BULK_CREATE: `${USER_API_BASE}/BulkCreate`,
27+
INVITE: `${USER_API_BASE}/InviteUsers`,
2728
} as const;

tests/.env.integration.example

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,5 +86,10 @@ TASKS_TEST_USER_GROUP_ID=
8686
TASKS_TEST_USER_ID=
8787

8888
# Organization GUID (from UiPath Cloud > Admin > Organization Settings)
89-
# Required by the Identity Users tests (user create operations)
89+
# Required by the Identity Users tests (user create/invite operations)
9090
UIPATH_ORGANIZATION_ID=3aa10965-a82d-4d9e-8366-0eff8e87bf7a
91+
92+
# Portal host for invite accept-links (e.g. https://cloud.uipath.com).
93+
# Only needed when UIPATH_BASE_URL points at the API gateway host — invite
94+
# redirect URLs must use the portal host. Defaults to UIPATH_BASE_URL.
95+
UIPATH_PORTAL_URL=

tests/integration/config/test-config.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ export interface IntegrationConfig {
1414
* as the partition the users belong to, and invite redirect URLs embed it).
1515
*/
1616
organizationId?: string;
17+
/**
18+
* Portal host for invite accept-links (e.g. `https://alpha.uipath.com`). Invite
19+
* redirect URLs must point at the portal host — the API gateway host is rejected
20+
* with `Redirect URL is not valid`. Defaults to `baseUrl` when they coincide.
21+
*/
22+
portalUrl?: string;
1723
secret: string;
1824
timeout: number;
1925
skipCleanup: boolean;
@@ -85,6 +91,7 @@ function validateConfig(rawConfig: Record<string, unknown>): IntegrationConfig {
8591
tenantName: rawConfig.tenantName as string,
8692
tenantId: typeof rawConfig.tenantId === 'string' ? rawConfig.tenantId : undefined,
8793
organizationId: typeof rawConfig.organizationId === 'string' ? rawConfig.organizationId : undefined,
94+
portalUrl: typeof rawConfig.portalUrl === 'string' ? rawConfig.portalUrl : undefined,
8895
secret: rawConfig.secret as string,
8996
timeout: typeof rawConfig.timeout === 'number' && rawConfig.timeout > 0 ? rawConfig.timeout : 30000,
9097
skipCleanup: typeof rawConfig.skipCleanup === 'boolean' ? rawConfig.skipCleanup : false,
@@ -129,6 +136,7 @@ export function loadIntegrationConfig(): IntegrationConfig {
129136
tenantName: process.env.UIPATH_TENANT_NAME,
130137
tenantId: process.env.UIPATH_TENANT_ID_DEV || undefined,
131138
organizationId: process.env.UIPATH_ORGANIZATION_ID || undefined,
139+
portalUrl: process.env.UIPATH_PORTAL_URL || undefined,
132140
secret: process.env.UIPATH_SECRET,
133141
timeout: process.env.INTEGRATION_TEST_TIMEOUT
134142
? parseInt(process.env.INTEGRATION_TEST_TIMEOUT, 10)

0 commit comments

Comments
 (0)