fix(auth): make login/signup fields password-manager friendly

Password managers (1Password, Bitwarden, …) could not reliably detect or
fill the auth fields because the inputs were loose labels inside a div with
no real form, no name attributes, and a static autocomplete hint.

Changes:
- Wrap the auth card in a real <form @submit.prevent> so password managers
  recognize the credential form and its submit flow
- Add id/name attributes to all fields (username, displayName, password)
- Use a dynamic autocomplete hint on the password field:
  current-password for login (autofill) and new-password for registration
  (offer a generated strong password)
- Set autocomplete="nickname" on the display-name field so it is not
  mistaken for a username or password field
- Mark the close, tab-switch, and OIDC buttons as type="button" so they no
  longer default to type="submit" and trigger form submission
- Make the primary action a type="submit" button so Enter submits natively
- Add margin: 0 to .card since it is now a form element
This commit is contained in:
Tronax 2026-08-16 16:51:16 +02:00
parent d400a4a2a0
commit f97b4f92f0
Signed by: Tronax
SSH key fingerprint: SHA256:2pKKXDZucWvaF/GzXNz0FY53EAO1YDLN80bqS+TTz/o

View file

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { computed, ref } from 'vue'
import { login, oidcLogin, register } from '@/game/auth' import { login, oidcLogin, register } from '@/game/auth'
import { store } from '@/game/store' import { store } from '@/game/store'
@ -10,6 +10,11 @@ const displayName = ref('')
const busy = ref(false) const busy = ref(false)
const error = ref('') const error = ref('')
// Password managers (1Password, Bitwarden, ) rely on distinct autocomplete
// hints: "current-password" for login, "new-password" so a strong password is
// suggested/saved during registration.
const passwordAutocomplete = computed(() => (mode.value === 'login' ? 'current-password' : 'new-password'))
function close(): void { function close(): void {
store.auth.showAuth = false store.auth.showAuth = false
error.value = '' error.value = ''
@ -33,40 +38,40 @@ async function submit(): Promise<void> {
<template> <template>
<div v-if="store.auth.showAuth" class="backdrop" @click.self="close"> <div v-if="store.auth.showAuth" class="backdrop" @click.self="close">
<div class="card"> <form class="card" @submit.prevent="submit">
<button class="close" @click="close"></button> <button type="button" class="close" @click="close"></button>
<h2>{{ mode === 'login' ? 'Anmelden' : 'Konto erstellen' }}</h2> <h2>{{ mode === 'login' ? 'Anmelden' : 'Konto erstellen' }}</h2>
<p class="sub">Speichere Kristalle, Forschung & Statistiken über alle Runden hinweg.</p> <p class="sub">Speichere Kristalle, Forschung & Statistiken über alle Runden hinweg.</p>
<div class="tabs"> <div class="tabs">
<button :class="{ active: mode === 'login' }" @click="mode = 'login'">Anmelden</button> <button type="button" :class="{ active: mode === 'login' }" @click="mode = 'login'">Anmelden</button>
<button :class="{ active: mode === 'register' }" @click="mode = 'register'">Registrieren</button> <button type="button" :class="{ active: mode === 'register' }" @click="mode = 'register'">Registrieren</button>
</div> </div>
<label class="field"> <label class="field">
<span>Benutzername</span> <span>Benutzername</span>
<input v-model="username" maxlength="16" autocomplete="username" @keyup.enter="submit" /> <input id="username" name="username" v-model="username" maxlength="16" autocomplete="username" />
</label> </label>
<label v-if="mode === 'register'" class="field"> <label v-if="mode === 'register'" class="field">
<span>Anzeigename (optional)</span> <span>Anzeigename (optional)</span>
<input v-model="displayName" maxlength="24" @keyup.enter="submit" /> <input id="displayName" name="displayName" v-model="displayName" maxlength="24" autocomplete="nickname" />
</label> </label>
<label class="field"> <label class="field">
<span>Passwort</span> <span>Passwort</span>
<input v-model="password" type="password" autocomplete="current-password" @keyup.enter="submit" /> <input id="password" name="password" v-model="password" type="password" :autocomplete="passwordAutocomplete" />
</label> </label>
<p v-if="error" class="error">{{ error }}</p> <p v-if="error" class="error">{{ error }}</p>
<button class="primary" :disabled="busy" @click="submit"> <button type="submit" class="primary" :disabled="busy">
{{ busy ? '…' : mode === 'login' ? '▶ Anmelden' : '▶ Konto erstellen' }} {{ busy ? '…' : mode === 'login' ? '▶ Anmelden' : '▶ Konto erstellen' }}
</button> </button>
<div v-if="store.auth.oidcEnabled" class="divider"><span>oder</span></div> <div v-if="store.auth.oidcEnabled" class="divider"><span>oder</span></div>
<button v-if="store.auth.oidcEnabled" class="oidc" @click="oidcLogin">🔑 {{ store.auth.oidcLabel }}</button> <button type="button" v-if="store.auth.oidcEnabled" class="oidc" @click="oidcLogin">🔑 {{ store.auth.oidcLabel }}</button>
<p class="hint">Als Gast spielen? Einfach schließen Fortschritt wird lokal gespeichert.</p> <p class="hint">Als Gast spielen? Einfach schließen Fortschritt wird lokal gespeichert.</p>
</div> </form>
</div> </div>
</template> </template>
@ -83,6 +88,7 @@ async function submit(): Promise<void> {
} }
.card { .card {
position: relative; position: relative;
margin: 0;
background: var(--panel); background: var(--panel);
border: 1px solid var(--panel-border); border: 1px solid var(--panel-border);
border-radius: 16px; border-radius: 16px;