KI-AGENT: Kontaktformular und Datenschutzseite ergänzt
This commit is contained in:
9
website/.env.example
Normal file
9
website/.env.example
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
SMTP_HOST=smtp.example.com
|
||||||
|
SMTP_PORT=587
|
||||||
|
SMTP_SECURE=false
|
||||||
|
SMTP_USER=website@example.com
|
||||||
|
SMTP_PASSWORD=change-me
|
||||||
|
SMTP_FROM=website@example.com
|
||||||
|
SMTP_HELO=fedeo.de
|
||||||
|
SMTP_REJECT_UNAUTHORIZED=true
|
||||||
|
CONTACT_TO=f.federspiel@federspiel.tech
|
||||||
@@ -676,21 +676,25 @@ footer {
|
|||||||
padding: clamp(3rem, 7vw, 6rem) clamp(1rem, 4vw, 2rem) 5rem;
|
padding: clamp(3rem, 7vw, 6rem) clamp(1rem, 4vw, 2rem) 5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.contact-page,
|
||||||
.audience-page {
|
.audience-page {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
max-width: 1180px;
|
max-width: 1180px;
|
||||||
padding: clamp(3rem, 7vw, 6rem) clamp(1rem, 4vw, 2rem) 5rem;
|
padding: clamp(3rem, 7vw, 6rem) clamp(1rem, 4vw, 2rem) 5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.contact-heading,
|
||||||
.audience-heading {
|
.audience-heading {
|
||||||
max-width: 58rem;
|
max-width: 58rem;
|
||||||
margin-bottom: 2.5rem;
|
margin-bottom: 2.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.contact-heading h1,
|
||||||
.audience-heading h1 {
|
.audience-heading h1 {
|
||||||
max-width: 12ch;
|
max-width: 12ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.contact-heading p,
|
||||||
.audience-heading p {
|
.audience-heading p {
|
||||||
color: #51605c;
|
color: #51605c;
|
||||||
font-size: 1.08rem;
|
font-size: 1.08rem;
|
||||||
@@ -698,6 +702,103 @@ footer {
|
|||||||
max-width: 46rem;
|
max-width: 46rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.contact-form {
|
||||||
|
background: rgba(255, 255, 255, 0.76);
|
||||||
|
border: 1px solid rgba(23, 33, 31, 0.1);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
max-width: 820px;
|
||||||
|
padding: clamp(1.25rem, 4vw, 2rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-form label {
|
||||||
|
color: #17211f;
|
||||||
|
display: grid;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 800;
|
||||||
|
gap: 0.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-form input,
|
||||||
|
.contact-form textarea {
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid rgba(23, 33, 31, 0.14);
|
||||||
|
border-radius: 0.45rem;
|
||||||
|
color: #17211f;
|
||||||
|
font: inherit;
|
||||||
|
font-weight: 500;
|
||||||
|
min-height: 3rem;
|
||||||
|
padding: 0.8rem 0.9rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-form textarea {
|
||||||
|
line-height: 1.55;
|
||||||
|
min-height: 12rem;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-form input:focus,
|
||||||
|
.contact-form textarea:focus {
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(105, 195, 80, 0.18);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.honeypot {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-row {
|
||||||
|
align-items: start;
|
||||||
|
display: flex;
|
||||||
|
font-weight: 500;
|
||||||
|
gap: 0.7rem;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-row input {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
min-height: auto;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-row a,
|
||||||
|
.form-status.error {
|
||||||
|
color: var(--accent-dark);
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions button {
|
||||||
|
border: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions button:disabled {
|
||||||
|
cursor: wait;
|
||||||
|
opacity: 0.72;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-status {
|
||||||
|
color: #51605c;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.audience-grid {
|
.audience-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
@@ -771,6 +872,10 @@ footer {
|
|||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.privacy-grid article {
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
.legal-grid article {
|
.legal-grid article {
|
||||||
background: rgba(255, 255, 255, 0.76);
|
background: rgba(255, 255, 255, 0.76);
|
||||||
border: 1px solid rgba(23, 33, 31, 0.1);
|
border: 1px solid rgba(23, 33, 31, 0.1);
|
||||||
@@ -842,6 +947,7 @@ footer p {
|
|||||||
|
|
||||||
.highlights,
|
.highlights,
|
||||||
.feature-grid,
|
.feature-grid,
|
||||||
|
.form-grid,
|
||||||
.audience-grid,
|
.audience-grid,
|
||||||
.audience-fit,
|
.audience-fit,
|
||||||
.matrix-layout,
|
.matrix-layout,
|
||||||
@@ -886,6 +992,7 @@ footer p {
|
|||||||
font-size: 3rem;
|
font-size: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.contact-heading h1,
|
||||||
.audience-heading h1 {
|
.audience-heading h1 {
|
||||||
font-size: 2.55rem;
|
font-size: 2.55rem;
|
||||||
max-width: none;
|
max-width: none;
|
||||||
|
|||||||
105
website/app/pages/datenschutz.vue
Normal file
105
website/app/pages/datenschutz.vue
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
<template>
|
||||||
|
<main>
|
||||||
|
<header class="site-header">
|
||||||
|
<NuxtLink class="brand" to="/" aria-label="FEDEO Startseite">
|
||||||
|
<img src="/Logo.png" alt="FEDEO" />
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
|
<nav aria-label="Hauptnavigation">
|
||||||
|
<NuxtLink to="/zielgruppen">Zielgruppen</NuxtLink>
|
||||||
|
<NuxtLink to="/#funktionen">Funktionen</NuxtLink>
|
||||||
|
<NuxtLink to="/#open-source">Open Source</NuxtLink>
|
||||||
|
<NuxtLink to="/#selfhost">Selfhost</NuxtLink>
|
||||||
|
<NuxtLink to="/#matrix">Matrix</NuxtLink>
|
||||||
|
<NuxtLink to="/kontakt">Kontakt</NuxtLink>
|
||||||
|
<a class="login-link" href="https://app.fedeo.de">Einloggen</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="legal-page">
|
||||||
|
<div class="legal-heading">
|
||||||
|
<p class="eyebrow">Federspiel Technology</p>
|
||||||
|
<h1>Datenschutzerklärung</h1>
|
||||||
|
<p>Stand: 22. Mai 2026</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="legal-grid privacy-grid">
|
||||||
|
<article>
|
||||||
|
<h2>Verantwortlicher</h2>
|
||||||
|
<p>
|
||||||
|
Federspiel Technology UG (haftungsbeschränkt)<br>
|
||||||
|
Am Schwarzen Brack 14<br>
|
||||||
|
26452 Sande<br>
|
||||||
|
Deutschland
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
E-Mail: <a href="mailto:f.federspiel@federspiel.tech">f.federspiel@federspiel.tech</a>
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article>
|
||||||
|
<h2>Aufruf der Webseite</h2>
|
||||||
|
<p>
|
||||||
|
Beim Besuch dieser Webseite werden technisch notwendige Zugriffsdaten verarbeitet, damit die Seite ausgeliefert, gesichert und betrieben werden kann. Dazu können IP-Adresse, Datum, Uhrzeit, angeforderte Seite, Browserinformationen und Serverstatus gehören.
|
||||||
|
</p>
|
||||||
|
<p>Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO.</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article>
|
||||||
|
<h2>Kontaktformular</h2>
|
||||||
|
<p>
|
||||||
|
Wenn du das Kontaktformular nutzt, verarbeiten wir deinen Namen, deine E-Mail-Adresse, optional dein Unternehmen und deine Nachricht, um deine Anfrage zu beantworten.
|
||||||
|
</p>
|
||||||
|
<p>Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO, bei allgemeinen Anfragen zusätzlich Art. 6 Abs. 1 lit. f DSGVO.</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article>
|
||||||
|
<h2>E-Mail-Versand</h2>
|
||||||
|
<p>
|
||||||
|
Die Angaben aus dem Kontaktformular werden serverseitig per SMTP an die hinterlegte Kontaktadresse übermittelt. Die SMTP-Zugangsdaten werden ausschließlich als Umgebungsvariablen verarbeitet und nicht im Quellcode gespeichert.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article>
|
||||||
|
<h2>Speicherdauer</h2>
|
||||||
|
<p>
|
||||||
|
Kontaktanfragen werden nur so lange aufbewahrt, wie es für die Bearbeitung und gesetzliche Aufbewahrungspflichten erforderlich ist. Server-Logdaten werden nach Maßgabe der technischen und sicherheitsbezogenen Erfordernisse gelöscht.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article>
|
||||||
|
<h2>Empfänger</h2>
|
||||||
|
<p>
|
||||||
|
Eine Weitergabe personenbezogener Daten erfolgt nur, wenn sie für Betrieb, Sicherheit, Kommunikation oder gesetzliche Pflichten erforderlich ist.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article>
|
||||||
|
<h2>Deine Rechte</h2>
|
||||||
|
<p>
|
||||||
|
Du hast nach Maßgabe der DSGVO Rechte auf Auskunft, Berichtigung, Löschung, Einschränkung der Verarbeitung, Datenübertragbarkeit und Widerspruch. Außerdem kannst du dich bei einer Datenschutzaufsichtsbehörde beschweren.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article>
|
||||||
|
<h2>Pflichtangaben</h2>
|
||||||
|
<p>
|
||||||
|
Die Nutzung des Kontaktformulars ist freiwillig. Ohne die erforderlichen Angaben können wir deine Anfrage jedoch nicht beantworten.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<img src="/Logo.png" alt="FEDEO" />
|
||||||
|
<div>
|
||||||
|
<NuxtLink to="/zielgruppen">Zielgruppen</NuxtLink>
|
||||||
|
<NuxtLink to="/kontakt">Kontakt</NuxtLink>
|
||||||
|
<NuxtLink to="/impressum">Impressum</NuxtLink>
|
||||||
|
<NuxtLink to="/datenschutz">Datenschutz</NuxtLink>
|
||||||
|
<a href="https://app.fedeo.de">Einloggen</a>
|
||||||
|
</div>
|
||||||
|
<p>Copyright © 2026 Federspiel Technology UG haftungsbeschränkt.</p>
|
||||||
|
</footer>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
<NuxtLink to="/#open-source">Open Source</NuxtLink>
|
<NuxtLink to="/#open-source">Open Source</NuxtLink>
|
||||||
<NuxtLink to="/#selfhost">Selfhost</NuxtLink>
|
<NuxtLink to="/#selfhost">Selfhost</NuxtLink>
|
||||||
<NuxtLink to="/#matrix">Matrix</NuxtLink>
|
<NuxtLink to="/#matrix">Matrix</NuxtLink>
|
||||||
<NuxtLink to="/#kontakt">Kontakt</NuxtLink>
|
<NuxtLink to="/kontakt">Kontakt</NuxtLink>
|
||||||
<a class="login-link" href="https://app.fedeo.de">Einloggen</a>
|
<a class="login-link" href="https://app.fedeo.de">Einloggen</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -99,8 +99,9 @@
|
|||||||
<img src="/Logo.png" alt="FEDEO" />
|
<img src="/Logo.png" alt="FEDEO" />
|
||||||
<div>
|
<div>
|
||||||
<NuxtLink to="/zielgruppen">Zielgruppen</NuxtLink>
|
<NuxtLink to="/zielgruppen">Zielgruppen</NuxtLink>
|
||||||
|
<NuxtLink to="/kontakt">Kontakt</NuxtLink>
|
||||||
<NuxtLink to="/impressum">Impressum</NuxtLink>
|
<NuxtLink to="/impressum">Impressum</NuxtLink>
|
||||||
<a href="https://federspiel.tech/datenschutz">Datenschutz</a>
|
<NuxtLink to="/datenschutz">Datenschutz</NuxtLink>
|
||||||
<a href="https://app.fedeo.de">Einloggen</a>
|
<a href="https://app.fedeo.de">Einloggen</a>
|
||||||
</div>
|
</div>
|
||||||
<p>Copyright © 2026 Federspiel Technology UG haftungsbeschränkt.</p>
|
<p>Copyright © 2026 Federspiel Technology UG haftungsbeschränkt.</p>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<a href="#open-source">Open Source</a>
|
<a href="#open-source">Open Source</a>
|
||||||
<a href="#selfhost">Selfhost</a>
|
<a href="#selfhost">Selfhost</a>
|
||||||
<a href="#matrix">Matrix</a>
|
<a href="#matrix">Matrix</a>
|
||||||
<a href="#kontakt">Kontakt</a>
|
<NuxtLink to="/kontakt">Kontakt</NuxtLink>
|
||||||
<a class="login-link" href="https://app.fedeo.de">Einloggen</a>
|
<a class="login-link" href="https://app.fedeo.de">Einloggen</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
FEDEO bündelt Projekte, Zeiten, Buchhaltung, Kommunikation, Geräte und Warenflüsse in einer Oberfläche, die den Arbeitsalltag kleiner und mittlerer Teams spürbar ruhiger macht.
|
FEDEO bündelt Projekte, Zeiten, Buchhaltung, Kommunikation, Geräte und Warenflüsse in einer Oberfläche, die den Arbeitsalltag kleiner und mittlerer Teams spürbar ruhiger macht.
|
||||||
</p>
|
</p>
|
||||||
<div class="hero-actions">
|
<div class="hero-actions">
|
||||||
<a class="primary-action" href="#kontakt">Demo anfragen</a>
|
<NuxtLink class="primary-action" to="/kontakt">Demo anfragen</NuxtLink>
|
||||||
<a class="secondary-action" href="#funktionen">Funktionen ansehen</a>
|
<a class="secondary-action" href="#funktionen">Funktionen ansehen</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -215,15 +215,16 @@
|
|||||||
Frag eine Demo an und wir zeigen dir, wie FEDEO zu deinen Abläufen passt.
|
Frag eine Demo an und wir zeigen dir, wie FEDEO zu deinen Abläufen passt.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<a class="primary-action" href="https://fedeo.de/kontakt">Kontakt aufnehmen</a>
|
<NuxtLink class="primary-action" to="/kontakt">Kontakt aufnehmen</NuxtLink>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<img src="/Logo.png" alt="FEDEO" />
|
<img src="/Logo.png" alt="FEDEO" />
|
||||||
<div>
|
<div>
|
||||||
<NuxtLink to="/zielgruppen">Zielgruppen</NuxtLink>
|
<NuxtLink to="/zielgruppen">Zielgruppen</NuxtLink>
|
||||||
|
<NuxtLink to="/kontakt">Kontakt</NuxtLink>
|
||||||
<NuxtLink to="/impressum">Impressum</NuxtLink>
|
<NuxtLink to="/impressum">Impressum</NuxtLink>
|
||||||
<a href="https://federspiel.tech/datenschutz">Datenschutz</a>
|
<NuxtLink to="/datenschutz">Datenschutz</NuxtLink>
|
||||||
<a href="https://app.fedeo.de">Einloggen</a>
|
<a href="https://app.fedeo.de">Einloggen</a>
|
||||||
</div>
|
</div>
|
||||||
<p>Copyright © 2026 Federspiel Technology UG haftungsbeschränkt.</p>
|
<p>Copyright © 2026 Federspiel Technology UG haftungsbeschränkt.</p>
|
||||||
|
|||||||
124
website/app/pages/kontakt.vue
Normal file
124
website/app/pages/kontakt.vue
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
<template>
|
||||||
|
<main>
|
||||||
|
<header class="site-header">
|
||||||
|
<NuxtLink class="brand" to="/" aria-label="FEDEO Startseite">
|
||||||
|
<img src="/Logo.png" alt="FEDEO" />
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
|
<nav aria-label="Hauptnavigation">
|
||||||
|
<NuxtLink to="/zielgruppen">Zielgruppen</NuxtLink>
|
||||||
|
<NuxtLink to="/#funktionen">Funktionen</NuxtLink>
|
||||||
|
<NuxtLink to="/#open-source">Open Source</NuxtLink>
|
||||||
|
<NuxtLink to="/#selfhost">Selfhost</NuxtLink>
|
||||||
|
<NuxtLink to="/#matrix">Matrix</NuxtLink>
|
||||||
|
<NuxtLink to="/kontakt">Kontakt</NuxtLink>
|
||||||
|
<a class="login-link" href="https://app.fedeo.de">Einloggen</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="contact-page">
|
||||||
|
<div class="contact-heading">
|
||||||
|
<p class="eyebrow">Kontakt</p>
|
||||||
|
<h1>Sprich mit uns über FEDEO.</h1>
|
||||||
|
<p>
|
||||||
|
Ob Demo, Selfhosting, Integrationen oder ein konkreter Ablauf im Betrieb: Schreib uns kurz, worum es geht.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form class="contact-form" @submit.prevent="submitForm">
|
||||||
|
<div class="form-grid">
|
||||||
|
<label>
|
||||||
|
Name
|
||||||
|
<input v-model="form.name" name="name" autocomplete="name" required>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
E-Mail
|
||||||
|
<input v-model="form.email" name="email" autocomplete="email" required type="email">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Unternehmen
|
||||||
|
<input v-model="form.company" name="company" autocomplete="organization">
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="honeypot">
|
||||||
|
Website
|
||||||
|
<input v-model="form.website" name="website" tabindex="-1" autocomplete="off">
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Nachricht
|
||||||
|
<textarea v-model="form.message" name="message" required rows="8"></textarea>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="check-row">
|
||||||
|
<input v-model="form.privacy" type="checkbox" required>
|
||||||
|
<span>
|
||||||
|
Ich habe die <NuxtLink to="/datenschutz">Datenschutzerklärung</NuxtLink> gelesen und bin mit der Verarbeitung meiner Angaben zur Kontaktaufnahme einverstanden.
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="primary-action" :disabled="pending" type="submit">
|
||||||
|
{{ pending ? 'Wird gesendet' : 'Anfrage senden' }}
|
||||||
|
</button>
|
||||||
|
<p v-if="statusMessage" :class="['form-status', statusType]">{{ statusMessage }}</p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<img src="/Logo.png" alt="FEDEO" />
|
||||||
|
<div>
|
||||||
|
<NuxtLink to="/zielgruppen">Zielgruppen</NuxtLink>
|
||||||
|
<NuxtLink to="/kontakt">Kontakt</NuxtLink>
|
||||||
|
<NuxtLink to="/impressum">Impressum</NuxtLink>
|
||||||
|
<NuxtLink to="/datenschutz">Datenschutz</NuxtLink>
|
||||||
|
<a href="https://app.fedeo.de">Einloggen</a>
|
||||||
|
</div>
|
||||||
|
<p>Copyright © 2026 Federspiel Technology UG haftungsbeschränkt.</p>
|
||||||
|
</footer>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const form = reactive({
|
||||||
|
company: '',
|
||||||
|
email: '',
|
||||||
|
message: '',
|
||||||
|
name: '',
|
||||||
|
privacy: false,
|
||||||
|
website: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const pending = ref(false)
|
||||||
|
const statusMessage = ref('')
|
||||||
|
const statusType = ref<'error' | 'success'>('success')
|
||||||
|
|
||||||
|
const submitForm = async () => {
|
||||||
|
pending.value = true
|
||||||
|
statusMessage.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
await $fetch('/api/contact', {
|
||||||
|
body: form,
|
||||||
|
method: 'POST'
|
||||||
|
})
|
||||||
|
|
||||||
|
form.company = ''
|
||||||
|
form.email = ''
|
||||||
|
form.message = ''
|
||||||
|
form.name = ''
|
||||||
|
form.privacy = false
|
||||||
|
form.website = ''
|
||||||
|
statusType.value = 'success'
|
||||||
|
statusMessage.value = 'Danke, deine Nachricht wurde gesendet.'
|
||||||
|
} catch (error: any) {
|
||||||
|
statusType.value = 'error'
|
||||||
|
statusMessage.value = error?.statusMessage || 'Die Nachricht konnte gerade nicht gesendet werden.'
|
||||||
|
} finally {
|
||||||
|
pending.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
<NuxtLink to="/#open-source">Open Source</NuxtLink>
|
<NuxtLink to="/#open-source">Open Source</NuxtLink>
|
||||||
<NuxtLink to="/#selfhost">Selfhost</NuxtLink>
|
<NuxtLink to="/#selfhost">Selfhost</NuxtLink>
|
||||||
<NuxtLink to="/#matrix">Matrix</NuxtLink>
|
<NuxtLink to="/#matrix">Matrix</NuxtLink>
|
||||||
<NuxtLink to="/#kontakt">Kontakt</NuxtLink>
|
<NuxtLink to="/kontakt">Kontakt</NuxtLink>
|
||||||
<a class="login-link" href="https://app.fedeo.de">Einloggen</a>
|
<a class="login-link" href="https://app.fedeo.de">Einloggen</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -56,15 +56,16 @@
|
|||||||
<h2>Nicht sicher, ob FEDEO zu deinem Betrieb passt?</h2>
|
<h2>Nicht sicher, ob FEDEO zu deinem Betrieb passt?</h2>
|
||||||
<p>In einer Demo lässt sich schnell prüfen, welche Abläufe FEDEO direkt abdecken kann.</p>
|
<p>In einer Demo lässt sich schnell prüfen, welche Abläufe FEDEO direkt abdecken kann.</p>
|
||||||
</div>
|
</div>
|
||||||
<a class="primary-action" href="https://fedeo.de/kontakt">Demo anfragen</a>
|
<NuxtLink class="primary-action" to="/kontakt">Demo anfragen</NuxtLink>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<img src="/Logo.png" alt="FEDEO" />
|
<img src="/Logo.png" alt="FEDEO" />
|
||||||
<div>
|
<div>
|
||||||
<NuxtLink to="/zielgruppen">Zielgruppen</NuxtLink>
|
<NuxtLink to="/zielgruppen">Zielgruppen</NuxtLink>
|
||||||
|
<NuxtLink to="/kontakt">Kontakt</NuxtLink>
|
||||||
<NuxtLink to="/impressum">Impressum</NuxtLink>
|
<NuxtLink to="/impressum">Impressum</NuxtLink>
|
||||||
<a href="https://federspiel.tech/datenschutz">Datenschutz</a>
|
<NuxtLink to="/datenschutz">Datenschutz</NuxtLink>
|
||||||
<a href="https://app.fedeo.de">Einloggen</a>
|
<a href="https://app.fedeo.de">Einloggen</a>
|
||||||
</div>
|
</div>
|
||||||
<p>Copyright © 2026 Federspiel Technology UG haftungsbeschränkt.</p>
|
<p>Copyright © 2026 Federspiel Technology UG haftungsbeschränkt.</p>
|
||||||
|
|||||||
233
website/server/api/contact.post.ts
Normal file
233
website/server/api/contact.post.ts
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
import net from 'node:net'
|
||||||
|
import tls from 'node:tls'
|
||||||
|
import { Buffer } from 'node:buffer'
|
||||||
|
|
||||||
|
type ContactPayload = {
|
||||||
|
company?: string
|
||||||
|
email?: string
|
||||||
|
message?: string
|
||||||
|
name?: string
|
||||||
|
privacy?: boolean
|
||||||
|
website?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
class SmtpClient {
|
||||||
|
private buffer = ''
|
||||||
|
private socket: net.Socket | tls.TLSSocket
|
||||||
|
private waiters: Array<() => void> = []
|
||||||
|
|
||||||
|
constructor(socket: net.Socket | tls.TLSSocket) {
|
||||||
|
this.socket = socket
|
||||||
|
this.attach(socket)
|
||||||
|
}
|
||||||
|
|
||||||
|
private attach(socket: net.Socket | tls.TLSSocket) {
|
||||||
|
socket.setEncoding('utf8')
|
||||||
|
socket.on('data', (chunk) => {
|
||||||
|
this.buffer += chunk
|
||||||
|
this.waiters.splice(0).forEach((resolve) => resolve())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private waitForData() {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
const onError = (error: Error) => {
|
||||||
|
cleanup()
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
const onClose = () => {
|
||||||
|
cleanup()
|
||||||
|
reject(new Error('SMTP-Verbindung wurde geschlossen.'))
|
||||||
|
}
|
||||||
|
const cleanup = () => {
|
||||||
|
this.socket.off('error', onError)
|
||||||
|
this.socket.off('close', onClose)
|
||||||
|
}
|
||||||
|
this.socket.once('error', onError)
|
||||||
|
this.socket.once('close', onClose)
|
||||||
|
this.waiters.push(() => {
|
||||||
|
cleanup()
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private async readLine() {
|
||||||
|
while (!this.buffer.includes('\r\n')) {
|
||||||
|
await this.waitForData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = this.buffer.indexOf('\r\n')
|
||||||
|
const line = this.buffer.slice(0, index)
|
||||||
|
this.buffer = this.buffer.slice(index + 2)
|
||||||
|
return line
|
||||||
|
}
|
||||||
|
|
||||||
|
private async readResponse() {
|
||||||
|
const lines: string[] = []
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const line = await this.readLine()
|
||||||
|
lines.push(line)
|
||||||
|
|
||||||
|
if (/^\d{3} /.test(line)) {
|
||||||
|
const code = Number(line.slice(0, 3))
|
||||||
|
return { code, message: lines.join('\n') }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async connect(expected = 220) {
|
||||||
|
const response = await this.readResponse()
|
||||||
|
this.expect(response, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
async command(command: string, expected: number | number[]) {
|
||||||
|
this.socket.write(`${command}\r\n`)
|
||||||
|
const response = await this.readResponse()
|
||||||
|
this.expect(response, expected)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
async data(content: string) {
|
||||||
|
this.socket.write(`${content.replace(/^\./gm, '..')}\r\n.\r\n`)
|
||||||
|
const response = await this.readResponse()
|
||||||
|
this.expect(response, 250)
|
||||||
|
}
|
||||||
|
|
||||||
|
async startTls(host: string) {
|
||||||
|
this.socket = tls.connect({
|
||||||
|
rejectUnauthorized: process.env.SMTP_REJECT_UNAUTHORIZED !== 'false',
|
||||||
|
servername: host,
|
||||||
|
socket: this.socket
|
||||||
|
})
|
||||||
|
this.buffer = ''
|
||||||
|
this.attach(this.socket)
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
this.socket.once('secureConnect', resolve)
|
||||||
|
this.socket.once('error', reject)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.socket.end()
|
||||||
|
}
|
||||||
|
|
||||||
|
private expect(response: { code: number, message: string }, expected: number | number[]) {
|
||||||
|
const expectedCodes = Array.isArray(expected) ? expected : [expected]
|
||||||
|
|
||||||
|
if (!expectedCodes.includes(response.code)) {
|
||||||
|
throw new Error(`SMTP-Fehler ${response.code}: ${response.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanHeader = (value: string) => value.replace(/[\r\n]+/g, ' ').trim()
|
||||||
|
|
||||||
|
const isValidEmail = (value: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
|
||||||
|
|
||||||
|
const encodeHeader = (value: string) => {
|
||||||
|
const safeValue = cleanHeader(value)
|
||||||
|
return /^[\x20-\x7e]*$/.test(safeValue)
|
||||||
|
? safeValue
|
||||||
|
: `=?UTF-8?B?${Buffer.from(safeValue, 'utf8').toString('base64')}?=`
|
||||||
|
}
|
||||||
|
|
||||||
|
const createEmail = (payload: Required<Pick<ContactPayload, 'email' | 'message' | 'name'>> & Pick<ContactPayload, 'company'>, from: string, to: string) => {
|
||||||
|
const subject = `FEDEO Kontaktanfrage von ${payload.name}`
|
||||||
|
const lines = [
|
||||||
|
`Name: ${payload.name}`,
|
||||||
|
`E-Mail: ${payload.email}`,
|
||||||
|
`Unternehmen: ${payload.company || '-'}`,
|
||||||
|
'',
|
||||||
|
payload.message
|
||||||
|
]
|
||||||
|
|
||||||
|
return [
|
||||||
|
`From: ${encodeHeader('FEDEO Webseite')} <${cleanHeader(from)}>`,
|
||||||
|
`To: ${cleanHeader(to)}`,
|
||||||
|
`Reply-To: ${cleanHeader(payload.email)}`,
|
||||||
|
`Subject: ${encodeHeader(subject)}`,
|
||||||
|
'MIME-Version: 1.0',
|
||||||
|
'Content-Type: text/plain; charset=UTF-8',
|
||||||
|
'Content-Transfer-Encoding: 8bit',
|
||||||
|
'',
|
||||||
|
lines.join('\n')
|
||||||
|
].join('\r\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendMail = async (payload: Required<Pick<ContactPayload, 'email' | 'message' | 'name'>> & Pick<ContactPayload, 'company'>) => {
|
||||||
|
const host = process.env.SMTP_HOST
|
||||||
|
const user = process.env.SMTP_USER
|
||||||
|
const password = process.env.SMTP_PASSWORD
|
||||||
|
const to = process.env.CONTACT_TO
|
||||||
|
|
||||||
|
if (!host || !user || !password || !to) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 500,
|
||||||
|
statusMessage: 'Das Kontaktformular ist noch nicht vollständig konfiguriert.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const secure = process.env.SMTP_SECURE === 'true'
|
||||||
|
const port = Number(process.env.SMTP_PORT || (secure ? 465 : 587))
|
||||||
|
const from = process.env.SMTP_FROM || user
|
||||||
|
const client = new SmtpClient(secure
|
||||||
|
? tls.connect({ host, port, rejectUnauthorized: process.env.SMTP_REJECT_UNAUTHORIZED !== 'false', servername: host })
|
||||||
|
: net.connect({ host, port })
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.connect()
|
||||||
|
await client.command(`EHLO ${process.env.SMTP_HELO || 'fedeo.de'}`, 250)
|
||||||
|
|
||||||
|
if (!secure) {
|
||||||
|
await client.command('STARTTLS', 220)
|
||||||
|
await client.startTls(host)
|
||||||
|
await client.command(`EHLO ${process.env.SMTP_HELO || 'fedeo.de'}`, 250)
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.command('AUTH LOGIN', 334)
|
||||||
|
await client.command(Buffer.from(user).toString('base64'), 334)
|
||||||
|
await client.command(Buffer.from(password).toString('base64'), 235)
|
||||||
|
await client.command(`MAIL FROM:<${cleanHeader(from)}>`, 250)
|
||||||
|
await client.command(`RCPT TO:<${cleanHeader(to)}>`, [250, 251])
|
||||||
|
await client.command('DATA', 354)
|
||||||
|
await client.data(createEmail(payload, from, to))
|
||||||
|
await client.command('QUIT', 221)
|
||||||
|
} finally {
|
||||||
|
client.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const body = await readBody<ContactPayload>(event)
|
||||||
|
const name = body.name?.trim() || ''
|
||||||
|
const email = body.email?.trim() || ''
|
||||||
|
const company = body.company?.trim() || ''
|
||||||
|
const message = body.message?.trim() || ''
|
||||||
|
|
||||||
|
if (body.website) {
|
||||||
|
return { ok: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!name || name.length > 120) {
|
||||||
|
throw createError({ statusCode: 400, statusMessage: 'Bitte gib deinen Namen an.' })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!email || !isValidEmail(email) || email.length > 180) {
|
||||||
|
throw createError({ statusCode: 400, statusMessage: 'Bitte gib eine gültige E-Mail-Adresse an.' })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!message || message.length < 10 || message.length > 5000) {
|
||||||
|
throw createError({ statusCode: 400, statusMessage: 'Bitte beschreibe dein Anliegen etwas genauer.' })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!body.privacy) {
|
||||||
|
throw createError({ statusCode: 400, statusMessage: 'Bitte bestätige die Datenschutzhinweise.' })
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendMail({ company, email, message, name })
|
||||||
|
|
||||||
|
return { ok: true }
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user