Skip to content

Commit 87a54a1

Browse files
committed
Created urgent contact form
1 parent b3a0340 commit 87a54a1

File tree

8 files changed

+181
-11
lines changed

8 files changed

+181
-11
lines changed

bot.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
layout: default
3+
title: Bot
4+
---
5+
6+
If you're reading this page then you've probably found my user agent in your logs somewhere. If this is causing issues, please [send me a message via my contact form](/contact) and we can solve the problem.

contact-urgent.md

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
---
2+
layout: default
3+
title: Urgent Contact
4+
---
5+
6+
If you have an access token then you can send me an urgent message using this form:
7+
8+
<form id="contact-form" method="post" action="/api/contact-urgent">
9+
<fieldset style="display:none">
10+
<label for="name">Leave blank if you're human:</label>
11+
<input type="text" name="name" id="name" placeholder="Leave blank if you're human">
12+
</fieldset>
13+
<fieldset style="margin-bottom:1em">
14+
<label for="email" style="display:inline-block; margin-bottom:0.5em">Email address:</label>
15+
<input type="email" name="email" id="email" placeholder="Email address" style="box-sizing:border-box; width:100%; max-width:20em" required>
16+
</fieldset>
17+
<fieldset style="margin-bottom:0.5em">
18+
<label for="message">Message:</label>
19+
</fieldset>
20+
<fieldset style="margin-bottom:1em">
21+
<textarea name="message" id="message" placeholder="Message" style="box-sizing:border-box; width:100%; max-width:60em; height:15em" required></textarea>
22+
</fieldset>
23+
<fieldset style="margin-bottom:1em">
24+
<label for="token" style="display:inline-block; margin-bottom:0.5em">Token:</label>
25+
<input type="text" name="token" id="token" placeholder="token" style="box-sizing:border-box; width:100%; max-width:20em" required>
26+
</fieldset>
27+
<button type="submit" style="margin-bottom:1em">Send</button>
28+
</form>
29+
30+
<script>
31+
document.getElementById("contact-form").addEventListener("submit", event => {
32+
event.preventDefault();
33+
const formData = new FormData(event.target);
34+
const token = formData.get("token");
35+
fetch("/api/token-verify", {
36+
method: "POST",
37+
headers: {
38+
"accept": "application/json",
39+
"content-type": "application/json",
40+
},
41+
body: JSON.stringify({ token })
42+
})
43+
.then(res => res.json())
44+
.then(res => {
45+
const element = document.getElementById("token-verify-output");
46+
if (!res.ok) {
47+
alert(`Token ${token} does not appear to be valid.`)
48+
} else {
49+
fetch("/api/contact-urgent", {
50+
method: "POST",
51+
headers: {
52+
"accept": "application/json",
53+
"content-type": "application/json",
54+
},
55+
body: JSON.stringify(Object.fromEntries(formData.entries()))
56+
})
57+
.then(res => res.json())
58+
.then(res => {
59+
if (res.success) {
60+
alert("Your message has been sent successfully.");
61+
event.target.reset();
62+
} else {
63+
alert("An unknown error occurred.");
64+
}
65+
})
66+
}
67+
});
68+
});
69+
</script>

