account based feeds
authorxangelo <git@xangelo.ca>
Wed, 13 Apr 2022 14:54:08 +0000 (10:54 -0400)
committerxangelo <git@xangelo.ca>
Wed, 13 Apr 2022 14:54:08 +0000 (10:54 -0400)
The system now supports per-user account based feeds. This comes with
all the bells and whistles of generic account management:

- Signup/Login
This is done via email links. Right now the links don't do anything,
they simply display on screen and you navigate to them. Eventually we'll
set up postfix/sendgrid or something like that.

- All endpoints are now protected requiring a user to be logged in via
the magic email link/one-time code.

- A new home page that explains a bit about the project and gives you
the ability to signup/login.

- All feeds/feed-items are scoped to a user. We are definitely not doing
this optimally. Each feed is stored per-user.. if two users add the same
feeds.. then they get stored twice. We can address this if it actually
starts to become a problem.

html/app.html
html/home.css [new file with mode: 0644]
html/index.html
html/style.css
package-lock.json
package.json
sql/migrate.ts [new file with mode: 0644]
sql/seed.ts
src/ingester.ts
src/lib/db.ts
src/server.ts

index 0928724be0aa0d1b73fa324423e17248cfc49c16..e4c0b1de97ea505d026c4992f211d33861008d90 100644 (file)
@@ -3,16 +3,16 @@
   <head>
     <title>News River</title>
     <meta charset="utf-8">
-    <link rel="stylesheet" href="style.css">
+    <link rel="stylesheet" href="/style.css">
     <script src="https://unpkg.com/htmx.org@1.7.0"></script>
     <base target="_blank">
   </head>
   <body class="app">
-    <div id="feed-pane" valign="top" hx-get="/feeds" hx-trigger="load, newFeed from:body, every 900s" hx-target="#feed-list">
+    <div id="feed-pane" valign="top" hx-get="/accounts/{ACCOUNT_ID}/feeds" hx-trigger="load, newFeed from:body, every 900s" hx-target="#feed-list">
       <h1>Feed List</h1>
       <div id="feed-list"></div>
       <div id="add-feed">
-        <form hx-post="/feeds">
+        <form hx-post="/accounts/{ACCOUNT_ID}/feeds" hx-includes="#auth-token">
           <input type="url" name="link" placeholder="Link to RSS Feed">
           <button type="submit" class="btn">Add</button>
         </form>
     </div>
   </body>
   <script>
-    function $(sel, root, opts) {
-      const el = (root || document).querySelectorAll(sel);
-      if(opts && opts.array) { 
-        return Array.from(el);
-      }
-      if(el.length === 1) {
-        return el[0];
-      }
-      else {
-        return Array.from(el);
-      }
-    }
+function $(sel, root, opts) {
+  const el = (root || document).querySelectorAll(sel);
+  if(opts && opts.array) { 
+    return Array.from(el);
+  }
+  if(el.length === 1) {
+    return el[0];
+  }
+  else {
+    return Array.from(el);
+  }
+}
 
 function activeMarker(e) {
   // clear sibling active class.
@@ -50,31 +50,31 @@ function activeMarker(e) {
   e.target.classList.add('active');
 }
 
-    $('body').addEventListener('click', e => {
-      const actions = e.target.getAttribute('data-actions');
-      if(actions && actions.split(' ').includes('activate')) {
-        activeMarker(e);
-      }
-      if (e.target.classList.contains('unread') && e.target.getAttribute('data-feed-item-id') !== null) {
-        e.target.classList.remove('unread');
-        // ok it was unread.. so we should figure out the id that it belongs
-        // to and decr the counter for that!
-        const feedId = e.target.getAttribute('data-feed-id');
-        const el = $(`#feed-list a[data-feed-id="${feedId}"] .unread-count`);
-        if(el) {
-          if(el.innerHTML.length) {
-            let count = parseInt(el.innerHTML.split('(')[1].split(')')[0]);
-            count--;
-            if(count < 1) {
-              el.innerHTML = '';
-              el.parentElement.classList.remove('unread');
-            }
-            else {
-              el.innerHTML = `(${count})`;
-            }
-          }
+$('body').addEventListener('click', e => {
+  const actions = e.target.getAttribute('data-actions');
+  if(actions && actions.split(' ').includes('activate')) {
+    activeMarker(e);
+  }
+  if (e.target.classList.contains('unread') && e.target.getAttribute('data-feed-item-id') !== null) {
+    e.target.classList.remove('unread');
+    // ok it was unread.. so we should figure out the id that it belongs
+    // to and decr the counter for that!
+    const feedId = e.target.getAttribute('data-feed-id');
+    const el = $(`#feed-list a[data-feed-id="${feedId}"] .unread-count`);
+    if(el) {
+      if(el.innerHTML.length) {
+        let count = parseInt(el.innerHTML.split('(')[1].split(')')[0]);
+        count--;
+        if(count < 1) {
+          el.innerHTML = '';
+          el.parentElement.classList.remove('unread');
+        }
+        else {
+          el.innerHTML = `(${count})`;
         }
       }
-    });
+    }
+  }
+});
   </script>
 </html>
diff --git a/html/home.css b/html/home.css
new file mode 100644 (file)
index 0000000..d6b92d1
--- /dev/null
@@ -0,0 +1,35 @@
+body {
+  width: 60%;
+  padding: 0;
+  margin: 100px auto;
+  font-family: sans-serif;
+}
+h1 {
+  background-color: #eee;
+  padding: 4px 8px;
+  font-size: 2rem;
+}
+a {
+  color: #1e61df;
+}
+
+
+form {
+  border: solid 0 #eee;
+  border-width: 3px 0;
+  padding: 10px;
+  width: 60%;
+  margin: 50px auto;
+  text-align: center;
+}
+label {
+  font-weight: bold;
+  margin-right: 10px;
+}
+input {
+  padding: 5px;
+}
+button {
+  padding: 5px 10px;
+  cursor: pointer;
+}
index 47d890f6e183f44134a6ffa9b06ee58b55880db0..b2313c2ff9b25a603b9885a8bf9b838a7b7212a7 100644 (file)
@@ -3,78 +3,26 @@
   <head>
     <title>News River</title>
     <meta charset="utf-8">
-    <link rel="stylesheet" href="style.css">
+    <link rel="stylesheet" href="home.css">
     <script src="https://unpkg.com/htmx.org@1.7.0"></script>
     <base target="_blank">
   </head>
-  <body class="app">
-    <div id="feed-pane" valign="top" hx-get="/feeds" hx-trigger="load, newFeed from:body, every 900s" hx-target="#feed-list">
-      <h1>Feed List</h1>
-      <div id="feed-list"></div>
-      <div id="add-feed">
-        <form hx-post="/feeds">
-          <input type="url" name="link" placeholder="Link to RSS Feed">
-          <button type="submit" class="btn">Add</button>
-        </form>
-      </div>
-    </div>
-    <div id="list-pane"></div>
-    <div id="reading-pane"></div>
-    <div id="status-bar">
-      A project by <a href="https://xangelo.ca">xangelo</a> that is still in active <a href="https://git.xangelo.ca">development</a> (2022)
-    </div>
+  <body>
+    <section>
+      <h1>Rss Reader</h1>
+      <p>This is the home page for a very simple RSS reader project by <a href="https://xangelo.ca">xangelo</a>.</p>
+      <p>I've been a long time fan of RSS and since the death of <a href="https://en.wikipedia.org/wiki/Google_Reader">Google Reader</a> I've found myself bouncing around from RSS reader to RSS reader trying to find something that really worked for me. I spent along time (many years) using a river-of-news style reader I wrote called, unsurprisingly, <a href="https://newsriver.xangelo.ca">NewsRiver</a>. I am definitely a big fan of this style of reader, but I wanted to get back to curating a list of blogs/authors that I enjoyed following.</p>
+      <p>I ended up writing my own RSS Reader and will be working over the next little while to merge the News River functionality in. I'm hoping this will be the final version of the app where I'm able to not just catch up on "news", but also a centralized space for me to keep up with blogs/articles.</p>
+      <p>If you're interested in RSS readers, feel free to sign up and poke around - I would definitely appreciate you letting me know of any bugs that you run in to.</p>
+      <p>I'm also a big fan of supporting user privacy - I'm not going to ask for any information more than your email as a way to keep track of the feeds that are associated wtih you. If at any point you want your account deleted, just let me know.</p>
+      <form hx-post="/login">
+        <label>Email: </label>
+        <input type="email" name="email"> <button class="btn" type="submit">Login</button>
+        <br>
+        <small>
+          This is a "Passwordless" system. You enter your email, and we email you a one-time login code that expires within 30 minutes.
+        </small>
+      </form>
+    </section>
   </body>
-  <script>
-    function $(sel, root, opts) {
-      const el = (root || document).querySelectorAll(sel);
-      if(opts && opts.array) { 
-        return Array.from(el);
-      }
-      if(el.length === 1) {
-        return el[0];
-      }
-      else {
-        return Array.from(el);
-      }
-    }
-
-function activeMarker(e) {
-  // clear sibling active class.
-  if(e.target.parentElement.tagName === 'LI') {
-    const parent = e.target.parentElement.parentElement;
-    $('a.active', parent, {array: true}).forEach(el => {
-      el.classList.remove('active');
-    });
-  }
-
-  e.target.classList.add('active');
-}
-
-    $('body').addEventListener('click', e => {
-      const actions = e.target.getAttribute('data-actions');
-      if(actions && actions.split(' ').includes('activate')) {
-        activeMarker(e);
-      }
-      if (e.target.classList.contains('unread') && e.target.getAttribute('data-feed-item-id') !== null) {
-        e.target.classList.remove('unread');
-        // ok it was unread.. so we should figure out the id that it belongs
-        // to and decr the counter for that!
-        const feedId = e.target.getAttribute('data-feed-id');
-        const el = $(`#feed-list a[data-feed-id="${feedId}"] .unread-count`);
-        if(el) {
-          if(el.innerHTML.length) {
-            let count = parseInt(el.innerHTML.split('(')[1].split(')')[0]);
-            count--;
-            if(count < 1) {
-              el.innerHTML = '';
-              el.parentElement.classList.remove('unread');
-            }
-            else {
-              el.innerHTML = `(${count})`;
-            }
-          }
-        }
-      }
-    });
-  </script>
 </html>
index d7fdaa761806159e59f84c6350ae7407da58db35..cab43006e3377dd4e3b45198e36538ed912f44a6 100644 (file)
@@ -7,6 +7,7 @@ html, body {
   overflow: hidden;
 }
 
+
 h1,h2,h3,h4,h5,h6 {
   font-size: 1rem;
 }
@@ -159,3 +160,7 @@ a {
 #feed-item-list a.unread {
   font-weight: bold;
 }
+
+.hidden {
+  display: none !important;
+}
index 111e2478613be0eb6aec7e4779bfe5a18c6c573f..bf252ee734eda11ea21758d1b38b0eb206ca568f 100644 (file)
@@ -8,10 +8,12 @@
       "name": "newsriver",
       "version": "2.0-1",
       "dependencies": {
+        "axios": "^0.26.1",
         "better-sqlite3": "^7.5.0",
         "body-parser": "^1.20.0",
         "dotenv": "^16.0.0",
         "express": "^4.17.3",
+        "express-openid-connect": "^2.7.2",
         "moment": "^2.29.2",
         "rss-parser": "^3.12.0",
         "sqlite3": "^5.0.2",
@@ -20,7 +22,9 @@
       "devDependencies": {
         "@types/better-sqlite3": "^7.5.0",
         "@types/express": "^4.17.13",
+        "@types/minimist": "^1.2.2",
         "@types/uuid": "^8.3.4",
+        "minimist": "^1.2.6",
         "nodemon": "^2.0.15",
         "ts-node": "^10.7.0",
         "tsconfig-paths": "^3.14.1",
         "node": ">=12"
       }
     },
+    "node_modules/@hapi/hoek": {
+      "version": "9.2.1",
+      "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.2.1.tgz",
+      "integrity": "sha512-gfta+H8aziZsm8pZa0vj04KO6biEiisppNgA1kbJvFrrWu9Vm7eaUEy76DIxsuTaWvti5fkJVhllWc6ZTE+Mdw=="
+    },
+    "node_modules/@hapi/topo": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz",
+      "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==",
+      "dependencies": {
+        "@hapi/hoek": "^9.0.0"
+      }
+    },
+    "node_modules/@panva/asn1.js": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz",
+      "integrity": "sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw==",
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/@sideway/address": {
+      "version": "4.1.4",
+      "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz",
+      "integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==",
+      "dependencies": {
+        "@hapi/hoek": "^9.0.0"
+      }
+    },
+    "node_modules/@sideway/formula": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz",
+      "integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg=="
+    },
+    "node_modules/@sideway/pinpoint": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
+      "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="
+    },
     "node_modules/@sindresorhus/is": {
       "version": "0.14.0",
       "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
         "@types/node": "*"
       }
     },
