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:
@@ -35,6 +35,10 @@
|
||||
<v-icon start>mdi-account-plus</v-icon>
|
||||
Registration
|
||||
</v-tab>
|
||||
<v-tab value="email">
|
||||
<v-icon start>mdi-email</v-icon>
|
||||
Email
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<v-card-text class="pa-6" style="min-height: 400px;">
|
||||
@@ -281,6 +285,177 @@
|
||||
</v-row>
|
||||
</v-form>
|
||||
</v-tabs-window-item>
|
||||
|
||||
<!-- Email Configuration Tab -->
|
||||
<v-tabs-window-item value="email">
|
||||
<v-alert
|
||||
type="info"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
>
|
||||
<template #title>SMTP Email Configuration</template>
|
||||
Configure SMTP settings for sending emails from the portal (registration confirmations, password resets, dues reminders).
|
||||
</v-alert>
|
||||
|
||||
<v-form ref="emailFormRef" v-model="emailFormValid">
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="emailForm.host"
|
||||
label="SMTP Host"
|
||||
variant="outlined"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
placeholder="smtp.gmail.com"
|
||||
hint="Your SMTP server hostname"
|
||||
persistent-hint
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model.number="emailForm.port"
|
||||
label="Port"
|
||||
variant="outlined"
|
||||
:rules="[rules.required, rules.validPort]"
|
||||
required
|
||||
type="number"
|
||||
min="1"
|
||||
max="65535"
|
||||
placeholder="587"
|
||||
hint="Usually 587 (TLS) or 465 (SSL)"
|
||||
persistent-hint
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12">
|
||||
<v-switch
|
||||
v-model="emailForm.secure"
|
||||
label="Use SSL/TLS encryption"
|
||||
color="primary"
|
||||
hide-details
|
||||
class="mb-2"
|
||||
/>
|
||||
<div class="text-caption text-medium-emphasis">
|
||||
Enable for secure connection (recommended for production)
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="emailForm.username"
|
||||
label="Username"
|
||||
variant="outlined"
|
||||
placeholder="your-email@domain.com"
|
||||
hint="SMTP authentication username (usually your email)"
|
||||
persistent-hint
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="emailForm.password"
|
||||
label="Password"
|
||||
variant="outlined"
|
||||
:type="showEmailPassword ? 'text' : 'password'"
|
||||
:append-inner-icon="showEmailPassword ? 'mdi-eye' : 'mdi-eye-off'"
|
||||
@click:append-inner="showEmailPassword = !showEmailPassword"
|
||||
placeholder="Enter SMTP password"
|
||||
hint="SMTP authentication password or app password"
|
||||
persistent-hint
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="emailForm.fromAddress"
|
||||
label="From Email Address"
|
||||
variant="outlined"
|
||||
:rules="[rules.required, rules.email]"
|
||||
required
|
||||
type="email"
|
||||
placeholder="noreply@monacousa.org"
|
||||
hint="Email address that emails will be sent from"
|
||||
persistent-hint
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="emailForm.fromName"
|
||||
label="From Name"
|
||||
variant="outlined"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
placeholder="MonacoUSA Portal"
|
||||
hint="Display name for outgoing emails"
|
||||
persistent-hint
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
v-model="testEmailAddress"
|
||||
label="Test Email Address"
|
||||
variant="outlined"
|
||||
:rules="[rules.email]"
|
||||
type="email"
|
||||
placeholder="admin@monacousa.org"
|
||||
hint="Email address to send test email to"
|
||||
persistent-hint
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-btn
|
||||
@click="sendTestEmail"
|
||||
:loading="emailTestLoading"
|
||||
:disabled="!emailFormValid || emailLoading || !testEmailAddress"
|
||||
color="info"
|
||||
variant="outlined"
|
||||
block
|
||||
>
|
||||
<v-icon start>mdi-email-send</v-icon>
|
||||
Send Test Email
|
||||
</v-btn>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<div class="d-flex align-center h-100">
|
||||
<v-chip
|
||||
v-if="emailTestStatus"
|
||||
:color="emailTestStatus.success ? 'success' : 'error'"
|
||||
variant="flat"
|
||||
size="small"
|
||||
>
|
||||
<v-icon start size="14">
|
||||
{{ emailTestStatus.success ? 'mdi-check-circle' : 'mdi-alert-circle' }}
|
||||
</v-icon>
|
||||
{{ emailTestStatus.message }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12">
|
||||
<v-alert
|
||||
type="warning"
|
||||
variant="outlined"
|
||||
class="mt-2"
|
||||
>
|
||||
<div class="text-body-2">
|
||||
<strong>Common SMTP Providers:</strong>
|
||||
<ul class="mt-2 ml-4">
|
||||
<li><strong>Gmail:</strong> smtp.gmail.com:587 (use App Password, not regular password)</li>
|
||||
<li><strong>SendGrid:</strong> smtp.sendgrid.net:587</li>
|
||||
<li><strong>Amazon SES:</strong> email-smtp.region.amazonaws.com:587</li>
|
||||
<li><strong>Mailgun:</strong> smtp.mailgun.org:587</li>
|
||||
</ul>
|
||||
</div>
|
||||
</v-alert>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
</v-tabs-window-item>
|
||||
</v-tabs-window>
|
||||
|
||||
<!-- Success Message -->
|
||||
@@ -332,7 +507,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { NocoDBSettings, RecaptchaConfig, RegistrationConfig } from '~/utils/types';
|
||||
import type { NocoDBSettings, RecaptchaConfig, RegistrationConfig, SMTPConfig } from '~/utils/types';
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean;
|
||||
@@ -353,25 +528,34 @@ const activeTab = ref('nocodb');
|
||||
const nocodbFormRef = ref();
|
||||
const recaptchaFormRef = ref();
|
||||
const registrationFormRef = ref();
|
||||
const emailFormRef = ref();
|
||||
|
||||
// Form validity
|
||||
const nocodbFormValid = ref(false);
|
||||
const recaptchaFormValid = ref(false);
|
||||
const registrationFormValid = ref(false);
|
||||
const emailFormValid = ref(false);
|
||||
|
||||
// Loading states
|
||||
const nocodbLoading = ref(false);
|
||||
const recaptchaLoading = ref(false);
|
||||
const registrationLoading = ref(false);
|
||||
const emailLoading = ref(false);
|
||||
const nocodbTestLoading = ref(false);
|
||||
const emailTestLoading = ref(false);
|
||||
|
||||
// Display states
|
||||
const showNocodbApiKey = ref(false);
|
||||
const showRecaptchaSecret = ref(false);
|
||||
const showEmailPassword = ref(false);
|
||||
const showSuccessMessage = ref(false);
|
||||
const successMessage = ref('');
|
||||
const errorMessage = ref('');
|
||||
const nocodbConnectionStatus = ref<{ success: boolean; message: string } | null>(null);
|
||||
const emailTestStatus = ref<{ success: boolean; message: string } | null>(null);
|
||||
|
||||
// Test email address
|
||||
const testEmailAddress = ref('');
|
||||
|
||||
// Form data
|
||||
const nocodbForm = ref<NocoDBSettings>({
|
||||
@@ -392,6 +576,16 @@ const registrationForm = ref<RegistrationConfig>({
|
||||
accountHolder: ''
|
||||
});
|
||||
|
||||
const emailForm = ref<SMTPConfig>({
|
||||
host: '',
|
||||
port: 587,
|
||||
secure: false,
|
||||
username: '',
|
||||
password: '',
|
||||
fromAddress: '',
|
||||
fromName: 'MonacoUSA Portal'
|
||||
});
|
||||
|
||||
// Validation rules
|
||||
const rules = {
|
||||
required: (value: string | number) => {
|
||||
@@ -405,6 +599,14 @@ const rules = {
|
||||
positiveNumber: (value: number) => {
|
||||
return (value && value > 0) || 'Must be a positive number';
|
||||
},
|
||||
validPort: (value: number) => {
|
||||
return (value && value >= 1 && value <= 65535) || 'Port must be between 1 and 65535';
|
||||
},
|
||||
email: (value: string) => {
|
||||
if (!value) return true;
|
||||
const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return pattern.test(value) || 'Please enter a valid email address';
|
||||
},
|
||||
iban: (value: string) => {
|
||||
if (!value) return true;
|
||||
// Basic IBAN validation (length and format)
|
||||
@@ -426,6 +628,8 @@ const isCurrentTabValid = computed(() => {
|
||||
return recaptchaFormValid.value;
|
||||
case 'registration':
|
||||
return registrationFormValid.value;
|
||||
case 'email':
|
||||
return emailFormValid.value;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -439,6 +643,8 @@ const getCurrentTabName = computed(() => {
|
||||
return 'reCAPTCHA Settings';
|
||||
case 'registration':
|
||||
return 'Registration Settings';
|
||||
case 'email':
|
||||
return 'Email Settings';
|
||||
default:
|
||||
return 'Settings';
|
||||
}
|
||||
@@ -503,6 +709,53 @@ const testNocodbConnection = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Send test email
|
||||
const sendTestEmail = async () => {
|
||||
if (!emailFormRef.value || !testEmailAddress.value) return;
|
||||
|
||||
const isValid = await emailFormRef.value.validate();
|
||||
if (!isValid.valid) return;
|
||||
|
||||
// First save email configuration
|
||||
emailLoading.value = true;
|
||||
try {
|
||||
await $fetch('/api/admin/smtp-config', {
|
||||
method: 'POST',
|
||||
body: emailForm.value
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Failed to save SMTP config:', error);
|
||||
errorMessage.value = 'Failed to save SMTP configuration: ' + error.message;
|
||||
emailLoading.value = false;
|
||||
return;
|
||||
} finally {
|
||||
emailLoading.value = false;
|
||||
}
|
||||
|
||||
// Then send test email
|
||||
emailTestLoading.value = true;
|
||||
emailTestStatus.value = null;
|
||||
|
||||
try {
|
||||
const response = await $fetch<{ success: boolean; message: string }>('/api/admin/test-email', {
|
||||
method: 'POST',
|
||||
body: { testEmail: testEmailAddress.value }
|
||||
});
|
||||
|
||||
emailTestStatus.value = {
|
||||
success: response.success,
|
||||
message: response.message || (response.success ? 'Test email sent successfully' : 'Test email failed')
|
||||
};
|
||||
} catch (error: any) {
|
||||
emailTestStatus.value = {
|
||||
success: false,
|
||||
message: error.message || 'Test email failed'
|
||||
};
|
||||
} finally {
|
||||
emailTestLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Save current tab
|
||||
const saveCurrentTab = async () => {
|
||||
let formRef, loading, data, endpoint;
|
||||
@@ -526,6 +779,12 @@ const saveCurrentTab = async () => {
|
||||
data = registrationForm.value;
|
||||
endpoint = '/api/admin/registration-config';
|
||||
break;
|
||||
case 'email':
|
||||
formRef = emailFormRef.value;
|
||||
loading = emailLoading;
|
||||
data = emailForm.value;
|
||||
endpoint = '/api/admin/smtp-config';
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -21,10 +21,13 @@
|
||||
<v-menu
|
||||
v-model="dropdownOpen"
|
||||
:close-on-content-click="false"
|
||||
location="bottom start"
|
||||
:offset="isMobile ? 8 : 4"
|
||||
min-width="280"
|
||||
:transition="isMobile ? 'slide-y-transition' : 'fade-transition'"
|
||||
:location="isMobile ? undefined : 'bottom start'"
|
||||
:offset="isMobile ? 0 : 4"
|
||||
:min-width="isMobile ? undefined : '280'"
|
||||
:transition="isMobile ? 'fade-transition' : 'fade-transition'"
|
||||
:no-click-animation="isMobile"
|
||||
:attach="isMobile"
|
||||
:strategy="isMobile ? 'fixed' : 'absolute'"
|
||||
>
|
||||
<template #activator="{ props: menuProps }">
|
||||
<div
|
||||
@@ -498,8 +501,9 @@ watch(() => props.modelValue, (newValue) => {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 999;
|
||||
z-index: 1999;
|
||||
backdrop-filter: blur(4px);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
/* Mobile Header */
|
||||
@@ -553,15 +557,17 @@ watch(() => props.modelValue, (newValue) => {
|
||||
|
||||
.country-dropdown--mobile {
|
||||
position: fixed !important;
|
||||
top: 50% !important;
|
||||
left: 50% !important;
|
||||
transform: translate(-50%, -50%) !important;
|
||||
top: 10% !important;
|
||||
left: 5% !important;
|
||||
right: 5% !important;
|
||||
width: 90vw !important;
|
||||
max-width: 400px !important;
|
||||
max-height: 80vh !important;
|
||||
margin: 0 auto !important;
|
||||
border-radius: 16px !important;
|
||||
z-index: 1000 !important;
|
||||
box-shadow: 0 24px 48px rgba(0, 0, 0, 0.2) !important;
|
||||
z-index: 2000 !important;
|
||||
box-shadow: 0 24px 48px rgba(0, 0, 0, 0.3) !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.country-list--mobile {
|
||||
|
||||
Reference in New Issue
Block a user