- Implemented password reset functionality for admin users. - Updated the user edit and create forms to use a password meter component for password strength validation. - Modified the `AdminuserController` to handle the new password field and update user passwords. - Updated the `createUserValidator` and `updateUserValidator` to validate the new password field. - Updated the password field to `new_password` in the `Edit.vue` and `Create.vue` components. - Added `showRequiredMessage` prop to `SimplePasswordMeter` component. - Added conditional rendering for password strength bar in `SimplePasswordMeter` component. - Added `fieldLabel` prop to `SimplePasswordMeter` component. - Updated form submission to handle errors and reset password field.
166 lines
5.2 KiB
Vue
166 lines
5.2 KiB
Vue
<script lang="ts" setup>
|
|
import { computed } from 'vue';
|
|
import { checkStrength } from './logic/index';
|
|
import { mdiFormTextboxPassword } from '@mdi/js';
|
|
import FormField from '@/Components/FormField.vue';
|
|
import FormControl from '@/Components/FormControl.vue';
|
|
|
|
// Define props
|
|
// const props = defineProps<{
|
|
// modelValue: string,
|
|
// errors: Partial<Record<"new_password" | "old_password" | "confirm_password", string>>,
|
|
// showRequiredMessage: boolean,
|
|
// }>();
|
|
|
|
const props = defineProps({
|
|
modelValue: {
|
|
type: String,
|
|
},
|
|
errors: {
|
|
type: Object,
|
|
default: () => ({} as Partial<Record<"new_password" | "old_password" | "confirm_password", string>>),
|
|
},
|
|
showRequiredMessage: {
|
|
type: Boolean,
|
|
default:true,
|
|
},
|
|
fieldLabel: {
|
|
type: String,
|
|
default: 'New password',
|
|
}
|
|
});
|
|
|
|
const emit = defineEmits(['update:modelValue', 'score']);
|
|
|
|
// // A local reactive variable for password input
|
|
// const localPassword = ref(props.modelValue);
|
|
// // Watch localPassword and emit changes back to the parent
|
|
// watch(localPassword, (newValue) => {
|
|
// emit('update:modelValue', newValue);
|
|
// });
|
|
const localPassword = computed({
|
|
get: () => props.modelValue,
|
|
set: (value) => {
|
|
emit('update:modelValue', value);
|
|
// const { score } = checkStrength(localPassword.value);
|
|
// emit('score', score);
|
|
},
|
|
});
|
|
|
|
type PasswordMetrics = {
|
|
score: number;
|
|
scoreLabel: string | null;
|
|
hints: string[];
|
|
isSecure: boolean;
|
|
};
|
|
|
|
// Combined computed property for password strength metrics
|
|
const passwordMetrics = computed<PasswordMetrics>(() => {
|
|
if (!localPassword.value) {
|
|
return {
|
|
score: 0,
|
|
scoreLabel: null,
|
|
hints: [],
|
|
isSecure: false
|
|
};
|
|
}
|
|
const { score, scoreLabel, hints } = checkStrength(localPassword.value);
|
|
|
|
emit('score', score);
|
|
|
|
return {
|
|
score,
|
|
scoreLabel,
|
|
hints,
|
|
isSecure: score >= 4
|
|
};
|
|
});
|
|
|
|
</script>
|
|
|
|
<template>
|
|
<!-- Password input Form -->
|
|
<FormField :label="fieldLabel" :help="showRequiredMessage ? 'Required. New password' : ''" :class="{'text-red-400': errors.new_password }">
|
|
<FormControl v-model="localPassword" :icon="mdiFormTextboxPassword" name="new_password" type="password" :required="showRequiredMessage"
|
|
:error="errors.new_password">
|
|
<!-- Secure Icon -->
|
|
<template #right>
|
|
<span v-if="passwordMetrics.isSecure"
|
|
class="inline-flex justify-center items-center w-10 h-full absolute inset-y-0 right-2 pointer-events-none">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-green-600" viewBox="0 0 20 20"
|
|
fill="currentColor">
|
|
<path fill-rule="evenodd"
|
|
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-10.707a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
|
|
clip-rule="evenodd" />
|
|
</svg>
|
|
</span>
|
|
</template>
|
|
<div class="text-red-400 text-sm" v-if="errors.new_password">
|
|
{{ errors.new_password }}
|
|
</div>
|
|
</FormControl>
|
|
<!-- Score Display -->
|
|
<div class="text-gray-700 text-sm">
|
|
{{ passwordMetrics.score }} / 6 points max
|
|
</div>
|
|
</FormField>
|
|
|
|
<!-- Password Strength Bar -->
|
|
<div v-if="passwordMetrics.score > 0"class="po-password-strength-bar w-full h-2 rounded transition-all duration-200 mb-4"
|
|
:class="passwordMetrics.scoreLabel" :style="{ width: `${(passwordMetrics.score / 6) * 100}%` }"
|
|
role="progressbar" :aria-valuenow="passwordMetrics.score" aria-valuemin="0" aria-valuemax="6"
|
|
:aria-label="`Password strength: ${passwordMetrics.scoreLabel || 'unknown'}`">
|
|
</div>
|
|
|
|
<!-- Hint Message -->
|
|
<div v-if="passwordMetrics.hints.length > 0"
|
|
class="hint-message bg-gray-50 border-l-4 border-gray-300 text-gray-600 p-3 mb-4 rounded-md shadow-sm">
|
|
<p class="font-medium text-sm mb-2">To improve your password strength:</p>
|
|
<ul class="list-disc list-inside text-xs space-y-1">
|
|
<li v-for="(hint, index) in passwordMetrics.hints" :key="index"
|
|
class="hover:text-gray-800 transition-colors">
|
|
{{ hint }}
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<!-- Score Display -->
|
|
<!-- <div class="text-gray-700 text-sm">
|
|
{{ passwordMetrics.score }} / 6 points max
|
|
</div> -->
|
|
</template>
|
|
|
|
<style lang="css" scoped>
|
|
.po-password-strength-bar {
|
|
border-radius: 2px;
|
|
transition: all 0.2s linear;
|
|
height: 10px;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.po-password-strength-bar.risky {
|
|
background-color: #f95e68;
|
|
width: 10%;
|
|
}
|
|
|
|
.po-password-strength-bar.guessable {
|
|
background-color: #fb964d;
|
|
width: 32.5%;
|
|
}
|
|
|
|
.po-password-strength-bar.weak {
|
|
background-color: #fdd244;
|
|
width: 55%;
|
|
}
|
|
|
|
.po-password-strength-bar.safe {
|
|
background-color: #b0dc53;
|
|
width: 77.5%;
|
|
}
|
|
|
|
.po-password-strength-bar.secure,
|
|
.po-password-strength-bar.safe-secure,
|
|
.po-password-strength-bar.optimal {
|
|
background-color: #35cc62;
|
|
width: 100%;
|
|
}
|
|
</style>
|