create_form #38
7
.env
7
.env
|
|
@ -22,3 +22,10 @@ DATABASE_URL="file:./dev.db"
|
||||||
|
|
||||||
N8N_WEBHOOK_URL="https://flows2.carpa.com/webhook/news-summary"
|
N8N_WEBHOOK_URL="https://flows2.carpa.com/webhook/news-summary"
|
||||||
N8N_API_KEY="sk_live_JwjpTdZrLVvijCKuWylWZbUuhBm6zPDH"
|
N8N_API_KEY="sk_live_JwjpTdZrLVvijCKuWylWZbUuhBm6zPDH"
|
||||||
|
|
||||||
|
# Cloudflare Turnstile — public key (visible in frontend)
|
||||||
|
TURNSTILE_SITE_KEY=0x4AAAAAAD9IYWC6HPflpneO
|
||||||
|
|
||||||
|
# Cloudflare Turnstile — secret key (server-side, keep safe!)
|
||||||
|
# In production, set this as a Gitea Actions secret named TURNSTILE_SECRET_KEY
|
||||||
|
TURNSTILE_SECRET_KEY=0x4AAAAAAD9IYXASjQSpmo2iS902SDoXnT4
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
# Cloudflare Turnstile — public key (visible in frontend)
|
||||||
|
TURNSTILE_SITE_KEY=0x4AA...
|
||||||
|
|
||||||
|
# Cloudflare Turnstile — secret key (server-side, keep safe!)
|
||||||
|
# In production, set this as a Gitea Actions secret named TURNSTILE_SECRET_KEY
|
||||||
|
TURNSTILE_SECRET_KEY=0x4AA...
|
||||||
|
|
@ -54,5 +54,7 @@ jobs:
|
||||||
corepack prepare pnpm@11 --activate
|
corepack prepare pnpm@11 --activate
|
||||||
cd ${{ env.TARGET_DIR }}
|
cd ${{ env.TARGET_DIR }}
|
||||||
pnpm install --prod 2>&1
|
pnpm install --prod 2>&1
|
||||||
|
sed -i "s|TURNSTILE_SITE_KEY: \"\"|TURNSTILE_SITE_KEY: \"${{ secrets.TURNSTILE_SITE_KEY }}\"|" ecosystem.config.cjs
|
||||||
|
sed -i "s|TURNSTILE_SECRET_KEY: \"\"|TURNSTILE_SECRET_KEY: \"${{ secrets.TURNSTILE_SECRET_KEY }}\"|" ecosystem.config.cjs
|
||||||
pm2 reload ecosystem.config.cjs --env ${{ env.PM2_ENV }} --update-env || \
|
pm2 reload ecosystem.config.cjs --env ${{ env.PM2_ENV }} --update-env || \
|
||||||
pm2 start ecosystem.config.cjs --env ${{ env.PM2_ENV }} --update-env
|
pm2 start ecosystem.config.cjs --env ${{ env.PM2_ENV }} --update-env
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,14 @@ module.exports = {
|
||||||
env: {
|
env: {
|
||||||
NODE_ENV: "staging",
|
NODE_ENV: "staging",
|
||||||
PORT: 3310,
|
PORT: 3310,
|
||||||
|
TURNSTILE_SITE_KEY: "",
|
||||||
|
TURNSTILE_SECRET_KEY: "",
|
||||||
},
|
},
|
||||||
env_production: {
|
env_production: {
|
||||||
NODE_ENV: "production",
|
NODE_ENV: "production",
|
||||||
PORT: 3310,
|
PORT: 3310,
|
||||||
|
TURNSTILE_SITE_KEY: "",
|
||||||
|
TURNSTILE_SECRET_KEY: "",
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -40,8 +40,7 @@
|
||||||
"required": true,
|
"required": true,
|
||||||
"options": [
|
"options": [
|
||||||
{ "value": "M", "label": "form.sexo.m" },
|
{ "value": "M", "label": "form.sexo.m" },
|
||||||
{ "value": "F", "label": "form.sexo.f" },
|
{ "value": "F", "label": "form.sexo.f" }
|
||||||
{ "value": "Otro", "label": "form.sexo.o" }
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{ "key": "direccion_completa", "label": "form.direccion", "type": "text", "required": true, "colspan": 2 },
|
{ "key": "direccion_completa", "label": "form.direccion", "type": "text", "required": true, "colspan": 2 },
|
||||||
|
|
@ -49,8 +48,8 @@
|
||||||
{ "key": "estado", "label": "form.estado", "type": "text" },
|
{ "key": "estado", "label": "form.estado", "type": "text" },
|
||||||
{ "key": "pais", "label": "form.pais", "type": "select", "required": true, "source": "/forms/paises.json" },
|
{ "key": "pais", "label": "form.pais", "type": "select", "required": true, "source": "/forms/paises.json" },
|
||||||
{ "key": "postal", "label": "form.codigo_postal", "type": "text", "required": true },
|
{ "key": "postal", "label": "form.codigo_postal", "type": "text", "required": true },
|
||||||
{ "key": "telefono", "label": "form.mobile", "type": "text", "required": true },
|
{ "key": "telefono", "label": "form.mobile", "type": "phone", "required": true },
|
||||||
{ "key": "whatsapp", "label": "form.whatsapp", "type": "text" }
|
{ "key": "whatsapp", "label": "form.whatsapp", "type": "phone_country", "source": "/forms/paises.json" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -78,6 +77,7 @@
|
||||||
{ "value": "Ingles", "label": "form.idioma.ing" },
|
{ "value": "Ingles", "label": "form.idioma.ing" },
|
||||||
{ "value": "Hebreo", "label": "form.idioma.heb" },
|
{ "value": "Hebreo", "label": "form.idioma.heb" },
|
||||||
{ "value": "Portugues", "label": "form.idioma.port" },
|
{ "value": "Portugues", "label": "form.idioma.port" },
|
||||||
|
{ "value": "Frances", "label": "form.idioma.franc" },
|
||||||
{ "value": "Otro", "label": "form.idioma.otro" }
|
{ "value": "Otro", "label": "form.idioma.otro" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -5,7 +5,8 @@ import { createTranslator } from "../../i18n";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
locale: String,
|
locale: String,
|
||||||
formConfigUrl: { type: String, required: false, default: "/forms/formulario-inscripcion.json" }
|
formConfigUrl: { type: String, required: false, default: "/forms/formulario-inscripcion.json" },
|
||||||
|
turnstileSiteKey: { type: String, required: false, default: "" }
|
||||||
});
|
});
|
||||||
|
|
||||||
const tl = createTranslator(props.locale);
|
const tl = createTranslator(props.locale);
|
||||||
|
|
@ -25,6 +26,12 @@ const reglamentoLoading = ref(false);
|
||||||
const reglamentoScrolled = ref(false);
|
const reglamentoScrolled = ref(false);
|
||||||
const reglamentoFetchAttempted = ref(false);
|
const reglamentoFetchAttempted = ref(false);
|
||||||
|
|
||||||
|
const turnstileToken = ref("");
|
||||||
|
const turnstileContainer = ref(null);
|
||||||
|
const honeypot = ref("");
|
||||||
|
|
||||||
|
const turnstileReady = ref(false);
|
||||||
|
|
||||||
const totalSteps = computed(() => config.value?.steps?.length ?? 0);
|
const totalSteps = computed(() => config.value?.steps?.length ?? 0);
|
||||||
const currentStepData = computed(() => config.value?.steps?.[currentStep.value] ?? null);
|
const currentStepData = computed(() => config.value?.steps?.[currentStep.value] ?? null);
|
||||||
|
|
||||||
|
|
@ -32,6 +39,8 @@ const isFirstStep = computed(() => currentStep.value === 0);
|
||||||
const isLastStep = computed(() => currentStep.value === totalSteps.value - 1);
|
const isLastStep = computed(() => currentStep.value === totalSteps.value - 1);
|
||||||
|
|
||||||
const stepErrors = reactive({});
|
const stepErrors = reactive({});
|
||||||
|
const formatErrors = reactive({});
|
||||||
|
const phoneParts = reactive({});
|
||||||
|
|
||||||
const fetchConfig = async () => {
|
const fetchConfig = async () => {
|
||||||
isSuccess.value = false;
|
isSuccess.value = false;
|
||||||
|
|
@ -44,6 +53,9 @@ const fetchConfig = async () => {
|
||||||
step.fields.forEach((field) => {
|
step.fields.forEach((field) => {
|
||||||
if (field.type === "checkbox") {
|
if (field.type === "checkbox") {
|
||||||
formData[field.key] = [];
|
formData[field.key] = [];
|
||||||
|
} else if (field.type === "phone_country") {
|
||||||
|
formData[field.key] = "";
|
||||||
|
phoneParts[field.key] = { code: "", number: "" };
|
||||||
} else {
|
} else {
|
||||||
formData[field.key] = "";
|
formData[field.key] = "";
|
||||||
}
|
}
|
||||||
|
|
@ -54,7 +66,37 @@ const fetchConfig = async () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(fetchConfig);
|
const initTurnstile = () => {
|
||||||
|
if (!props.turnstileSiteKey || !turnstileContainer.value) return;
|
||||||
|
if (window.turnstile) {
|
||||||
|
window.turnstile.render(turnstileContainer.value, {
|
||||||
|
sitekey: props.turnstileSiteKey,
|
||||||
|
callback: (token) => { turnstileToken.value = token; },
|
||||||
|
});
|
||||||
|
turnstileReady.value = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchConfig();
|
||||||
|
if (props.turnstileSiteKey) {
|
||||||
|
if (document.querySelector('script[src*="turnstile"]')) {
|
||||||
|
const check = setInterval(() => {
|
||||||
|
if (window.turnstile) {
|
||||||
|
clearInterval(check);
|
||||||
|
initTurnstile();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
} else {
|
||||||
|
const script = document.createElement("script");
|
||||||
|
script.src = "https://challenges.cloudflare.com/turnstile/v0/api.js";
|
||||||
|
script.async = true;
|
||||||
|
script.defer = true;
|
||||||
|
script.onload = initTurnstile;
|
||||||
|
document.head.appendChild(script);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const validateStep = (stepIndex) => {
|
const validateStep = (stepIndex) => {
|
||||||
const step = config.value?.steps?.[stepIndex];
|
const step = config.value?.steps?.[stepIndex];
|
||||||
|
|
@ -111,6 +153,37 @@ const validateStep = (stepIndex) => {
|
||||||
stepErrors[field.key] = false;
|
stepErrors[field.key] = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (field.type === "phone" && val && val.trim() !== "") {
|
||||||
|
const phoneRegex = /^\d+$/;
|
||||||
|
if (!phoneRegex.test(val.trim())) {
|
||||||
|
formatErrors[field.key] = true;
|
||||||
|
stepErrors[field.key] = true;
|
||||||
|
valid = false;
|
||||||
|
} else {
|
||||||
|
formatErrors[field.key] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (field.type === "phone_country") {
|
||||||
|
const parts = phoneParts[field.key];
|
||||||
|
if (field.required && (!parts || !parts.code || !parts.number)) {
|
||||||
|
stepErrors[field.key] = true;
|
||||||
|
valid = false;
|
||||||
|
} else if (parts && parts.number) {
|
||||||
|
const full = parts.code + parts.number;
|
||||||
|
if (full.length > 15) {
|
||||||
|
formatErrors[field.key] = true;
|
||||||
|
stepErrors[field.key] = true;
|
||||||
|
valid = false;
|
||||||
|
} else if (parts.number.length < 7) {
|
||||||
|
formatErrors[field.key] = true;
|
||||||
|
stepErrors[field.key] = true;
|
||||||
|
valid = false;
|
||||||
|
} else {
|
||||||
|
stepErrors[field.key] = false;
|
||||||
|
formatErrors[field.key] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (field.type === "checkbox" && field.levels && Array.isArray(val)) {
|
if (field.type === "checkbox" && field.levels && Array.isArray(val)) {
|
||||||
val.forEach((optVal) => {
|
val.forEach((optVal) => {
|
||||||
if (!formData[`${field.key}_nivel_${optVal}`]) {
|
if (!formData[`${field.key}_nivel_${optVal}`]) {
|
||||||
|
|
@ -186,7 +259,7 @@ const buildPayload = () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return { formData: { ...formData }, responses };
|
return { formData: { ...formData }, responses, turnstileToken: turnstileToken.value, honeypot: honeypot.value };
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
|
|
@ -225,6 +298,29 @@ const handleSubmit = async () => {
|
||||||
|
|
||||||
const getFieldError = (key) => stepErrors[key] || false;
|
const getFieldError = (key) => stepErrors[key] || false;
|
||||||
|
|
||||||
|
const onPhoneInput = (key, event) => {
|
||||||
|
const cleaned = event.target.value.replace(/\D/g, "");
|
||||||
|
formData[key] = cleaned;
|
||||||
|
stepErrors[key] = false;
|
||||||
|
formatErrors[key] = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPhoneCountryCodeChange = (key, code) => {
|
||||||
|
if (!phoneParts[key]) return;
|
||||||
|
phoneParts[key].code = code;
|
||||||
|
formData[key] = code + phoneParts[key].number;
|
||||||
|
stepErrors[key] = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPhoneCountryNumberInput = (key, event) => {
|
||||||
|
if (!phoneParts[key]) return;
|
||||||
|
const cleaned = event.target.value.replace(/\D/g, "");
|
||||||
|
event.target.value = cleaned;
|
||||||
|
phoneParts[key].number = cleaned;
|
||||||
|
formData[key] = phoneParts[key].code + cleaned;
|
||||||
|
stepErrors[key] = false;
|
||||||
|
};
|
||||||
|
|
||||||
const suggestionsCache = ref({});
|
const suggestionsCache = ref({});
|
||||||
const showSuggestions = reactive({});
|
const showSuggestions = reactive({});
|
||||||
|
|
||||||
|
|
@ -448,7 +544,7 @@ const getFieldInputType = (field) => {
|
||||||
:type="getFieldInputType(field)"
|
:type="getFieldInputType(field)"
|
||||||
v-model="formData[field.key]"
|
v-model="formData[field.key]"
|
||||||
:placeholder="tl(field.placeholder || field.label)"
|
:placeholder="tl(field.placeholder || field.label)"
|
||||||
@input="stepErrors[field.key] = false"
|
@input="field.type === 'phone' ? onPhoneInput(field.key, $event) : (stepErrors[field.key] = false)"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
|
@ -485,6 +581,32 @@ const getFieldInputType = (field) => {
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="field.type === 'phone_country'" class="relative">
|
||||||
|
<div class="join w-full">
|
||||||
|
<select
|
||||||
|
class="select join-item bg-white border-gray-200 w-[8.5rem] rounded-l-xl text-sm focus:outline-2 focus:outline-[#22523F] grow text-[#1a1a1a]"
|
||||||
|
:class="{ 'border-red-500': getFieldError(field.key) }"
|
||||||
|
@change="onPhoneCountryCodeChange(field.key, $event.target.value)"
|
||||||
|
>
|
||||||
|
<option value="" selected disabled>{{ tl("form.phone_code_select") }}</option>
|
||||||
|
<option v-for="item in getSelectOptions(field)" :key="item.value" :value="item.dial_code">
|
||||||
|
{{ item.flag }} {{ item.dial_code }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<label
|
||||||
|
class="input join-item rounded-r-xl grow bg-white border-gray-200 focus-within:outline-2 focus-within:outline-[#22523F]"
|
||||||
|
:class="{ 'border-red-500': getFieldError(field.key) }"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
class="grow text-[#1a1a1a]"
|
||||||
|
type="tel"
|
||||||
|
:placeholder="tl(field.placeholder || field.label)"
|
||||||
|
@input="onPhoneCountryNumberInput(field.key, $event)"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-else-if="field.type === 'autocomplete'" class="relative">
|
<div v-else-if="field.type === 'autocomplete'" class="relative">
|
||||||
<label
|
<label
|
||||||
class="input w-full bg-white rounded-xl focus-within:outline-2 focus-within:outline-[#22523F]"
|
class="input w-full bg-white rounded-xl focus-within:outline-2 focus-within:outline-[#22523F]"
|
||||||
|
|
@ -540,7 +662,7 @@ const getFieldInputType = (field) => {
|
||||||
:name="field.key"
|
:name="field.key"
|
||||||
:value="opt.value"
|
:value="opt.value"
|
||||||
v-model="formData[field.key]"
|
v-model="formData[field.key]"
|
||||||
class="radio radio-sm border-gray-300 checked:border-[#4A8C6F] checked:bg-[#4A8C6F]"
|
class="radio radio-sm border-gray-300 bg-white checked:border-[#4A8C6F] checked:bg-[#4A8C6F]"
|
||||||
@change="stepErrors[field.key] = false"
|
@change="stepErrors[field.key] = false"
|
||||||
/>
|
/>
|
||||||
<span class="text-[#1a1a1a] text-sm">{{ tl(opt.label) }}</span>
|
<span class="text-[#1a1a1a] text-sm">{{ tl(opt.label) }}</span>
|
||||||
|
|
@ -561,7 +683,7 @@ const getFieldInputType = (field) => {
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:value="opt.value"
|
:value="opt.value"
|
||||||
:disabled="currentStep === 0 && !reglamentoScrolled"
|
:disabled="currentStep === 0 && !reglamentoScrolled"
|
||||||
class="checkbox checkbox-sm border-gray-300 checked:border-[#4A8C6F] checked:bg-[#4A8C6F]"
|
class="checkbox checkbox-sm border-gray-300 bg-white checked:border-[#4A8C6F] checked:bg-[#4A8C6F]"
|
||||||
:checked="formData[field.key]?.includes(opt.value)"
|
:checked="formData[field.key]?.includes(opt.value)"
|
||||||
@change="handleCheckboxChange(field.key, opt.value, $event.target.checked)"
|
@change="handleCheckboxChange(field.key, opt.value, $event.target.checked)"
|
||||||
/>
|
/>
|
||||||
|
|
@ -581,7 +703,7 @@ const getFieldInputType = (field) => {
|
||||||
:name="`${field.key}_nivel_${opt.value}`"
|
:name="`${field.key}_nivel_${opt.value}`"
|
||||||
:value="lvl.value"
|
:value="lvl.value"
|
||||||
v-model="formData[`${field.key}_nivel_${opt.value}`]"
|
v-model="formData[`${field.key}_nivel_${opt.value}`]"
|
||||||
class="radio radio-sm border-gray-300 checked:border-[#4A8C6F] checked:bg-[#4A8C6F]"
|
class="radio radio-sm border-gray-300 bg-white checked:border-[#4A8C6F] checked:bg-[#4A8C6F]"
|
||||||
/>
|
/>
|
||||||
<span class="text-[#1a1a1a]">{{ tl(lvl.label) }}</span>
|
<span class="text-[#1a1a1a]">{{ tl(lvl.label) }}</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -600,7 +722,7 @@ const getFieldInputType = (field) => {
|
||||||
class="text-xs text-red-500 flex items-center gap-1 mt-0.5"
|
class="text-xs text-red-500 flex items-center gap-1 mt-0.5"
|
||||||
>
|
>
|
||||||
<Icon icon="ph:warning-circle" class="text-sm shrink-0" />
|
<Icon icon="ph:warning-circle" class="text-sm shrink-0" />
|
||||||
{{ tl("form.required") }}
|
{{ formatErrors[field.key] ? (field.type === "phone_country" ? tl("form.phone_number_invalid") : tl("form.phone_invalid")) : tl("form.required") }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -610,6 +732,19 @@ const getFieldInputType = (field) => {
|
||||||
{{ errorMsg }}
|
{{ errorMsg }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isLastStep && props.turnstileSiteKey" class="flex justify-center my-4">
|
||||||
|
<div ref="turnstileContainer"></div>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
v-if="props.turnstileSiteKey"
|
||||||
|
type="text"
|
||||||
|
name="website"
|
||||||
|
v-model="honeypot"
|
||||||
|
class="hidden"
|
||||||
|
autocomplete="off"
|
||||||
|
tabindex="-1"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="border-t border-gray-300/30 mt-6 sm:mt-8 mb-4 sm:mb-6"></div>
|
<div class="border-t border-gray-300/30 mt-6 sm:mt-8 mb-4 sm:mb-6"></div>
|
||||||
|
|
||||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-3">
|
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-3">
|
||||||
|
|
|
||||||
|
|
@ -252,6 +252,7 @@
|
||||||
"form.idioma.ing": "Inglés",
|
"form.idioma.ing": "Inglés",
|
||||||
"form.idioma.heb": "Hebreo",
|
"form.idioma.heb": "Hebreo",
|
||||||
"form.idioma.port": "Portugués",
|
"form.idioma.port": "Portugués",
|
||||||
|
"form.idioma.franc": "Francés",
|
||||||
"form.idioma.otro": "Otro",
|
"form.idioma.otro": "Otro",
|
||||||
"form.idioma_otro": "Otro (especificar)",
|
"form.idioma_otro": "Otro (especificar)",
|
||||||
"form.voluntariado_anterior": "¿Ha realizado voluntariado anteriormente?",
|
"form.voluntariado_anterior": "¿Ha realizado voluntariado anteriormente?",
|
||||||
|
|
@ -322,6 +323,9 @@
|
||||||
"form.success": "¡Enviado con éxito!",
|
"form.success": "¡Enviado con éxito!",
|
||||||
"form.error": "Error al enviar",
|
"form.error": "Error al enviar",
|
||||||
"form.required": "Este campo es obligatorio",
|
"form.required": "Este campo es obligatorio",
|
||||||
|
"form.phone_invalid": "Solo se permiten números",
|
||||||
|
"form.phone_number_invalid": "Número inválido (mín. 7 dígitos, máx. 15)",
|
||||||
|
"form.phone_code_select": "Código",
|
||||||
"form.config_error": "Error al cargar el formulario. Intentá de nuevo más tarde.",
|
"form.config_error": "Error al cargar el formulario. Intentá de nuevo más tarde.",
|
||||||
"form.si": "Sí",
|
"form.si": "Sí",
|
||||||
"form.no": "No",
|
"form.no": "No",
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ if (routeKey === "formulario") {
|
||||||
<h1 class="text-3xl lg:text-4xl font-bold font-secondary text-[#EBE6D2] text-center mb-4 md:mb-9">
|
<h1 class="text-3xl lg:text-4xl font-bold font-secondary text-[#EBE6D2] text-center mb-4 md:mb-9">
|
||||||
{tl("form.page_title")}
|
{tl("form.page_title")}
|
||||||
</h1>
|
</h1>
|
||||||
<DynamicForm client:load locale={Astro.currentLocale} />
|
<DynamicForm client:load locale={Astro.currentLocale} turnstileSiteKey={import.meta.env.TURNSTILE_SITE_KEY} />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,34 @@ export const POST: APIRoute = async ({ request }) => {
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const formData = body.formData || {};
|
const formData = body.formData || {};
|
||||||
|
|
||||||
|
const honeypot = body.honeypot || "";
|
||||||
|
if (honeypot) {
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, message: "Solicitud rechazada" },
|
||||||
|
{ status: 403 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const turnstileToken = body.turnstileToken || "";
|
||||||
|
const turnstileSecret = import.meta.env.TURNSTILE_SECRET_KEY;
|
||||||
|
if (turnstileSecret && turnstileToken) {
|
||||||
|
const verify = await fetch(
|
||||||
|
"https://challenges.cloudflare.com/turnstile/v0/siteverify",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||||
|
body: `secret=${turnstileSecret}&response=${turnstileToken}`,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const outcome = await verify.json();
|
||||||
|
if (!outcome.success) {
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, message: "Captcha inválido" },
|
||||||
|
{ status: 403 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const timestamp = new Date().toLocaleString("es-ES", {
|
const timestamp = new Date().toLocaleString("es-ES", {
|
||||||
timeZone: "America/Puerto_Rico",
|
timeZone: "America/Puerto_Rico",
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue