TRXTD/src/components/AuthModal.vue
Tronax f97b4f92f0
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
2026-08-16 16:51:16 +02:00

219 lines
5.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import { computed, ref } from 'vue'
import { login, oidcLogin, register } from '@/game/auth'
import { store } from '@/game/store'
const mode = ref<'login' | 'register'>('login')
const username = ref('')
const password = ref('')
const displayName = ref('')
const busy = ref(false)
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 {
store.auth.showAuth = false
error.value = ''
}
async function submit(): Promise<void> {
busy.value = true
error.value = ''
const err =
mode.value === 'login'
? await login(username.value.trim(), password.value)
: await register(username.value.trim(), password.value, displayName.value.trim() || username.value.trim())
busy.value = false
if (err) {
error.value = err
} else {
close()
}
}
</script>
<template>
<div v-if="store.auth.showAuth" class="backdrop" @click.self="close">
<form class="card" @submit.prevent="submit">
<button type="button" class="close" @click="close">✕</button>
<h2>{{ mode === 'login' ? 'Anmelden' : 'Konto erstellen' }}</h2>
<p class="sub">Speichere Kristalle, Forschung & Statistiken über alle Runden hinweg.</p>
<div class="tabs">
<button type="button" :class="{ active: mode === 'login' }" @click="mode = 'login'">Anmelden</button>
<button type="button" :class="{ active: mode === 'register' }" @click="mode = 'register'">Registrieren</button>
</div>
<label class="field">
<span>Benutzername</span>
<input id="username" name="username" v-model="username" maxlength="16" autocomplete="username" />
</label>
<label v-if="mode === 'register'" class="field">
<span>Anzeigename (optional)</span>
<input id="displayName" name="displayName" v-model="displayName" maxlength="24" autocomplete="nickname" />
</label>
<label class="field">
<span>Passwort</span>
<input id="password" name="password" v-model="password" type="password" :autocomplete="passwordAutocomplete" />
</label>
<p v-if="error" class="error">{{ error }}</p>
<button type="submit" class="primary" :disabled="busy">
{{ busy ? '' : mode === 'login' ? ' Anmelden' : ' Konto erstellen' }}
</button>
<div v-if="store.auth.oidcEnabled" class="divider"><span>oder</span></div>
<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>
</form>
</div>
</template>
<style scoped>
.backdrop {
position: fixed;
inset: 0;
background: rgba(8, 11, 15, 0.72);
backdrop-filter: blur(3px);
display: flex;
align-items: center;
justify-content: center;
z-index: 50;
}
.card {
position: relative;
margin: 0;
background: var(--panel);
border: 1px solid var(--panel-border);
border-radius: 16px;
padding: 24px 28px;
width: 380px;
max-width: 92vw;
display: flex;
flex-direction: column;
gap: 12px;
}
.close {
position: absolute;
top: 10px;
right: 12px;
background: none;
border: none;
color: var(--text-dim);
font-size: 16px;
cursor: pointer;
}
h2 {
margin: 0;
font-size: 20px;
}
.sub {
margin: 0;
color: var(--text-dim);
font-size: 12.5px;
}
.tabs {
display: flex;
gap: 6px;
}
.tabs button {
flex: 1;
background: var(--panel-inset);
border: 1px solid var(--panel-border);
color: var(--text-dim);
font-family: inherit;
font-weight: 700;
font-size: 13px;
border-radius: 8px;
padding: 8px;
cursor: pointer;
}
.tabs button.active {
color: var(--text);
border-color: var(--accent);
}
.field {
display: flex;
flex-direction: column;
gap: 4px;
}
.field span {
font-size: 12px;
color: var(--text-dim);
}
.field input {
background: var(--panel-inset);
border: 1px solid var(--panel-border);
border-radius: 8px;
padding: 9px 12px;
color: var(--text);
font-family: inherit;
font-size: 14px;
outline: none;
}
.field input:focus {
border-color: var(--accent);
}
.error {
margin: 0;
color: #ff8a7a;
font-size: 13px;
}
.primary {
background: linear-gradient(180deg, #59b34d, #3f8f37);
border: none;
color: #fff;
font-weight: 800;
font-size: 15px;
padding: 11px;
border-radius: 10px;
cursor: pointer;
font-family: inherit;
}
.primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.divider {
display: flex;
align-items: center;
color: var(--text-dim);
font-size: 12px;
}
.divider::before,
.divider::after {
content: '';
flex: 1;
height: 1px;
background: var(--panel-border);
}
.divider span {
padding: 0 8px;
}
.oidc {
background: var(--panel-inset);
border: 1px solid #3d6f9e;
color: var(--text);
font-weight: 700;
font-size: 14px;
padding: 11px;
border-radius: 10px;
cursor: pointer;
font-family: inherit;
}
.oidc:hover {
border-color: var(--accent);
}
.hint {
margin: 0;
color: var(--text-dim);
font-size: 11.5px;
text-align: center;
}
</style>