Security: Cloudflare Turnstile für Kontaktformular, server-seitige Verifizierung
This commit is contained in:
parent
cbaac4b5a4
commit
cbbe6b9996
5 changed files with 64 additions and 8 deletions
2
Makefile
2
Makefile
|
|
@ -64,7 +64,7 @@ deploy: check-ssh
|
|||
@COPYFILE_DISABLE=1 tar czf - $(TAR_EXCLUDE) . | ssh $(DS_HOST) "tar xzf - -C $(DS_PATH)/"
|
||||
@echo "→ .env auf DS aktualisieren..."
|
||||
@if [ -f .env ]; then \
|
||||
grep BREVO_KEY .env | ssh $(DS_HOST) "cat > $(DS_PATH)/.env"; \
|
||||
grep -E "BREVO_KEY|TURNSTILE_SECRET" .env | ssh $(DS_HOST) "cat > $(DS_PATH)/.env"; \
|
||||
fi
|
||||
@echo "→ PocketBase Hooks synchronisieren..."
|
||||
@for f in $(HOOKS_SRC)/*.pb.js; do \
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png">
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import { pb } from '$lib/pb';
|
||||
import { onMount } from 'svelte';
|
||||
import { browser } from '$app/environment';
|
||||
const PUBLIC_TURNSTILE_SITE_KEY = '0x4AAAAAADRLILtAf9XMk-g0';
|
||||
|
||||
const DEMO_TENANT = 'mengbzc3ajxpccz';
|
||||
|
||||
|
|
@ -56,14 +57,22 @@
|
|||
if (!formName.trim() || !formEmail.trim()) { formError = 'Bitte Name und E-Mail ausfüllen.'; return; }
|
||||
formSending = true; formError = '';
|
||||
try {
|
||||
await pb.collection('inquiries').create({
|
||||
name: formName.trim(),
|
||||
company: formCompany.trim(),
|
||||
email: formEmail.trim(),
|
||||
phone: formPhone.trim(),
|
||||
message: formMsg.trim(),
|
||||
plan: modalPlan
|
||||
const token = (window as any).turnstile?.getResponse() ?? '';
|
||||
const res = await fetch('/api/contact', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
token, name: formName.trim(), email: formEmail.trim(),
|
||||
company: formCompany.trim(), phone: formPhone.trim(),
|
||||
message: formMsg.trim(), plan: modalPlan
|
||||
})
|
||||
});
|
||||
if (!res.ok) {
|
||||
const d = await res.json();
|
||||
formError = d.error ?? 'Fehler beim Senden.';
|
||||
(window as any).turnstile?.reset();
|
||||
return;
|
||||
}
|
||||
formDone = true;
|
||||
formName = formCompany = formEmail = formPhone = formMsg = '';
|
||||
} catch {
|
||||
|
|
@ -438,6 +447,7 @@
|
|||
<label for="m-msg">Nachricht (optional)</label>
|
||||
<textarea id="m-msg" rows="3" placeholder="Wie viele Kunden betreuen Sie? Welche Branche?" bind:value={formMsg}></textarea>
|
||||
</div>
|
||||
<div class="cf-turnstile" data-sitekey={PUBLIC_TURNSTILE_SITE_KEY} data-theme="light"></div>
|
||||
{#if formError}<p class="modal-error">{formError}</p>{/if}
|
||||
<button type="submit" class="btn-primary" disabled={formSending}>
|
||||
{formSending ? 'Wird gesendet…' : 'Anfrage senden'}
|
||||
|
|
|
|||
44
app/src/routes/api/contact/+server.ts
Normal file
44
app/src/routes/api/contact/+server.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { json } from '@sveltejs/kit';
|
||||
|
||||
export async function POST({ request }) {
|
||||
const body = await request.json();
|
||||
const { token, name, email, company, phone, message, plan } = body;
|
||||
|
||||
if (!name?.trim() || !email?.trim()) {
|
||||
return json({ error: 'Name und E-Mail sind Pflichtfelder.' }, { status: 400 });
|
||||
}
|
||||
|
||||
const secret = process.env.TURNSTILE_SECRET ?? '';
|
||||
|
||||
// Turnstile serverseitig verifizieren
|
||||
const verification = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ secret, response: token })
|
||||
});
|
||||
const result = await verification.json();
|
||||
|
||||
if (!result.success) {
|
||||
return json({ error: 'Captcha-Verifizierung fehlgeschlagen.' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Anfrage in PocketBase speichern
|
||||
const pb = await fetch('https://api.checkflo.de/api/collections/inquiries/records', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: name.trim(),
|
||||
email: email.trim(),
|
||||
company: company?.trim() ?? '',
|
||||
phone: phone?.trim() ?? '',
|
||||
message: message?.trim() ?? '',
|
||||
plan: plan ?? ''
|
||||
})
|
||||
});
|
||||
|
||||
if (!pb.ok) {
|
||||
return json({ error: 'Fehler beim Speichern.' }, { status: 500 });
|
||||
}
|
||||
|
||||
return json({ success: true });
|
||||
}
|
||||
|
|
@ -27,6 +27,7 @@ services:
|
|||
- TZ=Europe/Berlin
|
||||
- HOST=0.0.0.0
|
||||
- PORT=3000
|
||||
- TURNSTILE_SECRET=${TURNSTILE_SECRET}
|
||||
networks:
|
||||
- default
|
||||
- npm_bridge
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue