feat: implement custom login system with direct authentication
All checks were successful
Build And Push Image / docker (push) Successful in 2m51s
All checks were successful
Build And Push Image / docker (push) Successful in 2m51s
- Add custom login page with username/password form and SSO fallback - Implement direct login API endpoint with security features - Add forgot password functionality and email notifications - Create guest middleware for authentication routing - Update Keycloak configuration and add cookie domain settings - Add security utilities for rate limiting and validation - Include comprehensive documentation for custom login implementation
This commit is contained in:
201
components/ForgotPasswordDialog.vue
Normal file
201
components/ForgotPasswordDialog.vue
Normal file
@@ -0,0 +1,201 @@
|
||||
<template>
|
||||
<v-dialog v-model="show" max-width="400" persistent>
|
||||
<v-card>
|
||||
<v-card-title class="text-h5 text-center pa-6" style="color: #a31515;">
|
||||
Reset Password
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="px-6">
|
||||
<p class="text-body-2 mb-4 text-center text-medium-emphasis">
|
||||
Enter your email address and we'll send you a link to reset your password.
|
||||
</p>
|
||||
|
||||
<v-form @submit.prevent="handleSubmit" ref="resetForm">
|
||||
<v-text-field
|
||||
v-model="email"
|
||||
label="Email Address"
|
||||
type="email"
|
||||
prepend-inner-icon="mdi-email"
|
||||
variant="outlined"
|
||||
:error-messages="errors.email"
|
||||
:disabled="loading"
|
||||
required
|
||||
@input="clearErrors"
|
||||
/>
|
||||
|
||||
<v-alert
|
||||
v-if="message"
|
||||
:type="messageType"
|
||||
class="mb-4"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ message }}
|
||||
</v-alert>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions class="px-6 pb-6">
|
||||
<v-btn
|
||||
variant="text"
|
||||
@click="close"
|
||||
:disabled="loading"
|
||||
>
|
||||
Cancel
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="handleSubmit"
|
||||
:loading="loading"
|
||||
:disabled="!email || !isValidEmail"
|
||||
style="background-color: #a31515 !important;"
|
||||
>
|
||||
Send Reset Link
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
modelValue: boolean;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void;
|
||||
(e: 'success', message: string): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
// Reactive data
|
||||
const email = ref('');
|
||||
const loading = ref(false);
|
||||
const message = ref('');
|
||||
const messageType = ref<'success' | 'error' | 'warning' | 'info'>('info');
|
||||
const errors = ref({
|
||||
email: ''
|
||||
});
|
||||
|
||||
const resetForm = ref();
|
||||
|
||||
// Computed
|
||||
const show = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
});
|
||||
|
||||
const isValidEmail = computed(() => {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email.value);
|
||||
});
|
||||
|
||||
// Methods
|
||||
const clearErrors = () => {
|
||||
errors.value.email = '';
|
||||
message.value = '';
|
||||
};
|
||||
|
||||
const validateEmail = () => {
|
||||
errors.value.email = '';
|
||||
|
||||
if (!email.value) {
|
||||
errors.value.email = 'Email is required';
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isValidEmail.value) {
|
||||
errors.value.email = 'Please enter a valid email address';
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!validateEmail()) return;
|
||||
|
||||
loading.value = true;
|
||||
message.value = '';
|
||||
|
||||
try {
|
||||
const response = await $fetch<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
}>('/api/auth/forgot-password', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
email: email.value
|
||||
}
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
message.value = response.message;
|
||||
messageType.value = 'success';
|
||||
|
||||
// Emit success event
|
||||
emit('success', response.message);
|
||||
|
||||
// Auto-close after 3 seconds
|
||||
setTimeout(() => {
|
||||
close();
|
||||
}, 3000);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Password reset error:', error);
|
||||
message.value = error.data?.message || 'Failed to send reset email. Please try again.';
|
||||
messageType.value = 'error';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
show.value = false;
|
||||
|
||||
// Reset form after dialog closes
|
||||
setTimeout(() => {
|
||||
email.value = '';
|
||||
message.value = '';
|
||||
errors.value.email = '';
|
||||
loading.value = false;
|
||||
}, 300);
|
||||
};
|
||||
|
||||
// Auto-focus email field when dialog opens
|
||||
watch(show, (newValue) => {
|
||||
if (newValue) {
|
||||
nextTick(() => {
|
||||
const emailField = document.querySelector('input[type="email"]') as HTMLInputElement;
|
||||
if (emailField) {
|
||||
emailField.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-card {
|
||||
border-radius: 16px !important;
|
||||
}
|
||||
|
||||
.v-card-title {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.v-btn {
|
||||
text-transform: none !important;
|
||||
}
|
||||
|
||||
/* Form field focus styles */
|
||||
.v-field--focused {
|
||||
border-color: #a31515 !important;
|
||||
}
|
||||
|
||||
.v-field--focused .v-field__outline {
|
||||
border-color: #a31515 !important;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user