Repository navigation
Expand file tree
/
Copy pathworker.js
More file actions
144 lines (130 loc) · 4.32 KB
/
Copy pathworker.js
File metadata and controls
144 lines (130 loc) · 4.32 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
// contact form API
// only /api/* reaches this worker and the assets binding serves everything else
const MAX_BODY_BYTES = 10000;
const LONG_FIELDS = new Set(["details", "about", "notes"]);
const PROJECT_TYPES = {
"new-site": "New website",
"redesign": "Redesign",
"web-app": "Web app or online shop",
"ai": "AI integration or automation",
"not-sure": "Not sure yet",
};
// order and labels for the email body
// unanswered fields are skipped
const EMAIL_LINES = [
["projectType", "Project type", (v) => PROJECT_TYPES[v]],
["siteSize", "Site size"],
["branding", "Branding"],
["content", "Content"],
["currentUrl", "Current site"],
["problem", "Main problem"],
["features", "Features", (v) => v.join(", ")],
["aiGoals", "AI goals", (v) => v.join(", ")],
["details", "Details"],
["about", "About the business"],
["timeline", "Timeline"],
["budget", "Budget"],
["currency", "Currency shown"],
["carePlan", "Care plan interest"],
["name", "Name"],
["email", "Email"],
["company", "Company"],
["phone", "Phone"],
["notes", "Anything else"],
];
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname === "/api/contact") {
if (request.method !== "POST") {
return json({ ok: false, error: "method_not_allowed" }, 405);
}
return handleContact(request, env);
}
if (url.pathname.startsWith("/api/")) {
return json({ ok: false, error: "not_found" }, 404);
}
return env.ASSETS.fetch(request);
},
};
function json(body, status = 200) {
return new Response(JSON.stringify(body), {
status,
headers: {
"content-type": "application/json",
"x-content-type-options": "nosniff",
"cache-control": "no-store",
},
});
}
async function handleContact(request, env) {
const raw = await request.text();
if (raw.length > MAX_BODY_BYTES) {
return json({ ok: false, error: "too_large" }, 413);
}
let body;
try {
body = JSON.parse(raw);
} catch {
return json({ ok: false, error: "bad_json" }, 400);
}
if (typeof body !== "object" || body === null || Array.isArray(body)) {
return json({ ok: false, error: "bad_json" }, 400);
}
// bots that fill the honeypot field get a quiet success
if (body.website) {
return json({ ok: true });
}
const clean = {};
for (const [key, value] of Object.entries(body)) {
if (typeof value === "string") {
clean[key] = value.trim().slice(0, LONG_FIELDS.has(key) ? 2000 : 200);
} else if (Array.isArray(value)) {
clean[key] = value
.filter((item) => typeof item === "string")
.map((item) => item.trim().slice(0, 100))
.slice(0, 10);
}
}
const invalid = [];
if (!clean.name) invalid.push("name");
if (!clean.email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(clean.email)) invalid.push("email");
if (!clean.projectType || !(clean.projectType in PROJECT_TYPES)) invalid.push("projectType");
if (invalid.length) {
return json({ ok: false, error: "invalid_fields", fields: invalid }, 400);
}
const verification = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
method: "POST",
body: new URLSearchParams({
secret: env.TURNSTILE_SECRET_KEY,
response: typeof body.turnstileToken === "string" ? body.turnstileToken : "",
remoteip: request.headers.get("CF-Connecting-IP") || "",
}),
});
const outcome = await verification.json();
if (!outcome.success) {
return json({ ok: false, error: "verification_failed" }, 403);
}
const projectLabel = PROJECT_TYPES[clean.projectType];
const subject = `New enquiry: ${clean.name} (${projectLabel})`;
const lines = [];
for (const [key, label, format] of EMAIL_LINES) {
const value = clean[key];
if (value === undefined || value === "" || (Array.isArray(value) && value.length === 0)) continue;
lines.push(`${label}: ${format ? format(value) : value}`);
}
const text = `New project enquiry from angusgee.dev/contact\n\n${lines.join("\n")}\n`;
try {
await env.EMAIL.send({
to: "info@angusgee.dev",
from: "enquiries@angusgee.dev",
replyTo: clean.email,
subject,
text,
});
} catch (err) {
console.error("email send failed:", err && err.code, err && err.message);
return json({ ok: false, error: "send_failed" }, 502);
}
return json({ ok: true });
}