From 63d97d0b83215db6deed46feb57cee2c2a687c9e Mon Sep 17 00:00:00 2001
From: Julian Krauser <jkrauser209@gmail.com>
Date: Mon, 5 May 2025 14:21:22 +0200
Subject: [PATCH] login by password or totp

---
 src/helpers/crypto.ts |  7 ++++
 src/views/Login.vue   | 97 +++++++++++++++++++++++++++++++++++++++----
 2 files changed, 95 insertions(+), 9 deletions(-)
 create mode 100644 src/helpers/crypto.ts

diff --git a/src/helpers/crypto.ts b/src/helpers/crypto.ts
new file mode 100644
index 0000000..06b32cf
--- /dev/null
+++ b/src/helpers/crypto.ts
@@ -0,0 +1,7 @@
+export async function hashString(message = ""): Promise<string> {
+  const msgUint8 = new TextEncoder().encode(message);
+  const hashBuffer = await window.crypto.subtle.digest("SHA-256", msgUint8);
+  const hashArray = Array.from(new Uint8Array(hashBuffer));
+  const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
+  return hashHex;
+}
diff --git a/src/views/Login.vue b/src/views/Login.vue
index 2ad96f7..8b5ce8c 100644
--- a/src/views/Login.vue
+++ b/src/views/Login.vue
@@ -8,13 +8,28 @@
         </h2>
       </div>
 
-      <form class="flex flex-col gap-2" @submit.prevent="login">
+      <form class="flex flex-col gap-2" @submit.prevent="submit">
         <div class="-space-y-px">
-          <div>
-            <input id="username" name="username" type="text" required placeholder="Benutzer" class="rounded-b-none!" />
-          </div>
-          <div>
+          <div class="relative">
             <input
+              id="username"
+              name="username"
+              type="text"
+              required
+              placeholder="Benutzer"
+              :class="routine == '' ? '' : 'rounded-b-none!'"
+              :value="username"
+              :disabled="username != ''"
+            />
+            <div v-if="usernameStatus" class="h-full flex items-center justify-center w-5 absolute top-0 right-2">
+              <Spinner v-if="usernameStatus == 'loading'" class="my-auto" />
+              <SuccessCheckmark v-else-if="usernameStatus == 'success'" />
+              <FailureXMark v-else-if="usernameStatus == 'failed'" />
+            </div>
+          </div>
+          <div v-if="routine != ''">
+            <input
+              v-if="routine == 'totp'"
               id="totp"
               name="totp"
               type="text"
@@ -23,13 +38,26 @@
               class="rounded-t-none!"
               autocomplete="off"
             />
+            <input
+              v-else
+              id="password"
+              name="password"
+              type="password"
+              required
+              placeholder="Passwort"
+              class="rounded-t-none!"
+              autocomplete="off"
+            />
           </div>
         </div>
-        <RouterLink :to="{ name: 'reset-start' }" class="w-fit self-end text-primary">TOTP verloren</RouterLink>
+        <p v-if="username != ''" class="w-fit self-end text-primary cursor-pointer" @click="resetRoutine">
+          Benutzer wechseln
+        </p>
+        <RouterLink :to="{ name: 'reset-start' }" class="w-fit self-end text-primary">Zugang verloren</RouterLink>
 
         <div class="flex flex-row gap-2">
           <button type="submit" primary :disabled="loginStatus == 'loading' || loginStatus == 'success'">
-            anmelden
+            {{ routine == "" ? "Benutzer prüfen" : "anmelden" }}
           </button>
           <Spinner v-if="loginStatus == 'loading'" class="my-auto" />
           <SuccessCheckmark v-else-if="loginStatus == 'success'" />
@@ -53,6 +81,7 @@ import FormBottomBar from "@/components/FormBottomBar.vue";
 import AppLogo from "@/components/AppLogo.vue";
 import { mapState } from "pinia";
 import { useConfigurationStore } from "@/stores/configuration";
+import { hashString } from "../helpers/crypto";
 </script>
 
 <script lang="ts">
@@ -60,7 +89,10 @@ export default defineComponent({
   data() {
     return {
       loginStatus: undefined as undefined | "loading" | "success" | "failed",
+      usernameStatus: undefined as undefined | "loading" | "success" | "failed",
       loginError: "" as string,
+      username: "" as string,
+      routine: "" as string,
     };
   },
   computed: {
@@ -68,16 +100,63 @@ export default defineComponent({
   },
   mounted() {
     resetAllPiniaStores();
+    this.username = localStorage.getItem("username") ?? "";
+    this.routine = localStorage.getItem("routine") ?? "";
   },
   methods: {
-    login(e: any) {
+    resetRoutine() {
+      this.routine = "";
+      this.username = "";
+      localStorage.removeItem("routine");
+      localStorage.removeItem("username");
+    },
+    submit(e: any) {
+      if (this.routine == "") this.kickof(e);
+      else this.login(e);
+    },
+    kickof(e: any) {
+      let formData = e.target.elements;
+      let username = formData.username.value;
+      this.usernameStatus = "loading";
+      this.loginError = "";
+      this.$http
+        .post(`/auth/kickof`, {
+          username: username,
+        })
+        .then((result) => {
+          this.usernameStatus = "success";
+          this.routine = result.data.routine;
+          this.username = username;
+          localStorage.setItem("routine", result.data.routine);
+          localStorage.setItem("username", username);
+        })
+        .catch((err) => {
+          this.usernameStatus = "failed";
+          this.loginError = err.response?.data;
+        })
+        .finally(() => {
+          setTimeout(() => {
+            this.usernameStatus = undefined;
+            this.loginError = "";
+          }, 2000);
+        });
+    },
+    async login(e: any) {
       let formData = e.target.elements;
       this.loginStatus = "loading";
       this.loginError = "";
+
+      let secret = "";
+      if (this.routine == "totp") {
+        secret = formData.totp.value;
+      } else {
+        secret = await hashString(formData.password.value);
+      }
+
       this.$http
         .post(`/auth/login`, {
           username: formData.username.value,
-          totp: formData.totp.value,
+          secret: secret,
         })
         .then((result) => {
           this.loginStatus = "success";