Skip to content

Commit 00a9a84

Browse files
authored
feat: add captcha (#53)
1 parent 51911c6 commit 00a9a84

7 files changed

Lines changed: 74 additions & 3 deletions

File tree

.github/workflows/ci.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ env:
1919
PUBLIC_VAPID_PUBLIC_KEY: ${{ secrets.PUBLIC_VAPID_PUBLIC_KEY }}
2020
VAPID_PRIVATE_KEY: ${{ secrets.VAPID_PRIVATE_KEY }}
2121
VAPID_SUBJECT: "mailto:web@programmerbar.no"
22+
PUBLIC_CLOUDFLARE_TURNSTILE_SITE_KEY: 1x00000000000000000000AA
23+
CLOUDFLARE_TURNSTILE_SITE_SECRET: 1x0000000000000000000000000000000AA
2224

2325
jobs:
2426
ci:

pnpm-lock.yaml

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

programmerbar-web/.env.example

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,14 @@ PUBLIC_GITHUB_SHA="development"
5757
#
5858
# Enable Sentry error tracking and performance monitoring
5959
# --------------------------------------------------------
60-
SENTRY_AUTH_TOKEN=
60+
SENTRY_AUTH_TOKEN=
61+
62+
# --------------------------------------------------------
63+
# Cloudflare Turnstile
64+
#
65+
# Site key and secret key for Cloudflare Turnstile CAPTCHA
66+
# Values for testing can be found at:
67+
# https://developers.cloudflare.com/turnstile/troubleshooting/testing/
68+
# --------------------------------------------------------
69+
PUBLIC_CLOUDFLARE_TURNSTILE_SITE_KEY=1x00000000000000000000AA
70+
CLOUDFLARE_TURNSTILE_SITE_SECRET=1x0000000000000000000000000000000AA

programmerbar-web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
"react-dom": "19.2.3",
8585
"resend": "6.8.0",
8686
"svelte-sonner": "1.0.7",
87+
"svelte-turnstile": "^0.11.0",
8788
"tailwind-merge": "3.4.0",
8889
"web-push": "^3.6.7",
8990
"zod": "4.3.6",

programmerbar-web/src/lib/components/app/landing/ContactForm.svelte

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import { toast } from 'svelte-sonner';
33
import { createContactSubmissionAction } from '../../../../routes/(app)/common.remote';
44
import CLIWindow from '$lib/components/app/CLIWindow.svelte';
5+
import { Turnstile } from 'svelte-turnstile';
6+
import { env } from '$env/dynamic/public';
57
</script>
68

79
<CLIWindow title="nano kontakt.txt" class="h-full">
@@ -53,6 +55,8 @@
5355
></textarea>
5456
</label>
5557

58+
<Turnstile siteKey={env.PUBLIC_CLOUDFLARE_TURNSTILE_SITE_KEY!} class="my-2" />
59+
5660
<button
5761
type="submit"
5862
class="border-border bg-card-muted hover:bg-card-hover hover:border-primary focus:border-primary text-foreground-primary w-full border-2 px-4 py-2 text-center font-mono text-sm font-semibold transition-all focus:ring-0 focus:outline-none"
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { env } from '$env/dynamic/private';
2+
3+
export const validateTurnstile = async (
4+
token: FormDataEntryValue | null,
5+
remoteip: string
6+
): Promise<{ success: boolean; 'error-codes'?: string[] }> => {
7+
if (!token) {
8+
return { success: false, 'error-codes': ['missing-input-response'] };
9+
}
10+
11+
const formData = new FormData();
12+
formData.append('secret', env.CLOUDFLARE_TURNSTILE_SITE_SECRET!);
13+
formData.append('response', token);
14+
formData.append('remoteip', remoteip);
15+
16+
try {
17+
const response = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
18+
method: 'POST',
19+
body: formData
20+
});
21+
22+
const result = (await response.json()) as { success: boolean; 'error-codes'?: string[] };
23+
return result;
24+
} catch (error) {
25+
console.error('Turnstile validation error:', error);
26+
return { success: false, 'error-codes': ['internal-error'] };
27+
}
28+
};

programmerbar-web/src/routes/(app)/common.remote.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { form, getRequestEvent, query } from '$app/server';
2+
import { validateTurnstile } from '$lib/server/turnstile';
23
import { fail } from '@sveltejs/kit';
34
import z from 'zod';
45

@@ -45,12 +46,13 @@ const ContanctSubmissionSchema = z.object({
4546
// Actual fields
4647
namekjkj: z.string().min(1, 'Name is required'),
4748
emailkjkj: z.email('Invalid email address'),
48-
messagekjkj: z.string().min(1, 'Message is required')
49+
messagekjkj: z.string().min(1, 'Message is required'),
50+
'cf-turnstile-response': z.string().min(1, 'CAPTCHA verification is required')
4951
});
5052

5153
export const createContactSubmissionAction = form(
5254
ContanctSubmissionSchema,
53-
async ({ name, email, namekjkj, emailkjkj, messagekjkj }) => {
55+
async ({ name, email, namekjkj, emailkjkj, messagekjkj, 'cf-turnstile-response': token }) => {
5456
const event = getRequestEvent();
5557
const { locals, getClientAddress } = event;
5658
const ip = getClientAddress();
@@ -61,6 +63,12 @@ export const createContactSubmissionAction = form(
6163
return fail(400, { success: false });
6264
}
6365

66+
const validation = await validateTurnstile(token, ip);
67+
if (!validation.success) {
68+
console.log(`[ContactForm] 🚫 Turnstile validation failed from IP: ${ip}`);
69+
return fail(400, { success: false, error: 'CAPTCHA verification failed' });
70+
}
71+
6472
// Check for spam in message content
6573
if (isSpamMessage(messagekjkj)) {
6674
console.log(`[ContactForm] 🚫 Spam detected from IP: ${ip}`);

0 commit comments

Comments
 (0)