From 63d97d0b83215db6deed46feb57cee2c2a687c9e Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Mon, 5 May 2025 14:21:22 +0200 Subject: [PATCH 1/4] 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 { + 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 @@ -
+
-
- -
-
+
+
+ + + +
+
+
+ +
- TOTP verloren +

+ Benutzer wechseln +

+ Zugang verloren
@@ -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"; + + diff --git a/src/components/account/ChangeToTOTP.vue b/src/components/account/ChangeToTOTP.vue new file mode 100644 index 0000000..c04197c --- /dev/null +++ b/src/components/account/ChangeToTOTP.vue @@ -0,0 +1,92 @@ + + + + + diff --git a/src/components/account/PasswordChange.vue b/src/components/account/PasswordChange.vue new file mode 100644 index 0000000..f93614b --- /dev/null +++ b/src/components/account/PasswordChange.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/src/components/account/TotpCheckAndScan.vue b/src/components/account/TotpCheckAndScan.vue new file mode 100644 index 0000000..a9ba5ef --- /dev/null +++ b/src/components/account/TotpCheckAndScan.vue @@ -0,0 +1,83 @@ + + + + + diff --git a/src/views/Login.vue b/src/views/Login.vue index 8b5ce8c..908a2c7 100644 --- a/src/views/Login.vue +++ b/src/views/Login.vue @@ -102,6 +102,22 @@ export default defineComponent({ resetAllPiniaStores(); this.username = localStorage.getItem("username") ?? ""; this.routine = localStorage.getItem("routine") ?? ""; + + if (this.username != "") { + this.$http + .post(`/auth/kickof`, { + username: this.username, + }) + .then((result) => { + this.usernameStatus = "success"; + this.routine = result.data.routine; + localStorage.setItem("routine", result.data.routine); + }) + .catch((err) => { + this.usernameStatus = "failed"; + this.loginError = err.response?.data; + }); + } }, methods: { resetRoutine() { diff --git a/src/views/account/LoginData.vue b/src/views/account/LoginData.vue index 7ba75bb..f91ad25 100644 --- a/src/views/account/LoginData.vue +++ b/src/views/account/LoginData.vue @@ -6,29 +6,41 @@
@@ -42,53 +54,34 @@ import Spinner from "@/components/Spinner.vue"; import SuccessCheckmark from "@/components/SuccessCheckmark.vue"; import FailureXMark from "@/components/FailureXMark.vue"; import TextCopy from "@/components/TextCopy.vue"; +import TotpCheckAndScan from "../../components/account/TotpCheckAndScan.vue"; +import PasswordChange from "../../components/account/PasswordChange.vue"; +import ChangeToPassword from "../../components/account/ChangeToPassword.vue"; +import ChangeToTOTP from "../../components/account/ChangeToTOTP.vue"; From b39198c935f027039d8b0c3cc983d987ebb43ec6 Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Tue, 6 May 2025 08:38:28 +0200 Subject: [PATCH 3/4] enable password on invite or reset --- src/views/Login.vue | 2 +- src/views/invite/Verify.vue | 72 ++++++++++++++++++++++++++++++++---- src/views/reset/Reset.vue | 73 +++++++++++++++++++++++++++++++++---- src/views/reset/Start.vue | 6 ++- 4 files changed, 135 insertions(+), 18 deletions(-) diff --git a/src/views/Login.vue b/src/views/Login.vue index 908a2c7..6ef5c6b 100644 --- a/src/views/Login.vue +++ b/src/views/Login.vue @@ -46,7 +46,7 @@ required placeholder="Passwort" class="rounded-t-none!" - autocomplete="off" + autocomplete="current-password" />
diff --git a/src/views/invite/Verify.vue b/src/views/invite/Verify.vue index f2cbed2..ff1977f 100644 --- a/src/views/invite/Verify.vue +++ b/src/views/invite/Verify.vue @@ -14,17 +14,59 @@

Einladungslink nicht gültig - Melde dich bei einem Admin.

+
+

+ TOTP +

+

+ Passwort +

+

Dein Nutzername: {{ username }}

- totp +
+ totp - + -
-
- +
+
+ +
+
+ + +
- totp +
+

+ TOTP +

+

+ Passwort +

+
- +
+ totp -
-
- + + +
+
+ +
+
+ + +
@@ -21,8 +22,10 @@ placeholder="neues Passwort wiederholen" autocomplete="new-password" class="rounded-t-none!" + :class="notMatching ? 'border-red-600!' : ''" />
+

Passwörter stimmen nicht überein

@@ -62,12 +65,22 @@ export default defineComponent({ verification: "loading" as "success" | "loading" | "failed", changeStatus: undefined as undefined | "loading" | "success" | "failed", changeError: "" as string, + notMatching: false as boolean, }; }, mounted() {}, methods: { async change(e: any) { let formData = e.target.elements; + + let new_pw = await hashString(formData.new.value); + let new_rep = await hashString(formData.new_rep.value); + if (new_pw != new_rep) { + this.notMatching = true; + return; + } + this.notMatching = false; + this.changeStatus = "loading"; this.changeError = ""; this.$http diff --git a/src/components/account/PasswordChange.vue b/src/components/account/PasswordChange.vue index f93614b..268e5bc 100644 --- a/src/components/account/PasswordChange.vue +++ b/src/components/account/PasswordChange.vue @@ -21,6 +21,7 @@ placeholder="neues Passwort" autocomplete="new-password" class="rounded-none!" + :class="notMatching ? 'border-red-600!' : ''" />
@@ -32,8 +33,10 @@ placeholder="neues Passwort wiederholen" autocomplete="new-password" class="rounded-t-none!" + :class="notMatching ? 'border-red-600!' : ''" />
+

Passwörter stimmen nicht überein

@@ -65,12 +68,22 @@ export default defineComponent({ verification: "loading" as "success" | "loading" | "failed", changeStatus: undefined as undefined | "loading" | "success" | "failed", changeError: "" as string, + notMatching: false as boolean, }; }, mounted() {}, methods: { async change(e: any) { let formData = e.target.elements; + + let new_pw = await hashString(formData.new.value); + let new_rep = await hashString(formData.new_rep.value); + if (new_pw != new_rep) { + this.notMatching = true; + return; + } + this.notMatching = false; + this.changeStatus = "loading"; this.changeError = ""; this.$http diff --git a/src/views/invite/Verify.vue b/src/views/invite/Verify.vue index ff1977f..0547f3f 100644 --- a/src/views/invite/Verify.vue +++ b/src/views/invite/Verify.vue @@ -56,16 +56,19 @@ placeholder="Passwort" class="rounded-b-none!" autocomplete="new-password" + :class="notMatching ? 'border-red-600!' : ''" /> +

Passwörter stimmen nicht überein

@@ -110,6 +113,7 @@ export default defineComponent({ username: "" as string, inviteStatus: undefined as undefined | "loading" | "success" | "failed", inviteError: "" as string, + notMatching: false as boolean, }; }, mounted() { @@ -138,6 +142,8 @@ export default defineComponent({ if (this.tab == "totp") secret = this.totp(e); else secret = await this.password(e); + if (secret == "") return; + this.inviteStatus = "loading"; this.inviteError = ""; this.$http @@ -166,6 +172,15 @@ export default defineComponent({ }, async password(e: any) { let formData = e.target.elements; + + let new_pw = await hashString(formData.password.value); + let new_rep = await hashString(formData.password_rep.value); + if (new_pw != new_rep) { + this.notMatching = true; + return ""; + } + this.notMatching = false; + return await hashString(formData.password.value); }, }, diff --git a/src/views/reset/Reset.vue b/src/views/reset/Reset.vue index fb05868..9a6bc4c 100644 --- a/src/views/reset/Reset.vue +++ b/src/views/reset/Reset.vue @@ -3,7 +3,7 @@
-

TOTP zurücksetzen

+

Zugang zurücksetzen

@@ -56,16 +56,19 @@ placeholder="Passwort" class="rounded-b-none!" autocomplete="new-password" + :class="notMatching ? 'border-red-600!' : ''" /> +

Passwörter stimmen nicht überein

@@ -110,6 +113,7 @@ export default defineComponent({ otp: undefined as undefined | string, resetStatus: undefined as undefined | "loading" | "success" | "failed", resetError: "" as string, + notMatching: false as boolean, }; }, mounted() { @@ -137,6 +141,8 @@ export default defineComponent({ if (this.tab == "totp") secret = this.totp(e); else secret = await this.password(e); + if (secret == "") return; + this.resetStatus = "loading"; this.resetError = ""; this.$http @@ -165,6 +171,15 @@ export default defineComponent({ }, async password(e: any) { let formData = e.target.elements; + + let new_pw = await hashString(formData.password.value); + let new_rep = await hashString(formData.password_rep.value); + if (new_pw != new_rep) { + this.notMatching = true; + return ""; + } + this.notMatching = false; + return await hashString(formData.password.value); }, }, diff --git a/src/views/reset/Start.vue b/src/views/reset/Start.vue index 95dbad5..a1de3c4 100644 --- a/src/views/reset/Start.vue +++ b/src/views/reset/Start.vue @@ -3,7 +3,7 @@
-

TOTP zurücksetzen

+

Zugang zurücksetzen