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:
335
pages/login.vue
335
pages/login.vue
@@ -1,69 +1,324 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<v-main class="d-flex align-center justify-center min-h-screen bg-grey-lighten-4">
|
||||
<v-container>
|
||||
<v-row justify="center">
|
||||
<v-col cols="12" sm="8" md="6" lg="4">
|
||||
<v-card class="elevation-8 rounded-lg">
|
||||
<div class="login-container">
|
||||
<v-container fluid class="fill-height">
|
||||
<v-row class="fill-height" justify="center" align="center">
|
||||
<v-col cols="12" sm="8" md="6" lg="4" xl="3">
|
||||
<transition name="login-form" appear>
|
||||
<v-card class="login-card" elevation="24">
|
||||
<v-card-text class="pa-8">
|
||||
<div class="text-center mb-6">
|
||||
<!-- <v-img
|
||||
src="/logo.png"
|
||||
alt="MonacoUSA"
|
||||
max-width="120"
|
||||
class="mx-auto mb-4"
|
||||
/> -->
|
||||
<h1 class="text-h4 font-weight-bold text-primary mb-2">
|
||||
MonacoUSA Portal
|
||||
<!-- Logo and Welcome -->
|
||||
<div class="text-center mb-8">
|
||||
<v-img
|
||||
src="/MONACOUSA-Flags_376x376.png"
|
||||
width="120"
|
||||
height="120"
|
||||
class="mx-auto mb-4 pulse-animation"
|
||||
alt="MonacoUSA Logo"
|
||||
/>
|
||||
<h1 class="text-h4 font-weight-bold mb-2" style="color: #a31515;">
|
||||
Welcome Back
|
||||
</h1>
|
||||
<p class="text-body-1 text-grey-600">
|
||||
Sign in to access your dashboard
|
||||
<p class="text-body-1 text-medium-emphasis">
|
||||
Sign in to your MonacoUSA Portal
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<v-btn
|
||||
@click="handleLogin"
|
||||
:loading="loading"
|
||||
color="primary"
|
||||
size="large"
|
||||
block
|
||||
class="mb-4"
|
||||
prepend-icon="mdi-login"
|
||||
>
|
||||
Sign In with SSO
|
||||
</v-btn>
|
||||
<!-- Login Form -->
|
||||
<v-form @submit.prevent="handleLogin" ref="loginForm">
|
||||
<v-text-field
|
||||
v-model="credentials.username"
|
||||
label="Username or Email"
|
||||
prepend-inner-icon="mdi-account"
|
||||
variant="outlined"
|
||||
:error-messages="errors.username"
|
||||
:disabled="loading"
|
||||
class="mb-3"
|
||||
required
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
v-model="credentials.password"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
label="Password"
|
||||
prepend-inner-icon="mdi-lock"
|
||||
:append-inner-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
|
||||
@click:append-inner="showPassword = !showPassword"
|
||||
variant="outlined"
|
||||
:error-messages="errors.password"
|
||||
:disabled="loading"
|
||||
class="mb-3"
|
||||
required
|
||||
/>
|
||||
|
||||
<div class="d-flex justify-space-between align-center mb-6">
|
||||
<v-checkbox
|
||||
v-model="credentials.rememberMe"
|
||||
label="Remember me"
|
||||
density="compact"
|
||||
:disabled="loading"
|
||||
/>
|
||||
<v-btn
|
||||
variant="text"
|
||||
size="small"
|
||||
@click="showForgotPassword = true"
|
||||
:disabled="loading"
|
||||
style="color: #a31515;"
|
||||
>
|
||||
Forgot Password?
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<!-- Error Alert -->
|
||||
<v-alert
|
||||
v-if="loginError"
|
||||
type="error"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
closable
|
||||
@click:close="loginError = ''"
|
||||
>
|
||||
{{ loginError }}
|
||||
</v-alert>
|
||||
|
||||
<!-- Login Button -->
|
||||
<v-btn
|
||||
type="submit"
|
||||
color="primary"
|
||||
size="large"
|
||||
block
|
||||
:loading="loading"
|
||||
:disabled="!isFormValid"
|
||||
class="mb-4"
|
||||
style="background-color: #a31515 !important;"
|
||||
>
|
||||
<v-icon left>mdi-login</v-icon>
|
||||
Sign In
|
||||
</v-btn>
|
||||
</v-form>
|
||||
|
||||
<!-- Additional Options -->
|
||||
<div class="text-center">
|
||||
<p class="text-caption text-grey-600">
|
||||
Secure authentication powered by Keycloak
|
||||
<p class="text-body-2 text-medium-emphasis">
|
||||
Need help? Contact your administrator
|
||||
</p>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-main>
|
||||
</v-app>
|
||||
</transition>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
<!-- Forgot Password Dialog -->
|
||||
<ForgotPasswordDialog
|
||||
v-model="showForgotPassword"
|
||||
@success="handlePasswordResetSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
auth: false,
|
||||
layout: false,
|
||||
layout: false
|
||||
});
|
||||
|
||||
const { login } = useAuth();
|
||||
// Check if user is already authenticated
|
||||
const { user } = useAuth();
|
||||
if (user.value) {
|
||||
await navigateTo('/dashboard');
|
||||
}
|
||||
|
||||
// Reactive data
|
||||
const credentials = ref({
|
||||
username: '',
|
||||
password: '',
|
||||
rememberMe: false
|
||||
});
|
||||
|
||||
const showPassword = ref(false);
|
||||
const showForgotPassword = ref(false);
|
||||
const loading = ref(false);
|
||||
const loginError = ref('');
|
||||
const errors = ref({
|
||||
username: '',
|
||||
password: ''
|
||||
});
|
||||
|
||||
const loginForm = ref();
|
||||
|
||||
// Computed
|
||||
const isFormValid = computed(() => {
|
||||
return credentials.value.username.length > 0 &&
|
||||
credentials.value.password.length > 0 &&
|
||||
!loading.value;
|
||||
});
|
||||
|
||||
// Methods
|
||||
const validateForm = () => {
|
||||
errors.value = { username: '', password: '' };
|
||||
let isValid = true;
|
||||
|
||||
if (!credentials.value.username || credentials.value.username.length < 2) {
|
||||
errors.value.username = 'Username must be at least 2 characters';
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (!credentials.value.password || credentials.value.password.length < 6) {
|
||||
errors.value.password = 'Password must be at least 6 characters';
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
return isValid;
|
||||
};
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (!validateForm()) return;
|
||||
|
||||
loading.value = true;
|
||||
loginError.value = '';
|
||||
|
||||
try {
|
||||
await login();
|
||||
} catch (error) {
|
||||
const response = await $fetch<{
|
||||
success: boolean;
|
||||
redirectTo?: string;
|
||||
user?: any;
|
||||
}>('/api/auth/direct-login', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
username: credentials.value.username,
|
||||
password: credentials.value.password,
|
||||
rememberMe: credentials.value.rememberMe
|
||||
}
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
// Success! Redirect will be handled by the API response
|
||||
await navigateTo(response.redirectTo || '/dashboard');
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Login error:', error);
|
||||
loginError.value = error.data?.message || 'Login failed. Please check your credentials and try again.';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handlePasswordResetSuccess = (message: string) => {
|
||||
showForgotPassword.value = false;
|
||||
// Could show a success message here
|
||||
console.log('Password reset:', message);
|
||||
};
|
||||
|
||||
// Auto-focus username field on mount
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
const usernameField = document.querySelector('input[type="text"]') as HTMLInputElement;
|
||||
if (usernameField) {
|
||||
usernameField.focus();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-container {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(rgba(163, 21, 21, 0.7), rgba(0, 0, 0, 0.5)),
|
||||
url('/monaco_high_res.jpg');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-attachment: fixed;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
backdrop-filter: blur(15px);
|
||||
background: rgba(255, 255, 255, 0.95) !important;
|
||||
border-radius: 20px !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3) !important;
|
||||
transition: all 0.3s ease;
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4) !important;
|
||||
}
|
||||
|
||||
.pulse-animation {
|
||||
animation: pulse 3s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
opacity: 0.9;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.login-form-enter-active {
|
||||
transition: all 0.6s ease;
|
||||
}
|
||||
|
||||
.login-form-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px) scale(0.95);
|
||||
}
|
||||
|
||||
.login-form-enter-to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
|
||||
/* Custom scrollbar for mobile */
|
||||
::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(163, 21, 21, 0.5);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 600px) {
|
||||
.login-container {
|
||||
background-attachment: scroll;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Loading state styles */
|
||||
.v-btn--loading {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 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