+    "node_modules/@types/cacheable-request": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.2.tgz",
+      "integrity": "sha512-B3xVo+dlKM6nnKTcmm5ZtY/OL8bOAOd2Olee9M1zft65ox50OzjEHW91sDiU9j6cvW8Ejg1/Qkf4xd2kugApUA==",
+      "dependencies": {
+        "@types/http-cache-semantics": "*",
+        "@types/keyv": "*",
+        "@types/node": "*",
+        "@types/responselike": "*"
+      }
+    },
     "node_modules/@types/connect": {
       "version": "3.4.35",
       "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
         "@types/range-parser": "*"
       }
     },
+    "node_modules/@types/http-cache-semantics": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz",
+      "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ=="
+    },
+    "node_modules/@types/json-buffer": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/@types/json-buffer/-/json-buffer-3.0.0.tgz",
+      "integrity": "sha512-3YP80IxxFJB4b5tYC2SUPwkg0XQLiu0nWvhRgEatgjf+29IcWO9X1k8xRv5DGssJ/lCrjYTjQPcobJr2yWIVuQ=="
+    },
     "node_modules/@types/json5": {
       "version": "0.0.29",
       "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
       "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=",
       "dev": true
     },
+    "node_modules/@types/keyv": {
+      "version": "3.1.4",
+      "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
+      "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
     "node_modules/@types/mime": {
       "version": "1.3.2",
       "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
       "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==",
       "dev": true
     },
+    "node_modules/@types/minimist": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz",
+      "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==",
+      "dev": true
+    },
     "node_modules/@types/node": {
       "version": "17.0.23",
       "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.23.tgz",
-      "integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==",
-      "dev": true
+      "integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw=="
     },
     "node_modules/@types/qs": {
       "version": "6.9.7",
       "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==",
       "dev": true
     },
+    "node_modules/@types/responselike": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz",
+      "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
     "node_modules/@types/serve-static": {
       "version": "1.13.10",
       "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz",
         "node": ">=0.4.0"
       }
     },
+    "node_modules/aggregate-error": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
+      "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
+      "dependencies": {
+        "clean-stack": "^2.0.0",
+        "indent-string": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/ajv": {
       "version": "6.12.6",
       "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
       "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==",
       "optional": true
     },
+    "node_modules/axios": {
+      "version": "0.26.1",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
+      "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
+      "dependencies": {
+        "follow-redirects": "^1.14.8"
+      }
+    },
     "node_modules/balanced-match": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
         }
       ]
     },
+    "node_modules/base64url": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
+      "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
     "node_modules/bcrypt-pbkdf": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
         "node": ">= 0.8"
       }
     },
+    "node_modules/cacheable-lookup": {
+      "version": "5.0.4",
+      "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
+      "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==",
+      "engines": {
+        "node": ">=10.6.0"
+      }
+    },
     "node_modules/cacheable-request": {
       "version": "6.1.0",
       "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz",
       "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=",
       "optional": true
     },
+    "node_modules/cb": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/cb/-/cb-0.1.0.tgz",
+      "integrity": "sha1-JvfnQPKAj6zIPO97IDkuTYgbUgM=",
+      "engines": {
+        "node": ">=0.6.0"
+      }
+    },
     "node_modules/chalk": {
       "version": "4.1.2",
       "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
       "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==",
       "dev": true
     },
+    "node_modules/clean-stack": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
+      "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/cli-boxes": {
       "version": "2.2.1",
       "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz",
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/clone": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
+      "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=",
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/clone-response": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz",
       "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=",
-      "dev": true,
       "dependencies": {
         "mimic-response": "^1.0.0"
       }
         "node": ">= 0.8"
       }
     },
