feat: passwordless registration — defer account creation to approval
Removes password from the volunteer registration form. Account creation is now deferred to the approval step: Backend: - Registration creates Person without User (user_id=null) - On approval, system finds or creates User by person.email - New accounts get a "set password" email with activation link - Existing accounts get a portal link email - Added registration_source column to persons (self/organizer) - Fuzzy name matching skipped for self-registered persons - person.email is always source of truth for account linking Frontend: - Registration form no longer collects password - Email check shows info alert with login suggestion - New wachtwoord-instellen.vue page for account activation - PasswordRequirements.vue component (reused on reset page) - Success page updated with activation messaging Tests: 837 passed (all updated for new flow) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
31
apps/portal/src/components/auth/PasswordRequirements.vue
Normal file
31
apps/portal/src/components/auth/PasswordRequirements.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ password: string }>()
|
||||
|
||||
const requirements = computed(() => [
|
||||
{ label: 'Minimaal 8 tekens', met: props.password.length >= 8 },
|
||||
{ label: 'Een hoofdletter', met: /[A-Z]/.test(props.password) },
|
||||
{ label: 'Een kleine letter', met: /[a-z]/.test(props.password) },
|
||||
{ label: 'Een cijfer', met: /[0-9]/.test(props.password) },
|
||||
])
|
||||
|
||||
const allMet = computed(() => requirements.value.every(r => r.met))
|
||||
|
||||
defineExpose({ allMet })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="text-body-2 mt-2">
|
||||
<div
|
||||
v-for="req in requirements"
|
||||
:key="req.label"
|
||||
:class="req.met ? 'text-success' : 'text-medium-emphasis'"
|
||||
class="d-flex align-center gap-1 mb-1"
|
||||
>
|
||||
<VIcon
|
||||
:icon="req.met ? 'tabler-check' : 'tabler-x'"
|
||||
size="14"
|
||||
/>
|
||||
<span>{{ req.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -24,7 +24,6 @@ export function useRegistrationData(eventSlug: Ref<string>) {
|
||||
|
||||
export interface VolunteerRegistrationResponse {
|
||||
person: Record<string, unknown>
|
||||
has_existing_account: boolean
|
||||
}
|
||||
|
||||
export function useSubmitRegistration() {
|
||||
|
||||
@@ -32,6 +32,7 @@ const errorMessage = ref('')
|
||||
const isSubmitting = ref(false)
|
||||
|
||||
const passwordResetDone = computed(() => route.query.reset === '1')
|
||||
const accountActivated = computed(() => route.query.activated === '1')
|
||||
|
||||
// MFA challenge state
|
||||
const showMfaChallenge = ref(false)
|
||||
@@ -179,7 +180,17 @@ function onMfaCancelled() {
|
||||
|
||||
<VCardText>
|
||||
<VAlert
|
||||
v-if="passwordResetDone"
|
||||
v-if="accountActivated"
|
||||
type="success"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
density="comfortable"
|
||||
>
|
||||
Account geactiveerd! Je kunt nu inloggen.
|
||||
</VAlert>
|
||||
|
||||
<VAlert
|
||||
v-else-if="passwordResetDone"
|
||||
type="success"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
|
||||
@@ -42,21 +42,12 @@ const submitError = ref<string | null>(null)
|
||||
const fieldFormData = ref<Record<string, unknown>>({})
|
||||
const fieldErrors = ref<Record<string, string>>({})
|
||||
|
||||
const password = ref('')
|
||||
const passwordConfirmation = ref('')
|
||||
// Email existence check (privacy-safe — only shows boolean)
|
||||
const emailExists = ref<boolean | null>(null)
|
||||
const emailChecking = ref(false)
|
||||
const lastCheckedEmail = ref('')
|
||||
let checkEmailRequestSeq = 0
|
||||
|
||||
const showPassword = ref(false)
|
||||
const showPasswordConfirmation = ref(false)
|
||||
|
||||
const passwordServerError = ref('')
|
||||
const passwordConfirmationServerError = ref('')
|
||||
const passwordClientError = ref('')
|
||||
const passwordConfirmationClientError = ref('')
|
||||
|
||||
const { errors, defineField, validateField, setFieldValue, setFieldError } = useForm({
|
||||
validationSchema: toTypedSchema(fullRegistrationSchema),
|
||||
initialValues: {
|
||||
@@ -138,84 +129,12 @@ watchDebounced(
|
||||
|
||||
watch(email, val => {
|
||||
if (authStore.isAuthenticated) return
|
||||
|
||||
const t = (val ?? '').trim()
|
||||
if (t !== lastCheckedEmail.value) {
|
||||
emailExists.value = null
|
||||
password.value = ''
|
||||
passwordConfirmation.value = ''
|
||||
passwordServerError.value = ''
|
||||
passwordConfirmationServerError.value = ''
|
||||
passwordClientError.value = ''
|
||||
passwordConfirmationClientError.value = ''
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => authStore.isAuthenticated, authed => {
|
||||
if (!authed) return
|
||||
|
||||
emailExists.value = null
|
||||
emailChecking.value = false
|
||||
lastCheckedEmail.value = ''
|
||||
password.value = ''
|
||||
passwordConfirmation.value = ''
|
||||
passwordServerError.value = ''
|
||||
passwordConfirmationServerError.value = ''
|
||||
passwordClientError.value = ''
|
||||
passwordConfirmationClientError.value = ''
|
||||
})
|
||||
|
||||
watch(password, () => {
|
||||
passwordServerError.value = ''
|
||||
passwordClientError.value = ''
|
||||
})
|
||||
|
||||
watch(passwordConfirmation, () => {
|
||||
passwordConfirmationServerError.value = ''
|
||||
passwordConfirmationClientError.value = ''
|
||||
})
|
||||
|
||||
function validatePasswordsForStep(): boolean {
|
||||
passwordClientError.value = ''
|
||||
passwordConfirmationClientError.value = ''
|
||||
|
||||
if (authStore.isAuthenticated) return true
|
||||
if (emailChecking.value) return false
|
||||
|
||||
const em = (email.value ?? '').trim()
|
||||
if (!isEmailFormatForCheck(em)) return true
|
||||
|
||||
if (emailExists.value === null) return false
|
||||
|
||||
if (emailExists.value === true) {
|
||||
if (!password.value) {
|
||||
passwordClientError.value = 'Verplicht'
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
if (!password.value) {
|
||||
passwordClientError.value = 'Verplicht'
|
||||
|
||||
return false
|
||||
}
|
||||
if (password.value.length < 8) {
|
||||
passwordClientError.value = 'Minimaal 8 tekens'
|
||||
|
||||
return false
|
||||
}
|
||||
if (password.value !== passwordConfirmation.value) {
|
||||
passwordConfirmationClientError.value = 'Wachtwoorden komen niet overeen'
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const selectedSectionIds = ref<string[]>([])
|
||||
const selectedTimeSlotIds = ref<string[]>([])
|
||||
const timeSlotPreferences = ref<Record<string, number>>({})
|
||||
@@ -440,9 +359,8 @@ async function validateCurrentStep(): Promise<boolean> {
|
||||
const k = currentStepKind.value
|
||||
if (k === 'personal') {
|
||||
const results = await Promise.all(personalFieldKeys.map(f => validateField(f)))
|
||||
if (!results.every(r => r.valid)) return false
|
||||
|
||||
return validatePasswordsForStep()
|
||||
return results.every(r => r.valid)
|
||||
}
|
||||
if (k === 'dynamic')
|
||||
return validateDynamicFields()
|
||||
@@ -544,30 +462,14 @@ function applyServerValidationErrors(serverErrors: Record<string, string[]>) {
|
||||
|
||||
continue
|
||||
}
|
||||
if (key === 'password') {
|
||||
passwordServerError.value = msgs[0] ?? 'Ongeldige waarde.'
|
||||
|
||||
continue
|
||||
}
|
||||
if (key === 'password_confirmation') {
|
||||
passwordConfirmationServerError.value = msgs[0] ?? 'Ongeldige waarde.'
|
||||
|
||||
continue
|
||||
}
|
||||
if (isPersonalFieldKey(key))
|
||||
setFieldError(key, msgs[0] ?? 'Ongeldige waarde.')
|
||||
}
|
||||
}
|
||||
|
||||
function isRegistrationStepOneServerErrorKey(key: string): boolean {
|
||||
return isPersonalFieldKey(key) || key === 'password' || key === 'password_confirmation'
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
submitError.value = null
|
||||
fieldErrors.value = {}
|
||||
passwordServerError.value = ''
|
||||
passwordConfirmationServerError.value = ''
|
||||
|
||||
const rPersonal = await Promise.all(personalFieldKeys.map(f => validateField(f)))
|
||||
if (!rPersonal.every(x => x.valid)) {
|
||||
@@ -575,11 +477,6 @@ async function onSubmit() {
|
||||
|
||||
return
|
||||
}
|
||||
if (!validatePasswordsForStep()) {
|
||||
currentStep.value = 0
|
||||
|
||||
return
|
||||
}
|
||||
if (!validateDynamicFields()) {
|
||||
currentStep.value = 1
|
||||
|
||||
@@ -612,17 +509,11 @@ async function onSubmit() {
|
||||
availabilities,
|
||||
}
|
||||
|
||||
if (!authStore.isAuthenticated) {
|
||||
payload.password = password.value
|
||||
if (emailExists.value === false)
|
||||
payload.password_confirmation = passwordConfirmation.value
|
||||
}
|
||||
|
||||
if (field_values) payload.field_values = field_values
|
||||
if (section_preferences?.length) payload.section_preferences = section_preferences
|
||||
|
||||
try {
|
||||
const result = await submitRegistration({
|
||||
await submitRegistration({
|
||||
eventId: registrationData.value.event.id,
|
||||
form: payload,
|
||||
})
|
||||
@@ -644,7 +535,6 @@ async function onSubmit() {
|
||||
event: registrationData.value.event.name,
|
||||
banner: registrationData.value.event.registration_banner_url ?? '',
|
||||
authenticated: authStore.isAuthenticated ? '1' : '0',
|
||||
hasAccount: result.has_existing_account ? '1' : '0',
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -657,7 +547,7 @@ async function onSubmit() {
|
||||
if (serverErrors) {
|
||||
applyServerValidationErrors(serverErrors)
|
||||
const keys = Object.keys(serverErrors)
|
||||
if (keys.some(k => isRegistrationStepOneServerErrorKey(k))) {
|
||||
if (keys.some(k => isPersonalFieldKey(k))) {
|
||||
currentStep.value = 0
|
||||
|
||||
return
|
||||
@@ -981,7 +871,7 @@ async function onSubmit() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Step 0: Over jou + contact (Vuexy: vertical fields, form separator, contact block) -->
|
||||
<!-- Step 0: Over jou + contact -->
|
||||
<div v-show="currentStep === 0">
|
||||
<VRow>
|
||||
<VCol
|
||||
@@ -1063,7 +953,6 @@ async function onSubmit() {
|
||||
</h6>
|
||||
<p class="text-body-2 text-medium-emphasis mb-6">
|
||||
Vul deze gegevens zorgvuldig in: we gebruiken ze om alle informatie over het evenement naar je te versturen.
|
||||
Je e-mailadres is ook je gebruikersnaam voor Crewli, het systeem waar je straks alle informatie terugvindt.
|
||||
</p>
|
||||
|
||||
<VRow>
|
||||
@@ -1099,18 +988,40 @@ async function onSubmit() {
|
||||
/>
|
||||
</template>
|
||||
</VTextField>
|
||||
<p
|
||||
v-if="!authStore.isAuthenticated && emailExists === true"
|
||||
class="text-body-2 text-success mt-2 mb-0"
|
||||
</VCol>
|
||||
|
||||
<!-- Existing account notice -->
|
||||
<VCol
|
||||
v-if="!authStore.isAuthenticated && emailExists === true"
|
||||
cols="12"
|
||||
>
|
||||
<VAlert
|
||||
type="info"
|
||||
variant="tonal"
|
||||
>
|
||||
✓ We herkennen dit emailadres! Vul je wachtwoord in om verder te gaan.
|
||||
</p>
|
||||
<p
|
||||
v-else-if="!authStore.isAuthenticated && emailExists === false"
|
||||
class="text-caption text-medium-emphasis mt-2 mb-0"
|
||||
>
|
||||
Je maakt een nieuw account aan
|
||||
</p>
|
||||
<template #title>
|
||||
Er bestaat al een account met dit e-mailadres
|
||||
</template>
|
||||
<template #text>
|
||||
<p class="mb-2">
|
||||
Als je al eerder bij een evenement betrokken bent geweest,
|
||||
kun je inloggen om je registratie automatisch te koppelen
|
||||
aan je bestaande account.
|
||||
</p>
|
||||
<VBtn
|
||||
size="small"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
:to="`/login?to=/register/${eventSlug}`"
|
||||
>
|
||||
Inloggen en registreren
|
||||
</VBtn>
|
||||
<p class="text-caption text-medium-emphasis mt-2 mb-0">
|
||||
Je kunt ook gewoon doorgaan met registreren — we koppelen
|
||||
je registratie later aan je account.
|
||||
</p>
|
||||
</template>
|
||||
</VAlert>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
@@ -1132,82 +1043,6 @@ async function onSubmit() {
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
<template v-if="!authStore.isAuthenticated && emailExists !== null">
|
||||
<VRow v-if="emailExists === false">
|
||||
<VCol cols="12">
|
||||
<label
|
||||
class="text-body-2 d-block mb-1 text-high-emphasis"
|
||||
for="volunteer-reg-password"
|
||||
>
|
||||
Wachtwoord <span class="text-error">*</span>
|
||||
</label>
|
||||
<VTextField
|
||||
id="volunteer-reg-password"
|
||||
v-model="password"
|
||||
variant="outlined"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
density="comfortable"
|
||||
hide-details="auto"
|
||||
:append-inner-icon="showPassword ? 'mdi-eye-off' : 'mdi-eye'"
|
||||
:rules="[(v: string) => !!v || 'Verplicht', (v: string) => v.length >= 8 || 'Minimaal 8 tekens']"
|
||||
:error-messages="passwordServerError || passwordClientError ? [passwordServerError || passwordClientError] : undefined"
|
||||
@click:append-inner="showPassword = !showPassword"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<label
|
||||
class="text-body-2 d-block mb-1 text-high-emphasis"
|
||||
for="volunteer-reg-password-confirm"
|
||||
>
|
||||
Bevestig wachtwoord <span class="text-error">*</span>
|
||||
</label>
|
||||
<VTextField
|
||||
id="volunteer-reg-password-confirm"
|
||||
v-model="passwordConfirmation"
|
||||
variant="outlined"
|
||||
:type="showPasswordConfirmation ? 'text' : 'password'"
|
||||
density="comfortable"
|
||||
hide-details="auto"
|
||||
:append-inner-icon="showPasswordConfirmation ? 'mdi-eye-off' : 'mdi-eye'"
|
||||
:rules="[(v: string) => v === password || 'Wachtwoorden komen niet overeen']"
|
||||
:error-messages="passwordConfirmationServerError || passwordConfirmationClientError ? [passwordConfirmationServerError || passwordConfirmationClientError] : undefined"
|
||||
@click:append-inner="showPasswordConfirmation = !showPasswordConfirmation"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
<VRow v-else-if="emailExists === true">
|
||||
<VCol cols="12">
|
||||
<label
|
||||
class="text-body-2 d-block mb-1 text-high-emphasis"
|
||||
for="volunteer-reg-password-existing"
|
||||
>
|
||||
Wachtwoord <span class="text-error">*</span>
|
||||
</label>
|
||||
<VTextField
|
||||
id="volunteer-reg-password-existing"
|
||||
v-model="password"
|
||||
variant="outlined"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
density="comfortable"
|
||||
hide-details="auto"
|
||||
hint="Voer je bestaande wachtwoord in"
|
||||
persistent-hint
|
||||
:append-inner-icon="showPassword ? 'mdi-eye-off' : 'mdi-eye'"
|
||||
:rules="[(v: string) => !!v || 'Verplicht']"
|
||||
:error-messages="passwordServerError || passwordClientError ? [passwordServerError || passwordClientError] : undefined"
|
||||
@click:append-inner="showPassword = !showPassword"
|
||||
/>
|
||||
<RouterLink
|
||||
to="/wachtwoord-vergeten"
|
||||
class="text-caption d-inline-block mt-1"
|
||||
>
|
||||
Wachtwoord vergeten?
|
||||
</RouterLink>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Step 1: Extra informatie (dynamic registration_fields) -->
|
||||
@@ -1630,7 +1465,7 @@ async function onSubmit() {
|
||||
}
|
||||
|
||||
/*
|
||||
Vuetify’s .v-list uses overflow: auto, which shows scrollbars on this step when
|
||||
Vuetify's .v-list uses overflow: auto, which shows scrollbars on this step when
|
||||
list rows are slightly wider than the card (e.g. checkbox + text + rating).
|
||||
*/
|
||||
.registration-availability-list {
|
||||
|
||||
@@ -16,7 +16,6 @@ const authStore = useAuthStore()
|
||||
const eventName = computed(() => (route.query.event as string) || 'het evenement')
|
||||
const bannerUrl = computed(() => (route.query.banner as string) || null)
|
||||
const isAuthenticated = computed(() => route.query.authenticated === '1' || authStore.isAuthenticated)
|
||||
const hasExistingAccount = computed(() => route.query.hasAccount === '1' && !isAuthenticated.value)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -79,6 +78,7 @@ const hasExistingAccount = computed(() => route.query.hasAccount === '1' && !isA
|
||||
|
||||
<p class="text-body-2 text-disabled mb-8">
|
||||
Je ontvangt een e-mail zodra je aanmelding is goedgekeurd.
|
||||
Daarin vind je een link om je account te activeren.
|
||||
</p>
|
||||
|
||||
<div class="d-flex flex-wrap justify-center gap-4">
|
||||
@@ -93,45 +93,15 @@ const hasExistingAccount = computed(() => route.query.hasAccount === '1' && !isA
|
||||
|
||||
<VBtn
|
||||
v-else
|
||||
to="/login"
|
||||
to="/"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
prepend-icon="tabler-login"
|
||||
prepend-icon="tabler-home"
|
||||
>
|
||||
Heb je al een account? Log in
|
||||
Terug naar startpagina
|
||||
</VBtn>
|
||||
</div>
|
||||
</VCard>
|
||||
|
||||
<VAlert
|
||||
v-if="hasExistingAccount"
|
||||
type="info"
|
||||
variant="tonal"
|
||||
class="mt-4"
|
||||
>
|
||||
<div class="font-weight-medium mb-1">
|
||||
Er bestaat al een account met dit e-mailadres
|
||||
</div>
|
||||
<p class="text-body-2 mb-3">
|
||||
Log in om je aanmelding te koppelen aan je bestaande account.
|
||||
Zo kun je straks je diensten bekijken in het portaal.
|
||||
</p>
|
||||
<VBtn
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
:to="{ name: 'login' }"
|
||||
>
|
||||
<VIcon
|
||||
start
|
||||
size="16"
|
||||
>
|
||||
tabler-login
|
||||
</VIcon>
|
||||
Inloggen
|
||||
</VBtn>
|
||||
</VAlert>
|
||||
|
||||
</VContainer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
195
apps/portal/src/pages/wachtwoord-instellen.vue
Normal file
195
apps/portal/src/pages/wachtwoord-instellen.vue
Normal file
@@ -0,0 +1,195 @@
|
||||
<script setup lang="ts">
|
||||
import authV1BottomShape from '@images/svg/auth-v1-bottom-shape.svg?raw'
|
||||
import authV1TopShape from '@images/svg/auth-v1-top-shape.svg?raw'
|
||||
import { VNodeRenderer } from '@layouts/components/VNodeRenderer'
|
||||
import { themeConfig } from '@themeConfig'
|
||||
import PasswordRequirements from '@/components/auth/PasswordRequirements.vue'
|
||||
import { apiClient } from '@/lib/axios'
|
||||
|
||||
definePage({
|
||||
name: 'set-password',
|
||||
meta: {
|
||||
layout: 'blank',
|
||||
requiresAuth: false,
|
||||
},
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const email = ref(typeof route.query.email === 'string' ? route.query.email : '')
|
||||
const token = ref(typeof route.query.token === 'string' ? route.query.token : '')
|
||||
const password = ref('')
|
||||
const passwordConfirmation = ref('')
|
||||
const showPassword = ref(false)
|
||||
const showPasswordConfirmation = ref(false)
|
||||
|
||||
const errorMessage = ref('')
|
||||
const isSubmitting = ref(false)
|
||||
|
||||
const passwordReqsRef = ref<InstanceType<typeof PasswordRequirements>>()
|
||||
|
||||
const confirmationError = computed(() => {
|
||||
if (!passwordConfirmation.value) return ''
|
||||
if (password.value !== passwordConfirmation.value) return 'Wachtwoorden komen niet overeen'
|
||||
|
||||
return ''
|
||||
})
|
||||
|
||||
const canSubmit = computed(() =>
|
||||
password.value.length > 0
|
||||
&& passwordConfirmation.value.length > 0
|
||||
&& password.value === passwordConfirmation.value
|
||||
&& (passwordReqsRef.value?.allMet ?? false),
|
||||
)
|
||||
|
||||
async function onSubmit(): Promise<void> {
|
||||
errorMessage.value = ''
|
||||
if (!token.value || !email.value) {
|
||||
errorMessage.value = 'Ongeldige activatielink. Neem contact op met de organisatie.'
|
||||
|
||||
return
|
||||
}
|
||||
if (!canSubmit.value) return
|
||||
|
||||
isSubmitting.value = true
|
||||
try {
|
||||
await apiClient.post('/auth/reset-password', {
|
||||
email: email.value.trim(),
|
||||
password: password.value,
|
||||
password_confirmation: passwordConfirmation.value,
|
||||
token: token.value,
|
||||
})
|
||||
await router.replace({ path: '/login', query: { activated: '1' } })
|
||||
}
|
||||
catch (error: unknown) {
|
||||
const ax = error as { response?: { status?: number; data?: { message?: string } } }
|
||||
|
||||
if (ax.response?.status === 404 || ax.response?.status === 422)
|
||||
errorMessage.value = ax.response?.data?.message ?? 'Activatielink ongeldig of verlopen. Neem contact op met de organisatie.'
|
||||
else
|
||||
errorMessage.value = 'Er ging iets mis. Probeer het later opnieuw.'
|
||||
}
|
||||
finally {
|
||||
isSubmitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="auth-wrapper d-flex align-center justify-center pa-4">
|
||||
<div class="position-relative my-sm-16">
|
||||
<VNodeRenderer
|
||||
:nodes="h('div', { innerHTML: authV1TopShape })"
|
||||
class="text-primary auth-v1-top-shape d-none d-sm-block"
|
||||
/>
|
||||
|
||||
<VNodeRenderer
|
||||
:nodes="h('div', { innerHTML: authV1BottomShape })"
|
||||
class="text-primary auth-v1-bottom-shape d-none d-sm-block"
|
||||
/>
|
||||
|
||||
<VCard
|
||||
class="auth-card"
|
||||
max-width="460"
|
||||
:class="$vuetify.display.smAndUp ? 'pa-6' : 'pa-2'"
|
||||
>
|
||||
<VCardItem class="justify-center">
|
||||
<VCardTitle>
|
||||
<RouterLink to="/">
|
||||
<div class="app-logo">
|
||||
<VNodeRenderer :nodes="themeConfig.app.logo" />
|
||||
<h1 class="app-logo-title">
|
||||
{{ themeConfig.app.title }}
|
||||
</h1>
|
||||
</div>
|
||||
</RouterLink>
|
||||
</VCardTitle>
|
||||
</VCardItem>
|
||||
|
||||
<VCardText>
|
||||
<h4 class="text-h4 mb-1">
|
||||
Stel je wachtwoord in
|
||||
</h4>
|
||||
<p class="mb-0">
|
||||
Welkom bij Crewli! Kies een wachtwoord om je account te activeren.
|
||||
</p>
|
||||
</VCardText>
|
||||
|
||||
<VCardText>
|
||||
<VAlert
|
||||
v-if="errorMessage"
|
||||
type="error"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</VAlert>
|
||||
|
||||
<VForm @submit.prevent="onSubmit">
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
v-model="password"
|
||||
autofocus
|
||||
label="Wachtwoord"
|
||||
placeholder="············"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
autocomplete="new-password"
|
||||
:append-inner-icon="showPassword ? 'tabler-eye-off' : 'tabler-eye'"
|
||||
@click:append-inner="showPassword = !showPassword"
|
||||
/>
|
||||
<PasswordRequirements
|
||||
ref="passwordReqsRef"
|
||||
:password="password"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
v-model="passwordConfirmation"
|
||||
label="Bevestig wachtwoord"
|
||||
placeholder="············"
|
||||
:type="showPasswordConfirmation ? 'text' : 'password'"
|
||||
autocomplete="new-password"
|
||||
:error-messages="confirmationError ? [confirmationError] : undefined"
|
||||
:append-inner-icon="showPasswordConfirmation ? 'tabler-eye-off' : 'tabler-eye'"
|
||||
@click:append-inner="showPasswordConfirmation = !showPasswordConfirmation"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<VBtn
|
||||
block
|
||||
type="submit"
|
||||
:loading="isSubmitting"
|
||||
:disabled="!canSubmit"
|
||||
>
|
||||
Account activeren
|
||||
</VBtn>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<RouterLink
|
||||
class="d-flex align-center justify-center"
|
||||
to="/login"
|
||||
>
|
||||
<VIcon
|
||||
icon="tabler-chevron-left"
|
||||
size="20"
|
||||
class="me-1 flip-in-rtl"
|
||||
/>
|
||||
<span>Terug naar inloggen</span>
|
||||
</RouterLink>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@use "@core/scss/template/pages/page-auth";
|
||||
</style>
|
||||
@@ -3,6 +3,7 @@ import authV1BottomShape from '@images/svg/auth-v1-bottom-shape.svg?raw'
|
||||
import authV1TopShape from '@images/svg/auth-v1-top-shape.svg?raw'
|
||||
import { VNodeRenderer } from '@layouts/components/VNodeRenderer'
|
||||
import { themeConfig } from '@themeConfig'
|
||||
import PasswordRequirements from '@/components/auth/PasswordRequirements.vue'
|
||||
import { apiClient } from '@/lib/axios'
|
||||
|
||||
definePage({
|
||||
@@ -120,6 +121,7 @@ async function onSubmit(): Promise<void> {
|
||||
:append-inner-icon="showPassword ? 'tabler-eye-off' : 'tabler-eye'"
|
||||
@click:append-inner="showPassword = !showPassword"
|
||||
/>
|
||||
<PasswordRequirements :password="password" />
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Router } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/useAuthStore'
|
||||
import { usePortalStore } from '@/stores/usePortalStore'
|
||||
|
||||
const guestOnlyPaths = ['/login', '/wachtwoord-vergeten', '/wachtwoord-resetten', '/verify-email-change']
|
||||
const guestOnlyPaths = ['/login', '/wachtwoord-vergeten', '/wachtwoord-resetten', '/wachtwoord-instellen', '/verify-email-change']
|
||||
|
||||
// Old dashboard routes that need backward-compat redirects
|
||||
const dashboardRedirects: Record<string, string> = {
|
||||
|
||||
@@ -79,8 +79,6 @@ export interface VolunteerRegistrationForm {
|
||||
date_of_birth: string
|
||||
email: string
|
||||
phone: string
|
||||
password?: string
|
||||
password_confirmation?: string
|
||||
field_values?: Record<string, unknown>
|
||||
section_preferences?: SectionPreference[]
|
||||
availabilities: VolunteerAvailability[]
|
||||
|
||||
Reference in New Issue
Block a user