Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

135 implement recaptcha in forms #140

Merged
merged 10 commits into from
Feb 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ SENTRY_ORG=prototyp-vm
SENTRY_PROJECT=taco
PUBLIC_SITE_NAME=TACO
PUBLIC_SITE_URL=http://localhost:5173
PUBLIC_RECAPTCHA_SITE_KEY=
RECAPTCHA_SECRET_KEY=
7 changes: 5 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,17 @@ jobs:
--health-retries 5

env:
POSTMARK_API_KEY: SECRET
POSTMARK_API_KEY: ${{ secrets.POSTMARK_API_KEY }}
PUBLIC_SITE_NAME: TACO [TEST]
EMAIL_SENDER_SIGNATURE: [email protected]
PUBLIC_SENTRY_DSN: SECRET
PUBLIC_SENTRY_DSN: ${{ secrets.PUBLIC_SENTRY_DSN }}
PUBLIC_SENTRY_ENV: test
PUBLIC_SITE_URL: http://localhost:5173/
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/db
NODE_ENV: test
PUBLIC_RECAPTCHA_SITE_KEY: ${{ secrets.PUBLIC_RECAPTCHA_SITE_KEY }}
RECAPTCHA_SECRET_KEY: ${{ secrets.RECAPTCHA_SECRET_KEY }}


steps:
- name: Checkout repository
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 32 additions & 0 deletions src/lib/utils/recaptcha.client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { PUBLIC_RECAPTCHA_SITE_KEY } from '$env/static/public'

export interface ReCaptcha {
ready(callback: () => void): void

execute(siteKey: string, options: { action: string }): Promise<string>
}

export const executeRecaptcha = async (grecaptcha: ReCaptcha): Promise<string> => {
return new Promise<string>((resolve, reject) => {
if (!grecaptcha) {
reject(new Error('reCAPTCHA is not available'))
return
}
grecaptcha.ready(() => {
try {
grecaptcha
.execute(PUBLIC_RECAPTCHA_SITE_KEY, { action: 'submit' })
.then((token: string) => {
resolve(token)
})
.catch((error: Error) => {
console.error('reCAPTCHA error:', error)
reject(error)
})
} catch (error) {
console.error('reCAPTCHA error:', error)
reject(error)
}
})
})
}
16 changes: 16 additions & 0 deletions src/lib/utils/recaptcha.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { RECAPTCHA_SECRET_KEY } from '$env/static/private'

export async function verifyRecaptcha(response: string) {
const recaptchaResponse = await fetch('https://www.google.com/recaptcha/api/siteverify', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `secret=${RECAPTCHA_SECRET_KEY}&response=${response}`,
})

const result = await recaptchaResponse.json()
if (!result.success || result.score <= 0.5) {
throw new Error(`Recaptcha verification failed. result: ${JSON.stringify(result, null, 2)}`)
}
}
4 changes: 4 additions & 0 deletions src/routes/forgot-password/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { generateSecureRandomToken } from '$lib/server/utils/crypto'
import { fail } from '@sveltejs/kit'
import { z, ZodError } from 'zod'
import type { Actions } from './$types'
import { verifyRecaptcha } from '$lib/utils/recaptcha.server'

