From cbc149f9f8a27bb6fadb19d4cf0bfbe447fe32de Mon Sep 17 00:00:00 2001 From: root Date: Sat, 17 Oct 2026 17:53:02 +0000 Subject: [PATCH] =?UTF-8?q?Kontaktformular:=20Anfragen=20=C3=BCber=20die?= =?UTF-8?q?=20Kontakt-API,=20damit=20WEBklar=20per=20Mail=20davon=20erf?= =?UTF-8?q?=C3=A4hrt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bisher schrieb das Formular als Gast direkt in Appwrite, und niemand las die Einträge. Jetzt geht es an POST /api/contact (webklar-webclaw-contact-api): die API speichert in Appwrite und meldet jede Anfrage sofort an support@webklar.com, Antworten gehen an die Absenderadresse. - src/lib/contact.ts ersetzt src/lib/appwrite.ts, kein Appwrite-SDK mehr im Bundle - Honeypot-Feld "website" (unsichtbar, nicht per Tab erreichbar) - maxLength passend zu den Grenzen der API (120/200/200/4000) - eigener Hinweis bei 429 (zu viele Anfragen) Co-Authored-By: Claude Opus 5.5 --- src/lib/appwrite.ts | 52 --------------- src/lib/contact.ts | 40 ++++++++++++ src/pages/Contact.tsx | 34 ++++++++-- src/test/kontakt-api.test.tsx | 101 ++++++++++++++++++++++++++++++ src/test/relaunch-layout.test.tsx | 5 +- src/test/sofort-paket.test.tsx | 12 ++-- 6 files changed, 181 insertions(+), 63 deletions(-) delete mode 100644 src/lib/appwrite.ts create mode 100644 src/lib/contact.ts create mode 100644 src/test/kontakt-api.test.tsx diff --git a/src/lib/appwrite.ts b/src/lib/appwrite.ts deleted file mode 100644 index 8e97be9..0000000 --- a/src/lib/appwrite.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Appwrite-Anbindung für das Kontaktformular. - * - * In der Appwrite Console anlegen: - * 1. Database (z. B. ID: "contacts") - * 2. Collection (z. B. ID: "messages") mit String-Attributen: name, email, company, message - * 3. Unter "Settings" der Collection: Create-Berechtigung für "Any" aktivieren (öffentliches Formular) - * 4. IDs in .env setzen: VITE_APPWRITE_DATABASE_ID, VITE_APPWRITE_CONTACT_COLLECTION_ID - */ -import { Client, Databases, ID } from "appwrite"; - -const CONTACT_DATABASE_ID = import.meta.env.VITE_APPWRITE_DATABASE_ID ?? "698124a20035e8f6dc42"; -const CONTACT_COLLECTION_ID = import.meta.env.VITE_APPWRITE_CONTACT_COLLECTION_ID ?? "contact_submissions"; - -function getDatabases(): Databases { - const endpoint = import.meta.env.VITE_APPWRITE_ENDPOINT; - const projectId = import.meta.env.VITE_APPWRITE_PROJECT_ID; - if (!endpoint || !projectId) { - throw new Error( - "Appwrite ist nicht konfiguriert. Bitte VITE_APPWRITE_ENDPOINT und VITE_APPWRITE_PROJECT_ID setzen. " + - "Lokal: .env anlegen (z. B. mit „npm run setup:env“) und Build/Dev-Server neu starten. " + - "Server: gleiche Variablen in der Build-Umgebung setzen (z. B. in .env vor „npm run build“)." - ); - } - const client = new Client().setEndpoint(endpoint).setProject(projectId); - return new Databases(client); -} - -let _databases: Databases | null = null; - -export type ContactFormData = { - name: string; - email: string; - company: string; - message: string; -}; - -export async function createContactDocument(data: ContactFormData) { - if (!_databases) _databases = getDatabases(); - return _databases.createDocument( - CONTACT_DATABASE_ID, - CONTACT_COLLECTION_ID, - ID.unique(), - { - name: data.name, - email: data.email, - company: data.company, - message: data.message, - }, - [] // Keine Dokument-Permissions; nur Collection-Berechtigung „Create für Any“ wird genutzt - ); -} diff --git a/src/lib/contact.ts b/src/lib/contact.ts new file mode 100644 index 0000000..9b54e19 --- /dev/null +++ b/src/lib/contact.ts @@ -0,0 +1,40 @@ +/** + * Kontaktformular: schickt die Anfrage an die Kontakt-API von webklar.com (POST /api/contact). + * + * Die API speichert die Anfrage in Appwrite (Collection contact_submissions) und meldet sie sofort + * per Mail an support@webklar.com, Antworten gehen direkt an die Absenderadresse. Sie liegt auf dem + * Server unter apps/webklar.com/contact-api/server.js (nicht in diesem Repo). + * + * `website` ist das Honeypot-Feld: Menschen sehen es nicht, Bots füllen es aus. + */ +export type ContactFormData = { + name: string; + email: string; + company: string; + message: string; + website: string; +}; + +/** Feldgrenzen der Kontakt-API (liegen unter den Attributgrößen der Appwrite-Collection). */ +export const CONTACT_LIMITS = { name: 120, email: 200, company: 200, message: 4000 } as const; + +export class ContactSendError extends Error { + constructor(readonly status: number) { + super(`Kontakt-API antwortete mit ${status}`); + this.name = "ContactSendError"; + } + + /** 429: zu viele Anfragen in kurzer Zeit. */ + get tooMany() { + return this.status === 429; + } +} + +export async function sendContactMessage(data: ContactFormData): Promise { + const res = await fetch("/api/contact", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + }); + if (!res.ok) throw new ContactSendError(res.status); +} diff --git a/src/pages/Contact.tsx b/src/pages/Contact.tsx index dfc0918..9ae43bf 100644 --- a/src/pages/Contact.tsx +++ b/src/pages/Contact.tsx @@ -1,5 +1,6 @@ import { useRef, useState } from "react"; import { usePageMeta } from "@/hooks/use-page-meta"; +import { CONTACT_LIMITS, ContactSendError, sendContactMessage } from "@/lib/contact"; import "./unterseiten.css"; type Rueckmeldung = { art: "erfolg" | "fehler"; titel: string; text: string }; @@ -18,6 +19,7 @@ const Contact = () => { email: "", company: "", message: "", + website: "", }); const handleChange = ( @@ -35,20 +37,21 @@ const Contact = () => { setIsSubmitting(true); setRueckmeldung(null); try { - // Das Appwrite-SDK wird erst beim Absenden geladen (Spezifikation 9.7) - const { createContactDocument } = await import("@/lib/appwrite"); - await createContactDocument(formData); + await sendContactMessage(formData); setRueckmeldung({ art: "erfolg", titel: "Nachricht gesendet.", text: "Wir melden uns spätestens am nächsten Werktag bei Ihnen.", }); - setFormData({ name: "", email: "", company: "", message: "" }); - } catch { + setFormData({ name: "", email: "", company: "", message: "", website: "" }); + } catch (fehler) { setRueckmeldung({ art: "fehler", titel: "Die Nachricht konnte nicht gesendet werden.", - text: "Bitte versuchen Sie es noch einmal oder schreiben Sie uns direkt per E-Mail.", + text: + fehler instanceof ContactSendError && fehler.tooMany + ? "Es kamen gerade zu viele Anfragen. Bitte versuchen Sie es in ein paar Minuten noch einmal oder schreiben Sie uns direkt per E-Mail." + : "Bitte versuchen Sie es noch einmal oder schreiben Sie uns direkt per E-Mail.", }); } finally { setIsSubmitting(false); @@ -83,6 +86,7 @@ const Contact = () => { type="text" required autoComplete="name" + maxLength={CONTACT_LIMITS.name} value={formData.name} onChange={handleChange} placeholder="Ihr Name" @@ -96,6 +100,7 @@ const Contact = () => { type="email" required autoComplete="email" + maxLength={CONTACT_LIMITS.email} value={formData.email} onChange={handleChange} placeholder="ihre@email.de" @@ -112,6 +117,7 @@ const Contact = () => { name="company" type="text" autoComplete="organization" + maxLength={CONTACT_LIMITS.company} value={formData.company} onChange={handleChange} placeholder="Name Ihres Betriebs" @@ -124,12 +130,28 @@ const Contact = () => { id="message" name="message" required + maxLength={CONTACT_LIMITS.message} value={formData.message} onChange={handleChange} placeholder="z. B. neue Website, Online-Shop, Web-App oder Automatisierung …" /> + {/* Honeypot gegen Bots: für Menschen unsichtbar und nicht erreichbar, die Kontakt-API verwirft + Anfragen, in denen es ausgefüllt ist */} + +