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);