feat(KAN-22): rebuild password confirmation flows
CI/CD / test_and_check_app (push) Canceled after 0s
CI/CD / Publish immutable image (push) Canceled after 0s
CI/CD / test_and_check_app (pull_request) Canceled after 0s
CI/CD / Publish immutable image (pull_request) Canceled after 0s

This commit is contained in:
2026-09-11 14:18:45 +02:00
parent 65bf75ed77
commit 3f4945c20d
7 changed files with 506 additions and 269 deletions
+122 -74
View File
@@ -1,88 +1,136 @@
<script setup lang="ts">
import { Form, Head } from '@inertiajs/vue3';
import { ref } from 'vue';
import { Head, Link, useForm } from '@inertiajs/vue3';
import { KeyRound } from 'lucide-vue-next';
import { computed, onUnmounted } from 'vue';
import AuthFormFeedback from '@/components/AuthFormFeedback.vue';
import FeedbackState from '@/components/FeedbackState.vue';
import InputError from '@/components/InputError.vue';
import PasswordInput from '@/components/PasswordInput.vue';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Spinner } from '@/components/ui/spinner';
import AuthLayout from '@/layouts/AuthLayout.vue';
import { update } from '@/routes/password';
import { useFormProtection } from '@/composables/useFormProtection';
import { useLocalization } from '@/composables/useLocalization';
import AuthFlowLayout from '@/layouts/auth/AuthFlowLayout.vue';
import { request, update } from '@/routes/password';
const props = defineProps<{
token: string;
email: string;
}>();
const inputEmail = ref(props.email);
const props = defineProps<{ token: string; email: string }>();
const { t } = useLocalization();
const form = useForm({
token: props.token,
email: props.email,
password: '',
password_confirmation: '',
}).dontRemember('token', 'email', 'password', 'password_confirmation');
const protection = useFormProtection(form, { isDirty: () => false });
const invalidLink = computed(() => Boolean(form.errors.email));
const feedbackState = computed(() =>
invalidLink.value ? 'idle' : protection.state.value,
);
const submit = (): void =>
protection.submit(update(), {
onSuccess: () => form.reset('password', 'password_confirmation'),
});
onUnmounted(() => form.reset());
</script>
<template>
<AuthLayout
title="Reset password"
description="Please enter your new password below"
<AuthFlowLayout
:title="t('auth.reset.title')"
:description="t('auth.reset.description')"
:icon="KeyRound"
>
<Head title="Reset password" />
<Form
v-bind="update.form()"
:transform="(data) => ({ ...data, token, email })"
:reset-on-success="['password', 'password_confirmation']"
v-slot="{ errors, processing }"
<Head :title="t('auth.reset.title')" />
<FeedbackState
v-if="invalidLink"
state="error"
:title="t('auth.reset.invalidTitle')"
:description="form.errors.email"
>
<div class="grid gap-6">
<div class="grid gap-2">
<Label for="email">Email</Label>
<Input
id="email"
type="email"
name="email"
autocomplete="email"
v-model="inputEmail"
class="mt-1 block w-full"
readonly
/>
<InputError :message="errors.email" class="mt-2" />
</div>
<div class="grid gap-2">
<Label for="password">Password</Label>
<PasswordInput
id="password"
name="password"
autocomplete="new-password"
class="mt-1 block w-full"
autofocus
placeholder="Password"
/>
<InputError :message="errors.password" />
</div>
<div class="grid gap-2">
<Label for="password_confirmation">
Confirm password
</Label>
<PasswordInput
id="password_confirmation"
name="password_confirmation"
autocomplete="new-password"
class="mt-1 block w-full"
placeholder="Confirm password"
/>
<InputError :message="errors.password_confirmation" />
</div>
<Button
type="submit"
class="mt-4 w-full"
:disabled="processing"
data-test="reset-password-button"
>
<Spinner v-if="processing" />
Reset password
</Button>
<Button as-child variant="outline" class="w-full">
<Link :href="request()">{{ t('auth.reset.requestNew') }}</Link>
</Button>
</FeedbackState>
<form
:action="update.url()"
method="post"
class="grid gap-5"
@submit.prevent="submit"
>
<AuthFormFeedback
:state="feedbackState"
:response-status="protection.responseStatus.value"
:loading-title="t('auth.reset.pending')"
@retry="submit"
/>
<input type="hidden" name="token" :value="form.token" />
<div class="gap-stack grid">
<Label for="email">{{ t('auth.reset.email') }}</Label>
<Input
id="email"
v-model="form.email"
name="email"
type="email"
autocomplete="username"
readonly
:disabled="form.processing"
:invalid="Boolean(form.errors.email)"
aria-describedby="email-error"
/>
<InputError id="email-error" :message="form.errors.email" />
</div>
</Form>
</AuthLayout>
<div class="gap-stack grid">
<Label for="password">{{ t('auth.reset.password') }}</Label>
<PasswordInput
id="password"
v-model="form.password"
name="password"
autocomplete="new-password"
required
autofocus
:disabled="form.processing"
:invalid="Boolean(form.errors.password)"
aria-describedby="password-hint password-error"
/>
<p
id="password-hint"
class="text-caption text-muted-foreground"
>
{{ t('auth.reset.passwordHint') }}
</p>
<InputError
id="password-error"
:message="form.errors.password"
/>
</div>
<div class="gap-stack grid">
<Label for="password_confirmation">{{
t('auth.reset.passwordConfirmation')
}}</Label>
<PasswordInput
id="password_confirmation"
v-model="form.password_confirmation"
name="password_confirmation"
autocomplete="new-password"
required
:disabled="form.processing"
:invalid="Boolean(form.errors.password_confirmation)"
aria-describedby="password-confirmation-error"
/>
<InputError
id="password-confirmation-error"
:message="form.errors.password_confirmation"
/>
</div>
<Button
type="submit"
class="w-full"
:loading="form.processing"
:loading-label="t('auth.reset.pending')"
data-test="reset-password-button"
>{{ t('auth.reset.submit') }}</Button
>
</form>
<template #footer>{{ t('auth.reset.footer') }}</template>
</AuthFlowLayout>
</template>