sveltekit-superforms
sveltekit-superforms copied to clipboard
Integrate ReCaptcha v3 / v2 with SvelteKit Superforms
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.
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