9fea6504c6
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
247 lines
8.0 KiB
Vue
247 lines
8.0 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed } from 'vue';
|
|
|
|
const isLoading = ref(false);
|
|
const errorMessage = ref<string | null>(null);
|
|
const successMessage = ref<string | null>(null);
|
|
|
|
const currentPassword = ref('');
|
|
const newPassword = ref('');
|
|
const confirmPassword = ref('');
|
|
|
|
// Password visibility toggles
|
|
const isCurrentPasswordVisible = ref(false);
|
|
const isNewPasswordVisible = ref(false);
|
|
const isConfirmPasswordVisible = ref(false);
|
|
|
|
// Password strength and validation
|
|
const passwordRequirements = [
|
|
{
|
|
text: 'Minimum 8 characters long',
|
|
test: (password: string) => password.length >= 8
|
|
},
|
|
{
|
|
text: 'At least one lowercase character',
|
|
test: (password: string) => /[a-z]/.test(password)
|
|
},
|
|
{
|
|
text: 'At least one uppercase character',
|
|
test: (password: string) => /[A-Z]/.test(password)
|
|
},
|
|
{
|
|
text: 'At least one number',
|
|
test: (password: string) => /[0-9]/.test(password)
|
|
},
|
|
];
|
|
|
|
const passwordStrength = computed(() => {
|
|
if (!newPassword.value) return { score: 0, label: '', color: '' };
|
|
|
|
const metRequirements = passwordRequirements.filter(req => req.test(newPassword.value)).length;
|
|
const total = passwordRequirements.length;
|
|
const percentage = (metRequirements / total) * 100;
|
|
|
|
if (percentage < 50) return { score: percentage, label: 'Weak', color: 'error' };
|
|
if (percentage < 75) return { score: percentage, label: 'Fair', color: 'warning' };
|
|
if (percentage < 100) return { score: percentage, label: 'Good', color: 'info' };
|
|
return { score: percentage, label: 'Strong', color: 'success' };
|
|
});
|
|
|
|
const isFormValid = computed(() => {
|
|
return currentPassword.value.length > 0 &&
|
|
newPassword.value.length >= 8 &&
|
|
newPassword.value === confirmPassword.value &&
|
|
passwordRequirements.every(req => req.test(newPassword.value));
|
|
});
|
|
|
|
const saveChanges = async () => {
|
|
errorMessage.value = null;
|
|
successMessage.value = null;
|
|
|
|
if (!isFormValid.value) {
|
|
errorMessage.value = 'Please ensure all requirements are met.';
|
|
return;
|
|
}
|
|
|
|
if (newPassword.value !== confirmPassword.value) {
|
|
errorMessage.value = 'New passwords do not match.';
|
|
return;
|
|
}
|
|
|
|
isLoading.value = true;
|
|
|
|
try {
|
|
const response = await fetch('/m/authentication_provider_password/password/update', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
current_password: currentPassword.value,
|
|
new_password: newPassword.value,
|
|
}),
|
|
});
|
|
|
|
if (response.ok) {
|
|
currentPassword.value = '';
|
|
newPassword.value = '';
|
|
confirmPassword.value = '';
|
|
successMessage.value = 'Password updated successfully.';
|
|
} else {
|
|
const data = await response.json();
|
|
errorMessage.value = data.error || 'An error occurred while updating the password.';
|
|
}
|
|
} catch (error) {
|
|
errorMessage.value = 'An unexpected error occurred.';
|
|
} finally {
|
|
isLoading.value = false;
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<VCol cols="12" md="8" lg="4">
|
|
<VCard title="Change Password">
|
|
<VCardText>
|
|
<VAlert
|
|
v-if="errorMessage"
|
|
type="error"
|
|
class="mb-4"
|
|
closable
|
|
@click:close="errorMessage = null"
|
|
>
|
|
{{ errorMessage }}
|
|
</VAlert>
|
|
|
|
<VAlert
|
|
v-if="successMessage"
|
|
type="success"
|
|
class="mb-4"
|
|
closable
|
|
@click:close="successMessage = null"
|
|
>
|
|
{{ successMessage }}
|
|
</VAlert>
|
|
|
|
<!-- Current Password -->
|
|
<VRow class="mb-3">
|
|
<VCol cols="12">
|
|
<VTextField
|
|
v-model="currentPassword"
|
|
:type="isCurrentPasswordVisible ? 'text' : 'password'"
|
|
:append-inner-icon="isCurrentPasswordVisible ? 'mdi-eye-off' : 'mdi-eye'"
|
|
label="Current Password"
|
|
placeholder="············"
|
|
variant="outlined"
|
|
autocomplete="new-password"
|
|
@click:append-inner="isCurrentPasswordVisible = !isCurrentPasswordVisible"
|
|
/>
|
|
</VCol>
|
|
</VRow>
|
|
|
|
<!-- New Password -->
|
|
<VRow>
|
|
<VCol cols="12">
|
|
<VTextField
|
|
v-model="newPassword"
|
|
name="new-password"
|
|
:type="isNewPasswordVisible ? 'text' : 'password'"
|
|
:append-inner-icon="isNewPasswordVisible ? 'mdi-eye-off' : 'mdi-eye'"
|
|
label="New Password"
|
|
placeholder="············"
|
|
variant="outlined"
|
|
autocomplete="new-password"
|
|
@click:append-inner="isNewPasswordVisible = !isNewPasswordVisible"
|
|
/>
|
|
</VCol>
|
|
|
|
<VCol cols="12">
|
|
<VTextField
|
|
v-model="confirmPassword"
|
|
name="confirm-password"
|
|
:type="isConfirmPasswordVisible ? 'text' : 'password'"
|
|
:append-inner-icon="isConfirmPasswordVisible ? 'mdi-eye-off' : 'mdi-eye'"
|
|
label="Confirm New Password"
|
|
placeholder="············"
|
|
variant="outlined"
|
|
autocomplete="new-password"
|
|
:error="confirmPassword.length > 0 && confirmPassword !== newPassword"
|
|
:error-messages="confirmPassword.length > 0 && confirmPassword !== newPassword ? ['Passwords do not match'] : []"
|
|
@click:append-inner="isConfirmPasswordVisible = !isConfirmPasswordVisible"
|
|
/>
|
|
</VCol>
|
|
</VRow>
|
|
|
|
<!-- Password Strength Indicator -->
|
|
<VRow v-if="newPassword.length > 0" class="mt-2">
|
|
<VCol cols="12">
|
|
<div class="mb-2">
|
|
<div class="d-flex justify-space-between mb-1">
|
|
<span class="text-caption">Password Strength</span>
|
|
<span class="text-caption font-weight-bold" :class="`text-${passwordStrength.color}`">
|
|
{{ passwordStrength.label }}
|
|
</span>
|
|
</div>
|
|
<VProgressLinear
|
|
:model-value="passwordStrength.score"
|
|
:color="passwordStrength.color"
|
|
height="6"
|
|
rounded
|
|
/>
|
|
</div>
|
|
</VCol>
|
|
</VRow>
|
|
|
|
<!-- Password Requirements -->
|
|
<VRow class="mt-2">
|
|
<VCol cols="12">
|
|
<p class="text-base font-weight-medium mb-2">
|
|
Password Requirements:
|
|
</p>
|
|
<ul class="d-flex flex-column gap-y-2">
|
|
<li
|
|
v-for="(requirement, index) in passwordRequirements"
|
|
:key="index"
|
|
class="d-flex align-center"
|
|
>
|
|
<VIcon
|
|
:icon="newPassword.length > 0 && requirement.test(newPassword) ? 'mdi-check-circle' : 'mdi-circle-outline'"
|
|
:color="newPassword.length > 0 && requirement.test(newPassword) ? 'success' : 'grey'"
|
|
size="18"
|
|
class="me-2"
|
|
/>
|
|
<span :class="newPassword.length > 0 && requirement.test(newPassword) ? 'text-success' : ''">
|
|
{{ requirement.text }}
|
|
</span>
|
|
</li>
|
|
</ul>
|
|
</VCol>
|
|
</VRow>
|
|
|
|
<!-- Action Buttons -->
|
|
<VRow class="mt-4">
|
|
<VCol cols="12">
|
|
<VBtn
|
|
color="primary"
|
|
:disabled="!isFormValid"
|
|
:loading="isLoading"
|
|
@click="saveChanges"
|
|
>
|
|
Save Changes
|
|
</VBtn>
|
|
<VBtn
|
|
color="secondary"
|
|
variant="outlined"
|
|
class="ms-3"
|
|
:disabled="isLoading"
|
|
@click="currentPassword = ''; newPassword = ''; confirmPassword = ''; errorMessage = null; successMessage = null"
|
|
>
|
|
Reset
|
|
</VBtn>
|
|
</VCol>
|
|
</VRow>
|
|
</VCardText>
|
|
</VCard>
|
|
</VCol>
|
|
</template>
|