+    "node_modules/compress-brotli": {
+      "version": "1.3.6",
+      "resolved": "https://registry.npmjs.org/compress-brotli/-/compress-brotli-1.3.6.tgz",
+      "integrity": "sha512-au99/GqZtUtiCBliqLFbWlhnCxn+XSYjwZ77q6mKN4La4qOXDoLVPZ50iXr0WmAyMxl8yqoq3Yq4OeQNPPkyeQ==",
+      "dependencies": {
+        "@types/json-buffer": "~3.0.0",
+        "json-buffer": "~3.0.1"
+      },
+      "engines": {
+        "node": ">= 12"
+      }
+    },
+    "node_modules/compress-brotli/node_modules/json-buffer": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+      "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="
+    },
     "node_modules/concat-map": {
       "version": "0.0.1",
       "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
         "node": ">= 0.10.0"
       }
     },
+    "node_modules/express-openid-connect": {
+      "version": "2.7.2",
+      "resolved": "https://registry.npmjs.org/express-openid-connect/-/express-openid-connect-2.7.2.tgz",
+      "integrity": "sha512-NK/p0P9se8qUcH7ciTVST0rEGBZcefpjGWBEDIgHPjTPNKa7eaTqWAsGAs0d2+dHgZQmSQ7zP1jXH2CwKicyyw==",
+      "dependencies": {
+        "base64url": "^3.0.1",
+        "cb": "^0.1.0",
+        "clone": "^2.1.2",
+        "cookie": "^0.4.1",
+        "debug": "^4.3.3",
+        "futoin-hkdf": "^1.5.0",
+        "http-errors": "^1.8.1",
+        "joi": "^17.6.0",
+        "jose": "^2.0.5",
+        "on-headers": "^1.0.2",
+        "openid-client": "^4.9.1",
+        "p-memoize": "^4.0.4",
+        "url-join": "^4.0.1"
+      },
+      "engines": {
+        "node": "^10.19.0 || >=12.0.0 < 13 || >=13.7.0 < 14 || >= 14.2.0"
+      },
+      "peerDependencies": {
+        "express": ">= 4.17.0"
+      }
+    },
+    "node_modules/express-openid-connect/node_modules/debug": {
+      "version": "4.3.4",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+      "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+      "dependencies": {
+        "ms": "2.1.2"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/express-openid-connect/node_modules/ms": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+    },
     "node_modules/express/node_modules/body-parser": {
       "version": "1.19.2",
       "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.2.tgz",
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
       "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
     },
+    "node_modules/follow-redirects": {
+      "version": "1.14.9",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
+      "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/forever-agent": {
       "version": "0.6.1",
       "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
       "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
       "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
     },
+    "node_modules/futoin-hkdf": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/futoin-hkdf/-/futoin-hkdf-1.5.0.tgz",
+      "integrity": "sha512-4CerDhtTgx4i5PKccQIpEp4T9wqmosPIP9Kep35SdCpYkQeriD3zddUVhrO1Fc4QvGhsAnd2rXyoOr5047mJEg==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/gauge": {
       "version": "2.7.4",
       "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
     "node_modules/http-cache-semantics": {
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz",
-      "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==",
-      "dev": true
+      "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ=="
     },
     "node_modules/http-errors": {
       "version": "1.8.1",
         "npm": ">=1.3.7"
       }
     },
+    "node_modules/http2-wrapper": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
+      "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==",
+      "dependencies": {
+        "quick-lru": "^5.1.1",
+        "resolve-alpn": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=10.19.0"
+      }
+    },
     "node_modules/iconv-lite": {
       "version": "0.4.24",
       "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
         "node": ">=0.8.19"
       }
     },
+    "node_modules/indent-string": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+      "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/inflight": {
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
       "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=",
       "optional": true
     },
+    "node_modules/joi": {
+      "version": "17.6.0",
+      "resolved": "https://registry.npmjs.org/joi/-/joi-17.6.0.tgz",
+      "integrity": "sha512-OX5dG6DTbcr/kbMFj0KGYxuew69HPcAE3K/sZpEV2nP6e/j/C0HV+HNiBPCASxdx5T7DMoa0s8UeHWMnb6n2zw==",
+      "dependencies": {
+        "@hapi/hoek": "^9.0.0",
+        "@hapi/topo": "^5.0.0",
+        "@sideway/address": "^4.1.3",
+        "@sideway/formula": "^3.0.0",
+        "@sideway/pinpoint": "^2.0.0"
+      }
+    },
+    "node_modules/jose": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/jose/-/jose-2.0.5.tgz",
+      "integrity": "sha512-BAiDNeDKTMgk4tvD0BbxJ8xHEHBZgpeRZ1zGPPsitSyMgjoMWiLGYAE7H7NpP5h0lPppQajQs871E8NHUrzVPA==",
+      "dependencies": {
+        "@panva/asn1.js": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=10.13.0 < 13 || >=13.7.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/panva"
+      }
+    },
     "node_modules/jsbn": {
       "version": "0.1.1",
       "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
     "node_modules/make-error": {
       "version": "1.3.6",
       "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
-      "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
-      "dev": true
+      "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="
+    },
+    "node_modules/map-age-cleaner": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz",
+      "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==",
+      "dependencies": {
+        "p-defer": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
     },
     "node_modules/media-typer": {
       "version": "0.3.0",
         "node": ">= 0.6"
       }
     },
+    "node_modules/mimic-fn": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz",
+      "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/mimic-response": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
       "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
-      "dev": true,
       "engines": {
         "node": ">=4"
       }
         "node": ">=0.10.0"
       }
     },
+    "node_modules/object-hash": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
+      "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
     "node_modules/object-inspect": {
       "version": "1.12.0",
       "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz",
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/oidc-token-hash": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.1.tgz",
+      "integrity": "sha512-EvoOtz6FIEBzE+9q253HsLCVRiK/0doEJ2HCvvqMQb3dHZrP3WlJKYtJ55CRTw4jmYomzH4wkPuCj/I3ZvpKxQ==",
+      "engines": {
+        "node": "^10.13.0 || >=12.0.0"
+      }
+    },
     "node_modules/on-finished": {
       "version": "2.3.0",
       "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
         "node": ">= 0.8"
       }
     },
+    "node_modules/on-headers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
+      "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/once": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
         "wrappy": "1"
       }
     },
+    "node_modules/openid-client": {
+      "version": "4.9.1",
+      "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-4.9.1.tgz",
+      "integrity": "sha512-DYUF07AHjI3QDKqKbn2F7RqozT4hyi4JvmpodLrq0HHoNP7t/AjeG/uqiBK1/N2PZSAQEThVjDLHSmJN4iqu/w==",
+      "dependencies": {
+        "aggregate-error": "^3.1.0",
+        "got": "^11.8.0",
+        "jose": "^2.0.5",
+        "lru-cache": "^6.0.0",
+        "make-error": "^1.3.6",
+        "object-hash": "^2.0.1",
+        "oidc-token-hash": "^5.0.1"
+      },
+      "engines": {
+        "node": "^10.19.0 || >=12.0.0 < 13 || >=13.7.0 < 14 || >= 14.2.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/panva"
+      }
+    },
+    "node_modules/openid-client/node_modules/@sindresorhus/is": {
+      "version": "4.6.0",
+      "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
+      "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sindresorhus/is?sponsor=1"
+      }
+    },
+    "node_modules/openid-client/node_modules/@szmarczak/http-timer": {
+      "version": "4.0.6",
+      "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
+      "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==",
+      "dependencies": {
+        "defer-to-connect": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/openid-client/node_modules/cacheable-request": {
+      "version": "7.0.2",
+      "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz",
+      "integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==",
+      "dependencies": {
+        "clone-response": "^1.0.2",
+        "get-stream": "^5.1.0",
+        "http-cache-semantics": "^4.0.0",
+        "keyv": "^4.0.0",
+        "lowercase-keys": "^2.0.0",
+        "normalize-url": "^6.0.1",
+        "responselike": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/openid-client/node_modules/decompress-response": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
+      "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+      "dependencies": {
+        "mimic-response": "^3.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/openid-client/node_modules/defer-to-connect": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
+      "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/openid-client/node_modules/get-stream": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
+      "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
+      "dependencies": {
+        "pump": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/openid-client/node_modules/got": {
+      "version": "11.8.3",
+      "resolved": "https://registry.npmjs.org/got/-/got-11.8.3.tgz",
+      "integrity": "sha512-7gtQ5KiPh1RtGS9/Jbv1ofDpBFuq42gyfEib+ejaRBJuj/3tQFeR5+gw57e4ipaU8c/rCjvX6fkQz2lyDlGAOg==",
+      "dependencies": {
+        "@sindresorhus/is": "^4.0.0",
+        "@szmarczak/http-timer": "^4.0.5",
+        "@types/cacheable-request": "^6.0.1",
+        "@types/responselike": "^1.0.0",
+        "cacheable-lookup": "^5.0.3",
+        "cacheable-request": "^7.0.2",
+        "decompress-response": "^6.0.0",
+        "http2-wrapper": "^1.0.0-beta.5.2",
+        "lowercase-keys": "^2.0.0",
+        "p-cancelable": "^2.0.0",
+        "responselike": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=10.19.0"
+      },
+      "funding": {
+        "url": "https://github.com/sindresorhus/got?sponsor=1"
+      }
+    },
+    "node_modules/openid-client/node_modules/json-buffer": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+      "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="
+    },
+    "node_modules/openid-client/node_modules/keyv": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.2.2.tgz",
+      "integrity": "sha512-uYS0vKTlBIjNCAUqrjlxmruxOEiZxZIHXyp32sdcGmP+ukFrmWUnE//RcPXJH3Vxrni1H2gsQbjHE0bH7MtMQQ==",
+      "dependencies": {
+        "compress-brotli": "^1.3.6",
+        "json-buffer": "3.0.1"
+      }
+    },
+    "node_modules/openid-client/node_modules/lowercase-keys": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
+      "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/openid-client/node_modules/mimic-response": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
+      "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/openid-client/node_modules/normalize-url": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
+      "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/openid-client/node_modules/p-cancelable": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
+      "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/openid-client/node_modules/responselike": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.0.tgz",
+      "integrity": "sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==",
+      "dependencies": {
+        "lowercase-keys": "^2.0.0"
+      }
+    },
     "node_modules/os-homedir": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
         "node": ">=6"
       }
     },
