scouting: Re-authorize users when their auth expires

A few students noted at the San Francisco regional that they get 401
errors when trying to submit scouting data. I'm not entirely convinced
how to fix that, but this patch attempts to implement a work around.

Whenever a user tries to submit data, we check for the resulting
status code. If the code is 401, then the app now opens a new tab in
an attempt to re-authorize. The tab will close itself. When the tab
closes, the app will attempt to submit the scouting data again.

Change-Id: I3a4273c9c0f571f68332877f744c9c7013a7e66a
Signed-off-by: Philipp Schrader <philipp.schrader@gmail.com>
diff --git a/scouting/www/rpc/auth_handler.ts b/scouting/www/rpc/auth_handler.ts
new file mode 100644
index 0000000..02a790d
--- /dev/null
+++ b/scouting/www/rpc/auth_handler.ts
@@ -0,0 +1,54 @@
+import {Injectable} from '@angular/core';
+
+const AUTHORIZATION_URL = '/authorize';
+const AUTHORIZATION_CHECK_INTERVAL = 500; // ms
+const AUTHORIZATION_CHECK_DURATION = 20000; // ms
+const AUTHORIZATION_CHECK_ATTEMPTS =
+  AUTHORIZATION_CHECK_DURATION / AUTHORIZATION_CHECK_INTERVAL;
+
+@Injectable({providedIn: 'root'})
+export class RequestAuthorizer {
+  // Submits the buffer to the specified end point.
+  async submit(path: string, actionBuffer: Uint8Array): Promise<Response> {
+    const result = await this.singleSubmit(path, actionBuffer);
+    if (result.status === 401) {
+      await this.authorize();
+      return await this.singleSubmit(path, actionBuffer);
+    } else {
+      return result;
+    }
+  }
+
+  // Actually performs the underlying submission.
+  private async singleSubmit(
+    path: string,
+    actionBuffer: Uint8Array
+  ): Promise<Response> {
+    return await fetch(path, {
+      method: 'POST',
+      body: actionBuffer,
+    });
+  }
+
+  // Open a new tab in an attempt to re-authorize the user with the server.
+  private async authorize() {
+    const authorizationWindow = window.open(AUTHORIZATION_URL);
+    // We should deal with errors opening the tab, but this is good enough for now.
+
+    const tabIsClosed = new Promise<void>((resolve, reject) => {
+      let checkCounter = 0;
+      const tabCheckTimer = setInterval(() => {
+        if (authorizationWindow.closed) {
+          clearInterval(tabCheckTimer);
+          resolve();
+        }
+        checkCounter++;
+        if (checkCounter >= AUTHORIZATION_CHECK_ATTEMPTS) {
+          clearInterval(tabCheckTimer);
+          reject();
+        }
+      }, 500);
+    });
+    await tabIsClosed;
+  }
+}