@mindline/sync 1.0.36 → 1.0.37

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.
@@ -2,6 +2,5 @@
2
2
  "ExpandedNodes": [
3
3
  ""
4
4
  ],
5
- "SelectedNode": "\\hybridspa.ts",
6
5
  "PreviewInSolutionExplorer": false
7
6
  }
package/.vs/slnx.sqlite CHANGED
Binary file
Binary file
package/hybridspa.ts CHANGED
@@ -36,11 +36,17 @@ export const graphConfig = {
36
36
  graphGroupsEndpoint: "https://graph.microsoft.com/v1.0/groups",
37
37
  graphMailEndpoint: "https://graph.microsoft.com/v1.0/me/messages",
38
38
  graphMeEndpoint: "https://graph.microsoft.com/v1.0/me",
39
- graphTenantByDomainEndpoint:
40
- "https://graph.microsoft.com/beta/tenantRelationships/findTenantInformationByDomainName",
41
- graphTenantByIdEndpoint:
42
- "https://graph.microsoft.com/beta/tenantRelationships/findTenantInformationByTenantId",
43
39
  graphUsersEndpoint: "https://graph.microsoft.com/v1.0/users",
40
+ // sovereign cloud tenant info endpoints
41
+ graphTenantByDomainPredicate: "beta/tenantRelationships/findTenantInformationByDomainName",
42
+ graphTenantByIdPredicate: "beta/tenantRelationships/findTenantInformationByTenantId",
43
+ // authority values are based on the well-known OIDC auth endpoints
44
+ authorityWW: "https://login.microsoftonline.com/",
45
+ authorityWWRegex: /^(https:\/\/login\.microsoftonline\.(?:us|com)\/)([\dA-Fa-f]{8}-[\dA-Fa-f]{4}-[\dA-Fa-f]{4}-[\dA-Fa-f]{4}-[\dA-Fa-f]{12})\/oauth2\/authorize$/,
46
+ authorityUS: "https://login.microsoftonline.us/",
47
+ authorityUSRegex: /^(https:\/\/login\.microsoftonline\.(?:us|com)\/)([\dA-Fa-f]{8}-[\dA-Fa-f]{4}-[\dA-Fa-f]{4}-[\dA-Fa-f]{4}-[\dA-Fa-f]{12})\/oauth2\/authorize$/,
48
+ authorityCN: "https://login.partner.microsoftonline.cn/",
49
+ authorityCNRegex: /^(https:\/\/login\.partner\.microsoftonline\.cn\/)([\dA-Fa-f]{8}-[\dA-Fa-f]{4}-[\dA-Fa-f]{4}-[\dA-Fa-f]{4}-[\dA-Fa-f]{12})\/oauth2\/authorize$/,
44
50
  // reader endpoint to trigger sync start
45
51
  readerStartSyncEndpoint: "https://dev-fn-reader-westus.azurewebsites.net/api/startSync/",
46
52
  readerApiEndpoint: "https://dev-fn-reader-westus.azurewebsites.net/api/lookup/"
@@ -215,20 +221,9 @@ export async function adminPost(
215
221
  workspaceId: string
216
222
  ): Promise<APIResult> {
217
223
  let result: APIResult = new APIResult();
218
- if (
219
- user.mail == null ||
220
- user.mail === "" ||
221
- user.authority == null ||
222
- user.authority === "" ||
223
- user.tid == null ||
224
- user.tid === "" ||
225
- user.companyName == null ||
226
- user.companyName === "" ||
227
- user.companyDomain == null ||
228
- user.companyDomain === ""
229
- ) {
224
+ if (user.mail == "" || user.authority == "" || user.tid === "") {
230
225
  result.result = false;
231
- result.error = "configPost: invalid config ID";
226
+ result.error = "adminPost: invalid argument";
232
227
  result.status = 500;
233
228
  return result;
234
229
  }
package/index.d.ts CHANGED
@@ -139,13 +139,14 @@ declare module "@mindline/sync" {
139
139
  constructor(config: Config|null, syncPortalGlobalState: InitInfo|null, bClearLocalStorage: boolean);
140
140
  // populate tenantNodes based on config tenants
141
141
  init(config: Config|null, syncPortalGlobalState: InitInfo|null, bClearLocalStorage: boolean): void;
142
- startSync(instance: IPublicClientApplication, authorizedUser: User|null|undefined, config: Config|null|undefined): void;
142
+ startSync(instance: IPublicClientApplication, authorizedUser: User | null | undefined, config: Config | null | undefined, setConfigSyncResult: (syncUpdate: string) => void): APIResult;
143
143
  }
144
144
  export class TenantNode {
145
145
  expanded: boolean;
146
146
  status: string;
147
147
  name: string;
148
148
  tid: string;
149
+ batchId: string;
149
150
  total: number;
150
151
  read: number;
151
152
  written: number;
@@ -170,7 +171,8 @@ declare module "@mindline/sync" {
170
171
  export function signOut(user: User): void;
171
172
  export function tenantRelationshipsGetByDomain(loggedInuser: User, tenant: Tenant, instance: IPublicClientApplication, debug: boolean): boolean;
172
173
  export function tenantRelationshipsGetById(user: User, ii: InitInfo, instance: IPublicClientApplication, tasks: TaskArray, debug: boolean): boolean;
173
- export function usersGet(tenant: Tenant): {users: string[], error: string};
174
+ export function tenantUnauthenticatedLookup(tenant: Tenant, debug: boolean): Promise<boolean>;
175
+ export function usersGet(tenant: Tenant): { users: string[], error: string };
174
176
  //
175
177
  // Mindline Config API
176
178
  //
package/index.ts CHANGED
@@ -9,6 +9,7 @@ import tenants from "./tenants.json";
9
9
  import configs from "./configs.json";
10
10
  import workspaces from "./workspaces.json";
11
11
  import tasksData from "./tasks";
12
+ import { log } from "console";
12
13
  const FILTER_FIELD = "workspaceIDs";
13
14
  // called by unit tests
14
15
  export function sum(a: number, b: number): number {
@@ -42,7 +43,7 @@ export class User {
42
43
  this.oid = "";
43
44
  this.name = "";
44
45
  this.mail = "";
45
- this.authority = "https://login.microsoftonline.com/organizations/v2.0";
46
+ this.authority = "";
46
47
  this.tid = "";
47
48
  this.companyName = "";
48
49
  this.companyDomain = "";
@@ -87,12 +88,20 @@ export class Tenant {
87
88
  this.tenantType = "aad";
88
89
  this.permissionType = "notassigned";
89
90
  this.onboarded = "false";
90
- this.authority = "https://login.microsoftonline.com/organizations/v2.0";
91
+ this.authority = "";
91
92
  this.readServicePrincipal = "";
92
93
  this.writeServicePrincipal = "";
93
94
  this.workspaceIDs = "";
94
95
  }
95
96
  }
97
+ function getGraphEndpoint(authority: string): string {
98
+ switch (authority) {
99
+ case graphConfig.authorityWW: return "https://graph.microsoft.com/";
100
+ case graphConfig.authorityUS: return "https://graph.microsoft.us/";
101
+ case graphConfig.authorityCN: return "https://microsoftgraph.chinacloudapi.cn/";
102
+ default: debugger; return "";
103
+ }
104
+ }
96
105
  export enum TenantConfigType {
97
106
  source = 1,
98
107
  target = 2,
@@ -533,141 +542,179 @@ export class BatchArray {
533
542
  }
534
543
  }
535
544
  }
536
- /*
537
- monitorSyncProgress(): void {
538
- const connection = new signalR.HubConnectionBuilder()
539
- .withUrl("https://dev-signalrdispatcher-westus.azurewebsites.net/?statsId=df9c2e0a-f6fe-43bb-a155-d51f66dffe0e")
540
- .configureLogging(signalR.LogLevel.Information)
541
- .build();
542
-
543
- // when you get a message
544
- connection.on("newMessage", function (message) {
545
- console.log(message); // log the message
546
- const item = JSON.parse(message); //parse it into an object
547
- const statsarray = item.Stats; // get the array of statistics
548
- const statskeys = Object.keys(statsarray); // get the keys of the array
549
- const statsvalues = Object.values(statsarray); // get the values of the array
550
- let statistics = ""; // initialize statistics
551
- let total = 1;
552
- let currentR = 0;
553
- let currentW = 0;
554
- let currentD = 0;
555
- for (let j = 0; j < statskeys.length; j++) { // store the TotalCount and store as total
556
- if (statskeys[j].endsWith("TotalCount")) {
557
- total = statsvalues[j];
558
- }
559
- if (statskeys[j].endsWith("CurrentCount")) { // store the Reader value as currentR
560
- if (statskeys[j].startsWith("Reader")) {
561
- currentR = statsvalues[j];
545
+ // start a sync cycle
546
+ async startSync(instance: IPublicClientApplication, authorizedUser: User | null | undefined, config: Config | null | undefined, setConfigSyncResult: (syncUpdate: string) => void): Promise<APIResult> {
547
+ let result: APIResult = new APIResult();
548
+ if (this.tenantNodes == null || this.tenantNodes.length == 0) {
549
+ // we should not have an empty batch array for a test
550
+ debugger;
551
+ result.result = false;
552
+ result.error = "startSync: invalid parameters";
553
+ result.status = 500;
554
+ return result;
555
+ }
556
+ // define newMessage handler that can access *this*
557
+ let handler = (message) => {
558
+ console.log(message);
559
+ let item = JSON.parse(message);
560
+ // find the associated tenant for this SignalR message
561
+ let tenantNode: TenantNode = this.tenantNodes.find((t: TenantNode) => t.tid === item.TargetID);
562
+ if (tenantNode == null) { // null OR undefined
563
+ console.log(`${item.TargetID} not found in BatchArray.`);
564
+ debugger;
565
+ return;
566
+ }
567
+ let writerNode: TenantNode|null = null;
568
+ // process stats for this SignalR message
569
+ let statsarray = item.Stats; // get the array of statistics
570
+ let statskeys = Object.keys(statsarray); // get the keys of the array
571
+ let statsvalues = Object.values(statsarray); // get the values of the array
572
+ for (let j = 0; j < statskeys.length; j++) {
573
+ if (statskeys[j].startsWith("Reader")) {
574
+ if (statskeys[j].endsWith("TotalCount")) {
575
+ // parse batchId from key and store in TenantNode for this batch
576
+ let batchidRegexp = /Reader\/BID:(.+)\/TotalCount/;
577
+ let matchBID = statskeys[j].match(batchidRegexp);
578
+ if (matchBID == null) {
579
+ console.log(`batchId not found in ${statskeys[j]}.`);
580
+ debugger;
581
+ return;
582
+ }
583
+ // integrate any queued Writers that have been waiting for this batchId to be assigned to a Reader
584
+ let idx: number = this.tenantNodes.findIndex((t: TenantNode) => (t.name === "QUEUED WRITER" && t.batchId === matchBID[1]));
585
+ while (idx !== -1) {
586
+ let queuedWriter: TenantNode = this.tenantNodes.splice(idx, 1)[0];
587
+ let actualWriter: TenantNode = tenantNode.targets.find((t: TenantNode) => t.tid == queuedWriter.tid);
588
+ if (actualWriter == null) {
589
+ console.log(`could not find queued writer ${queuedWriter.tid}.`);
590
+ debugger;
591
+ }
592
+ else {
593
+ actualWriter.update(queuedWriter.total, queuedWriter.read, queuedWriter.written, queuedWriter.deferred);
594
+ }
595
+ idx = this.tenantNodes.findIndex((t: TenantNode) => (t.name === "QUEUED WRITER" && t.batchId === matchBID[1]));
596
+ }
597
+ // update Read node
598
+ tenantNode.batchId = matchBID[1];
599
+ tenantNode.total = Number(statsvalues[j]);
600
+ console.log(`----- ${tenantNode.name} batchId: ${tenantNode.batchId}`);
601
+ console.log(`----- ${tenantNode.name} Total Read: ${tenantNode.total}`);
562
602
  }
563
- if (statskeys[j].startsWith("Writer")) { // store the Writer value as currentW
564
- currentW = statsvalues[j];
603
+ if (statskeys[j].endsWith("CurrentCount")) {
604
+ tenantNode.read = Number(statsvalues[j]);
605
+ console.log(`----- ${tenantNode.name} Currently Read: ${tenantNode.read}`);
565
606
  }
566
607
  }
567
- if (statskeys[j].endsWith("DeferredCount")) { // store the deferred count
568
- currentD = statsvalues[j];
608
+ if (statskeys[j].startsWith("Writer")) {
609
+ if (statskeys[j].endsWith("TotalCount")) {
610
+ // parse batchId from Writer key
611
+ let batchidRegexp = /Writer\/BID:(.+)\/TotalCount/;
612
+ let matchBID = statskeys[j].match(batchidRegexp);
613
+ if (matchBID == null) {
614
+ console.log(`batchId not found in ${statskeys[j]}.`);
615
+ debugger;
616
+ return;
617
+ }
618
+ // find the Writer node for this tenant under the Reader node of a different tenant
619
+ let readerNode: TenantNode = this.tenantNodes.find((t: TenantNode) => t.batchId === matchBID[1]);
620
+ if (readerNode == null) {
621
+ // reader for this batch has not yet been encountered, queue this writer for processing when reader arrives
622
+ writerNode = new TenantNode(tenantNode.tid, "QUEUED WRITER");
623
+ writerNode.batchId = matchBID[1];
624
+ writerNode.total = Number(statsvalues[j]);
625
+ this.tenantNodes.push(writerNode);
626
+ console.log(`----- QUEUED batchId: ${writerNode.batchId}`);
627
+ console.log(`----- QUEUED Total To Write: ${writerNode.total}`);
628
+ }
629
+ else {
630
+ if (readerNode.name == "QUEUED WRITER") {
631
+ // update queued node
632
+ writerNode = readerNode;
633
+ }
634
+ else {
635
+ // reader is legit, find writer node under this reader node
636
+ writerNode = readerNode.targets.find((t: TenantNode) => t.tid === tenantNode.tid);
637
+ if (writerNode == null) {
638
+ console.log(`Writer ${tenantNode.name} not found under Reader ${readerNode.name}.`);
639
+ debugger;
640
+ return;
641
+ }
642
+ }
643
+ writerNode.batchId = matchBID[1];
644
+ writerNode.total = Number(statsvalues[j]);
645
+ console.log(`----- ${writerNode.name} batchId: ${writerNode.batchId}`);
646
+ console.log(`----- ${writerNode.name} Total To Write: ${writerNode.total}`);
647
+ }
648
+ }
649
+ if (statskeys[j].endsWith("CurrentCount")) {
650
+ // parse batchId from Writer key
651
+ let batchidRegexp = /Writer\/BID:(.+)\/CurrentCount/;
652
+ let matchBID = statskeys[j].match(batchidRegexp);
653
+ if (matchBID == null) {
654
+ console.log(`batchId not found in ${statskeys[j]}.`);
655
+ debugger;
656
+ return;
657
+ }
658
+ // find the Writer node for this tenant under the Reader node of a different tenant
659
+ let readerNode: TenantNode = this.tenantNodes.find((t: TenantNode) => t.batchId === matchBID[1]);
660
+ if (readerNode == null) {
661
+ // no reader or queued writer for this batch
662
+ console.log(`No reader or queued writer for batch ${matchBID[1]}.`);
663
+ debugger;
664
+ return;
665
+ }
666
+ if (readerNode.name == "QUEUED WRITER") {
667
+ // update queued node
668
+ writerNode = readerNode;
669
+ }
670
+ else {
671
+ // reader is legit, find writer node under this reader node
672
+ writerNode = readerNode.targets.find((t: TenantNode) => t.tid === tenantNode.tid);
673
+ if (writerNode == null) {
674
+ console.log(`Writer ${tenantNode.name} not found under Reader ${readerNode.name}.`);
675
+ debugger;
676
+ return;
677
+ }
678
+ }
679
+ writerNode.batchId = matchBID[1];
680
+ writerNode.written = Number(statsvalues[j]);
681
+ console.log(`${writerNode.name} batchId: ${writerNode.batchId}`);
682
+ console.log(`${writerNode.name} Total Written: ${writerNode.written}`);
683
+ }
569
684
  }
570
- statistics = statistics + statskeys[j] + "=" + statsvalues[j] + "<br />" //
571
685
  }
572
- updateProgress(total, currentR, currentW, currentD);
573
- myFunction(item.TargetID, statistics);
574
- });
575
-
576
- connection.start().catch(console.error);
577
-
578
- function myFunction(targetid, str) {
579
- const table = document.getElementById("the-table");
580
- let row = document.getElementById(targetid);
581
- if (row) {
582
- row.cells[1].innerHTML = "<code>" + str + "</ code>";
583
- } else {
584
- row = table.insertRow(1);
585
- row.id = targetid;
586
- let cell1 = row.insertCell(0);
587
- let cell2 = row.insertCell(1);
588
- let caption = (targetid === "00000000-0000-0000-0000-000000000000") ? "<B>Status of ServiceBus QUEUES</B>" : targetid;
589
- cell1.innerHTML = "<code>" + caption + "</ code>";
590
- cell2.innerHTML = "<code>" + str + "</ code>";
591
- }
592
- }
593
-
594
- function updateProgress(total, currentR, currentW, currentD) {
595
- updateRead(total, currentR);
596
- updateWrite(total, currentW, currentD);
597
- }
598
-
599
- function updateRead(total, current) {
600
- const element = document.getElementById("readBar");
601
- const width = Math.round(100 * current / total);
602
- element.style.width = width + '%';
603
- element.innerHTML = "R=" + width + '%';
604
- }
605
-
606
- function updateWrite(total, w, d) {
607
- const elementW = document.getElementById("writeBar");
608
- const widthW = Math.round(100 * w / total);
609
- elementW.style.width = widthW + '%';
610
- elementW.innerHTML = "W=" + widthW + '%';
611
-
612
- const elementD = document.getElementById("deferBar");
613
- let width = 0;
614
- let widthScaled = 0;
615
- if (d > 0) {
616
- width = Math.round(100 * d / total) + 1;
617
- if (width < 10) {
618
- widthScaled = width * 10;
619
- }
620
- else if (width < 20) {
621
- widthScaled = width * 5;
686
+ tenantNode.update(tenantNode.total, tenantNode.read, tenantNode.written, tenantNode.deferred);
687
+ if (writerNode != null) {
688
+ writerNode.update(writerNode.total, writerNode.read, writerNode.written, writerNode.deferred);
689
+ if (tenantNode.total === tenantNode.read && writerNode.total === writerNode.written) {
690
+ setConfigSyncResult("complete");
691
+ console.log(`Setting config sync result: "complete"`);
622
692
  }
623
693
  else {
624
- widthScaled = width;
694
+ setConfigSyncResult("writing in progress");
695
+ console.log(`Setting config sync result: "writing in progress"`);
625
696
  }
626
697
  }
627
- if (widthScaled + widthW >= 99) {
628
- widthScaled = 99 - widthW;
629
- }
630
- elementD.style.width = widthScaled + '%';
631
- elementD.innerHTML = width + '%';
632
- document.getElementById("deferred").innerHTML = 'Deferred:' + width + '% (' + d + ' of ' + total + ')';
633
- // cycle through desired states for sources and targets
634
- if (this.tenantNodes != null) {
635
- this.tenantNodes.map((sourceTenantNode: TenantNode) => {
636
- if (sourceTenantNode.read == 0) sourceTenantNode.update(100, 50, 0, 0);
637
- else if (sourceTenantNode.read == 50) sourceTenantNode.update(100, 100, 0, 0);
638
- else sourceTenantNode.update(0, 0, 0, 0);
639
- if (sourceTenantNode.targets != null) {
640
- sourceTenantNode.targets.map((targetTenantNode: TenantNode) => {
641
- if (targetTenantNode.written == 0) targetTenantNode.update(100, 0, 50, 0);
642
- else if (targetTenantNode.written == 50) targetTenantNode.update(100, 0, 100, 0);
643
- else if (targetTenantNode.written == 100) targetTenantNode.update(100, 0, 99, 1);
644
- else targetTenantNode.update(0, 0, 0, 0);
645
- });
646
- }
647
- });
698
+ else {
699
+ setConfigSyncResult("reading in progress");
700
+ console.log(`Setting config sync result: "reading in progress"`);
648
701
  }
649
702
  }
650
- }
651
- */
652
- // start a sync cycle
653
- startSync(instance: IPublicClientApplication, authorizedUser: User | null | undefined, config: Config | null | undefined): void {
654
- if (this.tenantNodes == null || this.tenantNodes.length == 0) {
655
- // we should not have an empty batch array for a test
656
- debugger;
657
- }
658
- // start SignalR connection
659
- const connection = new signalR.HubConnectionBuilder() // SignalR initialization
660
- .withUrl("https://dev-signalrdispatcher-westus.azurewebsites.net/statsHub?statsId=df9c2e0a-f6fe-43bb-a155-d51f66dffe0e", { skipNegotiation: true, transport: signalR.HttpTransportType.WebSockets } )
661
- .configureLogging(signalR.LogLevel.Information)
662
- .build();
663
- // when you get a message, log the message
664
- connection.on("newMessage", function (message) {
665
- console.log(message); // log the message
703
+ // start SignalR connection for each source tenant in the configuration
704
+ this.tenantNodes.map((sourceTenantNode: TenantNode) => {
705
+ const endpointUrl = `https://dev-signalrdispatcher-westus.azurewebsites.net/statsHub?statsId=${sourceTenantNode.tid}`;
706
+ const connection: signalR.HubConnection = new signalR.HubConnectionBuilder()
707
+ .withUrl(endpointUrl)
708
+ // .withAutomaticReconnect() TODO: full auto-reconnect implementation
709
+ .configureLogging(signalR.LogLevel.Information)
710
+ .build();
711
+ // when you get a message, process the message
712
+ connection.on("newMessage", handler);
713
+ connection.start().catch(console.error);
666
714
  });
667
- connection.start().catch(console.error);
668
715
  // execute post to reader endpoint
669
- readerPost(instance, authorizedUser, config);
670
- // refresh delta tokens for this Configuration (we read all Configurations at once)
716
+ result = await readerPost(instance, authorizedUser, config);
717
+ return result;
671
718
  }
672
719
  }
673
720
  export class TenantNode {
@@ -675,6 +722,7 @@ export class TenantNode {
675
722
  status: string;
676
723
  name: string;
677
724
  tid: string;
725
+ batchId: string;
678
726
  total: number;
679
727
  read: number;
680
728
  written: number;
@@ -684,6 +732,7 @@ export class TenantNode {
684
732
  this.expanded = false;
685
733
  this.name = name;
686
734
  this.tid = tid;
735
+ this.batchId = "";
687
736
  this.targets = new Array<TenantNode>();
688
737
  this.update(0, 0, 0, 0);
689
738
  }
@@ -815,9 +864,9 @@ export function signOut(user: User): void {
815
864
  export async function tenantRelationshipsGetByDomain(loggedInUser: User, tenant: Tenant, instance: IPublicClientApplication, debug: boolean): Promise<boolean> {
816
865
  if (debug) debugger;
817
866
  // do we already have a valid tenant name? if so, nothing to add
818
- if (typeof tenant.name !== 'undefined' && tenant.name !== "") return false;
867
+ if (tenant.name != null && tenant.name !== "") return false;
819
868
  // if needed, retrieve and cache access token
820
- if (typeof loggedInUser.accessToken === 'undefined' || loggedInUser.accessToken === "") {
869
+ if (loggedInUser.accessToken != null && loggedInUser.accessToken === "") {
821
870
  console.log(`tenantRelationshipsGetByDomain called with invalid logged in user: ${loggedInUser.name}`);
822
871
  try {
823
872
  let response: AuthenticationResult = await instance.acquireTokenByCode({ code: loggedInUser.spacode });
@@ -837,29 +886,32 @@ export async function tenantRelationshipsGetByDomain(loggedInUser: User, tenant:
837
886
  // make tenant endpoint call
838
887
  try {
839
888
  // create tenant info endpoint
840
- var tenantEndpoint = graphConfig.graphTenantByDomainEndpoint;
889
+ var tenantEndpoint = getGraphEndpoint(tenant.authority) + graphConfig.graphTenantByDomainPredicate;
841
890
  tenantEndpoint += "(domainName='";
842
891
  tenantEndpoint += tenant.domain;
843
892
  tenantEndpoint += "')";
844
893
  console.log("Attempting GET from /findTenantInformationByDomainName:", tenantEndpoint);
845
894
  let response = await fetch(tenantEndpoint, options);
846
- let data = await response.json();
847
- if (data) {
848
- if (typeof data.error !== "undefined") {
849
- console.log("Failed GET from /findTenantInformationByDomainName: ", data.error.message);
850
- return false;
895
+ if (response.status == 200 && response.statusText == "OK") {
896
+ let data = await response.json();
897
+ if (data) {
898
+ if (data.error != null) {
899
+ debugger;
900
+ console.log("Failed GET from /findTenantInformationByDomainName: ", data.error.message);
901
+ return false;
902
+ }
903
+ else if (data.displayName != null && data.displayName !== "") {
904
+ // set domain information on passed tenant
905
+ tenant.tid = data.tenantId;
906
+ tenant.name = data.displayName;
907
+ console.log("Successful GET from /findTenantInformationByDomainName: ", data.displayName);
908
+ return true; // success, need UX to re-render
909
+ }
851
910
  }
852
- else if (typeof data.displayName !== undefined && data.displayName !== "") {
853
- // set domain information on passed tenant
854
- tenant.tid = data.tenantId;
855
- tenant.name = data.displayName;
856
- console.log("Successful GET from /findTenantInformationByDomainName: ", data.displayName);
857
- return true; // success, need UX to re-render
911
+ else {
912
+ console.log("Failed to GET from /findTenantInformationByTenantId: ", tenantEndpoint);
858
913
  }
859
914
  }
860
- else {
861
- console.log("Failed to GET from /findTenantInformationByTenantId: ", tenantEndpoint);
862
- }
863
915
  }
864
916
  catch (error: any) {
865
917
  console.log("Failed to GET from /findTenantInformationByTenantId: ", error);
@@ -871,9 +923,9 @@ export async function tenantRelationshipsGetByDomain(loggedInUser: User, tenant:
871
923
  export async function tenantRelationshipsGetById(user: User, ii: InitInfo, instance: IPublicClientApplication, tasks: TaskArray, debug: boolean): Promise<boolean> {
872
924
  if (debug) debugger;
873
925
  // do we already have a valid company name? if so, nothing to add, no need for UX to re-render
874
- if (typeof user.companyName !== 'undefined' && user.companyName !== "") return false;
926
+ if (user.companyName != "") return false;
875
927
  // if needed, retrieve and cache access token
876
- if (typeof user.accessToken === 'undefined' || user.accessToken === "") {
928
+ if (user.accessToken === "") {
877
929
  try {
878
930
  let response: AuthenticationResult = await instance.acquireTokenByCode({ code: user.spacode });
879
931
  user.accessToken = response.accessToken; // cache access token
@@ -892,7 +944,7 @@ export async function tenantRelationshipsGetById(user: User, ii: InitInfo, insta
892
944
  // make tenant endpoint call
893
945
  try {
894
946
  // create tenant info endpoint
895
- var tenantEndpoint = graphConfig.graphTenantByIdEndpoint;
947
+ var tenantEndpoint = getGraphEndpoint(user.authority) + graphConfig.graphTenantByIdPredicate;
896
948
  tenantEndpoint += "(tenantId='";
897
949
  tenantEndpoint += user.tid;
898
950
  tenantEndpoint += "')";
@@ -931,6 +983,54 @@ export async function tenantRelationshipsGetById(user: User, ii: InitInfo, insta
931
983
  tasks.setTaskEnd("GET tenant details", new Date(), "failed");
932
984
  return false; // failed, no need for UX to re-render
933
985
  }
986
+ //tenantUnauthenticatedLookup (from https://gettenantpartitionweb.azurewebsites.net/js/gettenantpartition.js)
987
+ export async function tenantUnauthenticatedLookup(tenant: Tenant, debug: boolean): Promise<boolean> {
988
+ if (debug) debugger;
989
+ // do we already have a valid tenant ID? if so, nothing to add
990
+ if (tenant.tid !== "") return false;
991
+ // do we not have a valid domain? if so, nothing to lookup
992
+ if (tenant.domain == "") return false;
993
+ // prepare the 3 endpoints and corresponding regular expressions
994
+ let endpoints: string[] = [graphConfig.authorityWW, graphConfig.authorityUS, graphConfig.authorityCN];
995
+ let regexes: RegExp[] = [graphConfig.authorityWWRegex, graphConfig.authorityUSRegex, graphConfig.authorityCNRegex];
996
+ // make unauthenticated well-known openid endpoint call(s)
997
+ let response = null;
998
+ try {
999
+ for (let j = 0; j < 3; j++) {
1000
+ // create well-known openid endpoint
1001
+ var openidEndpoint = endpoints[j];
1002
+ openidEndpoint += tenant.domain;
1003
+ openidEndpoint += "/.well-known/openid-configuration";
1004
+ console.log("Attempting GET from openid well-known endpoint: ", openidEndpoint);
1005
+ response = await fetch(openidEndpoint);
1006
+ if (response.status == 200 && response.statusText == "OK") {
1007
+ let data = await response.json();
1008
+ if (data) {
1009
+ // store tenant ID and authority
1010
+ var tenantAuthEndpoint = data.authorization_endpoint;
1011
+ var authMatches = tenantAuthEndpoint.match(regexes[j]);
1012
+ tenant.tid = authMatches[2];
1013
+ tenant.authority = authMatches[1]; // USGov tenants are registered in WW with USGov authority values!
1014
+ console.log("Successful GET from openid well-known endpoint");
1015
+ return true; // success, need UX to re-render
1016
+ }
1017
+ else {
1018
+ console.log(`Failed JSON parse of openid well-known endpoint response ${openidEndpoint}.`);
1019
+ }
1020
+ }
1021
+ else {
1022
+ console.log(`Failed GET from ${openidEndpoint}.`);
1023
+ }
1024
+ }
1025
+ }
1026
+ catch (error: any) {
1027
+ console.log("Failed to GET from openid well-known endpoint: ", error);
1028
+ }
1029
+ if (tenant.tid == "" || tenant.authority == "") {
1030
+ console.log(`GET from openid well-known endpoint failed to find tenant: ${response ? response.statusText : "unknown"}`);
1031
+ }
1032
+ return false; // failed, no need for UX to re-render
1033
+ }
934
1034
  //usersGet - GET from AAD Users endpoint
935
1035
  export async function usersGet(tenant: Tenant): Promise<{ users: string[], error: string }> {
936
1036
  // need a read or write access token to get graph users
@@ -1011,23 +1111,45 @@ export async function configsRefresh(instance: IPublicClientApplication, authori
1011
1111
  export async function initGet(instance: IPublicClientApplication, authorizedUser: User, user: User, ii: InitInfo, tasks: TaskArray, debug: boolean): Promise<APIResult> {
1012
1112
  let result: APIResult = new APIResult();
1013
1113
  if (debug) debugger;
1014
- // get tenant name and domain from AAD
1015
- result.result = await tenantRelationshipsGetById(user, ii, instance, tasks, debug);
1016
- // 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
1017
- if (result.result) {
1018
- tasks.setTaskStart("POST config init", new Date());
1019
- result = await initPost(instance, authorizedUser, user, debug);
1020
- tasks.setTaskEnd("POST config init", new Date(), result.result ? "complete" : "failed");
1114
+ // lookup authority for this user (the lookup call does it based on domain, but TID works as well to find authority)
1115
+ let tenant: Tenant = new Tenant();
1116
+ tenant.domain = user.tid;
1117
+ let bResult: boolean = await tenantUnauthenticatedLookup(tenant, debug);
1118
+ if (bResult) {
1119
+ // success, we at least got authority as a new bit of information at this point
1120
+ user.authority = tenant.authority;
1121
+ // do we have a logged in user from the same authority as this newly proposed tenant?
1122
+ let loggedInUser: User | undefined = ii.us.find((u: User) => (u.session === "Sign Out" && u.authority === user.authority));
1123
+ if (loggedInUser != null) {
1124
+ // get tenant name and domain from AAD
1125
+ result.result = await tenantRelationshipsGetById(user, ii, instance, tasks, debug);
1126
+ // 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
1127
+ if (result.result) {
1128
+ tasks.setTaskStart("POST config init", new Date());
1129
+ result = await initPost(instance, authorizedUser, user, debug);
1130
+ tasks.setTaskEnd("POST config init", new Date(), result.result ? "complete" : "failed");
1131
+ }
1132
+ // simlarly, if we just did our first post, then query config backend for workspace(s) associated with this user
1133
+ if (result.result) {
1134
+ tasks.setTaskStart("GET workspaces", new Date());
1135
+ result = await workspaceInfoGet(instance, authorizedUser, user, ii, debug);
1136
+ tasks.setTaskEnd("GET workspaces", new Date(), result.result ? "complete" : "failed");
1137
+ }
1138
+ if (result.result) result.error = version;
1139
+ else console.log("@mindline/sync package version: " + version);
1140
+ return result;
1141
+ }
1142
+ else {
1143
+ result.error = `${user.mail} insufficient privileges to lookup under authority: ${user.authority}.`;
1144
+ result.result = false;
1145
+ return result;
1146
+ }
1021
1147
  }
1022
- // simlarly, if we just did our first post, then query config backend for workspace(s) associated with this user
1023
- if (result.result) {
1024
- tasks.setTaskStart("GET workspaces", new Date());
1025
- result = await workspaceInfoGet(instance, authorizedUser, user, ii, debug);
1026
- tasks.setTaskEnd("GET workspaces", new Date(), result.result ? "complete" : "failed");
1148
+ else {
1149
+ result.error = `Failed to retrieve authority for user "${user.mail}" TID ${user.tid}.`;
1150
+ result.result = false;
1151
+ return result;
1027
1152
  }
1028
- if (result.result) result.error = version;
1029
- else console.log("@mindline/sync package version: " + version);
1030
- return result;
1031
1153
  }
1032
1154
  export async function tenantAdd(instance: IPublicClientApplication, authorizedUser: User, tenant: Tenant, workspaceId: string): Promise<APIResult> {
1033
1155
  return tenantPost(instance, authorizedUser, tenant, workspaceId);
@@ -1091,7 +1213,7 @@ function processReturnedTenants(workspace: Workspace, ii: InitInfo, returnedTena
1091
1213
  // clear and overwrite dummy
1092
1214
  tenant = ii.ts.at(dummyIndex);
1093
1215
  } else {
1094
- // create and track new workspace
1216
+ // create and track new tenant
1095
1217
  tenant = new Tenant();
1096
1218
  ii.ts.push(tenant);
1097
1219
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mindline/sync",
3
3
  "type": "module",
4
- "version": "1.0.36",
4
+ "version": "1.0.37",
5
5
  "types": "index.d.ts",
6
6
  "exports": "./index.ts",
7
7
  "description": "sync is a node.js package encapsulating javscript classes required for configuring Mindline sync service.",
@@ -13,6 +13,9 @@
13
13
  "author": "",
14
14
  "license": "ISC",
15
15
  "devDependencies": {
16
+ "@types/react": "^18.2.21",
17
+ "@types/react-dom": "^18.2.7",
18
+ "typescript": "^5.2.2",
16
19
  "vitest": "^0.29.8"
17
20
  },
18
21
  "dependencies": {
package/tsconfig.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "lib": [ "DOM", "DOM.Iterable", "ESNext" ],
5
+ "types": [ "vite/client", "vite-plugin-svgr/client", "node", "jest" ],
6
+ "allowJs": false,
7
+ "skipLibCheck": false,
8
+ "esModuleInterop": false,
9
+ "allowSyntheticDefaultImports": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "module": "esnext",
12
+ "moduleResolution": "node",
13
+ "resolveJsonModule": true,
14
+ "isolatedModules": true,
15
+ "noEmit": true,
16
+ "jsx": "react-jsx",
17
+ "composite": false,
18
+
19
+ /* Linting */
20
+ "strict": true,
21
+ "noUnusedLocals": true,
22
+ "noUnusedParameters": true,
23
+ "noFallthroughCasesInSwitch": true
24
+ },
25
+ "include": ["src"],
26
+ "references": [{ "path": "./tsconfig.node.json" }]
27
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "compilerOptions": {
3
+ "composite": true,
4
+ "skipLibCheck": true,
5
+ "module": "ESNext",
6
+ "moduleResolution": "node",
7
+ "allowSyntheticDefaultImports": true
8
+ }
9
+ }