+    "node_modules/p-defer": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz",
+      "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/p-limit": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+      "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+      "dependencies": {
+        "p-try": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/p-memoize": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/p-memoize/-/p-memoize-4.0.4.tgz",
+      "integrity": "sha512-ijdh0DP4Mk6J4FXlOM6vPPoCjPytcEseW8p/k5SDTSSfGV3E9bpt9Yzfifvzp6iohIieoLTkXRb32OWV0fB2Lw==",
+      "dependencies": {
+        "map-age-cleaner": "^0.1.3",
+        "mimic-fn": "^3.0.0",
+        "p-settle": "^4.1.1"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sindresorhus/p-memoize?sponsor=1"
+      }
+    },
+    "node_modules/p-reflect": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/p-reflect/-/p-reflect-2.1.0.tgz",
+      "integrity": "sha512-paHV8NUz8zDHu5lhr/ngGWQiW067DK/+IbJ+RfZ4k+s8y4EKyYCz8pGYWjxCg35eHztpJAt+NUgvN4L+GCbPlg==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/p-settle": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/p-settle/-/p-settle-4.1.1.tgz",
+      "integrity": "sha512-6THGh13mt3gypcNMm0ADqVNCcYa3BK6DWsuJWFCuEKP1rpY+OKGp7gaZwVmLspmic01+fsg/fN57MfvDzZ/PuQ==",
+      "dependencies": {
+        "p-limit": "^2.2.2",
+        "p-reflect": "^2.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/p-try": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+      "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/package-json": {
       "version": "6.5.0",
       "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz",
         "node": ">=0.6"
       }
     },
+    "node_modules/quick-lru": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
+      "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/range-parser": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
         "uuid": "bin/uuid"
       }
     },
+    "node_modules/resolve-alpn": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
+      "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="
+    },
     "node_modules/responselike": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz",
         "punycode": "^2.1.0"
       }
     },
+    "node_modules/url-join": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
+      "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA=="
+    },
     "node_modules/url-parse-lax": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz",
         "@cspotcode/source-map-consumer": "0.8.0"
       }
     },
+    "@hapi/hoek": {
+      "version": "9.2.1",
+      "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.2.1.tgz",
+      "integrity": "sha512-gfta+H8aziZsm8pZa0vj04KO6biEiisppNgA1kbJvFrrWu9Vm7eaUEy76DIxsuTaWvti5fkJVhllWc6ZTE+Mdw=="
+    },
+    "@hapi/topo": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz",
+      "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==",
+      "requires": {
+        "@hapi/hoek": "^9.0.0"
+      }
+    },
+    "@panva/asn1.js": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz",
+      "integrity": "sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw=="
+    },
+    "@sideway/address": {
+      "version": "4.1.4",
+      "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz",
+      "integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==",
+      "requires": {
+        "@hapi/hoek": "^9.0.0"
+      }
+    },
+    "@sideway/formula": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz",
+      "integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg=="
+    },
+    "@sideway/pinpoint": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
+      "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="
+    },
     "@sindresorhus/is": {
       "version": "0.14.0",
       "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
         "@types/node": "*"
       }
     },
+    "@types/cacheable-request": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.2.tgz",
+      "integrity": "sha512-B3xVo+dlKM6nnKTcmm5ZtY/OL8bOAOd2Olee9M1zft65ox50OzjEHW91sDiU9j6cvW8Ejg1/Qkf4xd2kugApUA==",
+      "requires": {
+        "@types/http-cache-semantics": "*",
+        "@types/keyv": "*",
+        "@types/node": "*",
+        "@types/responselike": "*"
+      }
+    },
     "@types/connect": {
       "version": "3.4.35",
       "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
         "@types/range-parser": "*"
       }
     },
+    "@types/http-cache-semantics": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz",
+      "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ=="
+    },
+    "@types/json-buffer": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/@types/json-buffer/-/json-buffer-3.0.0.tgz",
+      "integrity": "sha512-3YP80IxxFJB4b5tYC2SUPwkg0XQLiu0nWvhRgEatgjf+29IcWO9X1k8xRv5DGssJ/lCrjYTjQPcobJr2yWIVuQ=="
+    },
     "@types/json5": {
       "version": "0.0.29",
       "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
       "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=",
       "dev": true
     },
+    "@types/keyv": {
+      "version": "3.1.4",
+      "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
+      "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==",
+      "requires": {
+        "@types/node": "*"
+      }
+    },
     "@types/mime": {
       "version": "1.3.2",
       "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
       "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==",
       "dev": true
     },
+    "@types/minimist": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz",
+      "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==",
+      "dev": true
+    },
     "@types/node": {
       "version": "17.0.23",
       "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.23.tgz",
-      "integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==",
-      "dev": true
+      "integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw=="
     },
     "@types/qs": {
       "version": "6.9.7",
       "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==",
       "dev": true
     },
+    "@types/responselike": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz",
+      "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==",
+      "requires": {
+        "@types/node": "*"
+      }
+    },
     "@types/serve-static": {
       "version": "1.13.10",
       "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz",
       "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
       "dev": true
     },
+    "aggregate-error": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
+      "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
+      "requires": {
+        "clean-stack": "^2.0.0",
+        "indent-string": "^4.0.0"
+      }
+    },
     "ajv": {
       "version": "6.12.6",
       "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
       "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==",
       "optional": true
     },
+    "axios": {
+      "version": "0.26.1",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
+      "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
+      "requires": {
+        "follow-redirects": "^1.14.8"
+      }
+    },
     "balanced-match": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
       "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
       "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
     },
+    "base64url": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
+      "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A=="
+    },
     "bcrypt-pbkdf": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
       "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
       "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="
     },
+    "cacheable-lookup": {
+      "version": "5.0.4",
+      "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
+      "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="
+    },
     "cacheable-request": {
       "version": "6.1.0",
       "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz",
       "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=",
       "optional": true
     },
+    "cb": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/cb/-/cb-0.1.0.tgz",
+      "integrity": "sha1-JvfnQPKAj6zIPO97IDkuTYgbUgM="
+    },
     "chalk": {
       "version": "4.1.2",
       "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
       "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==",
       "dev": true
     },
+    "clean-stack": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
+      "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="
+    },
     "cli-boxes": {
       "version": "2.2.1",
       "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz",
       "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==",
       "dev": true
     },
+    "clone": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
+      "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18="
+    },
     "clone-response": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz",
       "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=",
-      "dev": true,
       "requires": {
         "mimic-response": "^1.0.0"
       }
         "delayed-stream": "~1.0.0"
       }
     },
