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)/"
|
@COPYFILE_DISABLE=1 tar czf - $(TAR_EXCLUDE) . | ssh $(DS_HOST) "tar xzf - -C $(DS_PATH)/"
|
||||||
@echo "→ .env auf DS aktualisieren..."
|
@echo "→ .env auf DS aktualisieren..."
|
||||||
@if [ -f .env ]; then \
|
@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
|
fi
|
||||||
@echo "→ PocketBase Hooks synchronisieren..."
|
@echo "→ PocketBase Hooks synchronisieren..."
|
||||||
@for f in $(HOOKS_SRC)/*.pb.js; do \
|
@for f in $(HOOKS_SRC)/*.pb.js; do \
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-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">
|
<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/svg+xml" href="/favicon.svg">
|
||||||
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png">
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png">
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
import { pb } from '$lib/pb';
|
import { pb } from '$lib/pb';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
|
const PUBLIC_TURNSTILE_SITE_KEY = '0x4AAAAAADRLILtAf9XMk-g0';
|
||||||
|
|
||||||
const DEMO_TENANT = 'mengbzc3ajxpccz';
|
const DEMO_TENANT = 'mengbzc3ajxpccz';
|
||||||
|
|
||||||
|
|
@ -56,14 +57,22 @@
|
||||||
if (!formName.trim() || !formEmail.trim()) { formError = 'Bitte Name und E-Mail ausfüllen.'; return; }
|
if (!formName.trim() || !formEmail.trim()) { formError = 'Bitte Name und E-Mail ausfüllen.'; return; }
|
||||||
formSending = true; formError = '';
|
formSending = true; formError = '';
|
||||||
try {
|
try {
|
||||||
await pb.collection('inquiries').create({
|
const token = (window as any).turnstile?.getResponse() ?? '';
|
||||||
name: formName.trim(),
|
const res = await fetch('/api/contact', {
|
||||||
company: formCompany.trim(),
|
method: 'POST',
|
||||||
email: formEmail.trim(),
|
headers: { 'Content-Type': 'application/json' },
|
||||||
phone: formPhone.trim(),
|
body: JSON.stringify({
|
||||||
message: formMsg.trim(),
|
token, name: formName.trim(), email: formEmail.trim(),
|
||||||
plan: modalPlan
|
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;
|
formDone = true;
|
||||||
formName = formCompany = formEmail = formPhone = formMsg = '';
|
formName = formCompany = formEmail = formPhone = formMsg = '';
|
||||||
} catch {
|
} catch {
|
||||||
|
|
@ -438,6 +447,7 @@
|
||||||
<label for="m-msg">Nachricht (optional)</label>
|
<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>
|
<textarea id="m-msg" rows="3" placeholder="Wie viele Kunden betreuen Sie? Welche Branche?" bind:value={formMsg}></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="cf-turnstile" data-sitekey={PUBLIC_TURNSTILE_SITE_KEY} data-theme="light"></div>
|
||||||
{#if formError}<p class="modal-error">{formError}</p>{/if}
|
{#if formError}<p class="modal-error">{formError}</p>{/if}
|
||||||
<button type="submit" class="btn-primary" disabled={formSending}>
|
<button type="submit" class="btn-primary" disabled={formSending}>
|
||||||
{formSending ? 'Wird gesendet…' : 'Anfrage senden'}
|
{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
|
- TZ=Europe/Berlin
|
||||||
- HOST=0.0.0.0
|
- HOST=0.0.0.0
|
||||||
- PORT=3000
|
- PORT=3000
|
||||||
|
- TURNSTILE_SECRET=${TURNSTILE_SECRET}
|
||||||
networks:
|
networks:
|
||||||
- default
|
- default
|
||||||
- npm_bridge
|
- npm_bridge
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue