fix: add password constraint validation to all password-set/change forms
Login forms correctly only check for empty fields (no password constraints needed). But password-reset, password-set, and password-change forms now enforce constraints client-side: - App reset-password: add PasswordRequirements component, confirmation mismatch check, canSubmit guard, disabled button - Portal wachtwoord-resetten: add canSubmit guard, confirmation check, disabled button (PasswordRequirements was rendered but not enforced) - App SecurityTab (change password): replace static requirements list with interactive PasswordRequirements, add canSubmit guard Also created PasswordRequirements.vue component for the organizer app (portal already had one). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
|||||||
import MfaTotpSetupDialog from '@/components/settings/MfaTotpSetupDialog.vue'
|
import MfaTotpSetupDialog from '@/components/settings/MfaTotpSetupDialog.vue'
|
||||||
import MfaEmailSetupDialog from '@/components/settings/MfaEmailSetupDialog.vue'
|
import MfaEmailSetupDialog from '@/components/settings/MfaEmailSetupDialog.vue'
|
||||||
import MfaDisableDialog from '@/components/settings/MfaDisableDialog.vue'
|
import MfaDisableDialog from '@/components/settings/MfaDisableDialog.vue'
|
||||||
|
import PasswordRequirements from '@/components/auth/PasswordRequirements.vue'
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
@@ -26,10 +27,26 @@ const passwordSuccess = ref('')
|
|||||||
const showCurrentPw = ref(false)
|
const showCurrentPw = ref(false)
|
||||||
const showNewPw = ref(false)
|
const showNewPw = ref(false)
|
||||||
const showConfirmPw = ref(false)
|
const showConfirmPw = ref(false)
|
||||||
|
const passwordReqsRef = ref<InstanceType<typeof PasswordRequirements>>()
|
||||||
|
|
||||||
|
const confirmationError = computed(() => {
|
||||||
|
if (!passwordForm.value.password_confirmation) return ''
|
||||||
|
if (passwordForm.value.password !== passwordForm.value.password_confirmation) return 'Wachtwoorden komen niet overeen'
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const canSubmitPassword = computed(() =>
|
||||||
|
passwordForm.value.current_password.length > 0
|
||||||
|
&& passwordForm.value.password.length > 0
|
||||||
|
&& passwordForm.value.password_confirmation.length > 0
|
||||||
|
&& passwordForm.value.password === passwordForm.value.password_confirmation
|
||||||
|
&& (passwordReqsRef.value?.allMet ?? false),
|
||||||
|
)
|
||||||
|
|
||||||
const changePasswordMutation = useChangePassword()
|
const changePasswordMutation = useChangePassword()
|
||||||
|
|
||||||
async function handlePasswordChange() {
|
async function handlePasswordChange() {
|
||||||
|
if (!canSubmitPassword.value) return
|
||||||
passwordFieldErrors.value = {}
|
passwordFieldErrors.value = {}
|
||||||
passwordSuccess.value = ''
|
passwordSuccess.value = ''
|
||||||
|
|
||||||
@@ -186,6 +203,10 @@ function copyRegeneratedCodes() {
|
|||||||
placeholder="············"
|
placeholder="············"
|
||||||
@click:append-inner="showNewPw = !showNewPw"
|
@click:append-inner="showNewPw = !showNewPw"
|
||||||
/>
|
/>
|
||||||
|
<PasswordRequirements
|
||||||
|
ref="passwordReqsRef"
|
||||||
|
:password="passwordForm.password"
|
||||||
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
<VCol
|
<VCol
|
||||||
cols="12"
|
cols="12"
|
||||||
@@ -196,7 +217,7 @@ function copyRegeneratedCodes() {
|
|||||||
label="Bevestig nieuw wachtwoord"
|
label="Bevestig nieuw wachtwoord"
|
||||||
:type="showConfirmPw ? 'text' : 'password'"
|
:type="showConfirmPw ? 'text' : 'password'"
|
||||||
:append-inner-icon="showConfirmPw ? 'tabler-eye-off' : 'tabler-eye'"
|
:append-inner-icon="showConfirmPw ? 'tabler-eye-off' : 'tabler-eye'"
|
||||||
:error-messages="passwordFieldErrors.password_confirmation"
|
:error-messages="confirmationError ? [confirmationError] : (passwordFieldErrors.password_confirmation ? [passwordFieldErrors.password_confirmation] : undefined)"
|
||||||
placeholder="············"
|
placeholder="············"
|
||||||
@click:append-inner="showConfirmPw = !showConfirmPw"
|
@click:append-inner="showConfirmPw = !showConfirmPw"
|
||||||
/>
|
/>
|
||||||
@@ -204,45 +225,11 @@ function copyRegeneratedCodes() {
|
|||||||
</VRow>
|
</VRow>
|
||||||
</VCardText>
|
</VCardText>
|
||||||
|
|
||||||
<VCardText>
|
|
||||||
<h6 class="text-h6 text-medium-emphasis mb-4">
|
|
||||||
Wachtwoordvereisten:
|
|
||||||
</h6>
|
|
||||||
<VList class="card-list">
|
|
||||||
<VListItem>
|
|
||||||
<template #prepend>
|
|
||||||
<VIcon
|
|
||||||
size="10"
|
|
||||||
icon="tabler-circle-filled"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<span class="text-medium-emphasis">Minimaal 8 tekens</span>
|
|
||||||
</VListItem>
|
|
||||||
<VListItem>
|
|
||||||
<template #prepend>
|
|
||||||
<VIcon
|
|
||||||
size="10"
|
|
||||||
icon="tabler-circle-filled"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<span class="text-medium-emphasis">Minimaal 1 hoofdletter en 1 kleine letter</span>
|
|
||||||
</VListItem>
|
|
||||||
<VListItem>
|
|
||||||
<template #prepend>
|
|
||||||
<VIcon
|
|
||||||
size="10"
|
|
||||||
icon="tabler-circle-filled"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<span class="text-medium-emphasis">Minimaal 1 cijfer</span>
|
|
||||||
</VListItem>
|
|
||||||
</VList>
|
|
||||||
</VCardText>
|
|
||||||
|
|
||||||
<VCardText>
|
<VCardText>
|
||||||
<VBtn
|
<VBtn
|
||||||
type="submit"
|
type="submit"
|
||||||
:loading="changePasswordMutation.isPending.value"
|
:loading="changePasswordMutation.isPending.value"
|
||||||
|
:disabled="!canSubmitPassword"
|
||||||
>
|
>
|
||||||
Wachtwoord wijzigen
|
Wachtwoord wijzigen
|
||||||
</VBtn>
|
</VBtn>
|
||||||
|
|||||||
31
apps/app/src/components/auth/PasswordRequirements.vue
Normal file
31
apps/app/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>
|
||||||
@@ -6,6 +6,7 @@ import authV2MaskDark from '@images/pages/misc-mask-dark.png'
|
|||||||
import authV2MaskLight from '@images/pages/misc-mask-light.png'
|
import authV2MaskLight from '@images/pages/misc-mask-light.png'
|
||||||
import { VNodeRenderer } from '@layouts/components/VNodeRenderer'
|
import { VNodeRenderer } from '@layouts/components/VNodeRenderer'
|
||||||
import { themeConfig } from '@themeConfig'
|
import { themeConfig } from '@themeConfig'
|
||||||
|
import PasswordRequirements from '@/components/auth/PasswordRequirements.vue'
|
||||||
import { apiClient } from '@/lib/axios'
|
import { apiClient } from '@/lib/axios'
|
||||||
|
|
||||||
definePage({
|
definePage({
|
||||||
@@ -27,6 +28,20 @@ const showPasswordConfirmation = ref(false)
|
|||||||
|
|
||||||
const errorMessage = ref('')
|
const errorMessage = ref('')
|
||||||
const isSubmitting = ref(false)
|
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),
|
||||||
|
)
|
||||||
|
|
||||||
const authThemeImg = useGenerateImageVariant(
|
const authThemeImg = useGenerateImageVariant(
|
||||||
authV2ResetPasswordIllustrationLight,
|
authV2ResetPasswordIllustrationLight,
|
||||||
@@ -40,6 +55,7 @@ async function onSubmit(): Promise<void> {
|
|||||||
errorMessage.value = 'Ongeldige resetlink. Vraag een nieuwe link aan.'
|
errorMessage.value = 'Ongeldige resetlink. Vraag een nieuwe link aan.'
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!canSubmit.value) return
|
||||||
isSubmitting.value = true
|
isSubmitting.value = true
|
||||||
try {
|
try {
|
||||||
await apiClient.post('/auth/reset-password', {
|
await apiClient.post('/auth/reset-password', {
|
||||||
@@ -153,6 +169,10 @@ async function onSubmit(): Promise<void> {
|
|||||||
autocomplete="new-password"
|
autocomplete="new-password"
|
||||||
@click:append-inner="showPassword = !showPassword"
|
@click:append-inner="showPassword = !showPassword"
|
||||||
/>
|
/>
|
||||||
|
<PasswordRequirements
|
||||||
|
ref="passwordReqsRef"
|
||||||
|
:password="password"
|
||||||
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
|
|
||||||
<VCol cols="12">
|
<VCol cols="12">
|
||||||
@@ -161,6 +181,7 @@ async function onSubmit(): Promise<void> {
|
|||||||
label="Bevestig wachtwoord"
|
label="Bevestig wachtwoord"
|
||||||
:type="showPasswordConfirmation ? 'text' : 'password'"
|
:type="showPasswordConfirmation ? 'text' : 'password'"
|
||||||
:append-inner-icon="showPasswordConfirmation ? 'tabler-eye-off' : 'tabler-eye'"
|
:append-inner-icon="showPasswordConfirmation ? 'tabler-eye-off' : 'tabler-eye'"
|
||||||
|
:error-messages="confirmationError ? [confirmationError] : undefined"
|
||||||
autocomplete="new-password"
|
autocomplete="new-password"
|
||||||
@click:append-inner="showPasswordConfirmation = !showPasswordConfirmation"
|
@click:append-inner="showPasswordConfirmation = !showPasswordConfirmation"
|
||||||
/>
|
/>
|
||||||
@@ -171,6 +192,7 @@ async function onSubmit(): Promise<void> {
|
|||||||
block
|
block
|
||||||
type="submit"
|
type="submit"
|
||||||
:loading="isSubmitting"
|
:loading="isSubmitting"
|
||||||
|
:disabled="!canSubmit"
|
||||||
>
|
>
|
||||||
Wachtwoord opslaan
|
Wachtwoord opslaan
|
||||||
</VBtn>
|
</VBtn>
|
||||||
|
|||||||
@@ -26,6 +26,20 @@ const showPasswordConfirmation = ref(false)
|
|||||||
|
|
||||||
const errorMessage = ref('')
|
const errorMessage = ref('')
|
||||||
const isSubmitting = ref(false)
|
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> {
|
async function onSubmit(): Promise<void> {
|
||||||
errorMessage.value = ''
|
errorMessage.value = ''
|
||||||
@@ -34,6 +48,7 @@ async function onSubmit(): Promise<void> {
|
|||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!canSubmit.value) return
|
||||||
isSubmitting.value = true
|
isSubmitting.value = true
|
||||||
try {
|
try {
|
||||||
await apiClient.post('/auth/reset-password', {
|
await apiClient.post('/auth/reset-password', {
|
||||||
@@ -121,7 +136,10 @@ async function onSubmit(): Promise<void> {
|
|||||||
:append-inner-icon="showPassword ? 'tabler-eye-off' : 'tabler-eye'"
|
:append-inner-icon="showPassword ? 'tabler-eye-off' : 'tabler-eye'"
|
||||||
@click:append-inner="showPassword = !showPassword"
|
@click:append-inner="showPassword = !showPassword"
|
||||||
/>
|
/>
|
||||||
<PasswordRequirements :password="password" />
|
<PasswordRequirements
|
||||||
|
ref="passwordReqsRef"
|
||||||
|
:password="password"
|
||||||
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
|
|
||||||
<VCol cols="12">
|
<VCol cols="12">
|
||||||
@@ -131,6 +149,7 @@ async function onSubmit(): Promise<void> {
|
|||||||
placeholder="············"
|
placeholder="············"
|
||||||
:type="showPasswordConfirmation ? 'text' : 'password'"
|
:type="showPasswordConfirmation ? 'text' : 'password'"
|
||||||
autocomplete="new-password"
|
autocomplete="new-password"
|
||||||
|
:error-messages="confirmationError ? [confirmationError] : undefined"
|
||||||
:append-inner-icon="showPasswordConfirmation ? 'tabler-eye-off' : 'tabler-eye'"
|
:append-inner-icon="showPasswordConfirmation ? 'tabler-eye-off' : 'tabler-eye'"
|
||||||
@click:append-inner="showPasswordConfirmation = !showPasswordConfirmation"
|
@click:append-inner="showPasswordConfirmation = !showPasswordConfirmation"
|
||||||
/>
|
/>
|
||||||
@@ -141,6 +160,7 @@ async function onSubmit(): Promise<void> {
|
|||||||
block
|
block
|
||||||
type="submit"
|
type="submit"
|
||||||
:loading="isSubmitting"
|
:loading="isSubmitting"
|
||||||
|
:disabled="!canSubmit"
|
||||||
>
|
>
|
||||||
Wachtwoord opslaan
|
Wachtwoord opslaan
|
||||||
</VBtn>
|
</VBtn>
|
||||||
|
|||||||
Reference in New Issue
Block a user