@mindline/sync 1.0.28 → 1.0.30
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/configs.json +3 -14
- package/configs2.json +19 -31
- package/hybridspa.ts +822 -111
- package/index.d.ts +134 -25
- package/index.test.ts +1 -7
- package/index.ts +940 -134
- package/mockconfig.json +18 -0
- package/package.json +1 -2
- package/tasks.ts +103 -0
- package/{targets.json → tenants.json} +3 -1
- package/{targets2.json → tenants2.json} +6 -6
- package/workspaces.json +1 -1
- package/workspaces2.json +3 -3
package/index.ts
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
//index.
|
|
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 {
|
|
5
|
-
import {
|
|
4
|
+
import { adminDelete, adminPost, adminsGet, configDelete, configsGet, configPost, configPut, graphConfig, initPost, tenantPut, tenantPost, tenantDelete, tenantsGet, workspacesGet} from './hybridspa';
|
|
5
|
+
import { version } from './package.json';
|
|
6
6
|
import users from "./users.json";
|
|
7
|
-
import
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
type TenantPermissionTypeStrings = keyof typeof TenantPermissionType;
|
|
71
|
+
export class Tenant {
|
|
51
72
|
tid: string;
|
|
52
73
|
name: string;
|
|
53
74
|
domain: string;
|
|
54
|
-
|
|
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.
|
|
64
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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
|
-
|
|
82
|
-
|
|
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.
|
|
89
|
-
this.
|
|
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
|
-
|
|
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.
|
|
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:
|
|
173
|
+
ts: Tenant[];
|
|
112
174
|
cs: Config[];
|
|
113
175
|
ws: Workspace[];
|
|
114
|
-
constructor(){
|
|
115
|
-
this.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
//
|
|
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
|
|
137
|
-
for (let
|
|
138
|
-
let
|
|
139
|
-
(
|
|
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 (
|
|
142
|
-
// we found the
|
|
143
|
-
|
|
144
|
-
|
|
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,805 @@ 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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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() : 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
|
+
// cycle through desired states for sources and targets
|
|
537
|
+
if (this.tenantNodes != null) {
|
|
538
|
+
this.tenantNodes.map((sourceTenantNode: TenantNode) => {
|
|
539
|
+
if (sourceTenantNode.read == 0) sourceTenantNode.update(100, 50, 0, 0);
|
|
540
|
+
else if (sourceTenantNode.read == 50) sourceTenantNode.update(100, 100, 0, 0);
|
|
541
|
+
else sourceTenantNode.update(0, 0, 0, 0);
|
|
542
|
+
if (sourceTenantNode.targets != null) {
|
|
543
|
+
sourceTenantNode.targets.map((targetTenantNode: TenantNode) => {
|
|
544
|
+
if (targetTenantNode.written == 0) targetTenantNode.update(100, 0, 50, 0);
|
|
545
|
+
else if (targetTenantNode.written == 50) targetTenantNode.update(100, 0, 100, 0);
|
|
546
|
+
else if (targetTenantNode.written == 100) targetTenantNode.update(100, 0, 99, 1);
|
|
547
|
+
else targetTenantNode.update(0, 0, 0, 0);
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
export class TenantNode {
|
|
555
|
+
expanded: boolean;
|
|
556
|
+
status: string;
|
|
557
|
+
name: string;
|
|
558
|
+
tid: string;
|
|
559
|
+
total: number;
|
|
560
|
+
read: number;
|
|
561
|
+
written: number;
|
|
562
|
+
deferred: number;
|
|
563
|
+
targets: TenantNode[];
|
|
564
|
+
constructor(tid: string, name: string) {
|
|
565
|
+
this.expanded = false;
|
|
566
|
+
this.name = name;
|
|
567
|
+
this.tid = tid;
|
|
568
|
+
this.targets = new Array<TenantNode>();
|
|
569
|
+
this.update(0, 0, 0, 0);
|
|
570
|
+
}
|
|
571
|
+
update(total: number, read: number, written: number, deferred: number): void {
|
|
572
|
+
this.total = total;
|
|
573
|
+
this.read = read;
|
|
574
|
+
this.written = written;
|
|
575
|
+
this.deferred = deferred;
|
|
576
|
+
if(this.read === 0 && this.written === 0) this.status = "not started";
|
|
577
|
+
if(this.read > 0) {
|
|
578
|
+
if(this.read < this.total) this.status = "in progress";
|
|
579
|
+
else if(this.read === this.total) this.status = "complete";
|
|
580
|
+
}
|
|
581
|
+
else if(this.written > 0) {
|
|
582
|
+
if(this.written + this.deferred < this.total) this.status = "in progress";
|
|
583
|
+
else if(this.written === this.total) this.status = "complete";
|
|
584
|
+
else if(this.written + this.deferred === this.total) this.status = "failed";
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
export class APIResult {
|
|
589
|
+
result: boolean;
|
|
590
|
+
status: number;
|
|
591
|
+
error: string;
|
|
592
|
+
array: Array<Object> | null;
|
|
593
|
+
constructor() { this.result = true; this.status = 200; this.error = ""; this.array = null; }
|
|
594
|
+
}
|
|
595
|
+
//
|
|
596
|
+
// Azure AD Graph API
|
|
597
|
+
//
|
|
598
|
+
//groupGet - GET /groups/{id}
|
|
599
|
+
export async function groupGet(tenant: Tenant, groupid: string): Promise<{group: string, error: string}> {
|
|
600
|
+
// need a read or write access token to get graph users
|
|
601
|
+
let accessToken: string = "";
|
|
602
|
+
if(tenant.permissionType === TenantPermissionType[TenantPermissionType.read])
|
|
603
|
+
accessToken = tenant.readServicePrincipal;
|
|
604
|
+
if(tenant.permissionType === TenantPermissionType[TenantPermissionType.write])
|
|
605
|
+
accessToken = tenant.writeServicePrincipal;
|
|
606
|
+
if(accessToken === "") return { group: "", error: "no access token specified" };
|
|
607
|
+
// prepare Authorization headers as part of options
|
|
608
|
+
const headers = new Headers();
|
|
609
|
+
const bearer = `Bearer ${accessToken}`;
|
|
610
|
+
headers.append("Authorization", bearer);
|
|
611
|
+
let options = { method: "GET", headers: headers };
|
|
612
|
+
// make /groups endpoint call
|
|
178
613
|
try {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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);
|
|
228
|
-
|
|
229
|
-
// why would instance be null here? investigate!
|
|
230
|
-
if(instance===null) {
|
|
231
|
-
debugger;
|
|
232
|
-
return false;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// valid user, query AAD for associated company name and domain
|
|
236
|
-
if(debug) debugger;
|
|
237
|
-
getTenantInfo(user, instance);
|
|
238
|
-
return true;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function AddTarget(): boolean
|
|
242
|
-
{
|
|
243
|
-
return true;
|
|
614
|
+
let groupsEndpoint = `${graphConfig.graphGroupsEndpoint}/${groupid}`;
|
|
615
|
+
let response = await fetch(groupsEndpoint, options);
|
|
616
|
+
let data = await response.json();
|
|
617
|
+
if(typeof data.error !== "undefined"){
|
|
618
|
+
return { group: "", error: `${data.error.code}: ${data.error.message}` };
|
|
619
|
+
}
|
|
620
|
+
return { group: data.value, error: `` };
|
|
621
|
+
}
|
|
622
|
+
catch(error: any) {
|
|
623
|
+
console.log(error);
|
|
624
|
+
return { group: "", error: `Exception: ${error}` };
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
//groupsGet - GET /groups
|
|
628
|
+
export async function groupsGet(tenant: Tenant, groupSearchString: string): Promise<{groups: Group[], error: string}> {
|
|
629
|
+
// need a read or write access token to get graph users
|
|
630
|
+
let accessToken: string = "";
|
|
631
|
+
if(tenant.permissionType === TenantPermissionType[TenantPermissionType.read])
|
|
632
|
+
accessToken = tenant.readServicePrincipal;
|
|
633
|
+
if(tenant.permissionType === TenantPermissionType[TenantPermissionType.write])
|
|
634
|
+
accessToken = tenant.writeServicePrincipal;
|
|
635
|
+
if(accessToken === "") return { groups: [], error: "no access token specified" };
|
|
636
|
+
// prepare Authorization headers as part of options
|
|
637
|
+
const headers = new Headers();
|
|
638
|
+
const bearer = `Bearer ${accessToken}`;
|
|
639
|
+
headers.append("Authorization", bearer);
|
|
640
|
+
let options = { method: "GET", headers: headers };
|
|
641
|
+
// make /groups endpoint call
|
|
642
|
+
try {
|
|
643
|
+
let groupsEndpoint = `${graphConfig.graphGroupsEndpoint}/?$filter=startsWith(displayName, '${groupSearchString}')`;
|
|
644
|
+
let response = await fetch(groupsEndpoint, options);
|
|
645
|
+
let data = await response.json();
|
|
646
|
+
if(typeof data.error !== "undefined"){
|
|
647
|
+
return { groups: [], error: `${data.error.code}: ${data.error.message}` };
|
|
648
|
+
}
|
|
649
|
+
return { groups: data.value, error: `` };
|
|
650
|
+
}
|
|
651
|
+
catch(error: any) {
|
|
652
|
+
console.log(error);
|
|
653
|
+
return { group: "", error: `Exception: ${error}` };
|
|
654
|
+
}
|
|
244
655
|
}
|
|
245
|
-
|
|
246
|
-
|
|
656
|
+
export function signIn(user: User, tasks: TaskArray): void {
|
|
657
|
+
let tenantURL: string = window.location.href;
|
|
658
|
+
tenantURL += "MicrosoftIdentity/Account/Challenge";
|
|
659
|
+
let url: URL = new URL(tenantURL);
|
|
660
|
+
url.searchParams.append("redirectUri", window.location.origin);
|
|
661
|
+
url.searchParams.append("scope", "openid offline_access profile user.read contacts.read CrossTenantInformation.ReadBasic.All");
|
|
662
|
+
url.searchParams.append("domainHint", "organizations");
|
|
663
|
+
if (user.oid !== "1"){
|
|
664
|
+
url.searchParams.append("loginHint", user.mail);
|
|
665
|
+
}
|
|
666
|
+
tasks.setTaskStart("initialization", new Date());
|
|
667
|
+
tasks.setTaskStart("authenticate user", new Date());
|
|
668
|
+
window.location.assign(url.href);
|
|
669
|
+
}
|
|
670
|
+
export function signInIncrementally(user: User, scope: string): void {
|
|
671
|
+
if (user.oid == "1") return;
|
|
672
|
+
let tenantURL: string = window.location.href;
|
|
673
|
+
tenantURL += "MicrosoftIdentity/Account/Challenge";
|
|
674
|
+
let url: URL = new URL(tenantURL);
|
|
675
|
+
url.searchParams.append("redirectUri", window.location.origin);
|
|
676
|
+
let scopes = scope;
|
|
677
|
+
url.searchParams.append("scope", scopes);
|
|
678
|
+
url.searchParams.append("domainHint", "organizations");
|
|
679
|
+
url.searchParams.append("loginHint", user.mail);
|
|
680
|
+
window.location.assign(url.href);
|
|
681
|
+
}
|
|
682
|
+
export function signOut(user: User): void {
|
|
683
|
+
if (user.oid == "1") return;
|
|
684
|
+
// these lines provide more callbacks during logout
|
|
685
|
+
//let tenantURL: string = window.location.href;
|
|
686
|
+
//tenantURL += "MicrosoftIdentity/Account/SignOut";
|
|
687
|
+
// this line takes advantage of our saved loginHint to logout right away, but requires additional cleanup logic
|
|
688
|
+
// https://aaddevsup.azurewebsites.net/2022/03/how-to-logout-of-an-oauth2-application-without-getting-prompted-to-select-a-user/
|
|
689
|
+
let tenantURL: string = "https://login.microsoftonline.com/common/oauth2/logout";
|
|
690
|
+
let url: URL = new URL(tenantURL);
|
|
691
|
+
url.searchParams.append("post_logout_redirect_uri", window.location.origin);
|
|
692
|
+
url.searchParams.append("logout_hint", user.loginHint);
|
|
693
|
+
window.location.assign(url.href);
|
|
694
|
+
}
|
|
695
|
+
//tenantRelationshipsGetByDomain - query AAD for associated company name and id
|
|
696
|
+
export async function tenantRelationshipsGetByDomain(loggedInUser: User, tenant: Tenant, instance: IPublicClientApplication, debug: boolean): Promise<boolean> {
|
|
697
|
+
if (debug) debugger;
|
|
698
|
+
// do we already have a valid tenant name? if so, nothing to add
|
|
699
|
+
if (typeof tenant.name !== 'undefined' && tenant.name !== "") return false;
|
|
700
|
+
// if needed, retrieve and cache access token
|
|
701
|
+
if (typeof loggedInUser.accessToken === 'undefined' || loggedInUser.accessToken === "") {
|
|
702
|
+
console.log(`tenantRelationshipsGetByDomain called with invalid logged in user: ${loggedInUser.name}`);
|
|
703
|
+
try {
|
|
704
|
+
let response: AuthenticationResult = await instance.acquireTokenByCode({ code: loggedInUser.spacode });
|
|
705
|
+
loggedInUser.accessToken = response.accessToken; // cache access token on the user
|
|
706
|
+
console.log("Front end token acquired: " + loggedInUser.accessToken.slice(0,20));
|
|
707
|
+
}
|
|
708
|
+
catch(error: any) {
|
|
709
|
+
console.log("Front end token failure: " + error);
|
|
710
|
+
return false; // failed to get access token, no need to re-render
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
// prepare Authorization headers as part of options
|
|
714
|
+
const headers = new Headers();
|
|
715
|
+
const bearer = `Bearer ${loggedInUser.accessToken}`;
|
|
716
|
+
headers.append("Authorization", bearer);
|
|
717
|
+
let options = { method: "GET", headers: headers };
|
|
718
|
+
// make tenant endpoint call
|
|
719
|
+
try {
|
|
720
|
+
// create tenant info endpoint
|
|
721
|
+
var tenantEndpoint = graphConfig.graphTenantByDomainEndpoint;
|
|
722
|
+
tenantEndpoint += "(domainName='";
|
|
723
|
+
tenantEndpoint += tenant.domain;
|
|
724
|
+
tenantEndpoint += "')";
|
|
725
|
+
console.log("Attempting GET from /findTenantInformationByDomainName:", tenantEndpoint);
|
|
726
|
+
let response = await fetch(tenantEndpoint, options);
|
|
727
|
+
let data = await response.json();
|
|
728
|
+
if(data) {
|
|
729
|
+
if(typeof data.error !== "undefined") {
|
|
730
|
+
console.log("Failed GET from /findTenantInformationByDomainName: ", data.error.message);
|
|
731
|
+
return false;
|
|
732
|
+
}
|
|
733
|
+
else if (typeof data.displayName !== undefined && data.displayName !== "") {
|
|
734
|
+
// set domain information on passed tenant
|
|
735
|
+
tenant.tid = data.tenantId;
|
|
736
|
+
tenant.name = data.displayName;
|
|
737
|
+
console.log("Successful GET from /findTenantInformationByDomainName: ", data.displayName);
|
|
738
|
+
return true; // success, need UX to re-render
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
else{
|
|
742
|
+
console.log("Failed to GET from /findTenantInformationByTenantId: ", tenantEndpoint);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
catch(error: any) {
|
|
746
|
+
console.log("Failed to GET from /findTenantInformationByTenantId: ", error);
|
|
747
|
+
return false; // failed, no need for UX to re-render
|
|
748
|
+
}
|
|
749
|
+
return false; // failed, no need for UX to re-render
|
|
750
|
+
}
|
|
751
|
+
//tenantRelationshipsGetById - query AAD for associated company name and domain
|
|
752
|
+
export async function tenantRelationshipsGetById(user: User, ii: InitInfo, instance: IPublicClientApplication, tasks: TaskArray, debug: boolean): Promise<boolean> {
|
|
753
|
+
if (debug) debugger;
|
|
754
|
+
// do we already have a valid company name? if so, nothing to add, no need for UX to re-render
|
|
755
|
+
if (typeof user.companyName !== 'undefined' && user.companyName !== "") return false;
|
|
756
|
+
// if needed, retrieve and cache access token
|
|
757
|
+
if (typeof user.accessToken === 'undefined' || user.accessToken === "") {
|
|
758
|
+
try {
|
|
759
|
+
let response: AuthenticationResult = await instance.acquireTokenByCode({ code: user.spacode });
|
|
760
|
+
user.accessToken = response.accessToken; // cache access token
|
|
761
|
+
console.log("Front end token acquired: " + user.accessToken.slice(0,20));
|
|
762
|
+
}
|
|
763
|
+
catch(error: any) {
|
|
764
|
+
console.log("Front end token failure: " + error);
|
|
765
|
+
return false; // failed to get access token, no need to re-render
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
// prepare Authorization headers as part of options
|
|
769
|
+
const headers = new Headers();
|
|
770
|
+
const bearer = `Bearer ${user.accessToken}`;
|
|
771
|
+
headers.append("Authorization", bearer);
|
|
772
|
+
let options = { method: "GET", headers: headers };
|
|
773
|
+
// make tenant endpoint call
|
|
774
|
+
try {
|
|
775
|
+
// create tenant info endpoint
|
|
776
|
+
var tenantEndpoint = graphConfig.graphTenantByIdEndpoint;
|
|
777
|
+
tenantEndpoint += "(tenantId='";
|
|
778
|
+
tenantEndpoint += user.tid;
|
|
779
|
+
tenantEndpoint += "')";
|
|
780
|
+
// track time of tenant details query
|
|
781
|
+
tasks.setTaskStart("GET tenant details", new Date());
|
|
782
|
+
console.log("Attempting GET from /findTenantInformationByTenantId:", tenantEndpoint);
|
|
783
|
+
let response = await fetch(tenantEndpoint, options);
|
|
784
|
+
let data = await response.json();
|
|
785
|
+
if(data && typeof data.displayName !== undefined && data.displayName !== "") {
|
|
786
|
+
// set domain information on user
|
|
787
|
+
user.companyName = data.displayName;
|
|
788
|
+
user.companyDomain = data.defaultDomainName;
|
|
789
|
+
// set domain information on tenant
|
|
790
|
+
let tenant: Tenant | undefined = ii.ts.find((t) => t.tid === user.tid);
|
|
791
|
+
if(tenant !== undefined){
|
|
792
|
+
tenant.name = data.displayName;
|
|
793
|
+
tenant.domain = data.defaultDomainName;
|
|
794
|
+
}
|
|
795
|
+
else{
|
|
796
|
+
console.log("tenantRelationshipsGetById: missing associated tenant for logged in user.");
|
|
797
|
+
debugger;
|
|
798
|
+
}
|
|
799
|
+
console.log("Successful GET from /findTenantInformationByTenantId: ", data.displayName);
|
|
800
|
+
tasks.setTaskEnd("GET tenant details", new Date(), "complete");
|
|
801
|
+
return true; // success, need UX to re-render
|
|
802
|
+
}
|
|
803
|
+
else{
|
|
804
|
+
console.log("Failed to GET from /findTenantInformationByTenantId: ", tenantEndpoint);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
catch(error: any) {
|
|
808
|
+
console.log("Failed to GET from /findTenantInformationByTenantId: ", error);
|
|
809
|
+
tasks.setTaskEnd("GET tenant details", new Date(), "failed");
|
|
810
|
+
return false; // failed, no need for UX to re-render
|
|
811
|
+
}
|
|
812
|
+
tasks.setTaskEnd("GET tenant details", new Date(), "failed");
|
|
813
|
+
return false; // failed, no need for UX to re-render
|
|
814
|
+
}
|
|
815
|
+
//usersGet - GET from AAD Users endpoint
|
|
816
|
+
export async function usersGet(tenant: Tenant): Promise<{users: string[], error: string}> {
|
|
817
|
+
// need a read or write access token to get graph users
|
|
818
|
+
let accessToken: string = "";
|
|
819
|
+
if(tenant.permissionType === TenantPermissionType[TenantPermissionType.read])
|
|
820
|
+
accessToken = tenant.readServicePrincipal;
|
|
821
|
+
if(tenant.permissionType === TenantPermissionType[TenantPermissionType.write])
|
|
822
|
+
accessToken = tenant.writeServicePrincipal;
|
|
823
|
+
if(accessToken === "") return { users: [], error: "no access token specified" };
|
|
824
|
+
// prepare Authorization headers as part of options
|
|
825
|
+
const headers = new Headers();
|
|
826
|
+
const bearer = `Bearer ${accessToken}`;
|
|
827
|
+
headers.append("Authorization", bearer);
|
|
828
|
+
let options = { method: "GET", headers: headers };
|
|
829
|
+
// make /users endpoint call
|
|
830
|
+
try {
|
|
831
|
+
let response = await fetch(graphConfig.graphUsersEndpoint, options);
|
|
832
|
+
let data = await response.json();
|
|
833
|
+
if(typeof data.error !== "undefined"){
|
|
834
|
+
return { users: [], error: `${data.error.code}: ${data.error.message}` };
|
|
835
|
+
}
|
|
836
|
+
let users = new Array<User>();
|
|
837
|
+
for (let user of data.value) {
|
|
838
|
+
users.push(user.mail);
|
|
839
|
+
}
|
|
840
|
+
return { users: users, error: `` };
|
|
841
|
+
}
|
|
842
|
+
catch(error: any) {
|
|
843
|
+
console.log(error);
|
|
844
|
+
return { users: [], error: `Exception: ${error}` };
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
//
|
|
848
|
+
// Mindline Config API
|
|
849
|
+
//
|
|
850
|
+
export async function configEdit(instance: IPublicClientApplication, authorizedUser: User, config: Config, workspaceId: string, debug: boolean): Promise<APIResult> {
|
|
851
|
+
let result: APIResult = new APIResult();
|
|
852
|
+
if (config.id === "1") {
|
|
853
|
+
result = await configPost(instance, authorizedUser, config, workspaceId, debug);
|
|
854
|
+
}
|
|
855
|
+
else {
|
|
856
|
+
result = await configPut(instance, authorizedUser, config, debug);
|
|
857
|
+
}
|
|
858
|
+
return result;
|
|
859
|
+
}
|
|
860
|
+
export async function configRemove(instance: IPublicClientApplication, authorizedUser: User, config: Config, workspaceId: string, debug: boolean): Promise<APIResult> {
|
|
861
|
+
return configDelete(instance, authorizedUser, config, workspaceId, debug);
|
|
862
|
+
}
|
|
863
|
+
// retrieve Workspace(s), User(s), Tenant(s), Config(s) given newly logged in user
|
|
864
|
+
export async function initGet(instance: IPublicClientApplication, authorizedUser: User, user: User, ii: InitInfo, tasks: TaskArray, debug: boolean): Promise<APIResult>
|
|
247
865
|
{
|
|
248
|
-
|
|
866
|
+
let result: APIResult = new APIResult();
|
|
867
|
+
if (debug) debugger;
|
|
868
|
+
// get tenant name and domain from AAD
|
|
869
|
+
result.result = await tenantRelationshipsGetById(user, ii, instance, tasks, debug);
|
|
870
|
+
// 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
|
|
871
|
+
if (result.result) {
|
|
872
|
+
tasks.setTaskStart("POST config init", new Date());
|
|
873
|
+
result = await initPost(instance, authorizedUser, user, debug);
|
|
874
|
+
tasks.setTaskEnd("POST config init", new Date(), result.result ? "complete" : "failed");
|
|
875
|
+
}
|
|
876
|
+
// simlarly, if we just did our first post, then query config backend for workspace(s) associated with this user
|
|
877
|
+
if (result.result) {
|
|
878
|
+
tasks.setTaskStart("GET workspaces", new Date());
|
|
879
|
+
result = await workspaceInfoGet(instance, authorizedUser, user, ii, debug);
|
|
880
|
+
tasks.setTaskEnd("GET workspaces", new Date(), result ? "complete" : "failed");
|
|
881
|
+
}
|
|
882
|
+
if(result.result) result.error = version;
|
|
883
|
+
return result;
|
|
884
|
+
}
|
|
885
|
+
export async function tenantAdd(instance: IPublicClientApplication, authorizedUser: User, tenant: Tenant, workspaceId: string): Promise<APIResult> {
|
|
886
|
+
return tenantPost(instance, authorizedUser, tenant, workspaceId);
|
|
887
|
+
}
|
|
888
|
+
export async function tenantComplete(instance: IPublicClientApplication, authorizedUser: User, tenant: Tenant, debug: boolean): Promise<APIResult> {
|
|
889
|
+
return tenantPut(instance, authorizedUser, tenant, debug);
|
|
890
|
+
}
|
|
891
|
+
export async function tenantRemove(instance: IPublicClientApplication, authorizedUser: User, tenant: Tenant, workspaceId: string, debug: boolean): Promise<APIResult> {
|
|
892
|
+
return tenantDelete(instance, authorizedUser, tenant, workspaceId, debug);
|
|
893
|
+
}
|
|
894
|
+
export async function userAdd(instance: IPublicClientApplication, authorizedUser: User, user: User, workspaceId: string): Promise<APIResult> {
|
|
895
|
+
return adminPost(instance, authorizedUser, user, workspaceId);
|
|
249
896
|
}
|
|
250
|
-
|
|
251
|
-
|
|
897
|
+
export async function userRemove(instance: IPublicClientApplication, authorizedUser: User, user: User, workspaceId: string): Promise<APIResult> {
|
|
898
|
+
return adminDelete(instance, authorizedUser, user, workspaceId);
|
|
899
|
+
}
|
|
900
|
+
//
|
|
901
|
+
// Mindline Config API internal helper functions
|
|
902
|
+
//
|
|
903
|
+
function processReturnedAdmins(workspace: Workspace, ii: InitInfo, returnedAdmins: Array<Object>)
|
|
252
904
|
{
|
|
253
|
-
|
|
905
|
+
returnedAdmins.map((item) => {
|
|
906
|
+
// are we already tracking this user?
|
|
907
|
+
let user: User|null = null;
|
|
908
|
+
let usIndex = ii.us.findIndex((u) => u.oid === item.userId);
|
|
909
|
+
if(usIndex===-1) {
|
|
910
|
+
// start tracking
|
|
911
|
+
let dummyIndex = ii.us.findIndex((u) => u.oid === "1");
|
|
912
|
+
if(dummyIndex!==-1) {
|
|
913
|
+
// clear and overwrite dummy
|
|
914
|
+
user = ii.us.at(dummyIndex);
|
|
915
|
+
user.associatedWorkspaces.length = 0;
|
|
916
|
+
}
|
|
917
|
+
else {
|
|
918
|
+
// create and track new user
|
|
919
|
+
user = new User();
|
|
920
|
+
ii.us.push(user);
|
|
921
|
+
}
|
|
922
|
+
} else {
|
|
923
|
+
// already tracking this user
|
|
924
|
+
user = ii.us.at(usIndex);
|
|
925
|
+
}
|
|
926
|
+
// refresh all the data available from the server
|
|
927
|
+
user.oid = item.userId;
|
|
928
|
+
user.name = item.firstName;
|
|
929
|
+
user.mail = item.email;
|
|
930
|
+
user.tid = item.tenantId;
|
|
931
|
+
// ensure this workspace tracks this user
|
|
932
|
+
let idx = workspace.associatedUsers.findIndex((u) => u === item.userId);
|
|
933
|
+
if(idx == -1) workspace.associatedUsers.push(item.userId);
|
|
934
|
+
});
|
|
254
935
|
}
|
|
255
|
-
|
|
256
|
-
function CompleteUser(): boolean
|
|
936
|
+
function processReturnedTenants(workspace: Workspace, ii: InitInfo, returnedTenants: Array<Object>)
|
|
257
937
|
{
|
|
258
|
-
|
|
938
|
+
returnedTenants.map((item) => {
|
|
939
|
+
// are we already tracking this tenant?
|
|
940
|
+
let tenant: Tenant|null = null;
|
|
941
|
+
let tsIndex = ii.ts.findIndex((t) => t.tid === item.tenantId);
|
|
942
|
+
if (tsIndex === -1) {
|
|
943
|
+
// start tracking
|
|
944
|
+
let dummyIndex = ii.ts.findIndex((t) => t.tid === "1");
|
|
945
|
+
if (dummyIndex !== -1) {
|
|
946
|
+
// clear and overwrite dummy
|
|
947
|
+
tenant = ii.ts.at(dummyIndex);
|
|
948
|
+
} else {
|
|
949
|
+
// create and track new workspace
|
|
950
|
+
tenant = new Tenant();
|
|
951
|
+
ii.ts.push(tenant);
|
|
952
|
+
}
|
|
953
|
+
} else {
|
|
954
|
+
// already tracking this tenant
|
|
955
|
+
tenant = ii.ts.at(tsIndex);
|
|
956
|
+
}
|
|
957
|
+
tenant.tid = item.tenantId;
|
|
958
|
+
tenant.name = item.name;
|
|
959
|
+
tenant.domain = item.domain;
|
|
960
|
+
tenant.tenantType = item.type.toLowerCase(); // should now be strings
|
|
961
|
+
tenant.permissionType = item.permissionType.toLowerCase(); // should now be strings
|
|
962
|
+
tenant.onboarded = item.isOnboarded ? "true" : "false";
|
|
963
|
+
tenant.authority = item.authority;
|
|
964
|
+
tenant.readServicePrincipal = item.readServicePrincipal;
|
|
965
|
+
tenant.writeServicePrincipal = item.writeServicePrincipal;
|
|
966
|
+
// ensure this workspace tracks this tenant
|
|
967
|
+
let idx = workspace.associatedTenants.findIndex((t) => t === item.tenantId);
|
|
968
|
+
if (idx == -1) workspace.associatedTenants.push(item.tenantId);
|
|
969
|
+
});
|
|
259
970
|
}
|
|
260
|
-
|
|
261
|
-
function CreateConfig(): boolean
|
|
971
|
+
function processReturnedConfigs(workspace: Workspace, ii: InitInfo, returnedConfigs: Array<Object>)
|
|
262
972
|
{
|
|
263
|
-
|
|
973
|
+
// process returned configs
|
|
974
|
+
returnedConfigs.map((item) => {
|
|
975
|
+
// are we already tracking this config?
|
|
976
|
+
let config: Config | null = null;
|
|
977
|
+
let csIndex = ii.cs.findIndex((c) => c.id === item.id);
|
|
978
|
+
if (csIndex === -1) {
|
|
979
|
+
// start tracking
|
|
980
|
+
let dummyIndex = ii.cs.findIndex((c) => c.id === "1");
|
|
981
|
+
if (dummyIndex !== -1) {
|
|
982
|
+
// clear and overwrite dummy
|
|
983
|
+
config = ii.cs.at(dummyIndex);
|
|
984
|
+
} else {
|
|
985
|
+
// create and track new workspace
|
|
986
|
+
config = new Config();
|
|
987
|
+
ii.cs.push(config);
|
|
988
|
+
}
|
|
989
|
+
} else {
|
|
990
|
+
// already tracking this config
|
|
991
|
+
config = ii.cs.at(csIndex);
|
|
992
|
+
}
|
|
993
|
+
config!.id = item.id;
|
|
994
|
+
config!.name = item.name;
|
|
995
|
+
config!.description = item.description;
|
|
996
|
+
config!.isEnabled = item.isEnabled;
|
|
997
|
+
// create TenantConfigInfo array
|
|
998
|
+
config!.tenants.length = 0;
|
|
999
|
+
item.tenants.map((tci) => {
|
|
1000
|
+
let tenantConfigInfo = new TenantConfigInfo();
|
|
1001
|
+
tenantConfigInfo.tid = tci.tenantId;
|
|
1002
|
+
tenantConfigInfo.sourceGroupId = tci.sourceGroupId;
|
|
1003
|
+
tenantConfigInfo.sourceGroupName = tci.sourceGroupName;
|
|
1004
|
+
tenantConfigInfo.configurationTenantType = tci.configurationTenantType.toLowerCase();
|
|
1005
|
+
config!.tenants.push(tenantConfigInfo);
|
|
1006
|
+
});
|
|
1007
|
+
// ensure this workspace tracks this config
|
|
1008
|
+
let idx = workspace.associatedConfigs.findIndex((c) => c === item.id);
|
|
1009
|
+
if (idx == -1) workspace.associatedConfigs.push(item.id);
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
async function workspaceInfoGet(instance: IPublicClientApplication, authorizedUser: User, user: User, ii: InitInfo, debug: boolean): Promise<APIResult> {
|
|
1013
|
+
let result: APIResult = new APIResult();
|
|
1014
|
+
if (debug) debugger;
|
|
1015
|
+
try {
|
|
1016
|
+
result = await workspacesGet(instance, authorizedUser, user, debug);
|
|
1017
|
+
if (result.result) {
|
|
1018
|
+
for (let o of result.array!) {
|
|
1019
|
+
// are we already tracking this workspace?
|
|
1020
|
+
let workspace: Workspace = null;
|
|
1021
|
+
let wsIndex = ii.ws.findIndex((w) => w.id === o.id);
|
|
1022
|
+
if (wsIndex === -1) {
|
|
1023
|
+
// start tracking
|
|
1024
|
+
let dummyIndex = ii.ws.findIndex((w) => w.id === "1");
|
|
1025
|
+
if(dummyIndex !== -1) {
|
|
1026
|
+
// clear and overwrite dummy
|
|
1027
|
+
workspace = ii.ws.at(dummyIndex);
|
|
1028
|
+
}
|
|
1029
|
+
else {
|
|
1030
|
+
// create and track new workspace
|
|
1031
|
+
workspace = new Workspace();
|
|
1032
|
+
ii.ws.push(workspace);
|
|
1033
|
+
}
|
|
1034
|
+
} else {
|
|
1035
|
+
// already tracking this workspace
|
|
1036
|
+
workspace = ii.ws.at(wsIndex);
|
|
1037
|
+
}
|
|
1038
|
+
// clear associations as we are about to reset
|
|
1039
|
+
workspace.associatedUsers.length = 0;
|
|
1040
|
+
workspace.associatedTenants.length = 0;
|
|
1041
|
+
workspace.associatedConfigs.length = 0;
|
|
1042
|
+
workspace.id = o.id;
|
|
1043
|
+
workspace.name = o.name;
|
|
1044
|
+
// parallel GET admins, tenants, configs associated with this workspace
|
|
1045
|
+
let adminsPromise: Promise<APIResult> = adminsGet(instance, authorizedUser, workspace.id, debug);
|
|
1046
|
+
let tenantsPromise: Promise<APIResult> = tenantsGet(instance, authorizedUser, workspace.id, debug);
|
|
1047
|
+
let configsPromise: Promise<APIResult> = configsGet(instance, authorizedUser, workspace.id, debug);
|
|
1048
|
+
// wait for all to finish, return on any failure
|
|
1049
|
+
let [adminsResult, tenantsResult, configsResult] = await Promise.all([adminsPromise, tenantsPromise, configsPromise]);
|
|
1050
|
+
if(!adminsResult.result) return adminsResult;
|
|
1051
|
+
if(!tenantsResult.result) return tenantsResult;
|
|
1052
|
+
if(!configsResult.result) return configsResult;
|
|
1053
|
+
// process returned workspace components
|
|
1054
|
+
processReturnedAdmins(workspace, ii, adminsResult.array!);
|
|
1055
|
+
processReturnedTenants(workspace, ii, tenantsResult.array!);
|
|
1056
|
+
processReturnedConfigs(workspace, ii, configsResult.array!);
|
|
1057
|
+
// tag components with workspaceIDs
|
|
1058
|
+
ii.tagWithWorkspaces();
|
|
1059
|
+
}
|
|
1060
|
+
return result;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
catch (error: any) {
|
|
1064
|
+
console.log(error.message);
|
|
1065
|
+
result.error = error.message;
|
|
1066
|
+
}
|
|
1067
|
+
result.result = false;
|
|
1068
|
+
result.status = 500;
|
|
1069
|
+
return result;
|
|
264
1070
|
}
|