feat(portfolio): rebuild w-make.com as a bilingual editorial studio

Give clients a real IA, SMTP contact to eldov@w-make.de, and live Batch
evidence instead of a single-page placeholder.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jan Wagner
2026-08-16 03:20:16 +02:00
co-authored by Cursor
parent 1f6894d781
commit 5657b1d202
54 changed files with 2281 additions and 398 deletions
+38
View File
@@ -0,0 +1,38 @@
"use server";
import { headers } from "next/headers";
import { allowInquiry } from "./rate-limit";
import { sendInquiryMail } from "./mail";
import { validateInquiry } from "./schema";
import { saveInquiry } from "./store";
export type InquiryActionResult =
| { ok: true }
| { ok: false; code: "invalid" | "rate" | "error"; field?: string };
export async function submitInquiry(formData: FormData): Promise<InquiryActionResult> {
const parsed = validateInquiry({
name: formData.get("name"),
email: formData.get("email"),
company: formData.get("company"),
message: formData.get("message"),
locale: formData.get("locale"),
website: formData.get("website"),
});
if (!parsed.ok && parsed.code === "spam") return { ok: true };
if (!parsed.ok) return { ok: false, code: "invalid", field: parsed.field };
const headerList = await headers();
const forwarded = headerList.get("x-forwarded-for")?.split(",")[0]?.trim();
const ip = forwarded || headerList.get("x-real-ip") || "unknown";
if (!allowInquiry(ip)) return { ok: false, code: "rate" };
try {
saveInquiry(parsed.data);
await sendInquiryMail(parsed.data);
return { ok: true };
} catch {
return { ok: false, code: "error" };
}
}
+126
View File
@@ -0,0 +1,126 @@
import { createConnection } from "node:net";
import { connect as tlsConnect } from "node:tls";
import type { Inquiry } from "./schema";
type SmtpConfig = {
host: string;
port: number;
user: string;
pass: string;
from: string;
to: string;
};
function smtpConfig(): SmtpConfig | null {
const to = process.env.CONTACT_TO;
const host = process.env.SMTP_HOST;
const user = process.env.SMTP_USER;
const pass = process.env.SMTP_PASS;
if (!to || !host || !user || !pass) return null;
return {
host,
port: Number(process.env.SMTP_PORT || 465),
user,
pass,
from: process.env.CONTACT_FROM || user,
to,
};
}
function encodeAuth(value: string): string {
return Buffer.from(value, "utf8").toString("base64");
}
async function smtpSend(config: SmtpConfig, inquiry: Inquiry): Promise<void> {
const body = [
`Name: ${inquiry.name}`,
`E-Mail: ${inquiry.email}`,
`Unternehmen: ${inquiry.company || "—"}`,
`Sprache: ${inquiry.locale}`,
"",
inquiry.message,
].join("\n");
const subject = `W-MAKE Anfrage · ${inquiry.name}`.replace(/[\r\n]/g, " ");
const message = [
`From: ${config.from}`,
`To: ${config.to}`,
`Reply-To: ${inquiry.email}`,
`Subject: =?UTF-8?B?${Buffer.from(subject, "utf8").toString("base64")}?=`,
"MIME-Version: 1.0",
"Content-Type: text/plain; charset=utf-8",
"Content-Transfer-Encoding: 8bit",
"",
body,
"",
].join("\r\n");
await new Promise<void>((resolve, reject) => {
const socket =
config.port === 465
? tlsConnect({ host: config.host, port: config.port, servername: config.host })
: createConnection({ host: config.host, port: config.port });
let buffer = "";
let step = 0;
const commands = [
`EHLO w-make.com`,
`AUTH LOGIN`,
encodeAuth(config.user),
encodeAuth(config.pass),
`MAIL FROM:<${config.user}>`,
`RCPT TO:<${config.to}>`,
`DATA`,
];
const write = (line: string) => socket.write(`${line}\r\n`);
const fail = (error: Error) => {
socket.destroy();
reject(error);
};
const onLine = (line: string) => {
const code = Number(line.slice(0, 3));
if (code >= 400) {
fail(new Error(`SMTP ${line}`));
return;
}
if (step < commands.length) {
write(commands[step]);
step += 1;
return;
}
if (step === commands.length) {
write(`${message}\r\n.`);
step += 1;
return;
}
write("QUIT");
socket.end();
resolve();
};
socket.setEncoding("utf8");
socket.on("data", (chunk: string) => {
buffer += chunk;
const parts = buffer.split("\r\n");
buffer = parts.pop() || "";
for (const line of parts) {
if (line && (line[3] === " " || line.length === 3)) onLine(line);
}
});
socket.on("error", fail);
socket.setTimeout(20000, () => fail(new Error("SMTP timeout")));
});
}
export async function sendInquiryMail(inquiry: Inquiry): Promise<boolean> {
const config = smtpConfig();
if (!config) return false;
try {
await smtpSend(config, inquiry);
return true;
} catch {
return false;
}
}
+15
View File
@@ -0,0 +1,15 @@
const WINDOW_MS = 60 * 60 * 1000;
const MAX_PER_WINDOW = 3;
const hits = new Map<string, number[]>();
export function allowInquiry(key: string): boolean {
const now = Date.now();
const recent = (hits.get(key) || []).filter((time) => now - time < WINDOW_MS);
if (recent.length >= MAX_PER_WINDOW) {
hits.set(key, recent);
return false;
}
recent.push(now);
hits.set(key, recent);
return true;
}
+40
View File
@@ -0,0 +1,40 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { validateInquiry } from "./schema.ts";
describe("validateInquiry", () => {
const valid = {
name: "Anna Müller",
email: "anna@example.com",
company: "Glaswerk",
message: "Wir brauchen eine belastbare Abbildung unseres Satzprozesses.",
locale: "de",
website: "",
};
it("accepts a complete inquiry", () => {
const result = validateInquiry(valid);
assert.equal(result.ok, true);
if (result.ok) {
assert.equal(result.data.email, "anna@example.com");
assert.equal(result.data.company, "Glaswerk");
}
});
it("rejects a filled honeypot", () => {
const result = validateInquiry({ ...valid, website: "https://spam.test" });
assert.equal(result.ok, false);
if (!result.ok) assert.equal(result.code, "spam");
});
it("rejects short messages and invalid email", () => {
assert.equal(validateInquiry({ ...valid, message: "Hallo" }).ok, false);
assert.equal(validateInquiry({ ...valid, email: "not-an-email" }).ok, false);
});
it("allows an empty company field", () => {
const result = validateInquiry({ ...valid, company: " " });
assert.equal(result.ok, true);
if (result.ok) assert.equal(result.data.company, undefined);
});
});
+51
View File
@@ -0,0 +1,51 @@
export type InquiryInput = {
name: unknown;
email: unknown;
company?: unknown;
message: unknown;
locale: unknown;
website?: unknown;
};
export type Inquiry = {
name: string;
email: string;
company?: string;
message: string;
locale: "de" | "en";
};
export type ValidationResult =
| { ok: true; data: Inquiry }
| { ok: false; code: "spam" | "invalid"; field?: string };
const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
export function validateInquiry(input: InquiryInput): ValidationResult {
if (typeof input.website === "string" && input.website.trim() !== "") {
return { ok: false, code: "spam" };
}
const name = typeof input.name === "string" ? input.name.trim() : "";
const email = typeof input.email === "string" ? input.email.trim() : "";
const message = typeof input.message === "string" ? input.message.trim() : "";
const companyRaw = typeof input.company === "string" ? input.company.trim() : "";
const locale = input.locale === "en" || input.locale === "de" ? input.locale : null;
if (name.length < 2 || name.length > 80) return { ok: false, code: "invalid", field: "name" };
if (!EMAIL.test(email) || email.length > 160) return { ok: false, code: "invalid", field: "email" };
if (message.length < 20 || message.length > 4000) return { ok: false, code: "invalid", field: "message" };
if (!locale) return { ok: false, code: "invalid", field: "locale" };
if (companyRaw.length > 120) return { ok: false, code: "invalid", field: "company" };
return {
ok: true,
data: {
name,
email,
message,
locale,
...(companyRaw ? { company: companyRaw } : {}),
},
};
}
+43
View File
@@ -0,0 +1,43 @@
import { mkdirSync } from "node:fs";
import { dirname } from "node:path";
import { DatabaseSync } from "node:sqlite";
import { randomUUID } from "node:crypto";
import type { Inquiry } from "./schema";
let db: DatabaseSync | null = null;
function getDb(): DatabaseSync {
if (db) return db;
const dbPath = process.env.INQUIRIES_DB_PATH || "./data/inquiries.sqlite";
mkdirSync(dirname(dbPath), { recursive: true });
db = new DatabaseSync(dbPath);
db.exec("PRAGMA journal_mode = WAL;");
db.exec(`CREATE TABLE IF NOT EXISTS inquiries (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL,
company TEXT,
message TEXT NOT NULL,
locale TEXT NOT NULL,
created_at TEXT NOT NULL
)`);
return db;
}
export function saveInquiry(inquiry: Inquiry): string {
const id = randomUUID();
getDb()
.prepare(
"INSERT INTO inquiries (id, name, email, company, message, locale, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
)
.run(
id,
inquiry.name,
inquiry.email,
inquiry.company ?? null,
inquiry.message,
inquiry.locale,
new Date().toISOString(),
);
return id;
}