hotfix(admin/user): implement password reset and update user password
- 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.
This commit is contained in:
parent
9f5d35f7ba
commit
70f016422c
5 changed files with 77 additions and 25 deletions
|
@ -85,7 +85,9 @@ export default class AdminuserController {
|
||||||
// return response.badRequest(error.messages);
|
// return response.badRequest(error.messages);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
const input = request.only(['login', 'email', 'password', 'first_name', 'last_name']);
|
|
||||||
|
const input: Record<string, any> = request.only(['login', 'email','first_name', 'last_name']);
|
||||||
|
input.password = request.input('new_password');
|
||||||
const user = await User.create(input);
|
const user = await User.create(input);
|
||||||
if (request.input('roles')) {
|
if (request.input('roles')) {
|
||||||
const roles: Array<number> = request.input('roles');
|
const roles: Array<number> = request.input('roles');
|
||||||
|
@ -95,7 +97,6 @@ export default class AdminuserController {
|
||||||
session.flash('message', 'User has been created successfully');
|
session.flash('message', 'User has been created successfully');
|
||||||
return response.redirect().toRoute('settings.user.index');
|
return response.redirect().toRoute('settings.user.index');
|
||||||
}
|
}
|
||||||
|
|
||||||
public async show({ request, inertia }: HttpContext) {
|
public async show({ request, inertia }: HttpContext) {
|
||||||
const id = request.param('id');
|
const id = request.param('id');
|
||||||
const user = await User.query().where('id', id).firstOrFail();
|
const user = await User.query().where('id', id).firstOrFail();
|
||||||
|
@ -139,9 +140,11 @@ export default class AdminuserController {
|
||||||
});
|
});
|
||||||
|
|
||||||
// password is optional
|
// password is optional
|
||||||
let input;
|
let input: Record<string, any>;
|
||||||
if (request.input('password')) {
|
|
||||||
input = request.only(['login', 'email', 'password', 'first_name', 'last_name']);
|
if (request.input('new_password')) {
|
||||||
|
input = request.only(['login', 'email', 'first_name', 'last_name']);
|
||||||
|
input.password = request.input('new_password');
|
||||||
} else {
|
} else {
|
||||||
input = request.only(['login', 'email', 'first_name', 'last_name']);
|
input = request.only(['login', 'email', 'first_name', 'last_name']);
|
||||||
}
|
}
|
||||||
|
@ -156,7 +159,6 @@ export default class AdminuserController {
|
||||||
session.flash('message', 'User has been updated successfully');
|
session.flash('message', 'User has been updated successfully');
|
||||||
return response.redirect().toRoute('settings.user.index');
|
return response.redirect().toRoute('settings.user.index');
|
||||||
}
|
}
|
||||||
|
|
||||||
public async destroy({ request, response, session }: HttpContext) {
|
public async destroy({ request, response, session }: HttpContext) {
|
||||||
const id = request.param('id');
|
const id = request.param('id');
|
||||||
const user = await User.findOrFail(id);
|
const user = await User.findOrFail(id);
|
||||||
|
|
|
@ -16,7 +16,7 @@ export const createUserValidator = vine.compile(
|
||||||
first_name: vine.string().trim().minLength(3).maxLength(255),
|
first_name: vine.string().trim().minLength(3).maxLength(255),
|
||||||
last_name: vine.string().trim().minLength(3).maxLength(255),
|
last_name: vine.string().trim().minLength(3).maxLength(255),
|
||||||
email: vine.string().maxLength(255).email().normalizeEmail().isUnique({ table: 'accounts', column: 'email' }),
|
email: vine.string().maxLength(255).email().normalizeEmail().isUnique({ table: 'accounts', column: 'email' }),
|
||||||
password: vine.string().confirmed().trim().minLength(3).maxLength(60),
|
new_password: vine.string().confirmed({ confirmationField: 'password_confirmation' }).trim().minLength(3).maxLength(60),
|
||||||
roles: vine.array(vine.number()).minLength(1), // define at least one role for the new user
|
roles: vine.array(vine.number()).minLength(1), // define at least one role for the new user
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
@ -42,7 +42,7 @@ export const updateUserValidator = vine.withMetaData<{ objId: number }>().compil
|
||||||
.email()
|
.email()
|
||||||
.normalizeEmail()
|
.normalizeEmail()
|
||||||
.isUnique({ table: 'accounts', column: 'email', whereNot: (field) => field.meta.objId }),
|
.isUnique({ table: 'accounts', column: 'email', whereNot: (field) => field.meta.objId }),
|
||||||
password: vine.string().confirmed().trim().minLength(3).maxLength(60).optional(),
|
new_password: vine.string().confirmed({ confirmationField: 'password_confirmation' }).trim().minLength(3).maxLength(60).optional(),
|
||||||
roles: vine.array(vine.number()).minLength(1), // define at least one role for the new user
|
roles: vine.array(vine.number()).minLength(1), // define at least one role for the new user
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,10 +6,29 @@ import FormField from '@/Components/FormField.vue';
|
||||||
import FormControl from '@/Components/FormControl.vue';
|
import FormControl from '@/Components/FormControl.vue';
|
||||||
|
|
||||||
// Define props
|
// Define props
|
||||||
const props = defineProps<{
|
// const props = defineProps<{
|
||||||
modelValue: string;
|
// modelValue: string,
|
||||||
errors: Partial<Record<"new_password" | "old_password" | "confirm_password", 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']);
|
const emit = defineEmits(['update:modelValue', 'score']);
|
||||||
|
|
||||||
|
@ -61,8 +80,8 @@ const passwordMetrics = computed<PasswordMetrics>(() => {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- Password input Form -->
|
<!-- Password input Form -->
|
||||||
<FormField label="New password" help="Required. New password" :class="{'text-red-400': errors.new_password }">
|
<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
|
<FormControl v-model="localPassword" :icon="mdiFormTextboxPassword" name="new_password" type="password" :required="showRequiredMessage"
|
||||||
:error="errors.new_password">
|
:error="errors.new_password">
|
||||||
<!-- Secure Icon -->
|
<!-- Secure Icon -->
|
||||||
<template #right>
|
<template #right>
|
||||||
|
@ -84,10 +103,10 @@ const passwordMetrics = computed<PasswordMetrics>(() => {
|
||||||
<div class="text-gray-700 text-sm">
|
<div class="text-gray-700 text-sm">
|
||||||
{{ passwordMetrics.score }} / 6 points max
|
{{ passwordMetrics.score }} / 6 points max
|
||||||
</div>
|
</div>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<!-- Password Strength Bar -->
|
<!-- Password Strength Bar -->
|
||||||
<div class="po-password-strength-bar w-full h-2 rounded transition-all duration-200 mb-4"
|
<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}%` }"
|
:class="passwordMetrics.scoreLabel" :style="{ width: `${(passwordMetrics.score / 6) * 100}%` }"
|
||||||
role="progressbar" :aria-valuenow="passwordMetrics.score" aria-valuemin="0" aria-valuemax="6"
|
role="progressbar" :aria-valuenow="passwordMetrics.score" aria-valuemin="0" aria-valuemax="6"
|
||||||
:aria-label="`Password strength: ${passwordMetrics.scoreLabel || 'unknown'}`">
|
:aria-label="`Password strength: ${passwordMetrics.scoreLabel || 'unknown'}`">
|
||||||
|
|
|
@ -41,13 +41,28 @@ const form = useForm({
|
||||||
first_name: '',
|
first_name: '',
|
||||||
last_name: '',
|
last_name: '',
|
||||||
email: '',
|
email: '',
|
||||||
password: '',
|
new_password: '',
|
||||||
password_confirmation: '',
|
password_confirmation: '',
|
||||||
roles: [],
|
roles: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
await router.post(stardust.route('settings.user.store'), form);
|
// await router.post(stardust.route('settings.user.store'), form);
|
||||||
|
await form.post(stardust.route('settings.user.store'), {
|
||||||
|
preserveScroll: true,
|
||||||
|
onSuccess: () => {
|
||||||
|
form.reset();
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
if (form.errors.new_password) {
|
||||||
|
form.reset('new_password');
|
||||||
|
enabled.value = false;
|
||||||
|
// newPasswordInput.value.focus();
|
||||||
|
// newPasswordInput.value?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -109,7 +124,7 @@ const submit = async () => {
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormField>
|
</FormField>
|
||||||
<password-meter :password="form.password" @score="handleScore" /> -->
|
<password-meter :password="form.password" @score="handleScore" /> -->
|
||||||
<PasswordMeter v-model:password="form.password" :errors="form.errors" @score="handleScore" />
|
<PasswordMeter v-model="form.new_password" :errors="form.errors" @score="handleScore" />
|
||||||
|
|
||||||
<FormField label="Password Confirmation" :class="{ 'text-red-400': errors.password_confirmation }">
|
<FormField label="Password Confirmation" :class="{ 'text-red-400': errors.password_confirmation }">
|
||||||
<FormControl
|
<FormControl
|
||||||
|
|
|
@ -42,14 +42,29 @@ const form = useForm({
|
||||||
first_name: props.user.first_name,
|
first_name: props.user.first_name,
|
||||||
last_name: props.user.last_name,
|
last_name: props.user.last_name,
|
||||||
email: props.user.email,
|
email: props.user.email,
|
||||||
password: '',
|
new_password: '',
|
||||||
password_confirmation: '',
|
password_confirmation: '',
|
||||||
roles: props.userHasRoles, // fill actual user roles from db
|
roles: props.userHasRoles, // fill actual user roles from db
|
||||||
});
|
});
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
// await Inertia.post(stardust.route('user.store'), form);
|
// await Inertia.post(stardust.route('user.store'), form);
|
||||||
await router.put(stardust.route('settings.user.update', [props.user.id]), form);
|
// await router.put(stardust.route('settings.user.update', [props.user.id]), form);
|
||||||
|
await form.put(stardust.route('settings.user.update', [props.user.id]), {
|
||||||
|
preserveScroll: true,
|
||||||
|
onSuccess: () => {
|
||||||
|
form.reset();
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
if (form.errors.new_password) {
|
||||||
|
form.reset('new_password');
|
||||||
|
enabled.value = false;
|
||||||
|
// newPasswordInput.value.focus();
|
||||||
|
// newPasswordInput.value?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
const handleScore = (score: number) => {
|
const handleScore = (score: number) => {
|
||||||
if (score >= 4){
|
if (score >= 4){
|
||||||
|
@ -108,15 +123,16 @@ const handleScore = (score: number) => {
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField label="Password" :class="{ 'text-red-400': errors.password }">
|
<!-- <FormField label="Password" :class="{ 'text-red-400': errors.password }">
|
||||||
<FormControl v-model="form.password" type="password" placeholder="Enter Password" :errors="errors.password">
|
<FormControl v-model="form.password" type="password" placeholder="Enter Password" :errors="errors.password">
|
||||||
<div class="text-red-400 text-sm" v-if="errors.password">
|
<div class="text-red-400 text-sm" v-if="errors.password">
|
||||||
{{ errors.password }}
|
{{ errors.password }}
|
||||||
</div>
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormField>
|
</FormField> -->
|
||||||
|
|
||||||
|
<PasswordMeter field-label="Reset User Password" :show-required-message="false" ref="newPasswordInput" v-model="form.new_password" :errors="form.errors" @score="handleScore" />
|
||||||
|
|
||||||
<PasswordMeter v-model:password="form.password" :errors="form.errors" @score="handleScore" />
|
|
||||||
|
|
||||||
<FormField label="Password Confirmation" :class="{ 'text-red-400': errors.password_confirmation }">
|
<FormField label="Password Confirmation" :class="{ 'text-red-400': errors.password_confirmation }">
|
||||||
<FormControl
|
<FormControl
|
||||||
|
@ -151,7 +167,7 @@ const handleScore = (score: number) => {
|
||||||
color="info"
|
color="info"
|
||||||
label="Submit"
|
label="Submit"
|
||||||
:class="{ 'opacity-25': form.processing }"
|
:class="{ 'opacity-25': form.processing }"
|
||||||
:disabled="form.processing == true|| (form.password != '' && enabled == false)"
|
:disabled="form.processing == true|| (form.new_password != '' && enabled == false)"
|
||||||
/>
|
/>
|
||||||
</BaseButtons>
|
</BaseButtons>
|
||||||
</template>
|
</template>
|
||||||
|
|
Loading…
Add table
Reference in a new issue