monacousa-portal/pages/signup.vue

375 lines
11 KiB
Vue
Raw Normal View History

#### __1. Role-Based Security Architecture__ - Replaces group-based tiers with proper Keycloak realm roles - `monaco-user`, `monaco-board`, `monaco-admin` roles - Backward compatibility with existing group system #### __2. Advanced User Management__ - Comprehensive user profile synchronization - Membership data stored in Keycloak user attributes - Bidirectional sync between NocoDB and Keycloak #### __3. Session Security & Monitoring__ - Real-time session tracking and management - Administrative session control capabilities - Enhanced security analytics foundation #### __4. Email Workflow System__ - Multiple email types: DUES_REMINDER, MEMBERSHIP_RENEWAL, WELCOME, VERIFICATION - Customizable email parameters and lifespans - Advanced email template support #### __5. Seamless Migration Path__ - All existing functionality continues to work - New users automatically get realm roles - Gradual migration from groups to roles - Zero breaking changes ### 🔧 __What You Can Do Now__ #### __For New Users:__ - Public registrations automatically assign `monaco-user` role - Portal account creation syncs member data to Keycloak attributes - Enhanced email verification and welcome workflows #### __For Administrators:__ - Session management and monitoring capabilities - Advanced user profile management with member data sync - Comprehensive role assignment and management - Enhanced email communication workflows #### __For Developers:__ - Use `hasRole('monaco-admin')` for role-based checks - Access `getAllRoles()` for debugging and analytics - Enhanced `useAuth()` composable with backward compatibility - Comprehensive TypeScript support throughout ### 🛡️ __Security & Reliability__ - __Backward Compatibility__: Existing users continue to work seamlessly - __Enhanced Security__: Proper realm role-based authorization - __Error Handling__: Comprehensive error handling and fallbacks - __Type Safety__: Full TypeScript support throughout the system
2025-08-08 19:40:13 +02:00
<template>
<div class="fill-height">
<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" :loading="loading">
<v-toolbar color="primary" dark flat>
<v-toolbar-title class="text-h5 font-weight-bold">
<v-icon left>mdi-account-plus</v-icon>
Join MonacoUSA
</v-toolbar-title>
</v-toolbar>
<v-card-text class="py-6">
<div class="text-body-1 text-center mb-6">
Register as a member of MonacoUSA Association
</div>
<v-form ref="form" v-model="valid" @submit.prevent="submitRegistration">
<v-row>
<v-col cols="12" sm="6">
<v-text-field
v-model="form.first_name"
label="First Name"
:rules="nameRules"
prepend-icon="mdi-account"
variant="outlined"
required
/>
</v-col>
<v-col cols="12" sm="6">
<v-text-field
v-model="form.last_name"
label="Last Name"
:rules="nameRules"
variant="outlined"
required
/>
</v-col>
</v-row>
<v-text-field
v-model="form.email"
label="Email Address"
type="email"
:rules="emailRules"
prepend-icon="mdi-email"
variant="outlined"
required
/>
<PhoneInputWrapper
v-model="form.phone"
label="Phone Number"
:rules="phoneRules"
required
/>
<v-text-field
v-model="form.date_of_birth"
label="Date of Birth"
type="date"
:rules="dobRules"
prepend-icon="mdi-calendar"
variant="outlined"
required
/>
<v-textarea
v-model="form.address"
label="Address"
:rules="addressRules"
prepend-icon="mdi-map-marker"
variant="outlined"
rows="3"
required
/>
<MultipleNationalityInput
v-model="form.nationality"
label="Nationality"
:rules="nationalityRules"
required
/>
<!-- reCAPTCHA -->
<div class="d-flex justify-center my-4">
<vue-recaptcha
v-if="recaptchaConfig.siteKey"
ref="recaptcha"
:sitekey="recaptchaConfig.siteKey"
@verify="onRecaptchaVerified"
@expired="onRecaptchaExpired"
/>
<v-alert
v-else
type="warning"
variant="outlined"
class="text-body-2"
>
reCAPTCHA not configured. Please contact administrator.
</v-alert>
</div>
<v-btn
type="submit"
color="primary"
size="large"
block
:disabled="!valid || !recaptchaToken"
:loading="loading"
class="mt-4"
>
<v-icon left>mdi-account-plus</v-icon>
Register
</v-btn>
</v-form>
</v-card-text>
<!-- Payment Information -->
<v-divider />
<v-card-text>
<v-card
class="pa-4"
color="blue-grey-lighten-5"
variant="outlined"
>
<v-card-title class="text-h6 pb-2">
<v-icon left color="primary">mdi-bank</v-icon>
Membership Dues Payment
</v-card-title>
<v-card-text class="pt-2">
<div class="text-body-1 mb-3">
After registration, please transfer your annual membership dues:
</div>
<v-row dense>
<v-col cols="4" class="text-body-2 font-weight-bold">Amount:</v-col>
<v-col cols="8" class="text-body-2">{{ registrationConfig.membershipFee }}/year</v-col>
</v-row>
<v-row dense v-if="registrationConfig.iban">
<v-col cols="4" class="text-body-2 font-weight-bold">IBAN:</v-col>
<v-col cols="8" class="text-body-2 font-family-monospace">{{ registrationConfig.iban }}</v-col>
</v-row>
<v-row dense v-if="registrationConfig.accountHolder">
<v-col cols="4" class="text-body-2 font-weight-bold">Account:</v-col>
<v-col cols="8" class="text-body-2">{{ registrationConfig.accountHolder }}</v-col>
</v-row>
<v-divider class="my-3" />
<div class="text-body-2 text-medium-emphasis">
<v-icon size="small" class="mr-1">mdi-information</v-icon>
Your account will be activated once payment is verified by our administrators.
</div>
</v-card-text>
</v-card>
</v-card-text>
</v-card>
<!-- Success/Error Messages -->
<v-alert
v-if="successMessage"
type="success"
class="mt-4"
variant="tonal"
>
<v-alert-title>Registration Successful!</v-alert-title>
{{ successMessage }}
</v-alert>
<v-alert
v-if="errorMessage"
type="error"
class="mt-4"
variant="tonal"
>
<v-alert-title>Registration Failed</v-alert-title>
{{ errorMessage }}
</v-alert>
<!-- Back to Login Link -->
<div class="text-center mt-6">
<v-btn
variant="text"
color="primary"
to="/login"
>
<v-icon left>mdi-arrow-left</v-icon>
Back to Login
</v-btn>
</div>
</v-col>
</v-row>
</v-container>
</div>
</template>
<script setup lang="ts">
import type { RegistrationFormData, RecaptchaConfig, RegistrationConfig } from '~/utils/types';
// Page metadata
definePageMeta({
layout: false,
middleware: 'guest'
});
// Head configuration
useHead({
title: 'Register - MonacoUSA Portal',
meta: [
{ name: 'description', content: 'Register to become a member of MonacoUSA Association' },
{ name: 'robots', content: 'noindex, nofollow' }
]
});
// Reactive data
const form = ref<Omit<RegistrationFormData, 'recaptcha_token'>>({
first_name: '',
last_name: '',
email: '',
phone: '',
date_of_birth: '',
address: '',
nationality: ''
});
const valid = ref(false);
const loading = ref(false);
const recaptchaToken = ref('');
const successMessage = ref('');
const errorMessage = ref('');
// Configs
const recaptchaConfig = ref<RecaptchaConfig>({ siteKey: '', secretKey: '' });
const registrationConfig = ref<RegistrationConfig>({
membershipFee: 50,
iban: '',
accountHolder: ''
});
// Form validation rules
const nameRules = [
(v: string) => !!v || 'Name is required',
(v: string) => v.length >= 2 || 'Name must be at least 2 characters'
];
const emailRules = [
(v: string) => !!v || 'Email is required',
(v: string) => /.+@.+\..+/.test(v) || 'Email must be valid'
];
const phoneRules = [
(v: string) => !!v || 'Phone number is required'
];
const dobRules = [
(v: string) => !!v || 'Date of birth is required',
(v: string) => {
if (!v) return true;
const birthDate = new Date(v);
const today = new Date();
const age = today.getFullYear() - birthDate.getFullYear();
const monthDiff = today.getMonth() - birthDate.getMonth();
let actualAge = age;
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
actualAge = age - 1;
}
return actualAge >= 18 || 'You must be at least 18 years old';
}
];
const addressRules = [
(v: string) => !!v || 'Address is required',
(v: string) => v.length >= 10 || 'Please provide a complete address'
];
const nationalityRules = [
(v: string) => !!v || 'Nationality is required'
];
// reCAPTCHA handling
function onRecaptchaVerified(token: string) {
recaptchaToken.value = token;
}
function onRecaptchaExpired() {
recaptchaToken.value = '';
}
// Template refs
const recaptcha = ref<any>(null);
// Form submission
async function submitRegistration() {
if (!valid.value || !recaptchaToken.value) {
return;
}
loading.value = true;
errorMessage.value = '';
successMessage.value = '';
try {
const registrationData: RegistrationFormData = {
...form.value,
recaptcha_token: recaptchaToken.value
};
const response = await $fetch('/api/registration', {
method: 'POST',
body: registrationData
}) as any;
if (response?.success) {
successMessage.value = response.message || 'Registration successful!';
// Reset form
form.value = {
first_name: '',
last_name: '',
email: '',
phone: '',
date_of_birth: '',
address: '',
nationality: ''
};
recaptchaToken.value = '';
// Reset reCAPTCHA
recaptcha.value?.reset();
}
} catch (error: any) {
console.error('Registration failed:', error);
errorMessage.value = error.data?.message || error.message || 'Registration failed. Please try again.';
// Reset reCAPTCHA on error
recaptchaToken.value = '';
recaptcha.value?.reset();
} finally {
loading.value = false;
}
}
// Load configurations on mount
onMounted(async () => {
try {
// Load reCAPTCHA config (public endpoint)
const recaptchaResponse = await $fetch('/api/admin/recaptcha-config') as any;
if (recaptchaResponse?.success && recaptchaResponse?.data?.siteKey) {
recaptchaConfig.value.siteKey = recaptchaResponse.data.siteKey;
}
// Load registration config (public endpoint)
const registrationResponse = await $fetch('/api/admin/registration-config') as any;
if (registrationResponse?.success) {
registrationConfig.value = registrationResponse.data;
}
} catch (error) {
console.warn('Failed to load configuration:', error);
// Page will still work with default values
}
});
</script>
<style scoped>
.fill-height {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
</style>