+    "compress-brotli": {
+      "version": "1.3.6",
+      "resolved": "https://registry.npmjs.org/compress-brotli/-/compress-brotli-1.3.6.tgz",
+      "integrity": "sha512-au99/GqZtUtiCBliqLFbWlhnCxn+XSYjwZ77q6mKN4La4qOXDoLVPZ50iXr0WmAyMxl8yqoq3Yq4OeQNPPkyeQ==",
+      "requires": {
+        "@types/json-buffer": "~3.0.0",
+        "json-buffer": "~3.0.1"
+      },
+      "dependencies": {
+        "json-buffer": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+          "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="
+        }
+      }
+    },
     "concat-map": {
       "version": "0.0.1",
       "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
         }
       }
     },
+    "express-openid-connect": {
+      "version": "2.7.2",
+      "resolved": "https://registry.npmjs.org/express-openid-connect/-/express-openid-connect-2.7.2.tgz",
+      "integrity": "sha512-NK/p0P9se8qUcH7ciTVST0rEGBZcefpjGWBEDIgHPjTPNKa7eaTqWAsGAs0d2+dHgZQmSQ7zP1jXH2CwKicyyw==",
+      "requires": {
+        "base64url": "^3.0.1",
+        "cb": "^0.1.0",
+        "clone": "^2.1.2",
+        "cookie": "^0.4.1",
+        "debug": "^4.3.3",
+        "futoin-hkdf": "^1.5.0",
+        "http-errors": "^1.8.1",
+        "joi": "^17.6.0",
+        "jose": "^2.0.5",
+        "on-headers": "^1.0.2",
+        "openid-client": "^4.9.1",
+        "p-memoize": "^4.0.4",
+        "url-join": "^4.0.1"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "4.3.4",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+          "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+          "requires": {
+            "ms": "2.1.2"
+          }
+        },
+        "ms": {
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+        }
+      }
+    },
     "extend": {
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
         }
       }
     },
+    "follow-redirects": {
+      "version": "1.14.9",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
+      "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w=="
+    },
     "forever-agent": {
       "version": "0.6.1",
       "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
       "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
       "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
     },
+    "futoin-hkdf": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/futoin-hkdf/-/futoin-hkdf-1.5.0.tgz",
+      "integrity": "sha512-4CerDhtTgx4i5PKccQIpEp4T9wqmosPIP9Kep35SdCpYkQeriD3zddUVhrO1Fc4QvGhsAnd2rXyoOr5047mJEg=="
+    },
     "gauge": {
       "version": "2.7.4",
       "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
     "http-cache-semantics": {
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz",
-      "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==",
-      "dev": true
+      "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ=="
     },
     "http-errors": {
       "version": "1.8.1",
         "sshpk": "^1.7.0"
       }
     },
+    "http2-wrapper": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
+      "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==",
+      "requires": {
+        "quick-lru": "^5.1.1",
+        "resolve-alpn": "^1.0.0"
+      }
+    },
     "iconv-lite": {
       "version": "0.4.24",
       "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
       "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
       "dev": true
     },
+    "indent-string": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+      "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="
+    },
     "inflight": {
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
       "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=",
       "optional": true
     },
+    "joi": {
+      "version": "17.6.0",
+      "resolved": "https://registry.npmjs.org/joi/-/joi-17.6.0.tgz",
+      "integrity": "sha512-OX5dG6DTbcr/kbMFj0KGYxuew69HPcAE3K/sZpEV2nP6e/j/C0HV+HNiBPCASxdx5T7DMoa0s8UeHWMnb6n2zw==",
+      "requires": {
+        "@hapi/hoek": "^9.0.0",
+        "@hapi/topo": "^5.0.0",
+        "@sideway/address": "^4.1.3",
+        "@sideway/formula": "^3.0.0",
+        "@sideway/pinpoint": "^2.0.0"
+      }
+    },
+    "jose": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/jose/-/jose-2.0.5.tgz",
+      "integrity": "sha512-BAiDNeDKTMgk4tvD0BbxJ8xHEHBZgpeRZ1zGPPsitSyMgjoMWiLGYAE7H7NpP5h0lPppQajQs871E8NHUrzVPA==",
+      "requires": {
+        "@panva/asn1.js": "^1.0.0"
+      }
+    },
     "jsbn": {
       "version": "0.1.1",
       "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
     "make-error": {
       "version": "1.3.6",
       "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
-      "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
-      "dev": true
+      "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="
+    },
+    "map-age-cleaner": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz",
+      "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==",
+      "requires": {
+        "p-defer": "^1.0.0"
+      }
     },
     "media-typer": {
       "version": "0.3.0",
         "mime-db": "1.52.0"
       }
     },
+    "mimic-fn": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz",
+      "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ=="
+    },
     "mimic-response": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
-      "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
-      "dev": true
+      "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="
     },
     "minimatch": {
       "version": "3.1.2",
       "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
       "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
     },
+    "object-hash": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
+      "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw=="
+    },
     "object-inspect": {
       "version": "1.12.0",
       "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz",
       "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g=="
     },
+    "oidc-token-hash": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.1.tgz",
+      "integrity": "sha512-EvoOtz6FIEBzE+9q253HsLCVRiK/0doEJ2HCvvqMQb3dHZrP3WlJKYtJ55CRTw4jmYomzH4wkPuCj/I3ZvpKxQ=="
+    },
     "on-finished": {
       "version": "2.3.0",
       "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
         "ee-first": "1.1.1"
       }
     },
+    "on-headers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
+      "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA=="
+    },
     "once": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
         "wrappy": "1"
       }
     },
+    "openid-client": {
+      "version": "4.9.1",
+      "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-4.9.1.tgz",
+      "integrity": "sha512-DYUF07AHjI3QDKqKbn2F7RqozT4hyi4JvmpodLrq0HHoNP7t/AjeG/uqiBK1/N2PZSAQEThVjDLHSmJN4iqu/w==",
+      "requires": {
+        "aggregate-error": "^3.1.0",
+        "got": "^11.8.0",
+        "jose": "^2.0.5",
+        "lru-cache": "^6.0.0",
+        "make-error": "^1.3.6",
+        "object-hash": "^2.0.1",
+        "oidc-token-hash": "^5.0.1"
+      },
+      "dependencies": {
+        "@sindresorhus/is": {
+          "version": "4.6.0",
+          "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
+          "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="
+        },
+        "@szmarczak/http-timer": {
+          "version": "4.0.6",
+          "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
+          "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==",
+          "requires": {
+            "defer-to-connect": "^2.0.0"
+          }
+        },
+        "cacheable-request": {
+          "version": "7.0.2",
+          "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz",
+          "integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==",
+          "requires": {
+            "clone-response": "^1.0.2",
+            "get-stream": "^5.1.0",
+            "http-cache-semantics": "^4.0.0",
+            "keyv": "^4.0.0",
+            "lowercase-keys": "^2.0.0",
+            "normalize-url": "^6.0.1",
+            "responselike": "^2.0.0"
+          }
+        },
+        "decompress-response": {
+          "version": "6.0.0",
+          "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
+          "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+          "requires": {
+            "mimic-response": "^3.1.0"
+          }
+        },
+        "defer-to-connect": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
+          "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="
+        },
+        "get-stream": {
+          "version": "5.2.0",
+          "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
+          "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
+          "requires": {
+            "pump": "^3.0.0"
+          }
+        },
+        "got": {
+          "version": "11.8.3",
+          "resolved": "https://registry.npmjs.org/got/-/got-11.8.3.tgz",
+          "integrity": "sha512-7gtQ5KiPh1RtGS9/Jbv1ofDpBFuq42gyfEib+ejaRBJuj/3tQFeR5+gw57e4ipaU8c/rCjvX6fkQz2lyDlGAOg==",
+          "requires": {
+            "@sindresorhus/is": "^4.0.0",
+            "@szmarczak/http-timer": "^4.0.5",
+            "@types/cacheable-request": "^6.0.1",
+            "@types/responselike": "^1.0.0",
+            "cacheable-lookup": "^5.0.3",
+            "cacheable-request": "^7.0.2",
+            "decompress-response": "^6.0.0",
+            "http2-wrapper": "^1.0.0-beta.5.2",
+            "lowercase-keys": "^2.0.0",
+            "p-cancelable": "^2.0.0",
+            "responselike": "^2.0.0"
+          }
+        },
+        "json-buffer": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+          "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="
+        },
+        "keyv": {
+          "version": "4.2.2",
+          "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.2.2.tgz",
+          "integrity": "sha512-uYS0vKTlBIjNCAUqrjlxmruxOEiZxZIHXyp32sdcGmP+ukFrmWUnE//RcPXJH3Vxrni1H2gsQbjHE0bH7MtMQQ==",
+          "requires": {
+            "compress-brotli": "^1.3.6",
+            "json-buffer": "3.0.1"
+          }
+        },
+        "lowercase-keys": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
+          "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="
+        },
+        "mimic-response": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
+          "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="
+        },
+        "normalize-url": {
+          "version": "6.1.0",
+          "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
+          "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="
+        },
+        "p-cancelable": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
+          "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="
+        },
+        "responselike": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.0.tgz",
+          "integrity": "sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==",
+          "requires": {
+            "lowercase-keys": "^2.0.0"
+          }
+        }
+      }
+    },
     "os-homedir": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
       "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==",
       "dev": true
     },
