From 87edfd17654150b007776eeb308fe047a7274bb8 Mon Sep 17 00:00:00 2001
From: Dominik Hebeler <dominik@hebeler.club>
Date: Wed, 22 Feb 2023 12:22:20 +0100
Subject: [PATCH] scanning qr codes will work now

---
 pass/package-lock.json            |  28 +-----
 pass/public/styles/key/enter.less | 152 ++++++++++++++++++------------
 pass/resources/js/enter.js        |  58 ++++++++++--
 pass/routes/key.js                |   6 +-
 pass/views/login/key.ejs          |  66 ++++++++-----
 5 files changed, 195 insertions(+), 115 deletions(-)

diff --git a/pass/package-lock.json b/pass/package-lock.json
index 177f144..19c2026 100644
--- a/pass/package-lock.json
+++ b/pass/package-lock.json
@@ -588,11 +588,6 @@
       "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.17.tgz",
       "integrity": "sha512-HJSUJmni4BeDHhfzn6nF0sVmd1SMezP7/4F0Lq+aXzmp2xm9O7WXrUtHW/CHlYVtZUbByEvWidHqRtcJXGF2Ng=="
     },
-    "node_modules/@types/offscreencanvas": {
-      "version": "2019.7.0",
-      "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.0.tgz",
-      "integrity": "sha512-PGcyveRIpL1XIqK8eBsmRBt76eFgtzuPiSTyKHZxnGemp2yzGzWpjYKAfK3wIMiU7eH+851yEpiuP8JZerTmWg=="
-    },
     "node_modules/@types/responselike": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz",
@@ -5091,14 +5086,6 @@
       "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==",
       "optional": true
     },
-    "node_modules/qr-scanner": {
-      "version": "1.4.2",
-      "resolved": "https://registry.npmjs.org/qr-scanner/-/qr-scanner-1.4.2.tgz",
-      "integrity": "sha512-kV1yQUe2FENvn59tMZW6mOVfpq9mGxGf8l6+EGaXUOd4RBOLg7tRC83OrirM5AtDvZRpdjdlXURsHreAOSPOUw==",
-      "dependencies": {
-        "@types/offscreencanvas": "^2019.6.4"
-      }
-    },
     "node_modules/qrcode": {
       "version": "1.5.1",
       "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.1.tgz",
@@ -7023,11 +7010,6 @@
       "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.17.tgz",
       "integrity": "sha512-HJSUJmni4BeDHhfzn6nF0sVmd1SMezP7/4F0Lq+aXzmp2xm9O7WXrUtHW/CHlYVtZUbByEvWidHqRtcJXGF2Ng=="
     },
-    "@types/offscreencanvas": {
-      "version": "2019.7.0",
-      "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.0.tgz",
-      "integrity": "sha512-PGcyveRIpL1XIqK8eBsmRBt76eFgtzuPiSTyKHZxnGemp2yzGzWpjYKAfK3wIMiU7eH+851yEpiuP8JZerTmWg=="
-    },
     "@types/responselike": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz",
@@ -10569,14 +10551,6 @@
       "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==",
       "optional": true
     },
-    "qr-scanner": {
-      "version": "1.4.2",
-      "resolved": "https://registry.npmjs.org/qr-scanner/-/qr-scanner-1.4.2.tgz",
-      "integrity": "sha512-kV1yQUe2FENvn59tMZW6mOVfpq9mGxGf8l6+EGaXUOd4RBOLg7tRC83OrirM5AtDvZRpdjdlXURsHreAOSPOUw==",
-      "requires": {
-        "@types/offscreencanvas": "^2019.6.4"
-      }
-    },
     "qrcode": {
       "version": "1.5.1",
       "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.1.tgz",
@@ -11801,4 +11775,4 @@
       }
     }
   }
-}
\ No newline at end of file
+}
diff --git a/pass/public/styles/key/enter.less b/pass/public/styles/key/enter.less
index dbfb280..f3e5c2b 100644
--- a/pass/public/styles/key/enter.less
+++ b/pass/public/styles/key/enter.less
@@ -11,50 +11,75 @@
     flex-direction: column;
     gap: 1rem;
     align-items: center;