functions-src/comms/email.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { USER_AGENT } from "../util.js";
2+
13
function formToText(form) {
24
var text = "";
35
for (const property in form) {
@@ -26,8 +28,9 @@ async function sendFormViaResend(api_key, to, subject, form) {
2628
const response = await fetch(new Request("https://api.resend.com/emails", {
2729
method: "POST",
2830
headers: {
29-
"Authorization": `Bearer ${api_key}`,
31+
"authorization": `Bearer ${api_key}`,
3032
"content-type": "application/json",
33+
"user-agent": USER_AGENT,
3134
},
3235
body: JSON.stringify({
3336

functions-src/comms/push.js

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { USER_AGENT } from "../util.js";
2+
3+
function formToText(form) {
4+
var text = "";
5+
for (const property in form) {
6+
text += `${property}:\n`;
7+
var value = form[property];
8+
if (typeof value !== "string") {
9+
value = prettyJson(value);
10+
}
11+
text += `${value}\n\n`;
12+
}
13+
return text;
14+
}
15+
16+
async function sendFormViaPush({env, to, form}) {
17+
if (to === undefined) {
18+
to = env.PUSHOVER_BEN;
19+
}
20+
return await sendFormViaPushover(env.PUSHOVER_KEY, to, form);
21+
}
22+
23+
async function sendFormViaPushover(apiKey, to, form) {
24+
const response = await fetch(new Request("https://api.pushover.net/1/messages.json", {
25+
method: "POST",
26+
headers: {
27+
"user-agent": USER_AGENT,
28+
"content-type": "application/json",
29+
},
30+
body: JSON.stringify({
31+
token: apiKey,
32+
user: to,
33+
message: formToText(form),
34+
}),
35+
}));
36+
return response.ok;
37+
}
38+
39+
export { sendFormViaPush }

functions-src/forms.js

+29-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import { sendFormViaEmail } from "./comms/email.js"
2-
import { ulidFactory } from "./ulid.ts"
1+
import { sendFormViaEmail } from "./comms/email.js";
2+
import { sendFormViaPush } from "./comms/push.js";
3+
import { verifySecureToken } from "./secure-token.js";
4+
import { ulidFactory } from "./ulid.ts";
35

46
const ulid = ulidFactory({ monotonic: true });
57

@@ -8,13 +10,25 @@ async function handleForm({
810
formId,
911
honeypotField,
1012
}) {
13+
const headers = Object.fromEntries(context.request.headers.entries());
14+
const usingJson = (headers["content-type"] === "application/json");
1115
const submissionId = ulid();
1216
const submittedTs = new Date().toISOString();
13-
const fields = Object.fromEntries((await context.request.formData()).entries());
17+
const fields = usingJson ?
18+
await context.request.json() :
19+
Object.fromEntries((await context.request.formData()).entries());
1420
const replyEmail = fields.email;
1521
const spamReasons = [];
1622
const cf = context.request.cf;
17-
const headers = Object.fromEntries(context.request.headers.entries());
23+
24+
if (URGENT_TOKEN_FIELD_NAME in fields) {
25+
if (await verifySecureToken({
26+
token: fields[URGENT_TOKEN_FIELD_NAME],
27+
secret: context.env.TOKEN_GENERATOR_SECRET,
28+
}) === false) {
29+
spamReasons.push("BAD_TOKEN");
30+
}
31+
}
1832

1933
if (typeof honeypotField === "string") {
2034
if (fields[honeypotField] !== "") {
@@ -37,15 +51,24 @@ async function handleForm({
3751
.run();
3852

3953
if (spamReasons.length === 0) {
40-
return await sendFormViaEmail({
54+
const emailResult = await sendFormViaEmail({
4155
env: context.env,
4256
subject: `New ${formId} form submission from beh.uk`,
4357
form: fields,
44-
})
58+
});
59+
if (URGENT_TOKEN_FIELD_NAME in fields) {
60+
return await sendFormViaPush({
61+
env: context.env,
62+
form: fields,
63+
});
64+
}
65+
return emailResult;
4566
} else {
4667
// submission is belived to be spam, return OK and don't do anything else
4768
return true;
4869
}
4970
}
5071

72+
const URGENT_TOKEN_FIELD_NAME = "token";
73+
5174
export { handleForm }

functions-src/util.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const USER_AGENT = "Mozilla/5.0 (compatible; www.beh.uk/1.0; +https://www.beh.uk/bot)";
2+
3+
export {
4+
USER_AGENT,
5+
}

functions/api/contact-urgent.js

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { handleForm } from "../../functions-src/forms.js"
2+
3+
export async function onRequest(context) {
4+
if (context.request.method !== "POST") {
5+
return new Response("Invalid request method", { status: 405 });
6+
}
7+
8+
const headers = Object.fromEntries(context.request.headers.entries());
9+
const url = new URL(context.request.url)
10+
const usingJson = (headers["content-type"] === "application/json");
11+
12+
const success = await handleForm({
13+
context,
14+
formId: "URGENT-CONTACT",
15+
honeypotField: "name",
16+
});
17+
18+
if (!success && !usingJson) {
19+
return new Response("Oops! Something went wrong. Please try submitting the form again.", { status: 500 });
20+
}
21+
22+
return usingJson ?
23+
Response.json({ success }) :
24+
Response.redirect(`https://${url.hostname}/contact-success`, 303);
25+
}

secure/token-generator.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ Use this form to generate a token for the urgent contact form.
3939
fetch('/secure/api/token-generate', {
4040
method: 'POST',
4141
headers: {
42-
'Accept': 'application/json',
43-
'Content-Type': 'application/json'
42+
'accept': 'application/json',
43+
'content-type': 'application/json'
4444
},
4545
body: JSON.stringify({ name, expiry })
4646
})
@@ -55,8 +55,8 @@ Use this form to generate a token for the urgent contact form.
5555
fetch('/api/token-verify', {
5656
method: 'POST',
5757
headers: {
58-
'Accept': 'application/json',
59-
'Content-Type': 'application/json'
58+
'accept': 'application/json',
59+
'content-type': 'application/json'
6060
},
6161
body: JSON.stringify({ token })
6262
})

0 commit comments

Comments
 (0)