Replace the header "Byt lösenord" link with an Inställningar menu for changing email or password. Email changes are two-step: request with password, confirmation link to the new address, then password again on confirm so a wrong inbox cannot take over the account. - Backend: EmailChangeService, V10 email_change_tokens, confirm API - Frontend: ChangeEmailPage, ConfirmEmailChangePage, header dropdown - E2E: account-settings round-trips, Mailpit verification, wrong-password guard - Flyway: V9 restore for dev DBs, CI migration checks, V10 for email tokens Co-authored-by: Cursor <cursoragent@cursor.com>
316 lines
7.8 KiB
Vue
316 lines
7.8 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
|
import { RouterLink, useRouter, useRoute } from 'vue-router'
|
|
import { useAuthStore } from '@/stores/authStore'
|
|
|
|
const router = useRouter()
|
|
const route = useRoute()
|
|
const auth = useAuthStore()
|
|
const isSettingsActive = computed(
|
|
() =>
|
|
route.name === 'change-email' || route.name === 'change-password',
|
|
)
|
|
const settingsOpen = ref(false)
|
|
const settingsRef = ref<HTMLElement | null>(null)
|
|
|
|
function toggleSettings() {
|
|
settingsOpen.value = !settingsOpen.value
|
|
}
|
|
|
|
function closeSettings() {
|
|
settingsOpen.value = false
|
|
}
|
|
|
|
function handleDocumentClick(event: MouseEvent) {
|
|
if (!settingsRef.value?.contains(event.target as Node)) {
|
|
settingsOpen.value = false
|
|
}
|
|
}
|
|
|
|
function handleLogout() {
|
|
auth.logout()
|
|
router.push('/')
|
|
}
|
|
|
|
onMounted(() => {
|
|
document.addEventListener('click', handleDocumentClick)
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
document.removeEventListener('click', handleDocumentClick)
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<header class="app-header">
|
|
<div class="app-header__inner">
|
|
<RouterLink to="/" class="app-header__logo">
|
|
<svg
|
|
class="app-header__logo-icon"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
aria-hidden="true"
|
|
>
|
|
<rect
|
|
x="2"
|
|
y="5"
|
|
width="20"
|
|
height="14"
|
|
rx="2"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
/>
|
|
<path
|
|
d="M2 7l10 6 10-6"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
/>
|
|
</svg>
|
|
Bilhej
|
|
</RouterLink>
|
|
<nav class="app-header__nav">
|
|
<RouterLink to="/" class="app-header__link">Hem</RouterLink>
|
|
<template v-if="!auth.isAuthenticated">
|
|
<RouterLink to="/logga-in" class="app-header__link"
|
|
>Logga in</RouterLink
|
|
>
|
|
<RouterLink to="/registrera" class="app-header__link"
|
|
>Registrera</RouterLink
|
|
>
|
|
</template>
|
|
<template v-else>
|
|
<RouterLink
|
|
v-if="auth.isAdmin"
|
|
to="/admin"
|
|
class="app-header__link app-header__link--admin"
|
|
>Admin</RouterLink
|
|
>
|
|
<RouterLink to="/orders" class="app-header__link"
|
|
>Mina beställningar</RouterLink
|
|
>
|
|
<div ref="settingsRef" class="app-header__settings">
|
|
<button
|
|
type="button"
|
|
class="app-header__settings-trigger"
|
|
:class="{
|
|
'app-header__settings-trigger--active': isSettingsActive,
|
|
}"
|
|
aria-haspopup="menu"
|
|
:aria-expanded="settingsOpen"
|
|
@click.stop="toggleSettings"
|
|
>
|
|
Inställningar
|
|
<svg
|
|
class="app-header__settings-chevron"
|
|
:class="{ 'app-header__settings-chevron--open': settingsOpen }"
|
|
viewBox="0 0 20 20"
|
|
fill="currentColor"
|
|
aria-hidden="true"
|
|
>
|
|
<path
|
|
fill-rule="evenodd"
|
|
d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.94a.75.75 0 111.08 1.04l-4.24 4.5a.75.75 0 01-1.08 0l-4.24-4.5a.75.75 0 01.02-1.06z"
|
|
clip-rule="evenodd"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
<div
|
|
v-if="settingsOpen"
|
|
class="app-header__settings-menu"
|
|
role="menu"
|
|
>
|
|
<RouterLink
|
|
to="/andra-epost"
|
|
class="app-header__settings-item"
|
|
role="menuitem"
|
|
@click="closeSettings"
|
|
>
|
|
Byt e-postadress
|
|
</RouterLink>
|
|
<RouterLink
|
|
to="/andra-losenord"
|
|
class="app-header__settings-item"
|
|
role="menuitem"
|
|
@click="closeSettings"
|
|
>
|
|
Byt lösenord
|
|
</RouterLink>
|
|
</div>
|
|
</div>
|
|
<button class="app-header__logout" @click="handleLogout">
|
|
Logga ut
|
|
</button>
|
|
<span class="app-header__email">{{ auth.email }}</span>
|
|
</template>
|
|
</nav>
|
|
</div>
|
|
</header>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.app-header {
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 100;
|
|
background: rgba(253, 250, 245, 0.85);
|
|
backdrop-filter: blur(12px);
|
|
-webkit-backdrop-filter: blur(12px);
|
|
border-bottom: 1px solid var(--color-border);
|
|
}
|
|
|
|
.app-header__inner {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
max-width: 72rem;
|
|
margin: 0 auto;
|
|
padding: 0.875rem var(--space-lg);
|
|
}
|
|
|
|
.app-header__logo {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-sm);
|
|
font-size: 1.125rem;
|
|
font-weight: 700;
|
|
color: var(--color-ink);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.app-header__logo-icon {
|
|
width: 1.5rem;
|
|
height: 1.5rem;
|
|
}
|
|
|
|
.app-header__nav {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-sm);
|
|
}
|
|
|
|
.app-header__link {
|
|
padding: 0.4rem 0.875rem;
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
color: var(--color-muted);
|
|
text-decoration: none;
|
|
border-radius: var(--radius-full);
|
|
transition:
|
|
color var(--transition-fast),
|
|
background var(--transition-fast);
|
|
}
|
|
|
|
.app-header__link:hover,
|
|
.app-header__link.router-link-active {
|
|
color: var(--color-primary-dark);
|
|
background: var(--color-primary-soft);
|
|
}
|
|
|
|
.app-header__link--admin {
|
|
background: var(--color-primary-soft);
|
|
color: var(--color-primary);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.app-header__link--admin:hover {
|
|
background: #e9d5ff;
|
|
color: var(--color-primary-dark);
|
|
}
|
|
|
|
.app-header__email {
|
|
color: var(--color-muted);
|
|
font-size: 0.8125rem;
|
|
padding: 0 0.5rem;
|
|
}
|
|
|
|
.app-header__settings {
|
|
position: relative;
|
|
}
|
|
|
|
.app-header__settings-trigger {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
padding: 0.4rem 0.875rem;
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
color: var(--color-muted);
|
|
background: none;
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius-full);
|
|
cursor: pointer;
|
|
transition:
|
|
color var(--transition-fast),
|
|
border-color var(--transition-fast),
|
|
background var(--transition-fast);
|
|
}
|
|
|
|
.app-header__settings-trigger:hover,
|
|
.app-header__settings-trigger--active,
|
|
.app-header__settings-trigger[aria-expanded='true'] {
|
|
color: var(--color-primary-dark);
|
|
border-color: #bfdbfe;
|
|
background: var(--color-primary-soft);
|
|
}
|
|
|
|
.app-header__settings-chevron {
|
|
width: 1rem;
|
|
height: 1rem;
|
|
transition: transform var(--transition-fast);
|
|
}
|
|
|
|
.app-header__settings-chevron--open {
|
|
transform: rotate(180deg);
|
|
}
|
|
|
|
.app-header__settings-menu {
|
|
position: absolute;
|
|
top: calc(100% + 0.5rem);
|
|
right: 0;
|
|
min-width: 12rem;
|
|
padding: 0.35rem;
|
|
background: var(--color-surface);
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius-lg);
|
|
box-shadow: var(--shadow-lg);
|
|
}
|
|
|
|
.app-header__settings-item {
|
|
display: block;
|
|
padding: 0.625rem 0.875rem;
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
color: var(--color-ink);
|
|
text-decoration: none;
|
|
border-radius: var(--radius-md);
|
|
transition: background var(--transition-fast);
|
|
}
|
|
|
|
.app-header__settings-item:hover {
|
|
background: var(--color-border-light);
|
|
}
|
|
|
|
.app-header__logout {
|
|
background: none;
|
|
border: 1px solid var(--color-border);
|
|
color: var(--color-muted);
|
|
font-size: 0.8125rem;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
padding: 0.35rem 0.875rem;
|
|
border-radius: var(--radius-full);
|
|
transition:
|
|
color var(--transition-fast),
|
|
border-color var(--transition-fast),
|
|
background var(--transition-fast);
|
|
}
|
|
|
|
.app-header__logout:hover {
|
|
color: var(--color-danger);
|
|
border-color: var(--color-danger);
|
|
background: var(--color-danger-soft);
|
|
}
|
|
</style>
|