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
219 lines
5.2 KiB
Vue
219 lines
5.2 KiB
Vue
<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>
|