@joshuanode/n8n-nodes-cipp 0.0.10 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,9 +19,9 @@ This node provides full integration with the CIPP API, enabling automation of:
19
19
  - **Identity Management** - Users, groups, MFA, devices
20
20
  - **Tenant Administration** - Alerts, licenses, standards
21
21
  - **Intune** - Applications, Autopilot, device actions
22
- - **Teams & SharePoint** - Teams, sites, voice numbers
22
+ - **Teams & SharePoint** - Teams, sites, voice numbers, shifts scheduling
23
23
  - **Security & Compliance** - Defender alerts, incidents
24
- - **Tools** - Breach search, Graph API requests
24
+ - **Tools** - Breach search, Graph API requests, ExecGraphRequest
25
25
  - **CIPP System** - Scheduled jobs, backups
26
26
 
27
27
  ### User-Friendly Design
@@ -66,17 +66,18 @@ For detailed authentication setup, see the [CIPP API Documentation](https://docs
66
66
  | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
67
67
  | **Tenant** | Get Many, Get Licenses, Get CSP Licenses, CSP License Action, Clear Cache |
68
68
  | **User** | Get Many, Add, Disable, Enable, Reset Password, Reset MFA, Revoke Sessions, Remove, Create TAP, Set Per-User MFA, Send MFA Push, Clear Immutable ID, Offboard |
69
- | **Group** | Add, Edit Members, Delete, Hide from GAL, Set Delivery Management, Get Many |
70
- | **Device** | Get Many, Manage, Execute Action, Get Recovery Key, Get LAPS Password |
71
- | **Autopilot** | Get Many, Assign, Remove, Sync, Get Configurations |
72
- | **Mailbox** | Convert, Enable Archive, Set Out of Office, Set Email Forwarding |
73
- | **Alert** | Add, Get Many, Get Security Alerts, Get Security Incidents, Set Alert Status, Set Incident Status |
74
- | **Application** | Get Many, Assign, Remove, Add WinGet/Store/Chocolatey/MSP/Office Apps |
75
- | **Team** | Add, Get Many, Get Sites, Get Activity, Manage Site Members/Permissions |
76
- | **Voice** | Get Phone Numbers, Get Locations, Assign/Unassign Numbers |
77
- | **Scheduled Item** | Add, Get Many, Remove |
78
- | **Backup** | Get Many, Run, Restore, Set Auto-Backup |
79
- | **Tools** | Breach Search (Account/Tenant), Graph Request (List), Graph Request (Exec) |
69
+ | **Group** | Add, Edit Members, Delete, Hide from GAL, Set Delivery Management, Get Many |
70
+ | **Device** | Get Many, Manage, Execute Action, Get Recovery Key, Get LAPS Password |
71
+ | **Autopilot** | Get Many, Assign, Remove, Sync, Get Configurations |
72
+ | **Mailbox** | Convert, Enable Archive, Set Out of Office, Set Email Forwarding |
73
+ | **Alert** | Add, Get Many, Get Security Alerts, Get Security Incidents, Set Alert Status, Set Incident Status |
74
+ | **Application** | Get Many, Assign, Remove, Add WinGet/Store/Chocolatey/MSP/Office Apps |
75
+ | **Team** | Add, Get Many, Get Sites, Get Activity, Manage Site Members/Permissions |
76
+ | **Teams Shift** | List/Create/Update/Delete Shifts, Open Shifts, Scheduling Groups, Time Off Reasons; List/Create/Approve/Decline Time Off, Swap Shift & Offer Shift Requests |
77
+ | **Voice** | Get Phone Numbers, Get Locations, Assign/Unassign Numbers |
78
+ | **Scheduled Item** | Add, Get Many, Remove |
79
+ | **Backup** | Get Many, Run, Restore, Set Auto-Backup |
80
+ | **Tools** | Breach Search (Account/Tenant), Exec Graph Request, Graph Request (List), Graph Request (Exec) |
80
81
 
81
82
  ## Example Usage
82
83
 
@@ -130,7 +131,34 @@ $select: id,displayName,userPrincipalName
130
131
  $filter: startsWith(displayName,'John')
131
132
  ```
132
133
 
133
- ### Teams Shifts Graph Exec Request (POST/PATCH/GET)
134
+ ### Teams Shifts (Dedicated Resource)
135
+
136
+ ```
137
+ Resource: Teams Shift
138
+ Operation: List Shifts
139
+ Tenant: Select from dropdown
140
+ Team ID: <team-guid>
141
+ Filters → Start Date: 2024-03-01T00:00:00Z
142
+ Filters → End Date: 2024-03-31T23:59:59Z
143
+ ```
144
+
145
+ ```
146
+ Resource: Teams Shift
147
+ Operation: Create Shift
148
+ Tenant: Select from dropdown
149
+ Team ID: <team-guid>
150
+ User ID: <aad-user-id>
151
+ Start Date Time: 2024-03-15T08:00:00Z
152
+ End Date Time: 2024-03-15T16:00:00Z
153
+ Options → Display Name: Morning Shift
154
+ Options → Theme: blue
155
+ ```
156
+
157
+ > ⚠️ **CIPP-API Requirement**: The Teams Shift resource and the Exec Graph Request tool both use `POST /api/ExecGraphRequest`, which is **not part of the standard CIPP API**. You must be running a custom fork of [CIPP-API](https://github.com/KelvinTegelaar/CIPP-API) that exposes the `ExecGraphRequest` endpoint. Without this, all Teams Shift operations and the Exec Graph Request tool will return a 404 or 400 error.
158
+ >
159
+ > If your fork uses a different route name (e.g., `/api/GraphRequest`), the `Graph Request (Exec)` tool has a built-in fallback. The dedicated Teams Shift resource does not — it expects `/api/ExecGraphRequest` to exist.
160
+
161
+ ### Graph Request (Exec) — Raw Graph Calls
134
162
 
135
163
  ```
136
164
  Resource: Tools
@@ -142,6 +170,7 @@ Body: {"userId":"<aad-user-id>","schedulingGroupId":"<group-id>","sharedShift":{
142
170
  ```
143
171
 
144
172
  Notes:
173
+
145
174
  - `Graph Request (Exec)` sends a `POST` to `/api/ExecGraphRequest` and falls back to `/api/GraphRequest` if your fork uses that route name.
146
175
  - By default, client-side validation requires endpoints matching `teams/{id}/schedule/*` (can be disabled in `Exec Options`).
147
176
 
@@ -1 +1 @@
1
- {"version":3,"file":"Cipp.node.d.ts","sourceRoot":"","sources":["../../../nodes/Cipp/Cipp.node.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,qBAAqB,EACrB,wBAAwB,EAExB,iBAAiB,EACjB,qBAAqB,EACrB,yBAAyB,EACzB,kBAAkB,EAClB,qBAAqB,EACrB,SAAS,EACT,oBAAoB,EACpB,MAAM,cAAc,CAAC;AAWtB,qBAAa,IAAK,YAAW,SAAS;IACrC,WAAW,EAAE,oBAAoB,CA4H/B;IAEF,OAAO;;wCAGE,wBAAwB,cAClB,qBAAqB,GAC/B,OAAO,CAAC,yBAAyB,CAAC;;;+BAyD9B,qBAAqB,WAClB,MAAM,GACb,OAAO,CAAC,qBAAqB,CAAC;;MAqBjC;IAEI,OAAO,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,kBAAkB,EAAE,EAAE,CAAC;CAyzDvE"}
1
+ {"version":3,"file":"Cipp.node.d.ts","sourceRoot":"","sources":["../../../nodes/Cipp/Cipp.node.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,qBAAqB,EACrB,wBAAwB,EAExB,iBAAiB,EACjB,qBAAqB,EACrB,yBAAyB,EACzB,kBAAkB,EAClB,qBAAqB,EACrB,SAAS,EACT,oBAAoB,EACpB,MAAM,cAAc,CAAC;AAWtB,qBAAa,IAAK,YAAW,SAAS;IACrC,WAAW,EAAE,oBAAoB,CAiI/B;IAEF,OAAO;;wCAGE,wBAAwB,cAClB,qBAAqB,GAC/B,OAAO,CAAC,yBAAyB,CAAC;;;+BAyD9B,qBAAqB,WAClB,MAAM,GACb,OAAO,CAAC,qBAAqB,CAAC;;MAqBjC;IAEI,OAAO,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,kBAAkB,EAAE,EAAE,CAAC;CAymEvE"}
@@ -102,6 +102,11 @@ class Cipp {
102
102
  value: 'team',
103
103
  description: 'Manage Teams and SharePoint',
104
104
  },
105
+ {
106
+ name: 'Teams Shift',
107
+ value: 'teamsShift',
108
+ description: 'Manage Teams Shifts schedule — shifts, open shifts, groups, time off',
109
+ },
105
110
  {
106
111
  name: 'Tenant',
107
112
  value: 'tenant',
@@ -1201,7 +1206,8 @@ class Cipp {
1201
1206
  }
1202
1207
  else if (operation === 'execGraphRequest') {
1203
1208
  const tenantFilter = getTenantFilter();
1204
- const endpoint = this.getNodeParameter('execEndpoint', i);
1209
+ const rawEndpoint = this.getNodeParameter('execEndpoint', i);
1210
+ const endpoint = normalizeGraphEndpoint(rawEndpoint);
1205
1211
  const method = this.getNodeParameter('execMethod', i);
1206
1212
  const payload = { tenantFilter, endpoint, method };
1207
1213
  if (method === 'POST' || method === 'PATCH') {
@@ -1264,6 +1270,249 @@ class Cipp {
1264
1270
  }
1265
1271
  }
1266
1272
  }
1273
+ // ==================== TEAMS SHIFT ====================
1274
+ else if (resource === 'teamsShift') {
1275
+ const tenantFilter = getTenantFilter();
1276
+ const teamId = this.getNodeParameter('teamId', i);
1277
+ const basePath = `teams/${teamId}/schedule`;
1278
+ const graphExec = async (method, endpoint, body) => {
1279
+ const payload = { tenantFilter, endpoint, method };
1280
+ if (body && Object.keys(body).length > 0) {
1281
+ payload.body = body;
1282
+ }
1283
+ return GenericFunctions_1.cippApiRequest.call(this, 'POST', '/api/ExecGraphRequest', payload, {});
1284
+ };
1285
+ const buildFilteredEndpoint = (base) => {
1286
+ const filters = this.getNodeParameter('listFilters', i, {});
1287
+ // Raw OData filter takes priority
1288
+ if (filters.rawFilter) {
1289
+ return `${base}?$filter=${encodeURIComponent(filters.rawFilter)}`;
1290
+ }
1291
+ // Build from convenience date fields
1292
+ const parts = [];
1293
+ if (filters.startDate) {
1294
+ const d = new Date(filters.startDate).toISOString();
1295
+ parts.push(`sharedShift/startDateTime ge ${d}`);
1296
+ }
1297
+ if (filters.endDate) {
1298
+ const d = new Date(filters.endDate).toISOString();
1299
+ parts.push(`sharedShift/endDateTime le ${d}`);
1300
+ }
1301
+ if (parts.length > 0) {
1302
+ return `${base}?$filter=${encodeURIComponent(parts.join(' and '))}`;
1303
+ }
1304
+ return base;
1305
+ };
1306
+ // ── Shifts ──
1307
+ if (operation === 'listShifts') {
1308
+ responseData = await graphExec('GET', buildFilteredEndpoint(`${basePath}/shifts`));
1309
+ }
1310
+ else if (operation === 'createShift') {
1311
+ const userId = this.getNodeParameter('userId', i);
1312
+ const startDateTime = this.getNodeParameter('startDateTime', i);
1313
+ const endDateTime = this.getNodeParameter('endDateTime', i);
1314
+ const options = this.getNodeParameter('shiftOptions', i, {});
1315
+ const sharedShift = {
1316
+ startDateTime,
1317
+ endDateTime,
1318
+ };
1319
+ if (options.displayName)
1320
+ sharedShift.displayName = options.displayName;
1321
+ if (options.notes)
1322
+ sharedShift.notes = options.notes;
1323
+ if (options.theme)
1324
+ sharedShift.theme = options.theme;
1325
+ if (options.activities) {
1326
+ sharedShift.activities = JSON.parse(options.activities);
1327
+ }
1328
+ responseData = await graphExec('POST', `${basePath}/shifts`, {
1329
+ userId,
1330
+ sharedShift,
1331
+ });
1332
+ }
1333
+ else if (operation === 'updateShift') {
1334
+ const shiftId = this.getNodeParameter('shiftId', i);
1335
+ const shiftData = parseJsonObjectPayload(this.getNodeParameter('shiftUpdateData', i), 'Shift Data', i);
1336
+ responseData = await graphExec('PUT', `${basePath}/shifts/${shiftId}`, shiftData);
1337
+ }
1338
+ else if (operation === 'deleteShift') {
1339
+ const shiftId = this.getNodeParameter('shiftId', i);
1340
+ responseData = await graphExec('DELETE', `${basePath}/shifts/${shiftId}`);
1341
+ }
1342
+ // ── Open Shifts ──
1343
+ else if (operation === 'listOpenShifts') {
1344
+ responseData = await graphExec('GET', buildFilteredEndpoint(`${basePath}/openShifts`));
1345
+ }
1346
+ else if (operation === 'createOpenShift') {
1347
+ const schedulingGroupId = this.getNodeParameter('schedulingGroupId', i);
1348
+ const startDateTime = this.getNodeParameter('openShiftStart', i);
1349
+ const endDateTime = this.getNodeParameter('openShiftEnd', i);
1350
+ const openSlotCount = this.getNodeParameter('openSlotCount', i);
1351
+ const options = this.getNodeParameter('openShiftOptions', i, {});
1352
+ const sharedOpenShift = {
1353
+ startDateTime,
1354
+ endDateTime,
1355
+ openSlotCount,
1356
+ };
1357
+ if (options.displayName)
1358
+ sharedOpenShift.displayName = options.displayName;
1359
+ if (options.notes)
1360
+ sharedOpenShift.notes = options.notes;
1361
+ if (options.theme)
1362
+ sharedOpenShift.theme = options.theme;
1363
+ if (options.activities) {
1364
+ sharedOpenShift.activities = JSON.parse(options.activities);
1365
+ }
1366
+ responseData = await graphExec('POST', `${basePath}/openShifts`, {
1367
+ schedulingGroupId,
1368
+ sharedOpenShift,
1369
+ });
1370
+ }
1371
+ else if (operation === 'updateOpenShift') {
1372
+ const openShiftId = this.getNodeParameter('openShiftId', i);
1373
+ const data = parseJsonObjectPayload(this.getNodeParameter('openShiftUpdateData', i), 'Open Shift Data', i);
1374
+ responseData = await graphExec('PUT', `${basePath}/openShifts/${openShiftId}`, data);
1375
+ }
1376
+ else if (operation === 'deleteOpenShift') {
1377
+ const openShiftId = this.getNodeParameter('openShiftId', i);
1378
+ responseData = await graphExec('DELETE', `${basePath}/openShifts/${openShiftId}`);
1379
+ }
1380
+ // ── Scheduling Groups ──
1381
+ else if (operation === 'listSchedulingGroups') {
1382
+ responseData = await graphExec('GET', buildFilteredEndpoint(`${basePath}/schedulingGroups`));
1383
+ }
1384
+ else if (operation === 'createSchedulingGroup') {
1385
+ const displayName = this.getNodeParameter('groupDisplayName', i);
1386
+ const userIds = this.getNodeParameter('groupUserIds', i)
1387
+ .split(',')
1388
+ .map((id) => id.trim())
1389
+ .filter((id) => id);
1390
+ responseData = await graphExec('POST', `${basePath}/schedulingGroups`, {
1391
+ displayName,
1392
+ userIds,
1393
+ isActive: true,
1394
+ });
1395
+ }
1396
+ else if (operation === 'updateSchedulingGroup') {
1397
+ const groupId = this.getNodeParameter('schedulingGroupUpdateId', i);
1398
+ const data = parseJsonObjectPayload(this.getNodeParameter('schedulingGroupUpdateData', i), 'Scheduling Group Data', i);
1399
+ responseData = await graphExec('PUT', `${basePath}/schedulingGroups/${groupId}`, data);
1400
+ }
1401
+ else if (operation === 'deleteSchedulingGroup') {
1402
+ const groupId = this.getNodeParameter('schedulingGroupUpdateId', i);
1403
+ responseData = await graphExec('PUT', `${basePath}/schedulingGroups/${groupId}`, { isActive: false });
1404
+ }
1405
+ // ── Time Off Reasons ──
1406
+ else if (operation === 'listTimeOffReasons') {
1407
+ responseData = await graphExec('GET', buildFilteredEndpoint(`${basePath}/timeOffReasons`));
1408
+ }
1409
+ else if (operation === 'createTimeOffReason') {
1410
+ const displayName = this.getNodeParameter('reasonDisplayName', i);
1411
+ const iconType = this.getNodeParameter('iconType', i);
1412
+ responseData = await graphExec('POST', `${basePath}/timeOffReasons`, {
1413
+ displayName,
1414
+ iconType,
1415
+ isActive: true,
1416
+ });
1417
+ }
1418
+ else if (operation === 'updateTimeOffReason') {
1419
+ const reasonId = this.getNodeParameter('timeOffReasonId', i);
1420
+ const data = parseJsonObjectPayload(this.getNodeParameter('timeOffReasonUpdateData', i), 'Time Off Reason Data', i);
1421
+ responseData = await graphExec('PUT', `${basePath}/timeOffReasons/${reasonId}`, data);
1422
+ }
1423
+ else if (operation === 'deleteTimeOffReason') {
1424
+ const reasonId = this.getNodeParameter('timeOffReasonId', i);
1425
+ responseData = await graphExec('PUT', `${basePath}/timeOffReasons/${reasonId}`, { isActive: false });
1426
+ }
1427
+ // ── Time Off Requests ──
1428
+ else if (operation === 'listTimeOffRequests') {
1429
+ responseData = await graphExec('GET', buildFilteredEndpoint(`${basePath}/timeOffRequests`));
1430
+ }
1431
+ else if (operation === 'createTimeOffRequest') {
1432
+ const startDateTime = this.getNodeParameter('timeOffStart', i);
1433
+ const endDateTime = this.getNodeParameter('timeOffEnd', i);
1434
+ const timeOffReasonId = this.getNodeParameter('timeOffReasonIdForRequest', i);
1435
+ responseData = await graphExec('POST', `${basePath}/timeOffRequests`, {
1436
+ startDateTime,
1437
+ endDateTime,
1438
+ timeOffReasonId,
1439
+ });
1440
+ }
1441
+ else if (operation === 'approveTimeOffRequest') {
1442
+ const requestId = this.getNodeParameter('timeOffRequestId', i);
1443
+ const message = this.getNodeParameter('approvalMessage', i, '');
1444
+ const body = {};
1445
+ if (message)
1446
+ body.message = message;
1447
+ responseData = await graphExec('POST', `${basePath}/timeOffRequests/${requestId}/approve`, body);
1448
+ }
1449
+ else if (operation === 'declineTimeOffRequest') {
1450
+ const requestId = this.getNodeParameter('timeOffRequestId', i);
1451
+ const message = this.getNodeParameter('approvalMessage', i, '');
1452
+ const body = {};
1453
+ if (message)
1454
+ body.message = message;
1455
+ responseData = await graphExec('POST', `${basePath}/timeOffRequests/${requestId}/decline`, body);
1456
+ }
1457
+ // ── Swap Shift Requests ──
1458
+ else if (operation === 'listSwapShiftRequests') {
1459
+ responseData = await graphExec('GET', buildFilteredEndpoint(`${basePath}/swapShiftsChangeRequests`));
1460
+ }
1461
+ else if (operation === 'createSwapShiftRequest') {
1462
+ const senderShiftId = this.getNodeParameter('senderShiftId', i);
1463
+ const recipientShiftId = this.getNodeParameter('recipientShiftId', i);
1464
+ const recipientUserId = this.getNodeParameter('swapRecipientUserId', i);
1465
+ responseData = await graphExec('POST', `${basePath}/swapShiftsChangeRequests`, {
1466
+ senderShiftId,
1467
+ recipientShiftId,
1468
+ recipientUserId,
1469
+ });
1470
+ }
1471
+ else if (operation === 'approveSwapShiftRequest') {
1472
+ const requestId = this.getNodeParameter('swapShiftRequestId', i);
1473
+ const message = this.getNodeParameter('approvalMessage', i, '');
1474
+ const body = {};
1475
+ if (message)
1476
+ body.message = message;
1477
+ responseData = await graphExec('POST', `${basePath}/swapShiftsChangeRequests/${requestId}/approve`, body);
1478
+ }
1479
+ else if (operation === 'declineSwapShiftRequest') {
1480
+ const requestId = this.getNodeParameter('swapShiftRequestId', i);
1481
+ const message = this.getNodeParameter('approvalMessage', i, '');
1482
+ const body = {};
1483
+ if (message)
1484
+ body.message = message;
1485
+ responseData = await graphExec('POST', `${basePath}/swapShiftsChangeRequests/${requestId}/decline`, body);
1486
+ }
1487
+ // ── Offer Shift Requests ──
1488
+ else if (operation === 'listOfferShiftRequests') {
1489
+ responseData = await graphExec('GET', buildFilteredEndpoint(`${basePath}/offerShiftRequests`));
1490
+ }
1491
+ else if (operation === 'createOfferShiftRequest') {
1492
+ const senderShiftId = this.getNodeParameter('offerSenderShiftId', i);
1493
+ const recipientUserId = this.getNodeParameter('offerRecipientUserId', i);
1494
+ responseData = await graphExec('POST', `${basePath}/offerShiftRequests`, {
1495
+ senderShiftId,
1496
+ recipientUserId,
1497
+ });
1498
+ }
1499
+ else if (operation === 'approveOfferShiftRequest') {
1500
+ const requestId = this.getNodeParameter('offerShiftRequestId', i);
1501
+ const message = this.getNodeParameter('approvalMessage', i, '');
1502
+ const body = {};
1503
+ if (message)
1504
+ body.message = message;
1505
+ responseData = await graphExec('POST', `${basePath}/offerShiftRequests/${requestId}/approve`, body);
1506
+ }
1507
+ else if (operation === 'declineOfferShiftRequest') {
1508
+ const requestId = this.getNodeParameter('offerShiftRequestId', i);
1509
+ const message = this.getNodeParameter('approvalMessage', i, '');
1510
+ const body = {};
1511
+ if (message)
1512
+ body.message = message;
1513
+ responseData = await graphExec('POST', `${basePath}/offerShiftRequests/${requestId}/decline`, body);
1514
+ }
1515
+ }
1267
1516
  // ==================== IDENTITY ====================
1268
1517
  else if (resource === 'identity') {
1269
1518
  const tenantFilter = getTenantFilter();