@mindline/sync 1.0.110 → 1.0.112

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/src/index.ts CHANGED
@@ -9,6 +9,7 @@ import workspaces from "./workspaces.json";
9
9
  import syncmilestones from './syncmilestones.json';
10
10
  import resources from './resources.json';
11
11
  import actors from './actors.json';
12
+ import * as secureApiClient from './api-client';
12
13
 
13
14
  const FILTER_FIELD = "workspaceIDs";
14
15
  // called by unit tests
@@ -929,13 +930,6 @@ export class BatchArray {
929
930
  private pollAuthorizedUser: User | null = null;
930
931
  private pollBatchIdArray: Array<any> = [];
931
932
 
932
- // Last known backend queues (strongest completion signal)
933
- private lastQueues: { main: number | null; writer: number | null; deferred: number | null } = {
934
- main: null,
935
- writer: null,
936
- deferred: null
937
- };
938
- private hasQueueInfo: boolean = false;
939
933
 
940
934
  // Store UI callbacks so we can update the UI from poll/completion events.
941
935
  private setIdleText: ((idleText: string) => void) | null = null;
@@ -1049,10 +1043,6 @@ export class BatchArray {
1049
1043
  // Save callback so completion/polling can update the idle text too.
1050
1044
  this.setIdleText = setIdleText;
1051
1045
 
1052
- // IMPORTANT: reset completion signals between sync runs.
1053
- // Otherwise a previous run that ended with queues=0 could make the next run immediately "Complete".
1054
- this.hasQueueInfo = false;
1055
- this.lastQueues = { main: null, writer: null, deferred: null };
1056
1046
 
1057
1047
  this.pb_startTS = Date.now();
1058
1048
  this.pb_progress = 0;
@@ -1061,35 +1051,14 @@ export class BatchArray {
1061
1051
  setSyncProgress(this.pb_progress);
1062
1052
  setIdleText("Starting sync...");
1063
1053
  this.pb_timer = setInterval(() => {
1064
- // If backend indicates completion (prefer queue-empty when available), stop the timer
1054
+ // Completion check is now handled by the SignalR handler via isSyncCompleted()
1055
+ // This timer only increments elapsed time; actual completion logic lives in the handler.
1065
1056
  console.log("this.tenantNodes", this.tenantNodes)
1066
- let isCompletedOrNothingToSync = this.tenantNodes.every((tn: TenantNode) =>
1067
- tn.nothingtosync || tn.targets.every((ta) => ta.status === "complete" || ta.status === "failed")
1068
- );
1069
-
1070
- const queuesEmpty = this.hasQueueInfo &&
1071
- this.lastQueues.main === 0 &&
1072
- this.lastQueues.writer === 0 &&
1073
- this.lastQueues.deferred === 0;
1074
1057
 
1075
- // If we have queue info, trust it for completion. Otherwise fall back to node terminal states.
1076
- const isCompleted = queuesEmpty || (!this.hasQueueInfo && isCompletedOrNothingToSync);
1077
-
1078
- if (isCompleted) {
1079
- clearInterval(this.pb_timer!);
1080
- this.pb_timer = null;
1081
- this.pb_progress = 100;
1082
- setSyncProgress(this.pb_progress);
1083
- setIdleText(`Complete.`);
1084
- this.stopPolling();
1085
- this.clearStoredBatchIds();
1086
- }
1087
- else {
1088
- this.pb_total = this.pb_total + 1;
1089
- this.pb_idle = this.pb_idle + 1;
1058
+ this.pb_total = this.pb_total + 1;
1059
+ this.pb_idle = this.pb_idle + 1;
1090
1060
 
1091
- setIdleText(`${this.pb_total} seconds elapsed. Last update ${this.pb_idle} seconds ago.`);
1092
- }
1061
+ setIdleText(`${this.pb_total} seconds elapsed. Last update ${this.pb_idle} seconds ago.`);
1093
1062
  }, 1000);
1094
1063
  this.milestoneArray.start(setMilestones);
1095
1064
  }
@@ -1202,9 +1171,6 @@ export class BatchArray {
1202
1171
  bClearLocalStorage = bClearLocalStorage;
1203
1172
  message = message;
1204
1173
 
1205
- // Starting a new SignalR session for a (potentially new) run: reset queue-based completion signals.
1206
- this.hasQueueInfo = false;
1207
- this.lastQueues = { main: null, writer: null, deferred: null };
1208
1174
 
1209
1175
  // we have just completed a successful POST to startSync
1210
1176
  this.milestoneArray.post(setMilestones);
@@ -1235,10 +1201,6 @@ export class BatchArray {
1235
1201
  // does this tenantnode/batch have nothing to sync?
1236
1202
  let bTotalCountZero: boolean = false;
1237
1203
  let bCurrentCountZero: boolean = false;
1238
- // queue stats (better completion signal than only written/total)
1239
- let totalInMainQueue: number | null = null;
1240
- let totalInWriterQueue: number | null = null;
1241
- let totalInDeferredQueue: number | null = null;
1242
1204
  for (let j = 0; j < statskeys.length; j++) {
1243
1205
  let bTotalCount = statskeys[j].endsWith("TotalCount");
1244
1206
  let bCurrentCount = statskeys[j].endsWith("CurrentCount");
@@ -1292,16 +1254,6 @@ export class BatchArray {
1292
1254
  }
1293
1255
  tenantNode.nothingtosync = bTotalCountZero && bCurrentCountZero;
1294
1256
 
1295
- // queue stats are global for the batch
1296
- if (statskeys[j] === "TotalInMainQueue") {
1297
- totalInMainQueue = Number(statsvalues[j]);
1298
- }
1299
- if (statskeys[j] === "TotalInWriterQueue") {
1300
- totalInWriterQueue = Number(statsvalues[j]);
1301
- }
1302
- if (statskeys[j] === "TotalInDeferredQueue") {
1303
- totalInDeferredQueue = Number(statsvalues[j]);
1304
- }
1305
1257
 
1306
1258
  if (statskeys[j].startsWith("Writer")) {
1307
1259
  // parse tid from Writer key
@@ -1379,23 +1331,17 @@ export class BatchArray {
1379
1331
  readerExcluded += sourceTenantNode.excluded;
1380
1332
  });
1381
1333
 
1382
- // If queue stats are available, they are a stronger indicator of write completion.
1383
- // When all queues are empty, the backend has finished processing this batch and may stop emitting updates.
1384
- if (totalInMainQueue != null) this.lastQueues.main = totalInMainQueue;
1385
- if (totalInWriterQueue != null) this.lastQueues.writer = totalInWriterQueue;
1386
- if (totalInDeferredQueue != null) this.lastQueues.deferred = totalInDeferredQueue;
1387
- this.hasQueueInfo = this.lastQueues.main != null && this.lastQueues.writer != null && this.lastQueues.deferred != null;
1388
-
1389
- const queuesEmpty = this.hasQueueInfo &&
1390
- this.lastQueues.main === 0 &&
1391
- this.lastQueues.writer === 0 &&
1392
- this.lastQueues.deferred === 0;
1334
+ // New completion condition: sync is complete when:
1335
+ // (Reader/TotalCount - Reader/ExtCount) === (Writer/CurrentCount + Writer/DeferredCount)
1336
+ // This means all users that should be written (total minus excluded) have been processed.
1337
+ const usersToSync = readerTotal - readerExcluded;
1338
+ const usersProcessed = writerCurrent + writerDeferred;
1339
+ const isSyncCompleted = usersToSync > 0 && usersToSync === usersProcessed;
1393
1340
 
1394
- if (queuesEmpty) {
1341
+ if (isSyncCompleted) {
1395
1342
  bWritingComplete = true;
1396
1343
 
1397
- // Ensure the UI can reach a terminal state even if writer CurrentCount never reaches TotalCount.
1398
- // We trust the backend queues more than client-side totals in this scenario.
1344
+ // Mark all nodes as complete since sync condition is satisfied.
1399
1345
  this.tenantNodes.forEach((src) => {
1400
1346
  src.targets.forEach((t) => {
1401
1347
  if (t.status !== "failed") t.status = "complete";
@@ -1450,8 +1396,8 @@ export class BatchArray {
1450
1396
  setRefreshDeltaTrigger(config!.workspaceId);
1451
1397
  }
1452
1398
  // with that out of the way, is writing complete?
1453
- // Only allow terminal completion when backend queues are empty (if we have queue info).
1454
- if (bWritingComplete && (queuesEmpty || !this.hasQueueInfo)) {
1399
+ // Completion is determined by the new formula: usersToSync === usersProcessed
1400
+ if (bWritingComplete && isSyncCompleted) {
1455
1401
  this.milestoneArray.write(setMilestones);
1456
1402
  this.stopPolling();
1457
1403
  setConfigSyncResult("sync complete");
@@ -1684,152 +1630,14 @@ export class ActorNode {
1684
1630
  }
1685
1631
  }
1686
1632
  // ======================= Azure AD Graph API ===============================
1687
- // helper functions
1688
- function getGraphAPIScope(user: User): string {
1689
- user = user;
1690
- return "Group.Read.All User.Read.All openid profile offline_access User.Read Contacts.Read CrossTenantInformation.ReadBasic.All";
1633
+ export async function groupsGet(_instance: IPublicClientApplication, _user: User | undefined, groupSearchString: string): Promise<{ groups: Group[], error: string }> {
1634
+ return await secureApiClient.groupsGet(groupSearchString);
1691
1635
  }
1692
- // TODO: this is where you want to trigger a re-authentication if token expires
1693
- async function graphDefineHeaders(
1694
- instance: IPublicClientApplication,
1695
- user: User
1696
- ): Promise<Headers> {
1697
- const headers = new Headers();
1698
- headers.append("Content-Type", "application/json");
1699
- headers.append("accept", "*/*");
1700
- const graphAPIScope: string = getGraphAPIScope(user);
1701
- // only call acquireTokenByCode if we have never redeemed the code
1702
- if (user.graphAccessToken == null || user.graphAccessToken === "") {
1703
- try {
1704
- let response: AuthenticationResult = await instance.acquireTokenByCode({
1705
- code: user.spacode,
1706
- });
1707
- user.graphAccessToken = response.accessToken; // cache access token
1708
- console.log("Front end token acquired by code: " + user.graphAccessToken.slice(0, 20));
1709
- }
1710
- catch (error: any) {
1711
- console.log("Front end token failure: " + error);
1712
- }
1713
- }
1714
- // otherwise, call acquireTokenSilent and deal with token expiration on exception
1715
- else {
1716
- try {
1717
- let accounts: AccountInfo[] = instance.getAllAccounts();
1718
- let homeAccountId = user.oid + "." + user.tid;
1719
- let account: AccountInfo | undefined | null = null;
1720
- for (let i: number = 0; i < accounts.length; i++) {
1721
- if (accounts[i].homeAccountId == homeAccountId) {
1722
- account = accounts[i];
1723
- }
1724
- }
1725
- let response: AuthenticationResult = await instance.acquireTokenSilent({
1726
- scopes: [graphAPIScope],
1727
- account: account!
1728
- });
1729
- user.graphAccessToken = response.accessToken; // cache access token
1730
- console.log("Front end token graph acquired silently: " + user.graphAccessToken.slice(0, 20));
1731
- }
1732
- catch (error: any) {
1733
- try {
1734
- console.log("Front end graph token silent acquisition failure: " + error);
1735
- // fallback to redirect if silent acquisition fails
1736
- let accounts: AccountInfo[] = instance.getAllAccounts();
1737
- let homeAccountId = user.oid + "." + user.tid;
1738
- let account: AccountInfo | null = null;
1739
- for (let i: number = 0; i < accounts.length; i++) {
1740
- if (accounts[i].homeAccountId == homeAccountId) {
1741
- account = accounts[i];
1742
- }
1743
- }
1744
- // assumption: this redirect will trigger login flow callbacks in program.cs
1745
- instance.acquireTokenRedirect({
1746
- scopes: [graphAPIScope],
1747
- account: account!
1748
- });
1749
- }
1750
- catch (error: any) {
1751
- console.log("Front end graph token redirect acquisition failure: " + error);
1752
- }
1753
- }
1754
- }
1755
- headers.append("Authorization", `Bearer ${user.graphAccessToken}`);
1756
- return headers;
1757
- }
1758
- export async function groupsGet(instance: IPublicClientApplication, user: User | undefined, groupSearchString: string): Promise<{ groups: Group[], error: string }> {
1759
- // need a logged in user to get graph users
1760
- if (user == null || user.spacode == "") {
1761
- return { groups: [], error: `500: invalid user passed to groupsGet` };
1762
- }
1763
- // create headers
1764
- const headers = await graphDefineHeaders(instance, user);
1765
- let options = { method: "GET", headers: headers };
1766
- // make /groups endpoint call
1767
- try {
1768
- let groupsEndpoint: string = getGraphEndpoint(user.authority) + graphConfig.graphGroupsPredicate;
1769
- groupsEndpoint += `/?$filter=startsWith(displayName, '${groupSearchString}')`;
1770
- let response = await fetch(groupsEndpoint, options);
1771
- let data = await response.json();
1772
- if (typeof data.error !== "undefined") {
1773
- return { groups: [], error: `${data.error.code}: ${data.error.message}` };
1774
- }
1775
- return { groups: data.value, error: `` };
1776
- }
1777
- catch (error: any) {
1778
- console.log(error);
1779
- return { groups: [], error: `Exception: ${error}` };
1780
- }
1781
- }
1782
- export async function oauth2PermissionGrantsGet(options: RequestInit, user: User, spid: string, oid: string): Promise<{ grants: string | null, id: string | null, error: string }> {
1783
- try {
1784
- // make /oauth2PermissionGrants endpoint call
1785
- let spurl: string = getGraphEndpoint(user.authority) + graphConfig.graphOauth2PermissionGrantsPredicate;
1786
- let url: URL = new URL(spurl);
1787
- url.searchParams.append("$filter", `resourceId eq '${spid}' and consentType eq 'Principal' and principalId eq '${oid}'`);
1788
- let response = await fetch(url.href, options);
1789
- let data = await response.json();
1790
- if (typeof data.error != "undefined") {
1791
- return { grants: null, id: null, error: `${data.error.code}: ${data.error.message}` };
1792
- }
1793
- // we assume there is only one such grant
1794
- if (data.value.length != 1) {
1795
- debugger;
1796
- return { grants: null, id: null, error: `oauth2PermissionGrantsGet: more than one matching delegated consent grant.` };
1797
- }
1798
- return { grants: data.value[0].scope, id: data.value[0].id, error: `` };
1799
- }
1800
- catch (error: any) {
1801
- console.log(error);
1802
- return { grants: null, id: null, error: `Exception: ${error}` };
1803
- }
1636
+ export async function oauth2PermissionGrantsGet(_options: RequestInit, _user: User, spid: string, oid: string): Promise<{ grants: string | null, id: string | null, error: string }> {
1637
+ return await secureApiClient.oauth2PermissionGrantsGet(spid, oid);
1804
1638
  }
1805
- export async function oauth2PermissionGrantsSet(instance: IPublicClientApplication, loggedInUser: User, id: string, scopes: string): Promise<boolean> {
1806
- // need a logged in user to get graph users
1807
- if (loggedInUser == null || loggedInUser.spacode == "") {
1808
- return false;
1809
- }
1810
- // make /oauth2PermissionGrants endpoint call
1811
- try {
1812
- let grantsurl: string = getGraphEndpoint(loggedInUser.authority);
1813
- grantsurl += graphConfig.graphOauth2PermissionGrantsPredicate + `/${id}`;
1814
- let scopesBody: string = `{ "scope": "${scopes}" }`;
1815
- const headers = await graphDefineHeaders(instance, loggedInUser);
1816
- let options: RequestInit = { method: "PATCH", headers: headers, body: scopesBody };
1817
- let response = await fetch(grantsurl, options);
1818
- let data = await response.json();
1819
- if (response.status == 204 && response.statusText == "No Content") {
1820
- return true;
1821
- }
1822
- else {
1823
- debugger;
1824
- console.log(`oauth2PermissionGrantsSet: PATCH failed ${data.error.code}: ${data.error.message}`);
1825
- return false;
1826
- }
1827
- }
1828
- catch (error: any) {
1829
- debugger;
1830
- console.log(error);
1831
- return false;
1832
- }
1639
+ export async function oauth2PermissionGrantsSet(_instance: IPublicClientApplication, _loggedInUser: User, id: string, scopes: string): Promise<boolean> {
1640
+ return await secureApiClient.oauth2PermissionGrantsSet(id, scopes);
1833
1641
  }
1834
1642
  export function requestAdminConsent(admin: User, tct: TenantConfigType): void {
1835
1643
  //
@@ -1860,27 +1668,8 @@ export function requestAdminConsent(admin: User, tct: TenantConfigType): void {
1860
1668
  url.searchParams.append("login_hint", admin.mail);
1861
1669
  window.location.assign(url.href);
1862
1670
  }
1863
- export async function servicePrincipalGet(options: RequestInit, user: User, appid: string): Promise<{ spid: string, error: string }> {
1864
- try {
1865
- // make /servicePrincipals endpoint call to get the Service Principal ID
1866
- let spurl: string = getGraphEndpoint(user.authority);
1867
- spurl += graphConfig.graphServicePrincipalsPredicate;
1868
- spurl += `(appId='${appid}')`;
1869
- let url: URL = new URL(spurl);
1870
- url.searchParams.append("$select", "id,appId,displayName");
1871
- let response = await fetch(url.href, options);
1872
- let data = await response.json();
1873
- if (typeof data.error !== "undefined") {
1874
- return { spid: "", error: `${data.error.code}: ${data.error.message}` };
1875
- }
1876
- else {
1877
- return { spid: data.id, error: `` };
1878
- }
1879
- }
1880
- catch (error: any) {
1881
- console.log(error);
1882
- return { spid: "", error: `Exception: ${error}` };
1883
- }
1671
+ export async function servicePrincipalGet(_options: RequestInit, _user: User, appid: string): Promise<{ spid: string, error: string }> {
1672
+ return await secureApiClient.servicePrincipalGet(appid);
1884
1673
  }
1885
1674
  export async function signIn(user: User, tasks: TaskArray): Promise<boolean> {
1886
1675
  // admin authority is blank at signIn, lookup authority real-time
@@ -2171,59 +1960,47 @@ export async function tenantUnauthenticatedLookup(tenant: Tenant, debug: boolean
2171
1960
  }
2172
1961
  return false; // failed, no need for UX to re-render
2173
1962
  }
2174
- export async function userDelegatedScopesGet(instance: IPublicClientApplication, loggedInUser: User, tenant: Tenant): Promise<{ scopes: string | null, id: string | null, error: string }> {
2175
- // need a logged in user and valid tenant to query graph
2176
- if (loggedInUser == null || loggedInUser.spacode == "" || tenant == null) {
2177
- debugger;
1963
+ export async function userDelegatedScopesGet(_instance: IPublicClientApplication, loggedInUser: User, tenant: Tenant): Promise<{ scopes: string | null, id: string | null, error: string }> {
1964
+ if (loggedInUser == null || tenant == null) {
2178
1965
  return { scopes: null, id: null, error: `500: invalid parameter(s) passed to getUserDelegatedScopes` };
2179
1966
  }
2180
- // create headers
2181
- const headers = await graphDefineHeaders(instance, loggedInUser);
2182
- let options: RequestInit = { method: "GET", headers: headers };
2183
1967
  try {
2184
1968
  // first, cache Graph resource ID (service principal) for this tenant if we don't have it already
2185
1969
  if (tenant.graphSP == "") {
2186
- let { spid, error } = await servicePrincipalGet(options, loggedInUser, "00000003-0000-0000-c000-000000000000");
1970
+ let { spid, error } = await secureApiClient.servicePrincipalGet("00000003-0000-0000-c000-000000000000");
2187
1971
  if (error != "") {
2188
- debugger;
2189
1972
  return { scopes: null, id: null, error: `${error}` };
2190
1973
  }
2191
1974
  tenant.graphSP = spid;
2192
1975
  }
2193
1976
  // then, retrieve the delegated Graph permissions assigned to this user
2194
- let { grants, id, error } = await oauth2PermissionGrantsGet(options, loggedInUser, tenant.graphSP, loggedInUser.oid);
1977
+ let { grants, id, error } = await secureApiClient.oauth2PermissionGrantsGet(tenant.graphSP, loggedInUser.oid);
2195
1978
  if (error != "") {
2196
- debugger;
2197
1979
  return { scopes: null, id: null, error: `${error}` };
2198
1980
  }
2199
1981
  return { scopes: grants, id: id, error: `` };
2200
1982
  }
2201
1983
  catch (error: any) {
2202
- debugger;
2203
1984
  console.log(error);
2204
1985
  return { scopes: null, id: null, error: `Exception: ${error}` };
2205
1986
  }
2206
1987
  }
2207
1988
  export async function userDelegatedScopesRemove(instance: IPublicClientApplication, loggedInUser: User, tenant: Tenant, scope: string): Promise<boolean> {
2208
- // need a logged in user and valid tenant to query graph
2209
- if (loggedInUser == null || loggedInUser.spacode == "" || tenant == null) {
2210
- debugger;
1989
+ if (loggedInUser == null || tenant == null) {
2211
1990
  return false;
2212
1991
  }
2213
1992
  // get current set of delegated scopes in order to remove passed scope
2214
1993
  let { scopes, id, error } = await userDelegatedScopesGet(instance, loggedInUser, tenant);
2215
1994
  if (error != "") {
2216
- debugger;
2217
1995
  console.log(`userDelegatedScopesRemove: cannot find userDelegatedScopes for ${loggedInUser.mail}: ${error}`);
2218
1996
  return false;
2219
1997
  }
2220
1998
  // remove passed scope (case sensitive)
2221
1999
  scopes = scopes!.replace(scope, "");
2222
- // set updated oauth2permissions
2223
- let removed: boolean = await oauth2PermissionGrantsSet(instance, loggedInUser, id!, scopes);
2000
+ // set updated oauth2permissions using secure backend API
2001
+ let removed: boolean = await secureApiClient.oauth2PermissionGrantsSet(id!, scopes);
2224
2002
  if (!removed) {
2225
- debugger;
2226
- console.log(`userDelegatedScopesRemove: cannot set oauth2PermissionGrants for ${loggedInUser.mail}: ${error}`);
2003
+ console.log(`userDelegatedScopesRemove: cannot set oauth2PermissionGrants for ${loggedInUser.mail}`);
2227
2004
  return false;
2228
2005
  }
2229
2006
  // replace scope array on logged in user
@@ -2231,33 +2008,8 @@ export async function userDelegatedScopesRemove(instance: IPublicClientApplicati
2231
2008
  return removed;
2232
2009
  }
2233
2010
  //usersGet - GET from AAD Users endpoint
2234
- export async function usersGet(instance: IPublicClientApplication, user: User | undefined): Promise<{ users: string[], error: string }> {
2235
- // need a logged in user to get graph users
2236
- if (user == null || user.spacode == "") {
2237
- return { users: [], error: `500: invalid user passed to usersGet` };
2238
- }
2239
- // make /users endpoint call
2240
- try {
2241
- // create headers
2242
- const headers = await graphDefineHeaders(instance, user);
2243
- let options = { method: "GET", headers: headers };
2244
- let usersEndpoint = getGraphEndpoint(user.authority);
2245
- usersEndpoint += graphConfig.graphUsersPredicate;
2246
- let response = await fetch(usersEndpoint, options);
2247
- let data = await response.json();
2248
- if (typeof data.error !== "undefined") {
2249
- return { users: [], error: `${data.error.code}: ${data.error.message}` };
2250
- }
2251
- let users = new Array<string>();
2252
- for (let user of data.value) {
2253
- users.push(user.mail);
2254
- }
2255
- return { users: users, error: `` };
2256
- }
2257
- catch (error: any) {
2258
- console.log(error);
2259
- return { users: [], error: `Exception: ${error}` };
2260
- }
2011
+ export async function usersGet(_instance: IPublicClientApplication, _user: User | undefined, searchString?: string): Promise<{ users: string[], error: string }> {
2012
+ return await secureApiClient.usersGet(searchString);
2261
2013
  }
2262
2014
  // ======================= Mindline SyncConfig API ===============================
2263
2015
  export async function auditConfigAdd(instance: IPublicClientApplication, user: User, ac: AuditConfig, debug: boolean): Promise<APIResult> {
@@ -2725,147 +2477,28 @@ export async function getPowerBIAccessToken(
2725
2477
  return accesstoken;
2726
2478
  }
2727
2479
  // ======================= Azure REST API ===============================
2728
- // TODO: this is where you want to trigger a re-authentication if token expires
2729
- async function azureDefineHeaders(
2730
- instance: IPublicClientApplication,
2731
- user: User
2732
- ): Promise<Headers> {
2733
- const headers = new Headers();
2734
- headers.append("Content-Type", "application/json");
2735
- headers.append("accept", "*/*");
2736
- // authorization header - if needed, retrieve and cache access token
2737
- if (user.azureAccessToken == null || user.azureAccessToken === "") {
2738
- try {
2739
- let accounts: AccountInfo[] = instance.getAllAccounts();
2740
- let homeAccountId = user.oid + "." + user.tid;
2741
- let account: AccountInfo | null = null;
2742
- for (let i: number = 0; i < accounts.length; i++) {
2743
- if (accounts[i].homeAccountId == homeAccountId) {
2744
- account = accounts[i];
2745
- }
2746
- }
2747
- let response: AuthenticationResult = await instance.acquireTokenSilent({
2748
- scopes: ["https://management.azure.com/user_impersonation"],
2749
- account: account!
2750
- });
2751
- user.azureAccessToken = response.accessToken; // cache access token
2752
- console.log("Front end token acquired silently: " + user.azureAccessToken.slice(0, 20));
2753
- }
2754
- catch (error: any) {
2755
- try {
2756
- console.log("Front end token silent acquisition failure: " + error);
2757
- // fallback to redirect if silent acquisition fails
2758
- let accounts: AccountInfo[] = instance.getAllAccounts();
2759
- let homeAccountId = user.oid + "." + user.tid;
2760
- let account: AccountInfo | null = null;
2761
- for (let i: number = 0; i < accounts.length; i++) {
2762
- if (accounts[i].homeAccountId == homeAccountId) {
2763
- account = accounts[i];
2764
- }
2765
- }
2766
- instance.acquireTokenRedirect({
2767
- scopes: ["https://management.azure.com/user_impersonation"],
2768
- account: account!
2769
- });
2770
- }
2771
- catch (error: any) {
2772
- console.log("Front end token popup acquisition failure: " + error);
2773
- }
2774
- }
2775
- }
2776
- headers.append("Authorization", `Bearer ${user.azureAccessToken}`);
2777
- return headers;
2480
+ export async function canListRootAssignments(_instance: IPublicClientApplication, _user: User): Promise<boolean> {
2481
+ return await secureApiClient.canListRootAssignments();
2778
2482
  }
2779
- export async function canListRootAssignments(instance: IPublicClientApplication, user: User): Promise<boolean> {
2780
- // need a logged in user to call Azure REST API
2781
- if (user == null || user.spacode == "") {
2782
- return false;
2783
- }
2784
- // make Azure REST API call
2785
- try {
2786
- // create headers
2787
- const headers = await azureDefineHeaders(instance, user);
2788
- let options = { method: "GET", headers: headers };
2789
- let listrootassignmentsEndpoint: string = azureConfig.azureListRootAssignments;
2790
- listrootassignmentsEndpoint += "'";
2791
- listrootassignmentsEndpoint += user.oid;
2792
- listrootassignmentsEndpoint += "'";
2793
- let response = await fetch(listrootassignmentsEndpoint, options);
2794
- if (response.status == 200) {
2795
- let data: any = await response.json();
2796
- data = data;
2797
- debugger;
2798
- console.log("Successful call to Azure Resource Graph list root assignments");
2799
- }
2800
- else {
2801
- console.log(await processErrors(response));
2802
- return false;
2803
- }
2804
- }
2805
- catch (error: any) {
2806
- console.log(error);
2807
- return false;
2808
- }
2809
- return true;
2810
- }
2811
- export async function elevateGlobalAdminToUserAccessAdmin(instance: IPublicClientApplication, user: User): Promise<boolean> {
2812
- // need a logged in user to call Azure REST API
2813
- if (user == null || user.spacode == "") {
2814
- return false;
2815
- }
2816
- // make Azure REST API call
2817
- try {
2818
- // create headers
2819
- const headers = await azureDefineHeaders(instance, user);
2820
- let options = { method: "POST", headers: headers };
2821
- let elevateaccessEndpoint: string = azureConfig.azureElevateAccess;
2822
- let response = await fetch(elevateaccessEndpoint, options);
2823
- if (response.status == 200) {
2824
- console.log("Successful call to Azure Resource Graph list root assignments");
2825
- }
2826
- else {
2827
- console.log(await processErrors(response));
2828
- return false;
2829
- }
2830
- }
2831
- catch (error: any) {
2832
- console.log(error);
2833
- return false;
2834
- }
2835
- return true;
2483
+ export async function elevateGlobalAdminToUserAccessAdmin(_instance: IPublicClientApplication, _user: User): Promise<boolean> {
2484
+ return await secureApiClient.elevateGlobalAdminToUserAccessAdmin();
2836
2485
  }
2837
- async function readResources(instance: IPublicClientApplication, user: User): Promise<ResourceNode[]> {
2838
- // need a logged in user to call Azure REST API
2839
- let resources: ResourceNode[] = new Array<ResourceNode>();
2840
- if (user == null || user.spacode == "") {
2841
- return resources;
2842
- }
2843
- // make Azure REST API call
2486
+ async function readResources(_instance: IPublicClientApplication, _user: User): Promise<ResourceNode[]> {
2844
2487
  try {
2845
- // create headers
2846
- const headers = await azureDefineHeaders(instance, user);
2847
- let options = { method: "GET", headers: headers };
2848
- let listrootassignmentsEndpoint: string = azureConfig.azureListRootAssignments;
2849
- listrootassignmentsEndpoint += "'";
2850
- listrootassignmentsEndpoint += user.oid;
2851
- listrootassignmentsEndpoint += "'";
2852
- let response = await fetch(listrootassignmentsEndpoint, options);
2853
- if (response.status == 200) {
2854
- let data = await response.json();
2855
- data = data;
2856
- debugger;
2857
- console.log("Successful call to Azure Resource Graph list root assignments");
2858
- }
2859
- else {
2860
- console.log(await processErrors(response));
2861
- return resources;
2862
- }
2863
- }
2864
- catch (error: any) {
2865
- console.log(error);
2866
- return resources;
2488
+ const backendResources = await secureApiClient.readResources();
2489
+
2490
+ // Convert backend Resource[] to ResourceNode[]
2491
+ // Note: The original implementation returned empty array, so we map basic fields
2492
+ const resourceNodes: ResourceNode[] = backendResources.map(r => {
2493
+ const node = new ResourceNode(r.type, r.name, 0); // cost not available from backend
2494
+ return node;
2495
+ });
2496
+
2497
+ return resourceNodes;
2498
+ } catch (error: any) {
2499
+ console.error('Error in readResources:', error);
2500
+ return [];
2867
2501
  }
2868
- return resources;
2869
2502
  }
2870
2503
  // ======================= HYBRIDSPA.TS -- Mindline SyncConfig API helper functions ===============================
2871
2504
  function getAPIScope(user: User): string {