staging #55
7
.env
7
.env
|
|
@ -22,3 +22,10 @@ DATABASE_URL="file:./dev.db"
|
|||
|
||||
N8N_WEBHOOK_URL="https://flows2.carpa.com/webhook/news-summary"
|
||||
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
|
||||
cd ${{ env.TARGET_DIR }}
|
||||
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 start ecosystem.config.cjs --env ${{ env.PM2_ENV }} --update-env
|
||||
|
|
|
|||
|
|
@ -5,10 +5,14 @@ module.exports = {
|
|||
env: {
|
||||
NODE_ENV: "staging",
|
||||
PORT: 3310,
|
||||
TURNSTILE_SITE_KEY: "",
|
||||
TURNSTILE_SECRET_KEY: "",
|
||||
},
|
||||
env_production: {
|
||||
NODE_ENV: "production",
|
||||
PORT: 3310,
|
||||
TURNSTILE_SITE_KEY: "",
|
||||
TURNSTILE_SECRET_KEY: "",
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
|
|
|||
|
|
@ -40,8 +40,7 @@
|
|||
"required": true,
|
||||
"options": [
|
||||
{ "value": "M", "label": "form.sexo.m" },
|
||||
{ "value": "F", "label": "form.sexo.f" },
|
||||
{ "value": "Otro", "label": "form.sexo.o" }
|
||||
{ "value": "F", "label": "form.sexo.f" }
|
||||
]
|
||||
},
|
||||
{ "key": "direccion_completa", "label": "form.direccion", "type": "text", "required": true, "colspan": 2 },
|
||||
|
|
@ -49,8 +48,8 @@
|
|||
{ "key": "estado", "label": "form.estado", "type": "text" },
|
||||
{ "key": "pais", "label": "form.pais", "type": "select", "required": true, "source": "/forms/paises.json" },
|
||||
{ "key": "postal", "label": "form.codigo_postal", "type": "text", "required": true },
|
||||
{ "key": "telefono", "label": "form.mobile", "type": "text", "required": true },
|
||||
{ "key": "whatsapp", "label": "form.whatsapp", "type": "text" }
|
||||
{ "key": "telefono", "label": "form.mobile", "type": "phone", "required": true },
|
||||
{ "key": "whatsapp", "label": "form.whatsapp", "type": "phone_country", "source": "/forms/paises.json" }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
@ -78,6 +77,7 @@
|
|||
{ "value": "Ingles", "label": "form.idioma.ing" },
|
||||
{ "value": "Hebreo", "label": "form.idioma.heb" },
|
||||
{ "value": "Portugues", "label": "form.idioma.port" },
|
||||
{ "value": "Frances", "label": "form.idioma.franc" },
|
||||
{ "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({
|
||||
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);
|
||||
|
|
@ -25,6 +26,12 @@ const reglamentoLoading = ref(false);
|
|||
const reglamentoScrolled = 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 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 stepErrors = reactive({});
|
||||
const formatErrors = reactive({});
|
||||
const phoneParts = reactive({});
|
||||
|
||||
const fetchConfig = async () => {
|
||||
isSuccess.value = false;
|
||||
|
|
@ -44,6 +53,9 @@ const fetchConfig = async () => {
|
|||
step.fields.forEach((field) => {
|
||||
if (field.type === "checkbox") {
|
||||
formData[field.key] = [];
|
||||
} else if (field.type === "phone_country") {
|
||||
formData[field.key] = "";
|
||||
phoneParts[field.key] = { code: "", number: "", flag: "" };
|
||||
} else {
|
||||
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 step = config.value?.steps?.[stepIndex];
|
||||
|
|
@ -111,6 +153,37 @@ const validateStep = (stepIndex) => {
|
|||
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)) {
|
||||
val.forEach((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 () => {
|
||||
|
|
@ -225,8 +298,72 @@ const handleSubmit = async () => {
|
|||
|
||||
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 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 getPhoneCountryDisplay = (key) => {
|
||||
if (phoneCountrySearchText[key] !== undefined && phoneCountrySearchText[key] !== null) {
|
||||
return phoneCountrySearchText[key];
|
||||
}
|
||||
if (!phoneParts[key] || !phoneParts[key].code) return "";
|
||||
return phoneParts[key].flag
|
||||
? `${phoneParts[key].code}`
|
||||
: phoneParts[key].code;
|
||||
};
|
||||
|
||||
const filteredPhoneCountries = (field, key) => {
|
||||
const text = (phoneCountrySearchText[key] || "").toLowerCase().trim();
|
||||
const list = suggestionsCache.value[field.source] || [];
|
||||
if (!text) return list;
|
||||
return list.filter(
|
||||
item => item.label.toLowerCase().includes(text) || item.dial_code.includes(text)
|
||||
);
|
||||
};
|
||||
|
||||
const onPhoneCountrySearchFocus = (field, key) => {
|
||||
loadSuggestions(field.source);
|
||||
showCountrySuggestions[key] = true;
|
||||
};
|
||||
|
||||
const onPhoneCountrySearchBlur = (key) => {
|
||||
setTimeout(() => {
|
||||
showCountrySuggestions[key] = false;
|
||||
phoneCountrySearchText[key] = undefined;
|
||||
}, 200);
|
||||
};
|
||||
|
||||
const onPhoneCountrySearchInput = (key, event) => {
|
||||
phoneCountrySearchText[key] = event.target.value;
|
||||
showCountrySuggestions[key] = true;
|
||||
};
|
||||
|
||||
const onPhoneCountrySelect = (key, item) => {
|
||||
phoneParts[key].code = item.dial_code;
|
||||
phoneParts[key].flag = item.flag;
|
||||
phoneParts[key].number = phoneParts[key].number || "";
|
||||
formData[key] = item.dial_code + phoneParts[key].number;
|
||||
phoneCountrySearchText[key] = undefined;
|
||||
showCountrySuggestions[key] = false;
|
||||
stepErrors[key] = false;
|
||||
};
|
||||
|
||||
const suggestionsCache = ref({});
|
||||
const showSuggestions = reactive({});
|
||||
const phoneCountrySearchText = reactive({});
|
||||
const showCountrySuggestions = reactive({});
|
||||
|
||||
const loadSuggestions = async (source) => {
|
||||
if (suggestionsCache.value[source]) return;
|
||||
|
|
@ -448,7 +585,7 @@ const getFieldInputType = (field) => {
|
|||
:type="getFieldInputType(field)"
|
||||
v-model="formData[field.key]"
|
||||
:placeholder="tl(field.placeholder || field.label)"
|
||||
@input="stepErrors[field.key] = false"
|
||||
@input="field.type === 'phone' ? onPhoneInput(field.key, $event) : (stepErrors[field.key] = false)"
|
||||
/>
|
||||
</label>
|
||||
|
||||
|
|
@ -485,6 +622,53 @@ const getFieldInputType = (field) => {
|
|||
</label>
|
||||
</div>
|
||||
|
||||
<div v-else-if="field.type === 'phone_country'" class="relative">
|
||||
<div class="join w-full">
|
||||
<div class="join-item relative w-28">
|
||||
<label
|
||||
class="input border-gray-200 rounded-l-xl bg-white w-full focus-within:outline-2 focus-within:outline-[#22523F]"
|
||||
:class="{ 'border-red-500': getFieldError(field.key) }"
|
||||
>
|
||||
<input
|
||||
class="w-full text-[#1a1a1a]"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
:value="getPhoneCountryDisplay(field.key)"
|
||||
:placeholder="tl('form.phone_code_select')"
|
||||
@input="onPhoneCountrySearchInput(field.key, $event)"
|
||||
@focus="onPhoneCountrySearchFocus(field, field.key)"
|
||||
@blur="onPhoneCountrySearchBlur(field.key)"
|
||||
/>
|
||||
</label>
|
||||
<ul
|
||||
v-if="showCountrySuggestions[field.key] && filteredPhoneCountries(field, field.key).length"
|
||||
class="absolute left-0 right-0 z-50 bg-white shadow-lg rounded-b-xl border border-gray-200 border-t-0 max-h-[200px] overflow-y-auto"
|
||||
>
|
||||
<li
|
||||
v-for="item in filteredPhoneCountries(field, field.key)"
|
||||
:key="item.value"
|
||||
class="px-3 py-2 cursor-pointer text-sm text-[#1a1a1a] hover:bg-[#22523F]/10 transition-colors flex items-center gap-2"
|
||||
@mousedown.prevent="onPhoneCountrySelect(field.key, item)"
|
||||
>
|
||||
<span>{{ item.flag }}</span>
|
||||
<span class="font-mono">{{ item.dial_code }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<label
|
||||
class="input join-item grow bg-white rounded-r-xl 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">
|
||||
<label
|
||||
class="input w-full bg-white rounded-xl focus-within:outline-2 focus-within:outline-[#22523F]"
|
||||
|
|
@ -540,7 +724,7 @@ const getFieldInputType = (field) => {
|
|||
:name="field.key"
|
||||
:value="opt.value"
|
||||
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"
|
||||
/>
|
||||
<span class="text-[#1a1a1a] text-sm">{{ tl(opt.label) }}</span>
|
||||
|
|
@ -561,7 +745,7 @@ const getFieldInputType = (field) => {
|
|||
type="checkbox"
|
||||
:value="opt.value"
|
||||
: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)"
|
||||
@change="handleCheckboxChange(field.key, opt.value, $event.target.checked)"
|
||||
/>
|
||||
|
|
@ -581,7 +765,7 @@ const getFieldInputType = (field) => {
|
|||
:name="`${field.key}_nivel_${opt.value}`"
|
||||
:value="lvl.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>
|
||||
</label>
|
||||
|
|
@ -600,7 +784,7 @@ const getFieldInputType = (field) => {
|
|||
class="text-xs text-red-500 flex items-center gap-1 mt-0.5"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -610,6 +794,19 @@ const getFieldInputType = (field) => {
|
|||
{{ errorMsg }}
|
||||
</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="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.heb": "Hebreo",
|
||||
"form.idioma.port": "Portugués",
|
||||
"form.idioma.franc": "Francés",
|
||||
"form.idioma.otro": "Otro",
|
||||
"form.idioma_otro": "Otro (especificar)",
|
||||
"form.voluntariado_anterior": "¿Ha realizado voluntariado anteriormente?",
|
||||
|
|
@ -322,6 +323,9 @@
|
|||
"form.success": "¡Enviado con éxito!",
|
||||
"form.error": "Error al enviar",
|
||||
"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.si": "Sí",
|
||||
"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">
|
||||
{tl("form.page_title")}
|
||||
</h1>
|
||||
<DynamicForm client:load locale={Astro.currentLocale} />
|
||||
<DynamicForm client:load locale={Astro.currentLocale} turnstileSiteKey={import.meta.env.TURNSTILE_SITE_KEY} />
|
||||
</div>
|
||||
</main>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { APIRoute } from "astro";
|
||||
import { appendRegistrationToSheet } from "../lib/googleSheets";
|
||||
import { sendEmail } from "../lib/email";
|
||||
export const prerender = false;
|
||||
|
||||
const COLUMNS = [
|
||||
|
|
@ -66,6 +67,34 @@ export const POST: APIRoute = async ({ request }) => {
|
|||
const body = await request.json();
|
||||
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", {
|
||||
timeZone: "America/Puerto_Rico",
|
||||
});
|
||||
|
|
@ -75,8 +104,14 @@ export const POST: APIRoute = async ({ request }) => {
|
|||
|
||||
const result = await appendRegistrationToSheet(row);
|
||||
|
||||
await sendEmail({
|
||||
nombres: formData.nombre,
|
||||
apellidos: formData.apellido,
|
||||
}, formData.correo, "CENTRO_DEL_REINO_PAZ_Y_JUSTICIA_POSTULACION");
|
||||
|
||||
return Response.json(result);
|
||||
} catch (error) {
|
||||
console.error("Error en /api/formulario/send:", error);
|
||||
return Response.json(
|
||||
{
|
||||
success: false,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
type TypeEmailData = "CENTRO_DEL_REINO_PAZ_Y_JUSTICIA_INFO" | "CENTRO_DEL_REINO_PAZ_Y_JUSTICIA_POSTULACION";
|
||||
|
||||
export async function sendEmail(data: object, to: string, type: TypeEmailData) {
|
||||
const emailApiKey = import.meta.env.EMAIL_API_KEY;
|
||||
|
||||
const dataFinal = {
|
||||
...data,
|
||||
email: to,
|
||||
senderName: "Centro del Reino de Paz y Justicia",
|
||||
};
|
||||
|
||||
const emailResponse = await fetch("https://flows2.carpa.com/webhook/email/send", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": emailApiKey!,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
type: type,
|
||||
data: dataFinal,
|
||||
}),
|
||||
});
|
||||
|
||||
const responseBody = await emailResponse.text();
|
||||
|
||||
if (!emailResponse.ok) {
|
||||
console.error(`Error enviando correo (status ${emailResponse.status}): ${responseBody}`);
|
||||
throw new Error(`Error en servicio de correo: ${emailResponse.status}`);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue