bilhej/frontend/src/pages/ChangeEmailPage.vue
Joakim Mörling 3532e4d486
All checks were successful
CI / Lint, type check, unit tests, coverage (pull_request) Successful in 2m9s
CI / E2E browser tests (pull_request) Successful in 1m55s
Add account settings dropdown and verified email change flow.
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>
2026-05-22 14:33:06 +02:00

166 lines
4.1 KiB
Vue

<script setup lang="ts">
import { ref, computed } from 'vue'
import { RouterLink } from 'vue-router'
import { ApiError } from '@/api/client'
import { useAuthStore } from '@/stores/authStore'
const auth = useAuthStore()
const newEmail = ref('')
const password = ref('')
const submitting = ref(false)
const errorMessage = ref('')
const successMessage = ref('')
const emailError = computed(() => {
if (newEmail.value.length === 0) return ''
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(newEmail.value)
? ''
: 'Ange en giltig e-postadress'
})
const isValid = computed(
() =>
emailError.value === '' &&
newEmail.value.length > 0 &&
password.value.length > 0 &&
newEmail.value.toLowerCase().trim() !== auth.email?.toLowerCase(),
)
async function handleSubmit() {
if (!isValid.value || submitting.value) return
submitting.value = true
errorMessage.value = ''
successMessage.value = ''
try {
await auth.changeUserEmail(newEmail.value, password.value)
successMessage.value =
'Vi har skickat en bekräftelselänk till din nya e-postadress. Öppna länken i mejlet för att slutföra bytet.'
newEmail.value = ''
password.value = ''
} catch (err) {
if (err instanceof ApiError && err.status === 401) {
errorMessage.value = 'Lösenordet är felaktigt'
} else if (err instanceof ApiError && err.status === 409) {
errorMessage.value = 'E-postadressen är redan registrerad'
} else if (err instanceof ApiError) {
errorMessage.value = err.message
} else {
errorMessage.value = 'Något gick fel. Försök igen senare.'
}
} finally {
submitting.value = false
}
}
</script>
<template>
<div class="page">
<div class="page__card">
<h1 class="page__title">Byt e-postadress</h1>
<p class="page__subtitle">
Nuvarande adress: <strong>{{ auth.email }}</strong>
</p>
<form
v-if="!successMessage"
class="page__form"
@submit.prevent="handleSubmit"
>
<div class="field">
<label for="new-email" class="field__label">Ny e-postadress</label>
<input
id="new-email"
v-model="newEmail"
type="email"
name="newEmail"
autocomplete="email"
class="field__input"
/>
<p v-if="emailError" class="field__hint field__hint--error">
{{ emailError }}
</p>
</div>
<div class="field">
<label for="password" class="field__label">Lösenord</label>
<input
id="password"
v-model="password"
type="password"
name="password"
autocomplete="current-password"
class="field__input"
/>
<p class="field__hint">Bekräfta med ditt nuvarande lösenord.</p>
</div>
<div v-if="errorMessage" class="message message--error">
{{ errorMessage }}
</div>
<button
type="submit"
class="btn btn--primary btn--lg page__submit"
:disabled="!isValid || submitting"
>
{{ submitting ? 'Sparar...' : 'Spara ny e-postadress' }}
</button>
</form>
<div v-else class="message message--success">
{{ successMessage }}
</div>
<p class="page__footer-link">
<RouterLink to="/">Tillbaka till startsidan</RouterLink>
</p>
</div>
</div>
</template>
<style scoped>
.page {
max-width: 28rem;
margin: var(--space-3xl) auto 0;
padding: 0 var(--space-lg);
}
.page__card {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-xl);
padding: var(--space-xl);
box-shadow: var(--shadow-card);
}
.page__title {
margin: 0 0 var(--space-sm) 0;
font-size: 1.5rem;
color: var(--color-ink);
}
.page__subtitle {
margin: 0 0 var(--space-xl) 0;
font-size: 0.875rem;
color: var(--color-muted);
}
.page__form {
display: flex;
flex-direction: column;
gap: var(--space-md);
}
.page__footer-link {
margin-top: var(--space-lg);
text-align: center;
font-size: 0.875rem;
}
.page__submit {
width: 100%;
}
</style>