-    > h3 {
-      margin: 0;
-    }
-    > .divider {
-      position: relative;
-      display: flex;
-      align-items: center;
-      color: @font-color-on-white;
-      &::before,
-      &::after {
-        content: "";
-        background-color: @font-color-on-white;
-        width: 80%;
-        position: absolute;
-        height: 1px;
-        display: flex;
-      }
-      &::before {
-        left: -100%;
-      }
-      &::after {
-        left: calc(100% + 20%);
-      }
-    }
-    > #file-selector {
+    > div.enter-option {
       width: 100%;
       display: flex;
       flex-direction: column;
       gap: 1rem;
-      > input[type="file"] {
-        width: 100%;
-        text-align: center;
-        &::file-selector-button {
-          display: none;
+      align-items: center;
+      > h3 {
+        margin: 0;
+      }
+      > .divider {
+        position: relative;
+        display: flex;
+        align-items: center;
+        color: @font-color-on-white;
+        &::before,
+        &::after {
+          content: "";
+          background-color: @font-color-on-white;
+          width: 80%;
+          position: absolute;
+          height: 1px;
+          display: flex;
+        }
+        &::before {
+          left: -100%;
+        }
+        &::after {
+          left: calc(100% + 20%);
         }
       }
-      > label {
-        cursor: pointer;
+      > #file-selector {
         width: 100%;
+        display: flex;
+        flex-direction: column;
+        gap: 1rem;
+        > input[type="file"] {
+          width: 100%;
+          text-align: center;
+          &::file-selector-button {
+            display: none;
+          }
+        }
+        > label {
+          cursor: pointer;
+          width: 100%;
+          border: 1px solid #777;
+          padding: 0.5rem;
+          display: flex;
+          align-items: center;
+          gap: 0.5rem;
+          font-size: 0.8rem;
+          > img {
+            width: 1.3rem;
+          }
+          > span {
+            width: 100%;
+            text-align: center;
+          }
+        }
+      }
+      > a {
+        color: inherit;
+        text-decoration: none;
         border: 1px solid #777;
         padding: 0.5rem;
         display: flex;
         align-items: center;
         gap: 0.5rem;
+        width: 100%;
         font-size: 0.8rem;
         > img {
           width: 1.3rem;
@@ -64,32 +89,18 @@
           text-align: center;
         }
       }
-    }
-    > a {
-      color: inherit;
-      text-decoration: none;
-      border: 1px solid #777;
-      padding: 0.5rem;
-      display: flex;
-      align-items: center;
-      gap: 0.5rem;
-      width: 100%;
-      font-size: 0.8rem;
-      > img {
-        width: 1.3rem;
-      }
-      > span {
+      > form {
+        display: flex;
         width: 100%;
-        text-align: center;
+        > #key {
+          flex-grow: 1;
+          padding: 0.5rem;
+          font-size: 0.75rem;
+        }
       }
-    }
-    > form {
-      display: flex;
-      width: 100%;
-      > #key {
-        flex-grow: 1;
-        padding: 0.5rem;
-        font-size: 0.75rem;
+      .error {
+        display: none;
+        font-size: 0.8rem;
       }
     }
   }
@@ -110,8 +121,33 @@
 
   > #qr-scanner {
     width: 100%;
-    > video {
-      width: 100%;
+    height: 100vh;
+    position: absolute;
+    left: 0;
+    top: 0;
+    background-color: hsla(0, 0%, 47%, 0.439);
+    display: none;
+    justify-content: center;
+    align-content: center;
+    > #scanner-container {
+      width: 500px;
+      max-width: 100vw;
+      max-height: 80vh;
+      > video {
+        width: 100%;
+        max-height: 80vh;
+      }
+      > a {
+        display: block;
+        width: 100%;
+        text-align: center;
+        background-color: rgb(255, 127, 0);
+        padding: 0.5rem;
+        margin-top: -5px;
+        color: white;
+        font-weight: bold;
+        text-decoration: none;
+      }
     }
   }
 }