+    "p-defer": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz",
+      "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww="
+    },
+    "p-limit": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+      "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+      "requires": {
+        "p-try": "^2.0.0"
+      }
+    },
+    "p-memoize": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/p-memoize/-/p-memoize-4.0.4.tgz",
+      "integrity": "sha512-ijdh0DP4Mk6J4FXlOM6vPPoCjPytcEseW8p/k5SDTSSfGV3E9bpt9Yzfifvzp6iohIieoLTkXRb32OWV0fB2Lw==",
+      "requires": {
+        "map-age-cleaner": "^0.1.3",
+        "mimic-fn": "^3.0.0",
+        "p-settle": "^4.1.1"
+      }
+    },
+    "p-reflect": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/p-reflect/-/p-reflect-2.1.0.tgz",
+      "integrity": "sha512-paHV8NUz8zDHu5lhr/ngGWQiW067DK/+IbJ+RfZ4k+s8y4EKyYCz8pGYWjxCg35eHztpJAt+NUgvN4L+GCbPlg=="
+    },
+    "p-settle": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/p-settle/-/p-settle-4.1.1.tgz",
+      "integrity": "sha512-6THGh13mt3gypcNMm0ADqVNCcYa3BK6DWsuJWFCuEKP1rpY+OKGp7gaZwVmLspmic01+fsg/fN57MfvDzZ/PuQ==",
+      "requires": {
+        "p-limit": "^2.2.2",
+        "p-reflect": "^2.1.0"
+      }
+    },
+    "p-try": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+      "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
+    },
     "package-json": {
       "version": "6.5.0",
       "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz",
       "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==",
       "optional": true
     },
+    "quick-lru": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
+      "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="
+    },
     "range-parser": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
         }
       }
     },
+    "resolve-alpn": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
+      "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="
+    },
     "responselike": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz",
         "punycode": "^2.1.0"
       }
     },
+    "url-join": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
+      "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA=="
+    },
     "url-parse-lax": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz",