export const actions: Actions = {
default: async ({ request, url }) => {
Expand All @@ -12,9 +13,12 @@ export const actions: Actions = {
const schema = z
.object({
email: z.string().email().toLowerCase(),
recaptchaToken: z.string().min(1),
})
.parse(fields)

await verifyRecaptcha(schema.recaptchaToken)

let user = await getUserByEmail(schema.email)

if (!user) {
Expand Down
13 changes: 11 additions & 2 deletions src/routes/forgot-password/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@
import Input from '$lib/components/Input.svelte'
import TacoIcon from '$lib/components/icons/TacoIcon.svelte'
import type { ActionData } from './$types'
import { PUBLIC_RECAPTCHA_SITE_KEY } from '$env/static/public'
import { executeRecaptcha } from '$lib/utils/recaptcha.client'

export let form: ActionData
</script>

<svelte:head>
<title>Forgot Password</title>
<script
src={`https://www.google.com/recaptcha/api.js?render=${PUBLIC_RECAPTCHA_SITE_KEY}`}
></script>
</svelte:head>

<div
Expand Down Expand Up @@ -37,8 +42,12 @@
class="space-y-6 mt-4"
method="POST"
novalidate
use:enhance={() => {
return ({ update }) => update({ reset: false }) // workaround for this known issue: @link: https://github.com/sveltejs/kit/issues/8513#issuecomment-1382500465
use:enhance={async ({ formData }) => {
const recaptchaToken = await executeRecaptcha(window.grecaptcha)
formData.append('recaptchaToken', recaptchaToken)
return async ({ update }) => {
return update({ reset: false })
}
}}
>
<Input
Expand Down
3 changes: 3 additions & 0 deletions src/routes/signin/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
doesCredentialsMatch,
getUserByEmail,
} from '$lib/server/entities/user'
import { verifyRecaptcha } from '$lib/utils/recaptcha.server'
import { fail, redirect } from '@sveltejs/kit'
import { z, ZodError } from 'zod'
import type { Actions } from './$types'
Expand All @@ -16,13 +17,15 @@ export const actions: Actions = {
email: z.string().email(),
password: z.string().min(1),
remember: z.preprocess((value) => value === 'on', z.boolean()),
recaptchaToken: z.string().min(1),
})
.refine(async (data) => doesCredentialsMatch(data.email, data.password), {
message: 'Wrong credentials',
path: ['password'],
})
.parseAsync(fields)

await verifyRecaptcha(schema.recaptchaToken)
const user = await getUserByEmail(schema.email)

if (!user) {
Expand Down
14 changes: 12 additions & 2 deletions src/routes/signin/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@
import Alert from '$lib/components/Alert.svelte'
import Input from '$lib/components/Input.svelte'
import TacoIcon from '$lib/components/icons/TacoIcon.svelte'
import { executeRecaptcha } from '$lib/utils/recaptcha.client'
import type { ActionData } from './$types'
import { PUBLIC_RECAPTCHA_SITE_KEY } from '$env/static/public'

export let form: ActionData
</script>

<svelte:head>
<title>Sign in</title>
<script
src={`https://www.google.com/recaptcha/api.js?render=${PUBLIC_RECAPTCHA_SITE_KEY}`}
></script>
</svelte:head>

<div
Expand Down Expand Up @@ -37,8 +42,12 @@
class="space-y-6 mt-4"
method="POST"
novalidate
use:enhance={() => {
return ({ update }) => update({ reset: false }) // workaround for this known issue: @link: https://github.com/sveltejs/kit/issues/8513#issuecomment-1382500465
use:enhance={async ({ formData }) => {
const recaptchaToken = await executeRecaptcha(window.grecaptcha)
formData.append('recaptchaToken', recaptchaToken)
return async ({ update }) => {
return update({ reset: false })
}
}}
>
<Input
Expand All @@ -55,6 +64,7 @@
errors={form?.errors?.password}
label="Password"
id="password"
l78fv
name="password"
type="password"
autocomplete="current-password"
Expand Down
18 changes: 12 additions & 6 deletions src/routes/signup/+page.server.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,42 @@
import { sendVerifyUserEmail } from '$lib/email/mailer'
import { createUser, createUserSessionAndCookie } from '$lib/server/entities/user'
import type { Actions } from './$types'
import { z, ZodError } from 'zod'
import { fail, redirect } from '@sveltejs/kit'
import { verifyRecaptcha } from '$lib/utils/recaptcha.server'
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library'
import { fail, redirect } from '@sveltejs/kit'
import { z, ZodError } from 'zod'
import type { Actions } from './$types'

export const actions: Actions = {
default: async ({ request, cookies, url }) => {
const fields = Object.fromEntries(await request.formData())

try {
const schema = z
.object({
name: z.string().min(1),
email: z.string().email().toLowerCase(),
password: z.string().min(6),
confirmPassword: z.string().min(6),
recaptchaToken: z.string().min(1),
})
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ['confirmPassword'],
})
.parse(fields)

await verifyRecaptcha(schema.recaptchaToken)

const user = await createUser(schema.name, schema.email, schema.password)

await createUserSessionAndCookie(user.id, cookies)

if (user.password?.verificationToken) {
await sendVerifyUserEmail(user, url.origin, user.password.verificationToken)
} else {
throw new Error('User verification token not found')
return fail(500, {
fields,
error: 'User verification token not found',
})
}
} catch (error) {
if (error instanceof ZodError) {
Expand All @@ -47,7 +54,6 @@ export const actions: Actions = {
},
})
}

return fail(500, {
fields,
error: `${error}`,
Expand Down
17 changes: 13 additions & 4 deletions src/routes/signup/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
<script lang="ts">
import { enhance } from '$app/forms'
import Alert from '$lib/components/Alert.svelte'
import Input from '$lib/components/Input.svelte'
import TacoIcon from '$lib/components/icons/TacoIcon.svelte'
import Input from '$lib/components/Input.svelte'
import { executeRecaptcha } from '$lib/utils/recaptcha.client'
import type { ActionData } from './$types'
import { PUBLIC_RECAPTCHA_SITE_KEY } from '$env/static/public'

export let form: ActionData
</script>

<svelte:head>
<title>Signup</title>
<script
src={`https://www.google.com/recaptcha/api.js?render=${PUBLIC_RECAPTCHA_SITE_KEY}`}
></script>
</svelte:head>

<div
Expand All @@ -33,11 +38,16 @@
title={form?.error || form?.success}
/>
<form
id="signup"
class="space-y-6"
method="POST"
novalidate
use:enhance={() => {
return ({ update }) => update({ reset: false }) // workaround for this known issue: @link: https://github.com/sveltejs/kit/issues/8513#issuecomment-1382500465
use:enhance={async ({ formData }) => {
const recaptchaToken = await executeRecaptcha(window.grecaptcha)
formData.append('recaptchaToken', recaptchaToken)
return async ({ update }) => {
return update({ reset: false })
}
}}
>
<Input
Expand Down Expand Up @@ -74,7 +84,6 @@
name="confirmPassword"
type="password"
/>

<div>
<button
type="submit"
Expand Down
Loading