Closed itaibaruch closed 3 months ago
I've been trying to do this as well. The problem i'm having is getting the token on the client and sending it to an action on the server.
I tried using onSubmit with an async..await to get the ReCaptcha token on the client, add it to formData and submit but that didn't work.
I went outside use:enhance with a function call via on:submit|preventDefault={onFormSubmit} but it gets executed too late for the server action to get the token to query Google.
I need the server action to hold off until the client side function call has completed. I thought simple enough, but proving tricky with Superforms in the mix.
Had this working quite shortly after my last post as below, getting into the swing of Superforms, it's a very nice package :)
I say yes to 'browser error' from Google, not a good idea but hey, it's generally just a guide.
I added localhost to the approved domains on the Google side for development.
+page.svelte
const { form, errors, enhance } = superForm(data.form, {
onSubmit(cancel) {
onFormSubmit(cancel)
},
})
/** @param {{ currentTarget: EventTarget & HTMLFormElement}} event */
const onFormSubmit = async ({ cancel }) => {
const formStatus = await superValidate($form, zod(lostPasswordSchema))
if (!formStatus.valid) {
cancel()
return fail(400, {
formStatus,
})
} else {
try {
state = State.requesting
await window.grecaptcha.ready(async function () {
const token = await window.grecaptcha
.execute(PUBLIC_RECAPTCHA_SITE_KEY, {
action: 'submit',
})
.then(async function (t) {
$form.recaptchaToken = t
const response = await fetch('/api/lostPassword', {
method: 'POST',
body: JSON.stringify($form),
headers: {
'content-type': 'application/json',
},
})
/** @type {import('@sveltejs/kit').ActionResult} */
const result = deserialize(await response.text())
if (result.type == 'success') {
// Do stuff if its good
} else {
toastStore.trigger(errEmail)
}
return result
})
})
} catch (error) {
console.log(`ERROR: ${error}`)
toastStore.trigger(errEmail)
}
}
}
<form method="POST" class="mt-8 space-y-8" use:enhance>
/api/lostpassword/+server.ts
import { actionResult } from 'sveltekit-superforms';
import { SECRET_RECAPTCHA_KEY } from '$env/static/private';
/**
* This function is used to verify the reCAPTCHA token received from the client.
* It sends a POST request to Google's reCAPTCHA API and checks the response.
*
* @async
* @param {Object} event - The event object containing the client's request.
* @param {Object} event.request - The client's request.
* @param {Function} event.getClientAddress - Function to get the client's IP address.
*
* @returns {Promise<Object>} Returns an object with the result of the operation.
* If the reCAPTCHA token is successfully verified, it returns an object with a success message.
* If the verification fails, it returns an object with an error message.
*
* @throws {Error} If the hostname is 'localhost' and the environment is not development,
* an error is thrown indicating that the operation is not permitted.
*/
export async function POST(event) {
const clientIp = event.getClientAddress();
const data = await event.request.json()
const isDev = process.env.NODE_ENV === 'development';
/* Google verify recaptcha */
const postData = new URLSearchParams();
postData.append('secret', SECRET_RECAPTCHA_KEY);
postData.append('response', data.recaptchaToken);
postData.append('remoteip', clientIp);
// Make the request
const response = await fetch('https://www.google.com/recaptcha/api/siteverify', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: postData
});
const captchaData = await response.json();
if (captchaData.hostname === 'localhost' && !isDev) {
return actionResult('failure', { error: 'Operation not permitted for localhost in production' });
}
else if (captchaData.success && captchaData.score > 0.6 && captchaData.action === 'submit') {
return actionResult('success', { text: `Good score returned from Google` });
} else if (captchaData["error-codes"][0] == "browser-error") {
return actionResult('success', { text: `Google returned browser-error` });
} else {
return actionResult('failure', { error: `Failed to verify the token` });
}
}
I solve it the following way:
$lib/helpers/recaptcha.ts
import { PUBLIC_GOOGLE_RECAPTCHA_SITE_KEY } from '$env/static/public';
export async function validateReCaptchaServer(
token: string,
fetch: typeof window.fetch,
secret: string
) {
const res = await fetch('https://www.google.com/recaptcha/api/siteverify', {
method: 'POST',
headers: {
'content-type': 'application/x-www-form-urlencoded',
},
body: `secret=${secret}&response=${token}`,
});
const json = await res.json();
return json;
}
export async function createReCaptchaClient(
formToken: string | undefined,
grecaptcha: ReCaptchaV2.ReCaptcha
) {
return new Promise((resolve) => {
if (formToken) {
resolve(formToken);
} else {
return grecaptcha.ready(function () {
grecaptcha.execute(PUBLIC_GOOGLE_RECAPTCHA_SITE_KEY, { action: 'submit' }).then(function (
token: string
) {
resolve(token);
});
});
}
});
}
+page.svelte
<script lang="ts">
import { createReCaptchaClient } from '$lib/helpers/recaptcha';
const { form, errors, constraints, enhance, message } = superForm(data.contactForm, {
// Reset the form upon a successful result
resetForm: true,
onSubmit: async ({ formData }) => {
const token = await createReCaptchaClient($form.token, window.grecaptcha);
formData.append('token', String(token));
},
});
</script>
<svelte:head>
<script
src="https://www.google.com/recaptcha/api.js?render={PUBLIC_GOOGLE_RECAPTCHA_SITE_KEY}"
async
defer
></script>
</svelte:head>
<form method="POST" action="?/contact" use:enhance>
.....form
+page.server.ts
import { validateReCaptchaServer } from '$src/lib/helpers/recaptcha';
export const actions: Actions = {
contact: async (event) => {
const { request, fetch } = event;
const form = await superValidate(request, contactSchema);
if (!form.valid) {
return fail(400, { form });
}
// reCAPTCHA
const gToken = form.data.token;
if (!gToken) {
return message(form, 'Invalid reCAPTCHA', { status: 400 });
}
const res = await validateReCaptchaServer(gToken, fetch, GOOGLE_RECAPTCHA_SECRET_KEY);
if (!res.success) {
return message(form, 'Failed ReCaptcha', { status: 400 });
}
....
Hope it helps
Hi team,
Thank you for a great package.
I have tried to integrate ReCaptcha with your package, but I don't know what is the best way to implement it. can you provide example on the site or in the repo.
Thanks.