diff --git a/pages/signup.vue b/pages/signup.vue index 1661bff..29d95a2 100644 --- a/pages/signup.vue +++ b/pages/signup.vue @@ -112,24 +112,6 @@ required /> - -
- - - reCAPTCHA not configured. Please contact administrator. - -
import type { RegistrationFormData, RecaptchaConfig, RegistrationConfig } from '~/utils/types'; +// Declare global grecaptcha interface for TypeScript +declare global { + interface Window { + grecaptcha: { + ready: (callback: () => void) => void; + execute: (siteKey: string, options: { action: string }) => Promise; + }; + } +} + // Page metadata definePageMeta({ layout: false }); -// Head configuration -useHead({ - title: 'Register - MonacoUSA Portal', - meta: [ - { name: 'description', content: 'Register to become a member of MonacoUSA Association' }, - { name: 'robots', content: 'noindex, nofollow' } - ] +// Configs - need to be declared first +const recaptchaConfig = ref({ siteKey: '', secretKey: '' }); +const registrationConfig = ref({ + membershipFee: 50, + iban: '', + accountHolder: '' }); // Reactive data - Using individual refs to prevent Vue reactivity corruption @@ -272,12 +263,13 @@ const errorMessage = ref(''); const showSuccessDialog = ref(false); const registrationResult = ref<{ memberId: string; email: string } | null>(null); -// Configs -const recaptchaConfig = ref({ siteKey: '', secretKey: '' }); -const registrationConfig = ref({ - membershipFee: 50, - iban: '', - accountHolder: '' +// Head configuration +useHead({ + title: 'Register - MonacoUSA Portal', + meta: [ + { name: 'description', content: 'Register to become a member of MonacoUSA Association' }, + { name: 'robots', content: 'noindex, nofollow' } + ] }); // Form validation rules @@ -336,9 +328,30 @@ const recaptcha = ref(null); +// reCAPTCHA v3 token generation +async function generateRecaptchaToken(): Promise { + if (!recaptchaConfig.value.siteKey || typeof window === 'undefined') { + return ''; + } + + return new Promise((resolve) => { + if (window.grecaptcha && window.grecaptcha.ready) { + window.grecaptcha.ready(() => { + window.grecaptcha.execute(recaptchaConfig.value.siteKey, { action: 'registration' }).then((token: string) => { + resolve(token); + }).catch(() => { + resolve(''); + }); + }); + } else { + resolve(''); + } + }); +} + // Form submission async function submitRegistration() { - if (!valid.value || !recaptchaToken.value) { + if (!valid.value) { return; } @@ -347,9 +360,15 @@ async function submitRegistration() { successMessage.value = ''; try { + // Generate reCAPTCHA v3 token if configured + let token = ''; + if (recaptchaConfig.value.siteKey) { + token = await generateRecaptchaToken(); + } + const registrationData: RegistrationFormData = { ...form.value, - recaptcha_token: recaptchaToken.value + recaptcha_token: token }; const response = await $fetch('/api/registration', { @@ -375,18 +394,11 @@ async function submitRegistration() { dateOfBirth.value = ''; address.value = ''; nationality.value = ''; - 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; } @@ -397,6 +409,19 @@ const goToLogin = () => { navigateTo('/login'); }; +// Load reCAPTCHA script dynamically +function loadRecaptchaScript(siteKey: string) { + if (typeof window === 'undefined' || document.querySelector(`script[src*="recaptcha/api.js"]`)) { + return; // Already loaded or not in browser + } + + const script = document.createElement('script'); + script.src = `https://www.google.com/recaptcha/api.js?render=${siteKey}`; + script.async = true; + script.defer = true; + document.head.appendChild(script); +} + // Load configurations on mount onMounted(async () => { try { @@ -404,6 +429,10 @@ onMounted(async () => { const recaptchaResponse = await $fetch('/api/recaptcha-config') as any; if (recaptchaResponse?.success && recaptchaResponse?.data?.siteKey) { recaptchaConfig.value.siteKey = recaptchaResponse.data.siteKey; + + // Load reCAPTCHA script dynamically + loadRecaptchaScript(recaptchaConfig.value.siteKey); + console.log('✅ reCAPTCHA site key loaded successfully'); } else { console.warn('❌ reCAPTCHA not configured or failed to load'); diff --git a/server/api/admin/registration-config.post.ts b/server/api/admin/registration-config.post.ts index 6cc976c..63327ef 100644 --- a/server/api/admin/registration-config.post.ts +++ b/server/api/admin/registration-config.post.ts @@ -31,7 +31,8 @@ export default defineEventHandler(async (event) => { console.log('[api/admin/registration-config.post] Request body fields:', Object.keys(body)); // Validate required fields - if (!body.membershipFee || typeof body.membershipFee !== 'number' || body.membershipFee <= 0) { + const membershipFee = Number(body.membershipFee); + if (!body.membershipFee || isNaN(membershipFee) || membershipFee <= 0) { throw createError({ statusCode: 400, statusMessage: 'Valid membership fee is required' @@ -55,7 +56,7 @@ export default defineEventHandler(async (event) => { // Save registration configuration const { saveRegistrationConfig } = await import('~/server/utils/admin-config'); await saveRegistrationConfig({ - membershipFee: body.membershipFee, + membershipFee: membershipFee, iban: body.iban.trim(), accountHolder: body.accountHolder.trim() }, session.user.email);