diff --git a/pass/resources/js/enter.js b/pass/resources/js/enter.js
index 1c38449..0743990 100644
--- a/pass/resources/js/enter.js
+++ b/pass/resources/js/enter.js
@@ -1,18 +1,64 @@
+let scanner;
+let container = document.getElementById("qr-scanner");
 
 document.getElementById("qr-scan").addEventListener("pointerup", e => {
-    import('/js/qr-scanner.min.js').then(module => {
+    document.querySelectorAll("#scan-qr-option .error").forEach(error => {
+        error.style.display = "none";
+    })
+    import('/js/qr-scanner.min.js').then(async module => {
         const QrScanner = module.default;
-        let video = document.createElement("video");
-        let container = document.getElementById("qr-scanner");
-        container.appendChild(video);
 
-        let scanner = new QrScanner(video, result => console.log('decoded qr code:' + result), {
+        if (! await QrScanner.hasCamera()) {
+            document.querySelector("#qr-camera-error").style.display = "block";
+            return;
+        }
+
+        let video = document.querySelector("#qr-scanner > #scanner-container > video");
+        let close_button = document.querySelector("#qr-scanner > #scanner-container a");
+        container.style.display = "grid";
+
+        scanner = new QrScanner(video, result => {
+            scanner.stop();
+            let url = result.data;
+            try {
+                url = new URL(url);
+            } catch (error) {
+                document.getElementById("qr-decode-error").style.display = "block";
+                stopScanning();
+            }
+            let key = url.searchParams.get("key");
+            if (key !== null) {
+                location.href = "/key/" + encodeURIComponent(key);
+            } else {
+                document.getElementById("qr-decode-error").style.display = "block";
+                stopScanning();
+            }
+        }, {
             highlightScanRegion: true,
             highlightCodeOutline: true
         });
 
+        close_button.addEventListener("click", e => {
+            e.preventDefault();
+            stopScanning();
+            return false;
+        });
+
         scanner.start();
     });
 
 
-});
\ No newline at end of file
+});
+
+function stopScanning() {
+    scanner.stop();
+    let highlights = document.querySelectorAll("#qr-scanner .scan-region-highlight");
+    if (highlights) {
+        highlights.forEach(element => {
+            element.remove();
+        });
+    }
+    scanner.stop();
+    scanner.destroy();
+    container.style.display = "none";
+}
\ No newline at end of file
diff --git a/pass/routes/key.js b/pass/routes/key.js
index 638ed91..38c0515 100644
--- a/pass/routes/key.js
+++ b/pass/routes/key.js
@@ -67,7 +67,11 @@ router.post("/enter", upload.single('file'),
             return;
           }
           let key = url.searchParams.get("key");
-          res.redirect("/key/" + encodeURIComponent(key));
+          if (key !== null) {
+            res.redirect("/key/" + encodeURIComponent(key));
+          } else {
+            res.render("login/key", { errors: ["Error parsing URL"] });
+          }
         };
         qr.decode(image.bitmap);
       });
diff --git a/pass/views/login/key.ejs b/pass/views/login/key.ejs
index ec8d114..27277b2 100644
--- a/pass/views/login/key.ejs
+++ b/pass/views/login/key.ejs
@@ -3,37 +3,57 @@
 
 <div id="enter">
   <div id="form">
-    <h3>Schlüssel eingeben</h3>
-    <form
-      id="key-form"
-      action="/key/enter"
-      method="post"
-      enctype="multipart/form-data"
-    >
-      <input type="text" name="key" id="key" placeholder="Schlüssel eingeben" />
-    </form>
-    <div class="divider">oder</div>
-    <div id="file-selector">
-      <label for="file">
-        <img src="/images/upload.svg" alt="" />
-        <span> Sicherungsdatei hochladen </span>
-      </label>
-      <input type="file" name="file" id="file" form="key-form" />
+    <div id="direct-option" class="enter-option">
+      <h3>Schlüssel eingeben</h3>
+      <form
+        id="key-form"
+        action="/key/enter"
+        method="post"
+        enctype="multipart/form-data"
+      >
+        <input
+          type="text"
+          name="key"
+          id="key"
+          placeholder="Schlüssel eingeben"
+        />
+      </form>
+    </div>
+    <div id="file-option" class="enter-option">
+      <div class="divider">oder</div>
+      <div id="file-selector">
+        <label for="file">
+          <img src="/images/upload.svg" alt="" />
+          <span> Sicherungsdatei hochladen </span>
+        </label>
+        <input type="file" name="file" id="file" form="key-form" />
+      </div>
+    </div>
+    <div id="scan-qr-option" class="enter-option">
+      <div class="divider">oder</div>
+      <a href="javascript:void(0);" id="qr-scan">
+        <img src="/images/camera.svg" alt="" />
+        <span> QR-Code Scannen </span>
+      </a>
+      <div id="qr-camera-error" class="error">Keine Kamera verfügbar</div>
+      <div id="qr-decode-error" class="error">
+        Qr Code enthält keine gültigen Daten.
+      </div>
     </div>
-
-    <div class="divider">oder</div>
-    <a href="javascript:void(0);" id="qr-scan">
-      <img src="/images/camera.svg" alt="" />
-      <span> QR-Code Scannen </span>
-    </a>
   </div>
+
   <%_ if(typeof errors !== "undefined") { _%>
   <div class="error">
     Ein gültiger Schlüssel, oder eine gültige Sicherungsdatei ist erforderlich.
   </div>
   <%_ } _%>
   <button id="enter-submit" type="submit" form="key-form">Abschicken</button>
-  <div id="qr-scanner"></div>
+  <div id="qr-scanner">
+    <div id="scanner-container">
+      <video></video>
+      <a href="">Schließen</a>
+    </div>
+  </div>
 </div>
 
 <%- include('../templates/page_footer'); -%>
-- 
GitLab