Add email verification system for user registration
All checks were successful
Build And Push Image / docker (push) Successful in 3m1s
All checks were successful
Build And Push Image / docker (push) Successful in 3m1s
- Add SMTP configuration UI in admin panel with test functionality - Implement email verification workflow with tokens and templates - Add verification success/expired pages for user feedback - Include nodemailer, handlebars, and JWT dependencies - Create API endpoints for email config, testing, and verification
This commit is contained in:
278
pages/auth/verify-expired.vue
Normal file
278
pages/auth/verify-expired.vue
Normal file
@@ -0,0 +1,278 @@
|
||||
<template>
|
||||
<div class="verification-expired">
|
||||
<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="warning"
|
||||
size="80"
|
||||
class="mb-4"
|
||||
>
|
||||
mdi-clock-alert
|
||||
</v-icon>
|
||||
|
||||
<h1 class="text-h4 font-weight-bold text-warning mb-3">
|
||||
{{ pageTitle }}
|
||||
</h1>
|
||||
|
||||
<p class="text-body-1 text-medium-emphasis mb-6">
|
||||
{{ pageDescription }}
|
||||
</p>
|
||||
|
||||
<!-- Information alert -->
|
||||
<v-alert
|
||||
type="info"
|
||||
variant="tonal"
|
||||
class="mb-6 text-start"
|
||||
icon="mdi-information"
|
||||
>
|
||||
<div class="text-body-2">
|
||||
<strong>What to do next:</strong>
|
||||
<ul class="mt-2 pl-4">
|
||||
<li>Request a new verification email below</li>
|
||||
<li>Check your spam/junk folder for emails from MonacoUSA</li>
|
||||
<li>Make sure you're checking the correct email address</li>
|
||||
</ul>
|
||||
</div>
|
||||
</v-alert>
|
||||
</div>
|
||||
|
||||
<!-- Resend verification form -->
|
||||
<v-form @submit.prevent="resendVerification" :disabled="loading">
|
||||
<div class="mb-4">
|
||||
<v-text-field
|
||||
v-model="email"
|
||||
label="Email Address"
|
||||
type="email"
|
||||
variant="outlined"
|
||||
prepend-inner-icon="mdi-email"
|
||||
:rules="emailRules"
|
||||
:error-messages="emailError"
|
||||
required
|
||||
class="mb-3"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-column gap-3 mb-6">
|
||||
<v-btn
|
||||
type="submit"
|
||||
color="primary"
|
||||
size="large"
|
||||
variant="elevated"
|
||||
block
|
||||
:loading="loading"
|
||||
:disabled="!email || !isValidEmail(email)"
|
||||
class="text-none"
|
||||
>
|
||||
<v-icon start>mdi-email-send</v-icon>
|
||||
{{ loading ? 'Sending...' : 'Send New Verification Email' }}
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
color="secondary"
|
||||
size="large"
|
||||
variant="outlined"
|
||||
block
|
||||
to="/login"
|
||||
class="text-none"
|
||||
>
|
||||
<v-icon start>mdi-login</v-icon>
|
||||
Back to Login
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-form>
|
||||
|
||||
<!-- Success message -->
|
||||
<v-alert
|
||||
v-if="successMessage"
|
||||
type="success"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
icon="mdi-check"
|
||||
>
|
||||
{{ successMessage }}
|
||||
</v-alert>
|
||||
|
||||
<!-- Error message -->
|
||||
<v-alert
|
||||
v-if="errorMessage"
|
||||
type="error"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
icon="mdi-alert"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</v-alert>
|
||||
|
||||
<!-- Additional help -->
|
||||
<div class="mt-6 pt-4 border-t">
|
||||
<p class="text-caption text-medium-emphasis mb-2">
|
||||
Still having trouble? Contact support:
|
||||
</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'
|
||||
});
|
||||
|
||||
// Get query parameters
|
||||
const route = useRoute();
|
||||
const reason = computed(() => route.query.reason as string || 'expired');
|
||||
|
||||
// Reactive data
|
||||
const email = ref('');
|
||||
const loading = ref(false);
|
||||
const successMessage = ref('');
|
||||
const errorMessage = ref('');
|
||||
const emailError = ref('');
|
||||
|
||||
// Computed properties
|
||||
const pageTitle = computed(() => {
|
||||
switch (reason.value) {
|
||||
case 'used':
|
||||
return 'Verification Link Already Used';
|
||||
case 'invalid':
|
||||
return 'Invalid Verification Link';
|
||||
default:
|
||||
return 'Verification Link Expired';
|
||||
}
|
||||
});
|
||||
|
||||
const pageDescription = computed(() => {
|
||||
switch (reason.value) {
|
||||
case 'used':
|
||||
return 'This verification link has already been used. If you need to verify your email again, please request a new verification link below.';
|
||||
case 'invalid':
|
||||
return 'The verification link you clicked is invalid or malformed. Please request a new verification link below.';
|
||||
default:
|
||||
return 'Your verification link has expired. Verification links are valid for 24 hours. Please request a new verification link below.';
|
||||
}
|
||||
});
|
||||
|
||||
// Validation rules
|
||||
const emailRules = [
|
||||
(v: string) => !!v || 'Email is required',
|
||||
(v: string) => isValidEmail(v) || 'Please enter a valid email address'
|
||||
];
|
||||
|
||||
// Helper function
|
||||
function isValidEmail(email: string): boolean {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
|
||||
// Resend verification email
|
||||
async function resendVerification() {
|
||||
if (!email.value || !isValidEmail(email.value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
successMessage.value = '';
|
||||
errorMessage.value = '';
|
||||
emailError.value = '';
|
||||
|
||||
try {
|
||||
const response = await $fetch('/api/auth/send-verification-email', {
|
||||
method: 'POST',
|
||||
body: { email: email.value }
|
||||
});
|
||||
|
||||
successMessage.value = 'A new verification email has been sent! Please check your inbox and spam folder.';
|
||||
email.value = ''; // Clear the form
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('[verify-expired] Failed to resend verification:', error);
|
||||
|
||||
if (error.status === 404) {
|
||||
emailError.value = 'No account found with this email address.';
|
||||
} else if (error.status === 429) {
|
||||
errorMessage.value = 'Please wait a few minutes before requesting another verification email.';
|
||||
} else {
|
||||
errorMessage.value = error.data?.message || 'Failed to send verification email. Please try again.';
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Set page title
|
||||
useHead({
|
||||
title: `${pageTitle.value} - MonacoUSA Portal`,
|
||||
meta: [
|
||||
{
|
||||
name: 'description',
|
||||
content: 'Request a new email verification link for your MonacoUSA Portal account.'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// Track page view
|
||||
onMounted(() => {
|
||||
console.log('[verify-expired] Page accessed', { reason: reason.value });
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.verification-expired {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
}
|
||||
|
||||
.fill-height {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.border-t {
|
||||
border-top: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
}
|
||||
|
||||
.gap-3 {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* Animation for the warning icon */
|
||||
.v-icon {
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* List styling */
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
</style>
|
||||
161
pages/auth/verify-success.vue
Normal file
161
pages/auth/verify-success.vue
Normal file
@@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<div class="verification-success">
|
||||
<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="success"
|
||||
size="80"
|
||||
class="mb-4"
|
||||
>
|
||||
mdi-check-circle
|
||||
</v-icon>
|
||||
|
||||
<h1 class="text-h4 font-weight-bold text-success mb-3">
|
||||
Email Verified Successfully!
|
||||
</h1>
|
||||
|
||||
<p class="text-body-1 text-medium-emphasis mb-2" v-if="email">
|
||||
Your email address <strong>{{ email }}</strong> has been verified.
|
||||
</p>
|
||||
|
||||
<p class="text-body-1 text-medium-emphasis mb-6">
|
||||
Your MonacoUSA Portal account is now active and ready to use.
|
||||
You can now log in to access your dashboard and member features.
|
||||
</p>
|
||||
|
||||
<!-- Warning message for partial verification -->
|
||||
<v-alert
|
||||
v-if="partialWarning"
|
||||
type="warning"
|
||||
variant="tonal"
|
||||
class="mb-4 text-start"
|
||||
icon="mdi-information"
|
||||
>
|
||||
<div class="text-body-2">
|
||||
<strong>Note:</strong> Your email has been verified, but there may have been
|
||||
a minor issue updating your account status. If you experience any login
|
||||
problems, please contact support.
|
||||
</div>
|
||||
</v-alert>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-column gap-3">
|
||||
<v-btn
|
||||
color="primary"
|
||||
size="large"
|
||||
variant="elevated"
|
||||
block
|
||||
:to="{ path: '/login', query: { verified: 'true' } }"
|
||||
class="text-none"
|
||||
>
|
||||
<v-icon start>mdi-login</v-icon>
|
||||
Log In to Portal
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
color="secondary"
|
||||
size="large"
|
||||
variant="outlined"
|
||||
block
|
||||
to="/"
|
||||
class="text-none"
|
||||
>
|
||||
<v-icon start>mdi-home</v-icon>
|
||||
Return to Home
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<!-- 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'
|
||||
});
|
||||
|
||||
// Get query parameters
|
||||
const route = useRoute();
|
||||
const email = computed(() => route.query.email as string || '');
|
||||
const partialWarning = computed(() => route.query.warning === 'partial');
|
||||
|
||||
// Set page title
|
||||
useHead({
|
||||
title: 'Email Verified - MonacoUSA Portal',
|
||||
meta: [
|
||||
{
|
||||
name: 'description',
|
||||
content: 'Your email has been successfully verified. You can now log in to the MonacoUSA Portal.'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// Track successful verification
|
||||
onMounted(() => {
|
||||
console.log('[verify-success] Email verification completed', {
|
||||
email: email.value,
|
||||
partialWarning: partialWarning.value
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.verification-success {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
}
|
||||
|
||||
.fill-height {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.border-t {
|
||||
border-top: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
}
|
||||
|
||||
.gap-3 {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* Animation for the success icon */
|
||||
.v-icon {
|
||||
animation: bounce 0.6s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 20%, 53%, 80%, 100% {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
40%, 43% {
|
||||
transform: translate3d(0, -8px, 0);
|
||||
}
|
||||
70% {
|
||||
transform: translate3d(0, -4px, 0);
|
||||
}
|
||||
90% {
|
||||
transform: translate3d(0, -2px, 0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user