index f7520d54afd103176934d195f95ad1a8fc71056a..93a079eb8674dd1f07113d112e2520a1cf351ed3 100644 (file)
@@ -3,13 +3,16 @@
   "version": "2.0-1",
   "scripts": {
     "dev": "npx nodemon src/server.ts",
-    "setup": "npx ts-node sql/seed.ts"
+    "setup": "npx ts-node sql/seed.ts",
+    "migrate": "npx ts-node sql/migrate.ts"
   },
   "dependencies": {
+    "axios": "^0.26.1",
     "better-sqlite3": "^7.5.0",
     "body-parser": "^1.20.0",
     "dotenv": "^16.0.0",
     "express": "^4.17.3",
+    "express-openid-connect": "^2.7.2",
     "moment": "^2.29.2",
     "rss-parser": "^3.12.0",
     "sqlite3": "^5.0.2",
@@ -18,7 +21,9 @@
   "devDependencies": {
     "@types/better-sqlite3": "^7.5.0",
     "@types/express": "^4.17.13",
+    "@types/minimist": "^1.2.2",
     "@types/uuid": "^8.3.4",
+    "minimist": "^1.2.6",
     "nodemon": "^2.0.15",
     "ts-node": "^10.7.0",
     "tsconfig-paths": "^3.14.1",
diff --git a/sql/migrate.ts b/sql/migrate.ts
new file mode 100644 (file)
index 0000000..4e6b045
--- /dev/null
@@ -0,0 +1,11 @@
+import minimist from 'minimist';
+
+export function cli(args: string[]) {
+  const argv = minimist(args.slice(2));
+
+  const dir = argv._[0] ? argv._[0] : 'up';
+
+  return {
+    dir
+  }
+}
index 46af5a41b17510b5972f395377b689767b5ae495..bf0118c53df3f699f01a0820a8e3b6743f9c6342 100644 (file)
@@ -1,24 +1,39 @@
 import Database from 'better-sqlite3';
-import {RSSParser} from 'src/parsers/rss';
-import { v4 as uuidv4 } from 'uuid';
+import { cli } from './migrate';
 
 const DB = 'feeds.db';
 export const writer = new Database(DB, {
   verbose: console.log
 });
 
+export function down() {
+  writer.prepare('drop table feed_items').run();
+  writer.prepare('drop table feedlist').run();
+  writer.prepare('drop table accounts').run();
+}
+
+export function up() {
+  writer.prepare(`
+                 create table accounts (
+                   id text unique,
+                   email text unique,
+                   signup_date number,
+                   login_code text,
+                   login_code_expires number
+                 )
+                 `).run();
 
-writer.prepare('drop table if exists feed_items').run();
-writer.prepare('drop table feedlist').run();
 
 writer.prepare(`
                create table feedlist (
                  id text unique,
+                 account_id text,
                  title text,
-                 link text unique
+                 link text
                )
                `).run();
 
+writer.prepare(`create index idx_account_feed on feedlist (account_id, link)`).run();
 
 
 writer.prepare(`
@@ -34,7 +49,9 @@ writer.prepare(`
                )
                `).run();
 
+}
 
+/*
 const feeds = [
   {
     id: uuidv4(),
@@ -68,3 +85,17 @@ feeds.forEach(async feed => {
     insert.run(id, feed.id, item.guid, item.title, item.link, item.pubDate, item.content);
   });
 });
+*/
+const args = cli(process.argv);
+if(args.dir === 'up') {
+  up();
+}
+else if(args.dir === 'down') {
+  down();
+}
+else {
+  console.error('Invalid dir');
+  process.exit(1);
+}
+
+process.exit(0);
index 9c7b80d703f95cd6740537ac8efec6e171634de8..ac83e80359c77db3df028461c091e17997d0c407 100644 (file)
@@ -21,7 +21,7 @@ export async function ingestSingle(feed_id: string) {
 
 export async function ingest() {
   // get a list of feeds!
-  const feeds = query.getFeedList.all();
+  const feeds = query._.ingest_getFeedList.all();
 
   feeds.forEach(async feed => {
     console.log(`Ingesting ${feed.title} @ ${feed.link}`);
index 0d89478130a8b7eefa48dd07b6f1e7e02d3d9a56..19a2885738ebd4bc4baf0dd695fc74108ce07d2f 100644 (file)
@@ -1,7 +1,11 @@
 import Database from 'better-sqlite3';
+import { v4 as uuidv4 } from 'uuid';
 
 const DB = 'feeds.db';
 
+// 10 minutes
+const LOGIN_CODE_TIMEOUT = 1000 * 60 * 10;
+
 export const writer = new Database(DB, {
   verbose: console.log
 });
@@ -11,15 +15,44 @@ export const reader = new Database(DB, {
 });
 
 export const query = {
-  addFeed: writer.prepare('insert into feedlist (id, title, link) values (?, ?, ?)'),
-  getFeedList: reader.prepare('select * from feedlist'),
+  _: {
+    createAccount: writer.prepare('insert into accounts (id, email, signup_date) values (?, ?, ?)'),
+    createLoginCode: writer.prepare('update accounts set login_code = ?, login_code_expires = ? where id = ?'),
+    isValidLoginCode: reader.prepare('select * from accounts where id = ? and login_code = ?'),
+    getAccountByEmail: reader.prepare('select * from accounts where email = ?'),
+    addFeed: writer.prepare('insert into feedlist (id, title, link, account_id) values (?, ?, ?, ?)'),
+    getFeed: reader.prepare('select * from feedlist where account_id = ? and id = ?'),
+    getFeedList: reader.prepare('select * from feedlist where account_id = ?'),
+    ingest_getFeedList: reader.prepare('select * from feedlist'),
+    getUnreadCountForAll: reader.prepare('select count(fi.id) as unread, feed_id, fl.title from feed_items fi join feedlist fl on fl.id = feed_id where fi.read_at = 0 and fl.account_id = ? group by feed_id;'),
+    readAllItems: writer.prepare(`update feed_items set read_at = datetime('now') where feed_id = ?`),
+  },
+  addFeed: (title: string, link: string, account_id: string) => {
+    const id = uuidv4();
+    query._.addFeed.run(id, title, link, account_id);
+    return {
+      id
+    }
+  },
+  readAllItems: (feed_id: string) => {
+    return query._.readAllItems.run(feed_id);
+  },
+  isFeedOwnedBy: (account_id: string, feed_id: string): boolean => {
+    const res = query._.getFeed.get(account_id, feed_id);
+    console.log('OUTPUT', res);
+    return !!res
+  },
+  getFeedList: (account_id: string) => {
+    return query._.getFeedList.all(account_id);
+  },
+  getUnreadCountForAll: (account_id: string) => {
+    return query._.getUnreadCountForAll.all(account_id);
+  },
   getFeedInfo: reader.prepare('select * from feedlist where id = ?'),
   getFeedItemInfo: reader.prepare('select * from feed_items where id = ? and feed_id = ?'),
   addFeedItem: writer.prepare('insert into feed_items (id, feed_id, guid, title, link, pub_date, content) values (?, ?, ?, ?, ?, ?, ?)'),
   getFeedsFor: reader.prepare('select id, feed_id, guid, title, link, pub_date,read_at from feed_items where feed_id = ? and pub_date > ? order by pub_date desc'),
   readItem: writer.prepare('update feed_items set read_at = ? where id = ? and read_at = 0'),
-  getUnreadCountForAll: reader.prepare('select count(id) as unread, feed_id from feed_items where read_at = 0 group by feed_id'),
-  readAllItems: writer.prepare(`update feed_items set read_at = datetime('now') where feed_id = ?`),
   _deleteFeedItems: writer.prepare(`delete from feed_items where feed_id = ?`),
   _deleteFeed: writer.prepare(`delete from feedlist where id = ?`),
   deleteFeed: {
@@ -27,5 +60,56 @@ export const query = {
       query._deleteFeedItems.run(feed_id);
       query._deleteFeed.run(feed_id)
     }
-  } 
+  },
+  createAccount: (email: string) => {
+    const id = uuidv4();
+    const signup_date = Date.now();
+
+    query._.createAccount.run(id, email, signup_date);
+
+    return {
+      id: id,
+      signup_date: signup_date,
+      email: email
+    }
+  },
+  createLoginCode: (email: string, code: string) => {
+    let acct;
+    const expires = Date.now() + LOGIN_CODE_TIMEOUT;
+
+    try {
+      acct = query.getAccountByEmail(email);
+    }
+    catch(e) {
+        acct = query.createAccount(email);
+    }
+    
+    try {
+      const output = query._.createLoginCode.run(code, expires, acct.id);
+
+      return {
+        ...acct,
+        login_code: code,
+        login_code_expires: expires
+      };
+    }
+    catch(e) {
+      console.log(e);
+    }
+  },
+  validateLoginCode: (id: string, code: string): boolean => {
+    const validCode = query._.isValidLoginCode.get(id, code);
+    // code is valid for 10 minues
+    const now = Date.now();
+    return (validCode && now <= validCode.login_code_expires);
+  },
+  getAccountByEmail: (email: string) => {
+    const account = query._.getAccountByEmail.get(email);
+
+    if(!account) {
+      throw new Error('Invalid Account');
+    }
+
+    return account;
+  },
 };
index 4e1404e5385254aff7098fa56191ee991e2f3cf7..7add8509c2cbeb6b322eef587862f09fe159d502 100644 (file)
@@ -6,23 +6,44 @@ import { ingest, ingestSingle } from './ingester';
 import {RSSParser} from './parsers/rss';
 import bodyParser from 'body-parser';
 import {BaseParser} from './parsers/base';
+import fs from 'fs';
+import { promisify } from 'util';
 
-
+const HTML_ROOT = join(__dirname, '..', 'html');
 const app = express();
+const session: Map<string, string> = new Map<string, string>();
 
 app.use(bodyParser.json());
 app.use(bodyParser.urlencoded({ extended: true}));
-app.use(express.static(join(__dirname, '..', 'html')));
 app.use((req, res, next) => {
   console.log(`${req.method} ${req.path}`);
   next();
 });
 
 type WrappedApiHandler = (req: Request, res: Response) => Promise<any>;
+type WrapOptions = {
+  auth?: boolean;
+}
 
-function apiWrapper(method: 'get' | 'post' | 'delete', endpoint: string, fn: WrappedApiHandler, view?: (args: any) => string) : void {
+function apiWrapper(method: 'get' | 'post' | 'delete', endpoint: string, options: WrapOptions, fn: WrappedApiHandler, view?: (args: any) => string) : void {
   app[method](endpoint, async(req, res) => {
     try {
+      if(options.auth) {
+        if(!req.headers['hx-current-url']) {
+          throw new Error('Invald user');
+        }
+        const query = new URLSearchParams(req.headers['hx-current-url'].toString().split('?')[1]);
+        const token = query.get('token');
+        const id = query.get('id');
+
+        if(!token || !id) {
+          throw new Error('Invalid user');
+        }
+        if(!session.get(token) || session.get(token) !== id) {
+          throw new Error('Invalid user');
+        }
+      }
+
       const output = await fn(req, res);
       if(req.headers['content-type'] === 'application/json') {
         res.json(output);
@@ -37,7 +58,12 @@ function apiWrapper(method: 'get' | 'post' | 'delete', endpoint: string, fn: Wra
         }
       }
 
-      res.status(204);
+      if(!output) {
+        res.status(204);
+        return;
+      }
+
+      return res.json(output);
     }
     catch(e) {
       console.error(e);
@@ -49,23 +75,97 @@ function apiWrapper(method: 'get' | 'post' | 'delete', endpoint: string, fn: Wra
   });
 }
 
-function apiGet(endpoint: string, fn: WrappedApiHandler, view?: (arr: any) => string): void {
-  apiWrapper('get', endpoint, fn, view);
+app.get('/', (req, res) => {
+  res.sendFile(join(HTML_ROOT, 'index.html'));
+});
+app.get('/home.css', (req, res) => {
+  res.sendFile(join(HTML_ROOT, 'home.css'));
+});
+app.get('/style.css', (req, res) => {
+  res.sendFile(join(HTML_ROOT, 'style.css'));
+});
+
+function apiGet(endpoint: string, options: WrapOptions, fn: WrappedApiHandler, view?: (arr: any) => string): void {
+  apiWrapper('get', endpoint, options, fn, view);
 }
 
-function apiPost(endpoint: string, fn: WrappedApiHandler, view?: (arr: any) => string): void {
-  apiWrapper('post', endpoint, fn, view);
+function apiPost(endpoint: string, options: WrapOptions, fn: WrappedApiHandler, view?: (arr: any) => string): void {
+  apiWrapper('post', endpoint, options, fn, view);
 }
-function apiDelete(endpoint: string, fn: WrappedApiHandler, view?: (arr: any) => string): void {
-  apiWrapper('delete', endpoint, fn, view);
+function apiDelete(endpoint: string, options: WrapOptions, fn: WrappedApiHandler, view?: (arr: any) => string): void {
+  apiWrapper('delete', endpoint, options, fn, view);
 }
 
-apiPost('/login', async (req, res): Promise<any> => {
+apiPost('/login', {auth: false}, async (req, res): Promise<any> => {
+  const email = req.body.email;
+  const loginCode = uuidv4().substr(0, 6).toUpperCase();
+
+  const account = query.createLoginCode(email, loginCode);
+  const token = uuidv4();
+
+  const login_link = `/app?code=${account.login_code}&id=${account.id}`
+  console.log('login link:', login_link);
+
+  // this should actually just email the link and return some text 
+  // about what a great person you are.
+  return {
+    login: login_link
+  }
 });
 
-apiPost('/feeds', async (req, res): Promise<any> => {
+apiGet('/app', {auth: false}, async (req, res) => {
+  const id = req.query.id?.toString();
+  const token = req.query.token?.toString();
+  const code = req.query.code?.toString();
+
+
+  if(code && id) {
+    console.log('validating', id, code);
+    if(!query.validateLoginCode(id, code)) {
+      throw new Error('Invalid login');
+    }
+    let token = uuidv4();
+    let i = 0;
+    while(session.has(token) && i < 10) {
+      token = uuidv4();
+      ++i;
+    }
+
+    if(i >= 10) {
+      throw new Error('Please login again');
+    }
+
+    session.set(token, id);
+    res.redirect(`/app?id=${id}&token=${token}`);
+    return;
+  }
+
+  if(token && id) {
+    // validate it.
+    if(!session.has(token) || session.get(token) !== id) {
+      res.redirect('/');
+      return;
+    }
+    const data = await promisify(fs.readFile)(join(HTML_ROOT, 'app.html'), 'utf-8');
+
+    return {
+      html: data,
+      account_id: id,
+      token: token
+    };
+  }
+
+  res.redirect('/');
+  return;
+
+}, data => {
+  return data.html.replace(/{ACCOUNT_ID}/g, data.account_id);
+});
+
+apiPost('/accounts/:account_id/feeds', {auth: true}, async (req, res): Promise<any> => {
   // get info about the feed
   const url = req.body.link;
+  const account_id = req.params.account_id;
 
   let parser: BaseParser;
 
@@ -75,27 +175,27 @@ apiPost('/feeds', async (req, res): Promise<any> => {
   const feedData = await parser.parse(url);
 
   const title = feedData.title;
-  const id = uuidv4();
 
   // ingest teh feed, 
 
-  query.addFeed.run(id, title, url);
+  const feed = query.addFeed(title, url, account_id);
 
-  await ingestSingle(id);
+  await ingestSingle(feed.id);
 
   res.setHeader('HX-Trigger', 'newFeed');
 
   return {
-    id,
+    id: feed.id,
     title,
     url
   }
 });
 
-apiGet('/feeds', async (req, res): Promise<any> => {
-  const feeds = query.getFeedList.all();
+apiGet('/accounts/:account_id/feeds', {auth: true}, async (req, res): Promise<any> => {
+  const account_id = req.params.account_id;
+  const feeds = query.getFeedList(account_id);
   // get unread counts
-  const unread_count = query.getUnreadCountForAll.all();
+  const unread_count = query.getUnreadCountForAll(account_id);
 
   const feedsWithUnread = feeds.map(feed => {
     const unread = unread_count.filter(i => i.feed_id === feed.id);
@@ -108,7 +208,8 @@ apiGet('/feeds', async (req, res): Promise<any> => {
   });
 
   return { 
-    feeds: feedsWithUnread
+    feeds: feedsWithUnread,
+    account_id: account_id
   }
 }, (output: any): string => {
   const feeds = output.feeds;
@@ -116,7 +217,7 @@ apiGet('/feeds', async (req, res): Promise<any> => {
     const display = feed.unread ? `(${feed.unread})` : '';
     const first = idx === 0;
 
-    return `<li><a href="#" class="${first ? 'active' : ''} ${feed.unread ? 'unread' : ''}" data-actions="activate" hx-get="/feeds/${feed.id}/items" hx-trigger="${first ? 'load,' : ''}click" hx-target="#list-pane" data-feed-id="${feed.id}">${feed.title} 
+    return `<li><a href="#" class="${first ? 'active' : ''} ${feed.unread ? 'unread' : ''}" data-actions="activate" hx-get="/accounts/${output.account_id}/feeds/${feed.id}/items" hx-trigger="${first ? 'load,' : ''}click" hx-target="#list-pane" data-feed-id="${feed.id}">${feed.title} 
     <span class="unread-count">${display}</span>
     </a></li>`
   }).join("\n")}</ul>`;
@@ -128,7 +229,7 @@ function reasonable(date: Date): string {
   return `${date.getFullYear()}-${month}-${day}`;
 }
 
-apiGet('/feeds/:feed_id', async (req, res): Promise<any> => {
+apiGet('/accounts/:account_id/feeds/:feed_id',{auth: true},  async (req, res): Promise<any> => {
   const id = req.params.feed_id;
   return query.getFeedInfo.get(id);
 }, (feed: any): string => {
@@ -138,18 +239,20 @@ apiGet('/feeds/:feed_id', async (req, res): Promise<any> => {
   `;
 });
 
