471 lines
14 KiB
Vue
471 lines
14 KiB
Vue
<template>
|
|
<div :class="containerClasses">
|
|
<v-container class="fill-height" fluid>
|
|
<v-row align="center" justify="center">
|
|
<v-col cols="12" sm="8" md="6" lg="4">
|
|
<v-card class="elevation-12 rounded-lg">
|
|
<v-card-text class="text-center pa-8">
|
|
<div class="mb-6">
|
|
<v-icon
|
|
color="primary"
|
|
size="80"
|
|
class="mb-4"
|
|
>
|
|
mdi-lock-plus
|
|
</v-icon>
|
|
|
|
<h1 class="text-h4 font-weight-bold text-primary mb-3">
|
|
Set Your Password
|
|
</h1>
|
|
|
|
<p class="text-body-1 text-medium-emphasis mb-2" v-if="email">
|
|
Complete your registration by setting a secure password for <strong>{{ email }}</strong>
|
|
</p>
|
|
|
|
<p class="text-body-1 text-medium-emphasis">
|
|
Choose a strong password to secure your MonacoUSA Portal account.
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Password Setup Form -->
|
|
<v-form ref="formRef" v-model="formValid" @submit.prevent="setupPassword">
|
|
<v-text-field
|
|
v-model="password"
|
|
:type="showPassword ? 'text' : 'password'"
|
|
label="New Password"
|
|
variant="outlined"
|
|
density="comfortable"
|
|
:rules="passwordRules"
|
|
:error="!!errorMessage"
|
|
:append-inner-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
|
|
@click:append-inner="togglePasswordVisibility('password')"
|
|
class="mb-3 password-field"
|
|
autocomplete="new-password"
|
|
:autofocus="false"
|
|
/>
|
|
|
|
<v-text-field
|
|
v-model="confirmPassword"
|
|
:type="showConfirmPassword ? 'text' : 'password'"
|
|
label="Confirm Password"
|
|
variant="outlined"
|
|
density="comfortable"
|
|
:rules="confirmPasswordRules"
|
|
:error="!!errorMessage"
|
|
:append-inner-icon="showConfirmPassword ? 'mdi-eye' : 'mdi-eye-off'"
|
|
@click:append-inner="togglePasswordVisibility('confirm')"
|
|
class="mb-4 password-field"
|
|
autocomplete="new-password"
|
|
:autofocus="false"
|
|
/>
|
|
|
|
<!-- Password Strength Indicator -->
|
|
<v-progress-linear
|
|
:model-value="passwordStrength"
|
|
:color="passwordStrengthColor"
|
|
height="6"
|
|
class="mb-2"
|
|
/>
|
|
<p class="text-caption text-medium-emphasis mb-4">
|
|
Password strength: {{ passwordStrengthLabel }}
|
|
</p>
|
|
|
|
<!-- Error Alert -->
|
|
<v-alert
|
|
v-if="errorMessage"
|
|
type="error"
|
|
variant="tonal"
|
|
class="mb-4 text-start"
|
|
icon="mdi-alert"
|
|
>
|
|
{{ errorMessage }}
|
|
</v-alert>
|
|
|
|
<!-- Success Alert -->
|
|
<v-alert
|
|
v-if="successMessage"
|
|
type="success"
|
|
variant="tonal"
|
|
class="mb-4 text-start"
|
|
icon="mdi-check-circle"
|
|
>
|
|
{{ successMessage }}
|
|
</v-alert>
|
|
|
|
<div class="d-flex flex-column gap-3">
|
|
<v-btn
|
|
type="submit"
|
|
color="primary"
|
|
size="large"
|
|
variant="elevated"
|
|
block
|
|
:loading="loading"
|
|
:disabled="!formValid || loading"
|
|
class="text-none"
|
|
>
|
|
<v-icon start>mdi-check</v-icon>
|
|
Set Password & Continue
|
|
</v-btn>
|
|
|
|
<v-btn
|
|
color="secondary"
|
|
size="large"
|
|
variant="outlined"
|
|
block
|
|
:to="{ path: '/login' }"
|
|
:disabled="loading"
|
|
class="text-none"
|
|
>
|
|
<v-icon start>mdi-login</v-icon>
|
|
I Already Have a Password
|
|
</v-btn>
|
|
|
|
<v-btn
|
|
color="outline"
|
|
size="small"
|
|
variant="text"
|
|
block
|
|
to="/"
|
|
:disabled="loading"
|
|
class="text-none"
|
|
>
|
|
<v-icon start>mdi-home</v-icon>
|
|
Return to Home
|
|
</v-btn>
|
|
</div>
|
|
</v-form>
|
|
|
|
<!-- Additional help -->
|
|
<div class="mt-6 pt-4 border-t">
|
|
<p class="text-caption text-medium-emphasis mb-2">
|
|
Need help? Contact support at:
|
|
</p>
|
|
<v-chip
|
|
size="small"
|
|
variant="outlined"
|
|
prepend-icon="mdi-email"
|
|
>
|
|
support@monacousa.org
|
|
</v-chip>
|
|
</div>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-col>
|
|
</v-row>
|
|
</v-container>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
|
|
definePageMeta({
|
|
layout: false,
|
|
middleware: 'guest'
|
|
});
|
|
|
|
// Static CSS classes based on device (no reactive dependencies)
|
|
const containerClasses = ref('password-setup-page');
|
|
|
|
// Reactive state
|
|
const loading = ref(false);
|
|
const errorMessage = ref('');
|
|
const successMessage = ref('');
|
|
const formValid = ref(false);
|
|
const showPassword = ref(false);
|
|
const showConfirmPassword = ref(false);
|
|
|
|
// Form data
|
|
const password = ref('');
|
|
const confirmPassword = ref('');
|
|
|
|
// Get query parameters
|
|
const route = useRoute();
|
|
const email = computed(() => route.query.email as string || '');
|
|
const token = computed(() => route.query.token as string || '');
|
|
|
|
// Form ref
|
|
const formRef = ref();
|
|
|
|
// Password strength calculation
|
|
const passwordStrength = computed(() => {
|
|
if (!password.value) return 0;
|
|
|
|
let score = 0;
|
|
// Length
|
|
if (password.value.length >= 8) score += 20;
|
|
if (password.value.length >= 12) score += 10;
|
|
|
|
// Character types
|
|
if (/[a-z]/.test(password.value)) score += 15;
|
|
if (/[A-Z]/.test(password.value)) score += 15;
|
|
if (/[0-9]/.test(password.value)) score += 15;
|
|
if (/[^A-Za-z0-9]/.test(password.value)) score += 25;
|
|
|
|
return Math.min(score, 100);
|
|
});
|
|
|
|
const passwordStrengthColor = computed(() => {
|
|
if (passwordStrength.value < 40) return 'error';
|
|
if (passwordStrength.value < 70) return 'warning';
|
|
return 'success';
|
|
});
|
|
|
|
const passwordStrengthLabel = computed(() => {
|
|
if (passwordStrength.value < 40) return 'Weak';
|
|
if (passwordStrength.value < 70) return 'Good';
|
|
return 'Strong';
|
|
});
|
|
|
|
// Validation rules
|
|
const passwordRules = [
|
|
(v: string) => !!v || 'Password is required',
|
|
(v: string) => v.length >= 8 || 'Password must be at least 8 characters',
|
|
(v: string) => /[A-Z]/.test(v) || 'Password must contain at least one uppercase letter',
|
|
(v: string) => /[a-z]/.test(v) || 'Password must contain at least one lowercase letter',
|
|
(v: string) => /[0-9]/.test(v) || 'Password must contain at least one number',
|
|
];
|
|
|
|
const confirmPasswordRules = [
|
|
(v: string) => !!v || 'Please confirm your password',
|
|
(v: string) => v === password.value || 'Passwords do not match',
|
|
];
|
|
|
|
// Set page title with mobile viewport optimization
|
|
useHead({
|
|
title: 'Set Your Password - MonacoUSA Portal',
|
|
meta: [
|
|
{
|
|
name: 'description',
|
|
content: 'Set your password to complete your MonacoUSA Portal registration.'
|
|
},
|
|
{ name: 'viewport', content: 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no' }
|
|
]
|
|
});
|
|
|
|
// Toggle password visibility - simplified for static detection
|
|
const togglePasswordVisibility = (field: 'password' | 'confirm') => {
|
|
if (field === 'password') {
|
|
showPassword.value = !showPassword.value;
|
|
} else {
|
|
showConfirmPassword.value = !showConfirmPassword.value;
|
|
}
|
|
};
|
|
|
|
// Setup password function
|
|
const setupPassword = async () => {
|
|
if (!formValid.value) return;
|
|
|
|
if (!email.value) {
|
|
errorMessage.value = 'No email address provided. Please check the link from your email.';
|
|
return;
|
|
}
|
|
|
|
try {
|
|
loading.value = true;
|
|
errorMessage.value = '';
|
|
successMessage.value = '';
|
|
|
|
// Call our password setup API
|
|
const response = await $fetch('/api/auth/setup-password', {
|
|
method: 'POST',
|
|
body: {
|
|
email: email.value,
|
|
password: password.value,
|
|
token: token.value
|
|
}
|
|
});
|
|
|
|
console.log('[setup-password] Password setup successful:', response);
|
|
|
|
successMessage.value = 'Password set successfully! Redirecting to login...';
|
|
|
|
// Wait a moment to show success message, then redirect
|
|
setTimeout(() => {
|
|
navigateTo({
|
|
path: '/login',
|
|
query: { email: email.value, passwordSet: 'true' }
|
|
});
|
|
}, 2000);
|
|
|
|
} catch (err: any) {
|
|
console.error('[setup-password] Password setup failed:', err);
|
|
|
|
if (err.statusCode === 400) {
|
|
errorMessage.value = 'Invalid request. Please check your information and try again.';
|
|
} else if (err.statusCode === 404) {
|
|
errorMessage.value = 'User not found. The link may be invalid or expired.';
|
|
} else if (err.statusCode === 409) {
|
|
errorMessage.value = 'Password has already been set. You can log in with your existing password.';
|
|
} else if (err.statusCode === 422) {
|
|
errorMessage.value = 'Password does not meet security requirements. Please choose a stronger password.';
|
|
} else {
|
|
errorMessage.value = err.message || 'Failed to set password. Please try again or contact support.';
|
|
}
|
|
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
// Component initialization
|
|
onMounted(() => {
|
|
console.log('[setup-password] Password setup page loaded for:', email.value);
|
|
|
|
// Static device detection from Nuxt Device Module - no reactive dependencies
|
|
const { isMobile, isIos, isSafari } = useDevice();
|
|
|
|
// Detect mobile Safari specifically
|
|
const isMobileSafari = isMobile && isIos && isSafari;
|
|
|
|
// Apply classes once (static, no reactivity)
|
|
const containerClassList = ['password-setup-page'];
|
|
if (isMobile) containerClassList.push('is-mobile');
|
|
if (isMobileSafari) containerClassList.push('is-mobile-safari');
|
|
if (isIos) containerClassList.push('is-ios');
|
|
containerClasses.value = containerClassList.join(' ');
|
|
|
|
// Check if we have required parameters
|
|
if (!email.value) {
|
|
errorMessage.value = 'No email address provided. Please use the link from your verification email.';
|
|
}
|
|
|
|
// Prevent auto-zoom on iOS when focusing input fields
|
|
if (isIos) {
|
|
const metaViewport = document.querySelector('meta[name="viewport"]');
|
|
if (metaViewport) {
|
|
metaViewport.setAttribute('content', 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no');
|
|
}
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.password-setup-page {
|
|
min-height: 100vh;
|
|
min-height: calc(var(--vh, 1vh) * 100); /* Mobile Safari fallback */
|
|
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
|
overflow-x: hidden; /* Prevent horizontal scroll on mobile */
|
|
}
|
|
|
|
/* Mobile Safari optimizations */
|
|
.password-setup-page.is-mobile-safari {
|
|
min-height: 100vh;
|
|
min-height: -webkit-fill-available;
|
|
}
|
|
|
|
.password-setup-page.performance-mode {
|
|
will-change: auto;
|
|
transform: translateZ(0); /* Lighter hardware acceleration */
|
|
}
|
|
|
|
.fill-height {
|
|
min-height: 100vh;
|
|
min-height: calc(var(--vh, 1vh) * 100); /* Mobile Safari fallback */
|
|
}
|
|
|
|
/* Mobile Safari fill-height optimization */
|
|
.is-mobile-safari .fill-height {
|
|
min-height: -webkit-fill-available;
|
|
}
|
|
|
|
.border-t {
|
|
border-top: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
|
}
|
|
|
|
.gap-3 {
|
|
gap: 12px;
|
|
}
|
|
|
|
/* 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) {
|
|
.password-setup-page {
|
|
padding: 16px;
|
|
}
|
|
|
|
.v-card {
|
|
margin: 0;
|
|
}
|
|
|
|
/* Optimize button spacing on mobile */
|
|
.gap-3 {
|
|
gap: 8px;
|
|
}
|
|
}
|
|
|
|
/* Improve touch targets on mobile */
|
|
@media (hover: none) and (pointer: coarse) {
|
|
.v-btn {
|
|
min-height: 48px; /* Ensure touch-friendly button size */
|
|
}
|
|
}
|
|
|
|
/* Performance mode optimizations */
|
|
.performance-mode .v-card {
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important; /* Lighter shadow */
|
|
}
|
|
|
|
.performance-mode .v-btn {
|
|
transition: none; /* Remove button transitions for better performance */
|
|
}
|
|
|
|
/* Form styling improvements */
|
|
.v-text-field {
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.v-progress-linear {
|
|
border-radius: 3px;
|
|
}
|
|
|
|
/* Password field specific optimizations for mobile */
|
|
.password-field :deep(.v-field__input) {
|
|
font-size: 16px !important; /* Prevent zoom on iOS */
|
|
-webkit-text-fill-color: currentColor !important;
|
|
}
|
|
|
|
/* Prevent auto-zoom on focus for mobile Safari */
|
|
@media screen and (max-width: 768px) {
|
|
.password-field :deep(input) {
|
|
font-size: 16px !important;
|
|
}
|
|
|
|
.password-field :deep(.v-field__append-inner) {
|
|
/* Make eye icon easier to tap on mobile */
|
|
min-width: 44px;
|
|
min-height: 44px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
}
|
|
|
|
/* iOS specific fixes to prevent zoom */
|
|
@supports (-webkit-touch-callout: none) {
|
|
.password-field :deep(input) {
|
|
font-size: 16px !important;
|
|
}
|
|
}
|
|
|
|
/* Disable transitions on mobile for better performance */
|
|
.is-mobile .password-field :deep(.v-field__append-inner) {
|
|
transition: none !important;
|
|
}
|
|
|
|
.is-mobile .password-field :deep(.v-icon) {
|
|
transition: none !important;
|
|
}
|
|
</style>
|