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";