-apiGet('/feeds/:feed_id/items', async (req, res): Promise<any> => {
-  const id = req.params.feed_id;
+apiGet('/accounts/:account_id/feeds/:feed_id/items',{auth: true},  async (req, res): Promise<any> => {
+  const { account_id, feed_id } = req.params;
+
   return {
-    items: query.getFeedsFor.all(id, 0),
-    info: query.getFeedInfo.get(id)
+    items: query.getFeedsFor.all(feed_id, 0),
+    info: query.getFeedInfo.get(feed_id),
+    account_id
   }
 }, (feedData: any): string => {
   return `
       <div id="feed-info">
       <div id="feed-actions">
-        <a href="#" class="btn" hx-post="/feeds/${feedData.info.id}/items/markAsRead" hx-trigger="click">Mark all as Read</a>
-        <a href="#" class="btn" hx-delete="/feeds/${feedData.info.id}" hx-trigger="click" hx-confirm="Are you sure you want to delete this feed?">Delete Feed</a>
+        <a href="#" class="btn" hx-post="/accounts/${feedData.account_id}/feeds/${feedData.info.id}/items/markAsRead" hx-trigger="click">Mark all as Read</a>
+        <a href="#" class="btn" hx-delete="/accounts/${feedData.account_id}/feeds/${feedData.info.id}" hx-trigger="click" hx-confirm="Are you sure you want to delete this feed?">Delete Feed</a>
       </div>
       <b>Feed: </b>${feedData.info.title}<br>
       </div>
@@ -161,7 +264,7 @@ apiGet('/feeds/:feed_id/items', async (req, res): Promise<any> => {
     const read = !!item.read_at;
     const first = index === 0;
     return `<li>
-      <a href="#" class="${first ? 'active': ''} ${read ? '': 'unread'}" data-actions="activate" hx-get="/feeds/${item.feed_id}/items/${item.id}" hx-trigger="click" hx-target="#reading-pane" data-feed-item-id="${item.id}" data-feed-id="${item.feed_id}">${item.title}
+      <a href="#" class="${first ? 'active': ''} ${read ? '': 'unread'}" data-actions="activate" hx-get="/accounts/${feedData.account_id}/feeds/${item.feed_id}/items/${item.id}" hx-trigger="click" hx-target="#reading-pane" data-feed-item-id="${item.id}" data-feed-id="${item.feed_id}">${item.title}
       <span class="date">${reasonable(new Date(item.pub_date * 1000))}</span>
       </a>
     </li>
@@ -170,15 +273,21 @@ apiGet('/feeds/:feed_id/items', async (req, res): Promise<any> => {
   `;
 });
 
-apiPost('/feeds/:feed_id/items/markAsRead', async (req, res): Promise<any> => {
-  const id = req.params.feed_id;
-  query.readAllItems.run(id);
+apiPost('/accounts/:account_id/feeds/:feed_id/items/markAsRead',{auth: true},  async (req, res): Promise<any> => {
+  const {account_id, feed_id} = req.params;
+  if(!query.isFeedOwnedBy(account_id, feed_id)) {
+    throw new Error('Invalid feed');
+  }
+  query.readAllItems(feed_id);
   return;
 });
 
-apiGet('/feeds/:feed_id/items/:item_id', async (req, res) => {
-  const feed_id = req.params.feed_id;
-  const item_id = req.params.item_id;
+apiGet('/accounts/:account_id/feeds/:feed_id/items/:item_id',{auth: true},  async (req, res) => {
+  const  {account_id, feed_id, item_id} = req.params;
+
+  if(!query.isFeedOwnedBy(account_id, feed_id)) {
+    throw new Error('Invalid feed');
+  }
 
   query.readItem.run(Date.now(), item_id);
   return query.getFeedItemInfo.get(item_id, feed_id);
@@ -192,11 +301,10 @@ apiGet('/feeds/:feed_id/items/:item_id', async (req, res) => {
   <b>Read: </b> ${new Date(output.read_at || undefined)}
   </div>
   <div class="scrollable" id="feed-content">${output.content}</div>
-  `
-  return output.content;
+  `;
 });
 
-apiDelete('/feeds/:feed_id', async (req, res) => {
+apiDelete('/feeds/:feed_id',{auth: true},  async (req, res) => {
   const id = req.params.feed_id;
 
   query.deleteFeed.run(id);