@mindline/sync 1.0.29 → 1.0.31

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/index.ts CHANGED
@@ -1,15 +1,14 @@
1
- //index.js
2
-
1
+ //index.ts - published interface - AAD implementations, facade to Mindline Config API
2
+ import { IPublicClientApplication, AuthenticationResult } from "@azure/msal-browser"
3
3
  import { deserializeArray } from 'class-transformer';
4
- import { IPublicClientApplication } from '@azure/msal-browser';
5
- import { getTenantInfo } from './hybridspa';
4
+ import { adminDelete, adminPost, adminsGet, configDelete, configsGet, configPost, configPut, graphConfig, initPost, readerPost, tenantPut, tenantPost, tenantDelete, tenantsGet, workspacesGet} from './hybridspa';
5
+ import { version } from './package.json';
6
6
  import users from "./users.json";
7
- import targets from "./targets.json";
7
+ import tenants from "./tenants.json";
8
8
  import configs from "./configs.json";
9
9
  import workspaces from "./workspaces.json";
10
-
10
+ import tasksData from "./tasks";
11
11
  const FILTER_FIELD = "workspaceIDs";
12
-
13
12
  // called by unit tests
14
13
  export function sum(a: number, b: number): number {
15
14
  return a + b;
@@ -17,8 +16,12 @@ export function sum(a: number, b: number): number {
17
16
  export function helloNpm() : string {
18
17
  return "hello NPM";
19
18
  }
20
-
21
- class User {
19
+ export class Group {
20
+ id: string;
21
+ displayName: string;
22
+ description: string;
23
+ }
24
+ export class User {
22
25
  oid: string;
23
26
  name: string;
24
27
  mail: string;
@@ -28,30 +31,50 @@ class User {
28
31
  companyDomain: string;
29
32
  associatedWorkspaces: string[];
30
33
  workspaceIDs: string;
31
- session: string;
32
- spacode: string;
33
- accessToken: string;
34
+ session: string; // button text
35
+ spacode: string; // to get front end access token
36
+ accessToken: string; // front end access token
37
+ loginHint: string; // to help sign out without prompt
38
+ scopes: string[]; // to detect if incremental consent has happened
39
+ authTS: Date; // timestamp user was authenticated
34
40
  constructor() {
35
41
  this.oid = "";
36
42
  this.name = "";
37
43
  this.mail = "";
38
- this.authority = "";
44
+ this.authority = "https://login.microsoftonline.com/organizations/v2.0";
39
45
  this.tid = "";
40
46
  this.companyName = "";
41
47
  this.companyDomain = "";
42
48
  this.associatedWorkspaces = new Array();
43
49
  this.workspaceIDs = "";
44
- this.session = "";
50
+ this.session = "Sign In";
45
51
  this.spacode = "";
46
52
  this.accessToken = "";
53
+ this.loginHint = "";
54
+ this.scopes = new Array();
55
+ this.authTS = new Date(0);
47
56
  }
48
57
  }
49
-
50
- class Target {
58
+ export enum TenantType {
59
+ invalid = 0,
60
+ aad = 1,
61
+ ad = 2,
62
+ googleworkspace = 3
63
+ }
64
+ type TenantTypeStrings = keyof typeof TenantType;
65
+ export enum TenantPermissionType {
66
+ read = 1,
67
+ write = 2,
68
+ notassigned = 3
69
+ }
70
+ export type TenantPermissionTypeStrings = keyof typeof TenantPermissionType;
71
+ export class Tenant {
51
72
  tid: string;
52
73
  name: string;
53
74
  domain: string;
54
- type: string;
75
+ tenantType: TenantTypeStrings;
76
+ permissionType: TenantPermissionTypeStrings;
77
+ onboarded: string;
55
78
  authority: string;
56
79
  readServicePrincipal: string;
57
80
  writeServicePrincipal: string;
@@ -60,65 +83,142 @@ class Target {
60
83
  this.tid = "";
61
84
  this.name = "";
62
85
  this.domain = "";
63
- this.type = "";
64
- this.authority = "";
86
+ this.tenantType = "aad";
87
+ this.permissionType = "notassigned";
88
+ this.onboarded = "false";
89
+ this.authority = "https://login.microsoftonline.com/organizations/v2.0";
65
90
  this.readServicePrincipal = "";
66
91
  this.writeServicePrincipal = "";
67
92
  this.workspaceIDs = "";
68
93
  }
69
94
  }
70
-
71
- class TargetConfigInfo {
95
+ export enum TenantConfigType {
96
+ source = 1,
97
+ target = 2,
98
+ sourcetarget = 3
99
+ }
100
+ export type TenantConfigTypeStrings = keyof typeof TenantConfigType;
101
+ export class TenantConfigInfo {
72
102
  tid: string;
73
- sourceGroups: string[];
74
- targetGroup: string;
103
+ sourceGroupId: string;
104
+ sourceGroupName: string;
105
+ configurationTenantType: TenantConfigTypeStrings;
106
+ constructor(){
107
+ this.tid = "";
108
+ this.sourceGroupId = "";
109
+ this.sourceGroupName = "";
110
+ this.configurationTenantType = "source";
111
+ }
75
112
  }
76
-
77
- class Config {
113
+ export class Config {
78
114
  id: string;
115
+ workspaceId: string;
79
116
  name: string;
80
117
  description: string;
81
- targetConfigs: TargetConfigInfo[];
82
- enabled: boolean;
118
+ tenants: TenantConfigInfo[];
119
+ isEnabled: boolean;
83
120
  workspaceIDs: string;
84
121
  constructor(){
85
122
  this.id = "";
86
123
  this.name = "";
87
124
  this.description = "";
88
- this.targetConfigs = new Array();
89
- this.enabled = false;
125
+ this.tenants = new Array();
126
+ this.isEnabled = false;
90
127
  this.workspaceIDs = "";
91
128
  }
92
129
  }
93
-
94
- class Workspace {
130
+ export class Workspace {
95
131
  id: string;
96
132
  name: string;
97
133
  associatedUsers: string[];
98
- associatedTargets: string[];
134
+ associatedTenants: string[];
99
135
  associatedConfigs: string[];
100
136
  constructor(){
101
137
  this.id = "";
102
138
  this.name = "";
103
139
  this.associatedUsers = new Array();
104
- this.associatedTargets = new Array();
140
+ this.associatedTenants = new Array();
105
141
  this.associatedConfigs = new Array();
106
142
  }
107
143
  }
108
-
144
+ // check for localStorage availability
145
+ function storageAvailable(type) {
146
+ let storage;
147
+ try {
148
+ storage = window[type];
149
+ const x = "__storage_test__";
150
+ storage.setItem(x, x);
151
+ storage.removeItem(x);
152
+ return true;
153
+ } catch (e) {
154
+ return (
155
+ e instanceof DOMException &&
156
+ // everything except Firefox
157
+ (e.code === 22 ||
158
+ // Firefox
159
+ e.code === 1014 ||
160
+ // test name field too, because code might not be present
161
+ // everything except Firefox
162
+ e.name === "QuotaExceededError" ||
163
+ // Firefox
164
+ e.name === "NS_ERROR_DOM_QUOTA_REACHED") &&
165
+ // acknowledge QuotaExceededError only if there's something already stored
166
+ storage &&
167
+ storage.length !== 0
168
+ );
169
+ }
170
+ }
109
171
  export class InitInfo {
110
172
  us: User[];
111
- ts: Target[];
173
+ ts: Tenant[];
112
174
  cs: Config[];
113
175
  ws: Workspace[];
114
- constructor(){
115
- this.us = new Array();
116
- this.ts = new Array();
117
- this.cs = new Array();
118
- this.ws = new Array();
176
+ constructor(bClearLocalStorage: boolean) {
177
+ this.init(bClearLocalStorage);
178
+ }
179
+ // get initial data from localStorage or file
180
+ init(bClearLocalStorage: boolean): void {
181
+ console.log(`Calling InitInfo::init(bClearLocalStorage: ${bClearLocalStorage?"true":"false"})`);
182
+ // if we have a non-zero value stored, read it from localStorage
183
+ if (storageAvailable("localStorage")) {
184
+ let result = localStorage.getItem("InitInfo");
185
+ if (result != null && typeof result === "string" && result !== "") {
186
+ let initInfoString: string = result;
187
+ let iiReadFromLocalStorage: InitInfo = JSON.parse(initInfoString);
188
+ if (iiReadFromLocalStorage.us.length !== 0) {
189
+ if(bClearLocalStorage) { localStorage.removeItem("InitInfo"); }
190
+ else{
191
+ this.#initFromObjects(iiReadFromLocalStorage);
192
+ return;
193
+ }
194
+ }
195
+ }
196
+ }
197
+ // if storage unavailable or we were just asked to clear, read from default files to enable usable UI
198
+ var usersString = JSON.stringify(users);
199
+ var tenantsString = JSON.stringify(tenants);
200
+ var configsString = JSON.stringify(configs);
201
+ var workspacesString = JSON.stringify(workspaces);
202
+ try {
203
+ this.us = deserializeArray(User, usersString);
204
+ this.ts = deserializeArray(Tenant, tenantsString);
205
+ this.cs = deserializeArray(Config, configsString);
206
+ this.ws = deserializeArray(Workspace, workspacesString);
207
+ this.tagWithWorkspaces();
208
+ } catch (e) {
209
+ debugger;
210
+ }
211
+ }
212
+ save(): void{
213
+ let initInfoString: string = JSON.stringify(this);
214
+ localStorage.setItem("InitInfo", initInfoString);
119
215
  }
120
216
  tagWithWorkspaces(): boolean {
121
- // for each Workspace tag associated User, Target, Config with Workspace.id
217
+ // first clear everyone's workspaceIDs
218
+ this.us.map((item) => item.workspaceIDs = "");
219
+ this.ts.map((item) => item.workspaceIDs = "");
220
+ this.cs.map((item) => item.workspaceIDs = "");
221
+ // for each workspace tag WorkspaceIDs of associated Users, Tenants, Configs
122
222
  for (let workspace of this.ws) {
123
223
  // find matching Users to tag with this workspace
124
224
  for (let userID of workspace.associatedUsers) {
@@ -133,15 +233,15 @@ export class InitInfo {
133
233
  return false;
134
234
  }
135
235
  }
136
- // find matching Targets to tag with this workspace
137
- for (let targetID of workspace.associatedTargets) {
138
- let target = this.ts.find(
139
- (currentTarget) => currentTarget.tid === targetID
236
+ // find matching Tenants to tag with this workspace
237
+ for (let tenantID of workspace.associatedTenants) {
238
+ let tenant = this.ts.find(
239
+ (currentTenant) => currentTenant.tid === tenantID
140
240
  );
141
- if (target !== undefined) {
142
- // we found the target
143
- target[FILTER_FIELD] += workspace.id;
144
- target[FILTER_FIELD] += " ";
241
+ if (tenant !== undefined) {
242
+ // we found the tenant
243
+ tenant[FILTER_FIELD] += workspace.id;
244
+ tenant[FILTER_FIELD] += " ";
145
245
  } else {
146
246
  // we should not have InitInfo missing Workspace components
147
247
  debugger;
@@ -166,99 +266,811 @@ export class InitInfo {
166
266
  }
167
267
  return true;
168
268
  }
269
+ #initFromObjects(ii: InitInfo) : void {
270
+ // user array is the only one that has a Date that must be re-created on every read
271
+ if(typeof ii.us === "undefined") this.us = new Array();
272
+ else this.us = ii.us.map((user: User) => { user.authTS = new Date(user.authTS); return user } );
273
+ if(typeof ii.ts === "undefined") this.ts = new Array();
274
+ else this.ts = ii.ts;
275
+ if(typeof ii.cs === "undefined") this.cs = new Array();
276
+ else this.cs = ii.cs;
277
+ if(typeof ii.ws === "undefined") this.ws = new Array();
278
+ else this.ws = ii.ws;
279
+ }
169
280
  }
170
-
171
- // get hardcoded data from JSON
172
- function DummyInit(ii: InitInfo)
173
- {
174
- var usersString = JSON.stringify(users);
175
- var targetsString = JSON.stringify(targets);
176
- var configsString = JSON.stringify(configs);
177
- var workspacesString = JSON.stringify(workspaces);
178
- try {
179
- ii.us = deserializeArray(User, usersString);
180
- ii.ts = deserializeArray(Target, targetsString);
181
- ii.cs = deserializeArray(Config, configsString);
182
- ii.ws = deserializeArray(Workspace, workspacesString);
183
- if(!ii.tagWithWorkspaces()) return false;
184
- } catch (e) {
281
+ export type TaskType = "initialization" |
282
+ "authenticate user" |
283
+ "reload React" |
284
+ "GET tenant details" |
285
+ "POST config init" |
286
+ "GET workspaces" |
287
+ "onboard tenant" |
288
+ "create 2nd tenant" |
289
+ "invite 2nd admin" |
290
+ "onboard 2nd tenant" |
291
+ "create config";
292
+ export class TaskArray {
293
+ tasks: Task[];
294
+ constructor(bClearLocalStorage: boolean) {
295
+ this.tasks = [ new Task() ];
296
+ this.init(bClearLocalStorage);
297
+ }
298
+ // get initial data from localStorage or file
299
+ init(bClearLocalStorage: boolean): void {
300
+ console.log(`Calling TaskArray::init(bClearLocalStorage: ${bClearLocalStorage ? "true" : "false"})`);
301
+ // first clear task array
302
+ this.tasks.length = 0;
303
+ // then clear localStorage if we have been asked to
304
+ if (bClearLocalStorage) {
305
+ if (storageAvailable("localStorage")) localStorage.removeItem("Tasks");
306
+ }
307
+ // then try localStorage
308
+ if (storageAvailable("localStorage")) {
309
+ let result = localStorage.getItem("Tasks");
310
+ if (result != null && typeof result === "string" && result !== "") {
311
+ // properly create Tasks and Dates from retrieved string
312
+ let tasksString: string = result;
313
+ let taskArray: TaskArray = JSON.parse(tasksString);
314
+ this.tasks = this.#initTasksFromObjects(taskArray.tasks);
315
+ let l = this.tasks.length;
316
+ if (l !== 0) return;
317
+ }
318
+ }
319
+ // if here, there was nothing in localStorage, use initialization file
320
+ this.tasks = this.#initTasksFromObjects(tasksData);
321
+ }
322
+ // set start time for a task
323
+ setTaskStart(taskType: TaskType, startDate: Date): void {
324
+ let task: Task | undefined = this.#findTask(taskType);
325
+ if (task != undefined && task != null) {
326
+ task.setStart(startDate);
327
+ task.status = "in progress";
328
+ this.#save();
329
+ }
330
+ else {
331
+ debugger;
332
+ }
333
+ }
334
+ // set end time for a task
335
+ setTaskEnd(taskType: TaskType, endDate: Date, status: string): void {
336
+ let task: Task | undefined = this.#findTask(taskType);
337
+ if (task != undefined && task != null) {
338
+ task.setEnd(endDate);
339
+ task.status = status;
340
+ this.#save();
341
+ }
342
+ else {
343
+ debugger;
344
+ }
345
+ }
346
+ //
347
+ // private
348
+ //
349
+ #findTask(taskType: TaskType): Task | undefined {
350
+ let task: Task | undefined = this.tasks.find(t => t.task == taskType);
351
+ if (task == undefined || task == null) {
352
+ for(task of this.tasks){
353
+ if(task.subtasks != undefined && task.subtasks != null){
354
+ task = task.subtasks.find(t => t.task == taskType);
355
+ if(task != undefined && task != null) break;
356
+ }
357
+ }
358
+ }
359
+ return task;
360
+ }
361
+ #initTasksFromObjects(tasks: Task[]): Task[]{
362
+ return tasks.map((t: Task) => {
363
+ let newTask: Task = new Task();
364
+ newTask.id = t.id;
365
+ newTask.task = t.task;
366
+ newTask.setStart(new Date(t.start));
367
+ newTask.setEnd(new Date(t.end));
368
+ newTask.expected = t.expected;
369
+ newTask.status = t.status;
370
+ newTask.expanded = t.expanded;
371
+ if(typeof t.subtasks !== "undefined" && t.subtasks != null){
372
+ newTask.subtasks = t.subtasks.map((st: Task) => {
373
+ let newSubtask: Task = new Task();
374
+ newSubtask.id = st.id;
375
+ newSubtask.task = st.task;
376
+ newSubtask.setStart(new Date(st.start))
377
+ newSubtask.setEnd(new Date(st.end));
378
+ newSubtask.expected = st.expected;
379
+ newSubtask.status = st.status;
380
+ newSubtask.expanded = st.expanded;
381
+ return newSubtask;
382
+ } )
383
+ }
384
+ return newTask;
385
+ } );
386
+ }
387
+ #save(): void{
388
+ let taskArrayString: string = JSON.stringify(this);
389
+ if (storageAvailable("localStorage")) {
390
+ localStorage.setItem("Tasks", taskArrayString);
391
+ }
392
+ }
393
+ }
394
+ export class Task {
395
+ id: number;
396
+ task: string;
397
+ start: Date;
398
+ startDisplay: string;
399
+ end: Date;
400
+ endDisplay: string;
401
+ elapsedDisplay: string;
402
+ expected: number;
403
+ status: string;
404
+ expanded: boolean;
405
+ subtasks: Task[];
406
+ setEnd(endDate: Date): void{
407
+ this.end = endDate;
408
+ this.endDisplay = `${this.end.getMinutes().toString().padStart(2, "0")}:${this.end.getSeconds().toString().padStart(2, "0")}`;
409
+ let minuteAdjustment: number = 0;
410
+ let elapsedSeconds: number = this.end.getSeconds() - this.start.getSeconds();
411
+ if (elapsedSeconds < 0) { elapsedSeconds += 60; minuteAdjustment = -1; }
412
+ let elapsedMinutes: number = this.end.getMinutes() - this.start.getMinutes() + minuteAdjustment;
413
+ if (elapsedMinutes < 0) elapsedMinutes += 60;
414
+ this.elapsedDisplay = `${elapsedMinutes.toString().padStart(2, "0")}:${elapsedSeconds.toString().padStart(2, "0")}`;
415
+ };
416
+ setStart(startDate: Date): void{
417
+ this.start = startDate;
418
+ this.startDisplay = `${this.start.getMinutes().toString().padStart(2, "0")}:${this.start.getSeconds().toString().padStart(2, "0")}`;
419
+ };
420
+ }
421
+ // class corresponding to an execution of a Config - a *TenantNode* for each source tenant, each with a *TenantNode* array of target tenants
422
+ export class BatchArray {
423
+ tenantNodes: TenantNode[];
424
+ constructor(
425
+ config: Config | null,
426
+ syncPortalGlobalState: InitInfo | null,
427
+ bClearLocalStorage: boolean
428
+ ) {
429
+ this.tenantNodes = new Array<TenantNode>();
430
+ this.init(config, syncPortalGlobalState, bClearLocalStorage);
431
+ }
432
+ // populate tenantNodes based on config tenants
433
+ init(
434
+ config: Config | null,
435
+ syncPortalGlobalState: InitInfo | null,
436
+ bClearLocalStorage: boolean
437
+ ) : void {
438
+ console.log(
439
+ `Calling BatchArray::init(config: "${
440
+ config ? config.name : "null"
441
+ }", bClearLocalStorage: ${bClearLocalStorage ? "true" : "false"})`
442
+ );
443
+ // first clear batch array
444
+ this.tenantNodes.length = 0;
445
+ // then clear localStorage if we have been asked to
446
+ if (bClearLocalStorage) {
447
+ if (storageAvailable("localStorage"))
448
+ localStorage.removeItem(config.name);
449
+ }
450
+ // create BatchArray if passed Config and InitInfo
451
+ if (
452
+ config != null &&
453
+ config.tenants != null &&
454
+ syncPortalGlobalState != null
455
+ ) {
456
+ // create a sourceTenantNode for each Source and SourceTarget
457
+ config.tenants.map((tciPotentialSource: TenantConfigInfo) => {
458
+ if (
459
+ tciPotentialSource.configurationTenantType === "source" ||
460
+ tciPotentialSource.configurationTenantType === "sourcetarget"
461
+ ) {
462
+ let sourceTenant = syncPortalGlobalState.ts.find(
463
+ (t) => t.tid === tciPotentialSource.tid
464
+ );
465
+ if (sourceTenant != null) {
466
+ let sourceTenantNode: TenantNode = new TenantNode(
467
+ tciPotentialSource.tid,
468
+ sourceTenant.name
469
+ );
470
+ this.tenantNodes.push(sourceTenantNode);
471
+ } else {
472
+ console.log(
473
+ `Error: no tenant found for config source tenant ${config.name}`
474
+ );
475
+ debugger;
476
+ return;
477
+ }
478
+ }
479
+ });
480
+ // create targetTenantNodes for each non-matching Target and SourceTarget
481
+ this.tenantNodes.map((sourceTenantNode: TenantNode) => {
482
+ config.tenants.map((tciPotentialTarget: TenantConfigInfo) => {
483
+ // is this a valid target?
484
+ if (
485
+ tciPotentialTarget.configurationTenantType === "target" ||
486
+ tciPotentialTarget.configurationTenantType === "sourcetarget"
487
+ ) {
488
+ // is this a valid target that does not match this source?
489
+ if (tciPotentialTarget.tid !== sourceTenantNode.tid) {
490
+ let targetTenant = syncPortalGlobalState.ts.find(
491
+ (t) => t.tid === tciPotentialTarget.tid
492
+ );
493
+ if (targetTenant != null) {
494
+ let targetTenantNode: TenantNode = new TenantNode(
495
+ tciPotentialTarget.tid,
496
+ targetTenant.name
497
+ );
498
+ sourceTenantNode.targets.push(targetTenantNode);
499
+ sourceTenantNode.expanded = true;
500
+ } else {
501
+ console.log(
502
+ `Error: no tenant found for config target tenant ${config.name}`
503
+ );
504
+ debugger;
505
+ return;
506
+ }
507
+ }
508
+ }
509
+ });
510
+ });
511
+ // then try localStorage to find any matching source tenant metrics
512
+ if (storageAvailable("localStorage")) {
513
+ let result = localStorage.getItem(config.name);
514
+ if (result != null && typeof result === "string" && result !== "") {
515
+ // TODO: retrieve any relevant stored statistics from localStorage
516
+ // let batchArrayString: string = result;
517
+ // let batchArray: BatchArray = JSON.parse(batchArrayString);
518
+ // batchArray.batches.map((batch: Batch) => {
519
+ // config.tenants.map((tciTarget: TenantConfigInfo) => {
520
+ // if(tciTarget.tid !== batch.tid) {
521
+ // let target: Target = new Target(tciTarget.tid);
522
+ // batch.targets.push(target);
523
+ // }
524
+ // });
525
+ // });
526
+ }
527
+ }
528
+ }
529
+ }
530
+ // cycle through test state machine
531
+ test(instance: IPublicClientApplication, authorizedUser: User, config: Config) : void {
532
+ if (this.tenantNodes == null || this.tenantNodes.length == 0) {
533
+ // we should not have an empty batch array for a test
534
+ debugger;
535
+ }
536
+ // execute post to reader endpoint
185
537
  debugger;
186
- return false;
187
- }
188
- return true;
189
- }
190
- // retrieve Workspace(s), User(s), Target(s), Config(s) given logged in user
191
- // three InitGet scenarios
192
- // scenario 1: empty user array, read defaults
193
- // scenario 2: user array with dummy user at end, return without doing anything
194
- // scenario 3: user array with non-dummy user at end, call back end Init
195
- // (1) TODO: Azure AD: lookup companyDomain and companyName
196
- // look up tenant name
197
- // look up tenant domain
198
- // TODO: Mindline: post complete user to init endpoint
199
- // create and push partial tenant at end of target array
200
- // post user and partial tenant to InitInfo
201
- // Return: success return value, need to query for associations
202
- // (2) Sync NPM: ii.ws: once InitInfo completes, retrieve workspaces for the new user
203
- // TODO: Mindline: retrieve associated workspaces
204
- // Returns: associated workspaces
205
- // (3) Sync NPM:
206
- // TODO: Mindline: retrieve associated admins, targets for each workspace
207
- // ii.us / ii.ts / ii.cs: query components of each associated workspaces
208
- // Returns: users, targets, configs for each workspace
209
- export function InitGet(ii: InitInfo, instance: IPublicClientApplication, debug: boolean): boolean
210
- {
211
- // if empty user array, retrieve dummy data from JSON to populate UI
212
- let l = ii.us.length;
213
- if(l === 0) return DummyInit(ii);
214
-
215
- // if last user is a dummy user, then we have nothing
216
- let user:User = ii.us[l-1];
217
- if(user.oid === "1") return true;
538
+ readerPost(instance, authorizedUser, config);
218
539
 
219
- // we have a real user: remove existing dummy user, target, config, workspace
220
- let dummyIndex = ii.us.findIndex((u) => u.oid === "1");
221
- if(dummyIndex!==-1) ii.us.splice(dummyIndex, 1);
222
- dummyIndex = ii.ts.findIndex((u) => u.tid === "1");
223
- if(dummyIndex!==-1) ii.ts.splice(dummyIndex, 1);
224
- dummyIndex = ii.cs.findIndex((u) => u.id === "1");
225
- if(dummyIndex!==-1) ii.cs.splice(dummyIndex, 1);
226
- dummyIndex = ii.ws.findIndex((u) => u.id === "1");
227
- if(dummyIndex!==-1) ii.ws.splice(dummyIndex, 1);
540
+ // start SignalR connection
228
541
 
229
- // why would instance be null here? investigate!
230
- if(instance===null) {
231
- debugger;
232
- return false;
542
+ // cycle through desired states for sources and targets
543
+ if (this.tenantNodes != null) {
544
+ this.tenantNodes.map((sourceTenantNode: TenantNode) => {
545
+ if (sourceTenantNode.read == 0) sourceTenantNode.update(100, 50, 0, 0);
546
+ else if (sourceTenantNode.read == 50) sourceTenantNode.update(100, 100, 0, 0);
547
+ else sourceTenantNode.update(0, 0, 0, 0);
548
+ if (sourceTenantNode.targets != null) {
549
+ sourceTenantNode.targets.map((targetTenantNode: TenantNode) => {
550
+ if (targetTenantNode.written == 0) targetTenantNode.update(100, 0, 50, 0);
551
+ else if (targetTenantNode.written == 50) targetTenantNode.update(100, 0, 100, 0);
552
+ else if (targetTenantNode.written == 100) targetTenantNode.update(100, 0, 99, 1);
553
+ else targetTenantNode.update(0, 0, 0, 0);
554
+ });
555
+ }
556
+ });
557
+ }
233
558
  }
234
-
235
- // valid user, query AAD for associated company name and domain
236
- if(debug) debugger;
237
- getTenantInfo(user, instance);
238
- return true;
239
559
  }
240
-
241
- function AddTarget(): boolean
242
- {
243
- return true;
560
+ export class TenantNode {
561
+ expanded: boolean;
562
+ status: string;
563
+ name: string;
564
+ tid: string;
565
+ total: number;
566
+ read: number;
567
+ written: number;
568
+ deferred: number;
569
+ targets: TenantNode[];
570
+ constructor(tid: string, name: string) {
571
+ this.expanded = false;
572
+ this.name = name;
573
+ this.tid = tid;
574
+ this.targets = new Array<TenantNode>();
575
+ this.update(0, 0, 0, 0);
576
+ }
577
+ update(total: number, read: number, written: number, deferred: number): void {
578
+ this.total = total;
579
+ this.read = read;
580
+ this.written = written;
581
+ this.deferred = deferred;
582
+ if(this.read === 0 && this.written === 0) this.status = "not started";
583
+ if(this.read > 0) {
584
+ if(this.read < this.total) this.status = "in progress";
585
+ else if(this.read === this.total) this.status = "complete";
586
+ }
587
+ else if(this.written > 0) {
588
+ if(this.written + this.deferred < this.total) this.status = "in progress";
589
+ else if(this.written === this.total) this.status = "complete";
590
+ else if(this.written + this.deferred === this.total) this.status = "failed";
591
+ }
592
+ }
244
593
  }
245
-
246
- function CompleteTarget(): boolean
594
+ export class APIResult {
595
+ result: boolean;
596
+ status: number;
597
+ error: string;
598
+ array: Array<Object> | null;
599
+ constructor() { this.result = true; this.status = 200; this.error = ""; this.array = null; }
600
+ }
601
+ //
602
+ // Azure AD Graph API
603
+ //
604
+ //groupGet - GET /groups/{id}
605
+ export async function groupGet(tenant: Tenant, groupid: string): Promise<{group: string, error: string}> {
606
+ // need a read or write access token to get graph users
607
+ let accessToken: string = "";
608
+ if(tenant.permissionType === TenantPermissionType[TenantPermissionType.read])
609
+ accessToken = tenant.readServicePrincipal;
610
+ if(tenant.permissionType === TenantPermissionType[TenantPermissionType.write])
611
+ accessToken = tenant.writeServicePrincipal;
612
+ if(accessToken === "") return { group: "", error: "no access token specified" };
613
+ // prepare Authorization headers as part of options
614
+ const headers = new Headers();
615
+ const bearer = `Bearer ${accessToken}`;
616
+ headers.append("Authorization", bearer);
617
+ let options = { method: "GET", headers: headers };
618
+ // make /groups endpoint call
619
+ try {
620
+ let groupsEndpoint = `${graphConfig.graphGroupsEndpoint}/${groupid}`;
621
+ let response = await fetch(groupsEndpoint, options);
622
+ let data = await response.json();
623
+ if(typeof data.error !== "undefined"){
624
+ return { group: "", error: `${data.error.code}: ${data.error.message}` };
625
+ }
626
+ return { group: data.value, error: `` };
627
+ }
628
+ catch(error: any) {
629
+ console.log(error);
630
+ return { group: "", error: `Exception: ${error}` };
631
+ }
632
+ }
633
+ //groupsGet - GET /groups
634
+ export async function groupsGet(tenant: Tenant, groupSearchString: string): Promise<{groups: Group[], error: string}> {
635
+ // need a read or write access token to get graph users
636
+ let accessToken: string = "";
637
+ if(tenant.permissionType === TenantPermissionType[TenantPermissionType.read])
638
+ accessToken = tenant.readServicePrincipal;
639
+ if(tenant.permissionType === TenantPermissionType[TenantPermissionType.write])
640
+ accessToken = tenant.writeServicePrincipal;
641
+ if(accessToken === "") return { groups: [], error: "no access token specified" };
642
+ // prepare Authorization headers as part of options
643
+ const headers = new Headers();
644
+ const bearer = `Bearer ${accessToken}`;
645
+ headers.append("Authorization", bearer);
646
+ let options = { method: "GET", headers: headers };
647
+ // make /groups endpoint call
648
+ try {
649
+ let groupsEndpoint = `${graphConfig.graphGroupsEndpoint}/?$filter=startsWith(displayName, '${groupSearchString}')`;
650
+ let response = await fetch(groupsEndpoint, options);
651
+ let data = await response.json();
652
+ if(typeof data.error !== "undefined"){
653
+ return { groups: [], error: `${data.error.code}: ${data.error.message}` };
654
+ }
655
+ return { groups: data.value, error: `` };
656
+ }
657
+ catch(error: any) {
658
+ console.log(error);
659
+ return { group: "", error: `Exception: ${error}` };
660
+ }
661
+ }
662
+ export function signIn(user: User, tasks: TaskArray): void {
663
+ let tenantURL: string = window.location.href;
664
+ tenantURL += "MicrosoftIdentity/Account/Challenge";
665
+ let url: URL = new URL(tenantURL);
666
+ url.searchParams.append("redirectUri", window.location.origin);
667
+ url.searchParams.append("scope", "openid offline_access profile user.read contacts.read CrossTenantInformation.ReadBasic.All");
668
+ url.searchParams.append("domainHint", "organizations");
669
+ if (user.oid !== "1"){
670
+ url.searchParams.append("loginHint", user.mail);
671
+ }
672
+ tasks.setTaskStart("initialization", new Date());
673
+ tasks.setTaskStart("authenticate user", new Date());
674
+ window.location.assign(url.href);
675
+ }
676
+ export function signInIncrementally(user: User, scope: string): void {
677
+ if (user.oid == "1") return;
678
+ let tenantURL: string = window.location.href;
679
+ tenantURL += "MicrosoftIdentity/Account/Challenge";
680
+ let url: URL = new URL(tenantURL);
681
+ url.searchParams.append("redirectUri", window.location.origin);
682
+ let scopes = scope;
683
+ url.searchParams.append("scope", scopes);
684
+ url.searchParams.append("domainHint", "organizations");
685
+ url.searchParams.append("loginHint", user.mail);
686
+ window.location.assign(url.href);
687
+ }
688
+ export function signOut(user: User): void {
689
+ if (user.oid == "1") return;
690
+ // these lines provide more callbacks during logout
691
+ //let tenantURL: string = window.location.href;
692
+ //tenantURL += "MicrosoftIdentity/Account/SignOut";
693
+ // this line takes advantage of our saved loginHint to logout right away, but requires additional cleanup logic
694
+ // https://aaddevsup.azurewebsites.net/2022/03/how-to-logout-of-an-oauth2-application-without-getting-prompted-to-select-a-user/
695
+ let tenantURL: string = "https://login.microsoftonline.com/common/oauth2/logout";
696
+ let url: URL = new URL(tenantURL);
697
+ url.searchParams.append("post_logout_redirect_uri", window.location.origin);
698
+ url.searchParams.append("logout_hint", user.loginHint);
699
+ window.location.assign(url.href);
700
+ }
701
+ //tenantRelationshipsGetByDomain - query AAD for associated company name and id
702
+ export async function tenantRelationshipsGetByDomain(loggedInUser: User, tenant: Tenant, instance: IPublicClientApplication, debug: boolean): Promise<boolean> {
703
+ if (debug) debugger;
704
+ // do we already have a valid tenant name? if so, nothing to add
705
+ if (typeof tenant.name !== 'undefined' && tenant.name !== "") return false;
706
+ // if needed, retrieve and cache access token
707
+ if (typeof loggedInUser.accessToken === 'undefined' || loggedInUser.accessToken === "") {
708
+ console.log(`tenantRelationshipsGetByDomain called with invalid logged in user: ${loggedInUser.name}`);
709
+ try {
710
+ let response: AuthenticationResult = await instance.acquireTokenByCode({ code: loggedInUser.spacode });
711
+ loggedInUser.accessToken = response.accessToken; // cache access token on the user
712
+ console.log("Front end token acquired: " + loggedInUser.accessToken.slice(0,20));
713
+ }
714
+ catch(error: any) {
715
+ console.log("Front end token failure: " + error);
716
+ return false; // failed to get access token, no need to re-render
717
+ }
718
+ }
719
+ // prepare Authorization headers as part of options
720
+ const headers = new Headers();
721
+ const bearer = `Bearer ${loggedInUser.accessToken}`;
722
+ headers.append("Authorization", bearer);
723
+ let options = { method: "GET", headers: headers };
724
+ // make tenant endpoint call
725
+ try {
726
+ // create tenant info endpoint
727
+ var tenantEndpoint = graphConfig.graphTenantByDomainEndpoint;
728
+ tenantEndpoint += "(domainName='";
729
+ tenantEndpoint += tenant.domain;
730
+ tenantEndpoint += "')";
731
+ console.log("Attempting GET from /findTenantInformationByDomainName:", tenantEndpoint);
732
+ let response = await fetch(tenantEndpoint, options);
733
+ let data = await response.json();
734
+ if(data) {
735
+ if(typeof data.error !== "undefined") {
736
+ console.log("Failed GET from /findTenantInformationByDomainName: ", data.error.message);
737
+ return false;
738
+ }
739
+ else if (typeof data.displayName !== undefined && data.displayName !== "") {
740
+ // set domain information on passed tenant
741
+ tenant.tid = data.tenantId;
742
+ tenant.name = data.displayName;
743
+ console.log("Successful GET from /findTenantInformationByDomainName: ", data.displayName);
744
+ return true; // success, need UX to re-render
745
+ }
746
+ }
747
+ else{
748
+ console.log("Failed to GET from /findTenantInformationByTenantId: ", tenantEndpoint);
749
+ }
750
+ }
751
+ catch(error: any) {
752
+ console.log("Failed to GET from /findTenantInformationByTenantId: ", error);
753
+ return false; // failed, no need for UX to re-render
754
+ }
755
+ return false; // failed, no need for UX to re-render
756
+ }
757
+ //tenantRelationshipsGetById - query AAD for associated company name and domain
758
+ export async function tenantRelationshipsGetById(user: User, ii: InitInfo, instance: IPublicClientApplication, tasks: TaskArray, debug: boolean): Promise<boolean> {
759
+ if (debug) debugger;
760
+ // do we already have a valid company name? if so, nothing to add, no need for UX to re-render
761
+ if (typeof user.companyName !== 'undefined' && user.companyName !== "") return false;
762
+ // if needed, retrieve and cache access token
763
+ if (typeof user.accessToken === 'undefined' || user.accessToken === "") {
764
+ try {
765
+ let response: AuthenticationResult = await instance.acquireTokenByCode({ code: user.spacode });
766
+ user.accessToken = response.accessToken; // cache access token
767
+ console.log("Front end token acquired: " + user.accessToken.slice(0,20));
768
+ }
769
+ catch(error: any) {
770
+ console.log("Front end token failure: " + error);
771
+ return false; // failed to get access token, no need to re-render
772
+ }
773
+ }
774
+ // prepare Authorization headers as part of options
775
+ const headers = new Headers();
776
+ const bearer = `Bearer ${user.accessToken}`;
777
+ headers.append("Authorization", bearer);
778
+ let options = { method: "GET", headers: headers };
779
+ // make tenant endpoint call
780
+ try {
781
+ // create tenant info endpoint
782
+ var tenantEndpoint = graphConfig.graphTenantByIdEndpoint;
783
+ tenantEndpoint += "(tenantId='";
784
+ tenantEndpoint += user.tid;
785
+ tenantEndpoint += "')";
786
+ // track time of tenant details query
787
+ tasks.setTaskStart("GET tenant details", new Date());
788
+ console.log("Attempting GET from /findTenantInformationByTenantId:", tenantEndpoint);
789
+ let response = await fetch(tenantEndpoint, options);
790
+ let data = await response.json();
791
+ if(data && typeof data.displayName !== undefined && data.displayName !== "") {
792
+ // set domain information on user
793
+ user.companyName = data.displayName;
794
+ user.companyDomain = data.defaultDomainName;
795
+ // set domain information on tenant
796
+ let tenant: Tenant | undefined = ii.ts.find((t) => t.tid === user.tid);
797
+ if(tenant !== undefined){
798
+ tenant.name = data.displayName;
799
+ tenant.domain = data.defaultDomainName;
800
+ }
801
+ else{
802
+ console.log("tenantRelationshipsGetById: missing associated tenant for logged in user.");
803
+ debugger;
804
+ }
805
+ console.log("Successful GET from /findTenantInformationByTenantId: ", data.displayName);
806
+ tasks.setTaskEnd("GET tenant details", new Date(), "complete");
807
+ return true; // success, need UX to re-render
808
+ }
809
+ else{
810
+ console.log("Failed to GET from /findTenantInformationByTenantId: ", tenantEndpoint);
811
+ }
812
+ }
813
+ catch(error: any) {
814
+ console.log("Failed to GET from /findTenantInformationByTenantId: ", error);
815
+ tasks.setTaskEnd("GET tenant details", new Date(), "failed");
816
+ return false; // failed, no need for UX to re-render
817
+ }
818
+ tasks.setTaskEnd("GET tenant details", new Date(), "failed");
819
+ return false; // failed, no need for UX to re-render
820
+ }
821
+ //usersGet - GET from AAD Users endpoint
822
+ export async function usersGet(tenant: Tenant): Promise<{users: string[], error: string}> {
823
+ // need a read or write access token to get graph users
824
+ let accessToken: string = "";
825
+ if(tenant.permissionType === TenantPermissionType[TenantPermissionType.read])
826
+ accessToken = tenant.readServicePrincipal;
827
+ if(tenant.permissionType === TenantPermissionType[TenantPermissionType.write])
828
+ accessToken = tenant.writeServicePrincipal;
829
+ if(accessToken === "") return { users: [], error: "no access token specified" };
830
+ // prepare Authorization headers as part of options
831
+ const headers = new Headers();
832
+ const bearer = `Bearer ${accessToken}`;
833
+ headers.append("Authorization", bearer);
834
+ let options = { method: "GET", headers: headers };
835
+ // make /users endpoint call
836
+ try {
837
+ let response = await fetch(graphConfig.graphUsersEndpoint, options);
838
+ let data = await response.json();
839
+ if(typeof data.error !== "undefined"){
840
+ return { users: [], error: `${data.error.code}: ${data.error.message}` };
841
+ }
842
+ let users = new Array<User>();
843
+ for (let user of data.value) {
844
+ users.push(user.mail);
845
+ }
846
+ return { users: users, error: `` };
847
+ }
848
+ catch(error: any) {
849
+ console.log(error);
850
+ return { users: [], error: `Exception: ${error}` };
851
+ }
852
+ }
853
+ //
854
+ // Mindline Config API
855
+ //
856
+ export async function configEdit(instance: IPublicClientApplication, authorizedUser: User, config: Config, workspaceId: string, debug: boolean): Promise<APIResult> {
857
+ let result: APIResult = new APIResult();
858
+ if (config.id === "1") {
859
+ result = await configPost(instance, authorizedUser, config, workspaceId, debug);
860
+ }
861
+ else {
862
+ result = await configPut(instance, authorizedUser, config, debug);
863
+ }
864
+ return result;
865
+ }
866
+ export async function configRemove(instance: IPublicClientApplication, authorizedUser: User, config: Config, workspaceId: string, debug: boolean): Promise<APIResult> {
867
+ return configDelete(instance, authorizedUser, config, workspaceId, debug);
868
+ }
869
+ // retrieve Workspace(s), User(s), Tenant(s), Config(s) given newly logged in user
870
+ export async function initGet(instance: IPublicClientApplication, authorizedUser: User, user: User, ii: InitInfo, tasks: TaskArray, debug: boolean): Promise<APIResult>
247
871
  {
248
- return true;
872
+ let result: APIResult = new APIResult();
873
+ if (debug) debugger;
874
+ // get tenant name and domain from AAD
875
+ result.result = await tenantRelationshipsGetById(user, ii, instance, tasks, debug);
876
+ // if this is the first time, we have just gotten tenant info, then we must POST user and not-yet-onboarded tenant to back end
877
+ if (result.result) {
878
+ tasks.setTaskStart("POST config init", new Date());
879
+ result = await initPost(instance, authorizedUser, user, debug);
880
+ tasks.setTaskEnd("POST config init", new Date(), result.result ? "complete" : "failed");
881
+ }
882
+ // simlarly, if we just did our first post, then query config backend for workspace(s) associated with this user
883
+ if (result.result) {
884
+ tasks.setTaskStart("GET workspaces", new Date());
885
+ result = await workspaceInfoGet(instance, authorizedUser, user, ii, debug);
886
+ tasks.setTaskEnd("GET workspaces", new Date(), result ? "complete" : "failed");
887
+ }
888
+ if(result.result) result.error = version;
889
+ return result;
249
890
  }
250
-
251
- function AddUser(): boolean
891
+ export async function tenantAdd(instance: IPublicClientApplication, authorizedUser: User, tenant: Tenant, workspaceId: string): Promise<APIResult> {
892
+ return tenantPost(instance, authorizedUser, tenant, workspaceId);
893
+ }
894
+ export async function tenantComplete(instance: IPublicClientApplication, authorizedUser: User, tenant: Tenant, debug: boolean): Promise<APIResult> {
895
+ return tenantPut(instance, authorizedUser, tenant, debug);
896
+ }
897
+ export async function tenantRemove(instance: IPublicClientApplication, authorizedUser: User, tenant: Tenant, workspaceId: string, debug: boolean): Promise<APIResult> {
898
+ return tenantDelete(instance, authorizedUser, tenant, workspaceId, debug);
899
+ }
900
+ export async function userAdd(instance: IPublicClientApplication, authorizedUser: User, user: User, workspaceId: string): Promise<APIResult> {
901
+ return adminPost(instance, authorizedUser, user, workspaceId);
902
+ }
903
+ export async function userRemove(instance: IPublicClientApplication, authorizedUser: User, user: User, workspaceId: string): Promise<APIResult> {
904
+ return adminDelete(instance, authorizedUser, user, workspaceId);
905
+ }
906
+ //
907
+ // Mindline Config API internal helper functions
908
+ //
909
+ function processReturnedAdmins(workspace: Workspace, ii: InitInfo, returnedAdmins: Array<Object>)
252
910
  {
253
- return true;
911
+ returnedAdmins.map((item) => {
912
+ // are we already tracking this user?
913
+ let user: User|null = null;
914
+ let usIndex = ii.us.findIndex((u) => u.oid === item.userId);
915
+ if(usIndex===-1) {
916
+ // start tracking
917
+ let dummyIndex = ii.us.findIndex((u) => u.oid === "1");
918
+ if(dummyIndex!==-1) {
919
+ // clear and overwrite dummy
920
+ user = ii.us.at(dummyIndex);
921
+ user.associatedWorkspaces.length = 0;
922
+ }
923
+ else {
924
+ // create and track new user
925
+ user = new User();
926
+ ii.us.push(user);
927
+ }
928
+ } else {
929
+ // already tracking this user
930
+ user = ii.us.at(usIndex);
931
+ }
932
+ // refresh all the data available from the server
933
+ user.oid = item.userId;
934
+ user.name = item.firstName;
935
+ user.mail = item.email;
936
+ user.tid = item.tenantId;
937
+ // ensure this workspace tracks this user
938
+ let idx = workspace.associatedUsers.findIndex((u) => u === item.userId);
939
+ if(idx == -1) workspace.associatedUsers.push(item.userId);
940
+ });
254
941
  }
255
-
256
- function CompleteUser(): boolean
942
+ function processReturnedTenants(workspace: Workspace, ii: InitInfo, returnedTenants: Array<Object>)
257
943
  {
258
- return true;
944
+ returnedTenants.map((item) => {
945
+ // are we already tracking this tenant?
946
+ let tenant: Tenant|null = null;
947
+ let tsIndex = ii.ts.findIndex((t) => t.tid === item.tenantId);
948
+ if (tsIndex === -1) {
949
+ // start tracking
950
+ let dummyIndex = ii.ts.findIndex((t) => t.tid === "1");
951
+ if (dummyIndex !== -1) {
952
+ // clear and overwrite dummy
953
+ tenant = ii.ts.at(dummyIndex);
954
+ } else {
955
+ // create and track new workspace
956
+ tenant = new Tenant();
957
+ ii.ts.push(tenant);
958
+ }
959
+ } else {
960
+ // already tracking this tenant
961
+ tenant = ii.ts.at(tsIndex);
962
+ }
963
+ tenant.tid = item.tenantId;
964
+ tenant.name = item.name;
965
+ tenant.domain = item.domain;
966
+ tenant.tenantType = item.type.toLowerCase(); // should now be strings
967
+ tenant.permissionType = item.permissionType.toLowerCase(); // should now be strings
968
+ tenant.onboarded = item.isOnboarded ? "true" : "false";
969
+ tenant.authority = item.authority;
970
+ tenant.readServicePrincipal = item.readServicePrincipal;
971
+ tenant.writeServicePrincipal = item.writeServicePrincipal;
972
+ // ensure this workspace tracks this tenant
973
+ let idx = workspace.associatedTenants.findIndex((t) => t === item.tenantId);
974
+ if (idx == -1) workspace.associatedTenants.push(item.tenantId);
975
+ });
259
976
  }
260
-
261
- function CreateConfig(): boolean
977
+ function processReturnedConfigs(workspace: Workspace, ii: InitInfo, returnedConfigs: Array<Object>)
262
978
  {
263
- return true;
979
+ // process returned configs
980
+ returnedConfigs.map((item) => {
981
+ // are we already tracking this config?
982
+ let config: Config | null = null;
983
+ let csIndex = ii.cs.findIndex((c) => c.id === item.id);
984
+ if (csIndex === -1) {
985
+ // start tracking
986
+ let dummyIndex = ii.cs.findIndex((c) => c.id === "1");
987
+ if (dummyIndex !== -1) {
988
+ // clear and overwrite dummy
989
+ config = ii.cs.at(dummyIndex);
990
+ } else {
991
+ // create and track new workspace
992
+ config = new Config();
993
+ ii.cs.push(config);
994
+ }
995
+ } else {
996
+ // already tracking this config
997
+ config = ii.cs.at(csIndex);
998
+ }
999
+ config!.id = item.id;
1000
+ config!.name = item.name;
1001
+ config!.description = item.description;
1002
+ config!.isEnabled = item.isEnabled;
1003
+ // create TenantConfigInfo array
1004
+ config!.tenants.length = 0;
1005
+ item.tenants.map((tci) => {
1006
+ let tenantConfigInfo = new TenantConfigInfo();
1007
+ tenantConfigInfo.tid = tci.tenantId;
1008
+ tenantConfigInfo.sourceGroupId = tci.sourceGroupId;
1009
+ tenantConfigInfo.sourceGroupName = tci.sourceGroupName;
1010
+ tenantConfigInfo.configurationTenantType = tci.configurationTenantType.toLowerCase();
1011
+ config!.tenants.push(tenantConfigInfo);
1012
+ });
1013
+ // ensure this workspace tracks this config
1014
+ let idx = workspace.associatedConfigs.findIndex((c) => c === item.id);
1015
+ if (idx == -1) workspace.associatedConfigs.push(item.id);
1016
+ });
1017
+ }
1018
+ async function workspaceInfoGet(instance: IPublicClientApplication, authorizedUser: User, user: User, ii: InitInfo, debug: boolean): Promise<APIResult> {
1019
+ let result: APIResult = new APIResult();
1020
+ if (debug) debugger;
1021
+ try {
1022
+ result = await workspacesGet(instance, authorizedUser, user, debug);
1023
+ if (result.result) {
1024
+ for (let o of result.array!) {
1025
+ // are we already tracking this workspace?
1026
+ let workspace: Workspace = null;
1027
+ let wsIndex = ii.ws.findIndex((w) => w.id === o.id);
1028
+ if (wsIndex === -1) {
1029
+ // start tracking
1030
+ let dummyIndex = ii.ws.findIndex((w) => w.id === "1");
1031
+ if(dummyIndex !== -1) {
1032
+ // clear and overwrite dummy
1033
+ workspace = ii.ws.at(dummyIndex);
1034
+ }
1035
+ else {
1036
+ // create and track new workspace
1037
+ workspace = new Workspace();
1038
+ ii.ws.push(workspace);
1039
+ }
1040
+ } else {
1041
+ // already tracking this workspace
1042
+ workspace = ii.ws.at(wsIndex);
1043
+ }
1044
+ // clear associations as we are about to reset
1045
+ workspace.associatedUsers.length = 0;
1046
+ workspace.associatedTenants.length = 0;
1047
+ workspace.associatedConfigs.length = 0;
1048
+ workspace.id = o.id;
1049
+ workspace.name = o.name;
1050
+ // parallel GET admins, tenants, configs associated with this workspace
1051
+ let adminsPromise: Promise<APIResult> = adminsGet(instance, authorizedUser, workspace.id, debug);
1052
+ let tenantsPromise: Promise<APIResult> = tenantsGet(instance, authorizedUser, workspace.id, debug);
1053
+ let configsPromise: Promise<APIResult> = configsGet(instance, authorizedUser, workspace.id, debug);
1054
+ // wait for all to finish, return on any failure
1055
+ let [adminsResult, tenantsResult, configsResult] = await Promise.all([adminsPromise, tenantsPromise, configsPromise]);
1056
+ if(!adminsResult.result) return adminsResult;
1057
+ if(!tenantsResult.result) return tenantsResult;
1058
+ if(!configsResult.result) return configsResult;
1059
+ // process returned workspace components
1060
+ processReturnedAdmins(workspace, ii, adminsResult.array!);
1061
+ processReturnedTenants(workspace, ii, tenantsResult.array!);
1062
+ processReturnedConfigs(workspace, ii, configsResult.array!);
1063
+ // tag components with workspaceIDs
1064
+ ii.tagWithWorkspaces();
1065
+ }
1066
+ return result;
1067
+ }
1068
+ }
1069
+ catch (error: any) {
1070
+ console.log(error.message);
1071
+ result.error = error.message;
1072
+ }
1073
+ result.result = false;
1074
+ result.status = 500;
1075
+ return result;
264
1076
  }