Skip to content

Commit d8014dd

Browse files
committed
feat(contact): switch worker target from github issues to resend email
1 parent f32625b commit d8014dd

2 files changed

Lines changed: 45 additions & 50 deletions

File tree

worker/src/index.ts

Lines changed: 36 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
import { ContactFormSchema, type ContactFormValues } from "./schema";
22

33
interface Env {
4-
GITHUB_ISSUE_TOKEN: string;
5-
GITHUB_ISSUE_REPO: string;
4+
RESEND_API_KEY: string;
5+
RESEND_FROM: string;
6+
CONTACT_TO_EMAIL: string;
67
ALLOWED_ORIGINS: string;
78
}
89

9-
interface GithubIssuePayload {
10-
title: string;
11-
body: string;
12-
labels?: string[];
10+
interface ResendEmailPayload {
11+
from: string;
12+
to: string[];
13+
subject: string;
14+
reply_to?: string;
15+
text: string;
1316
}
1417

1518
function corsHeaders(origin: string | null, allowList: string[]): HeadersInit {
@@ -34,43 +37,38 @@ function jsonResponse(
3437
});
3538
}
3639

37-
function buildIssueTitle(message: string, email: string): string {
40+
function buildEmailSubject(message: string, email: string): string {
3841
const oneLine = message.replace(/\s+/g, " ").trim();
3942
const snippet = oneLine.slice(0, 60);
4043
const truncated = oneLine.length > 60 ? `${snippet}…` : snippet;
4144
return `[Contact] ${truncated || email}`;
4245
}
4346

44-
function buildIssueBody(
47+
function buildEmailText(
4548
data: Pick<ContactFormValues, "email" | "message">,
4649
): string {
4750
return [
48-
`**From:** ${data.email}`,
49-
`**Received:** ${new Date().toISOString()}`,
51+
`From: ${data.email}`,
52+
`Received: ${new Date().toISOString()}`,
5053
"",
5154
"---",
5255
"",
5356
data.message,
5457
].join("\n");
5558
}
5659

57-
async function createGithubIssue(
58-
env: Env,
59-
payload: GithubIssuePayload,
60+
async function sendResendEmail(
61+
apiKey: string,
62+
payload: ResendEmailPayload,
6063
): Promise<{ ok: true } | { ok: false; status: number; error: string }> {
61-
const res = await fetch(
62-
`https://api.github.com/repos/${env.GITHUB_ISSUE_REPO}/issues`,
63-
{
64-
method: "POST",
65-
headers: {
66-
Authorization: `Bearer ${env.GITHUB_ISSUE_TOKEN}`,
67-
Accept: "application/vnd.github+json",
68-
"X-GitHub-Api-Version": "2022-11-28",
69-
"User-Agent": "firstfluke-contact-worker",
70-
},
71-
body: JSON.stringify(payload),
64+
const res = await fetch("https://api.resend.com/emails", {
65+
method: "POST",
66+
headers: {
67+
Authorization: `Bearer ${apiKey}`,
68+
"Content-Type": "application/json",
7269
},
73-
);
70+
body: JSON.stringify(payload),
71+
});
7472
if (!res.ok) {
7573
const error = await res.text();
7674
return { ok: false, status: res.status, error };
@@ -119,27 +117,26 @@ export default {
119117
return jsonResponse({ ok: true }, 200, cors);
120118
}
121119

122-
if (!env.GITHUB_ISSUE_TOKEN || !env.GITHUB_ISSUE_REPO) {
123-
// TODO(oma-deferred): set GITHUB_ISSUE_TOKEN and GITHUB_ISSUE_REPO via `wrangler secret put`.
124-
console.info(
125-
"[contact] GitHub credentials missing. Payload acknowledged:",
126-
{
127-
email: parsed.data.email,
128-
messageLength: parsed.data.message.length,
129-
},
130-
);
120+
if (!env.RESEND_API_KEY || !env.RESEND_FROM || !env.CONTACT_TO_EMAIL) {
121+
// TODO(oma-deferred): set RESEND_API_KEY, RESEND_FROM, CONTACT_TO_EMAIL via `wrangler secret put` / `[vars]`.
122+
console.info("[contact] Resend config missing. Payload acknowledged:", {
123+
email: parsed.data.email,
124+
messageLength: parsed.data.message.length,
125+
});
131126
return jsonResponse({ ok: true }, 200, cors);
132127
}
133128

134-
const result = await createGithubIssue(env, {
135-
title: buildIssueTitle(parsed.data.message, parsed.data.email),
136-
body: buildIssueBody(parsed.data),
137-
labels: ["contact"],
129+
const result = await sendResendEmail(env.RESEND_API_KEY, {
130+
from: env.RESEND_FROM,
131+
to: [env.CONTACT_TO_EMAIL],
132+
subject: buildEmailSubject(parsed.data.message, parsed.data.email),
133+
reply_to: parsed.data.email,
134+
text: buildEmailText(parsed.data),
138135
});
139136

140137
if (!result.ok) {
141138
console.error(
142-
"[contact] github issue create failed:",
139+
"[contact] resend send failed:",
143140
result.status,
144141
result.error,
145142
);

worker/wrangler.toml

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,19 @@
1-
# Cloudflare Worker — firstfluke contact → GitHub Issues
1+
# Cloudflare Worker — firstfluke contact → Resend email
22
#
3-
# Setup steps (run inside this directory):
4-
# 1) pnpm install --ignore-workspace
5-
# 2) ./node_modules/.bin/wrangler login
6-
# 3) ./node_modules/.bin/wrangler secret put GITHUB_ISSUE_TOKEN
7-
# (Fine-grained PAT, Issues: read+write on the target private repo)
8-
# 4) ./node_modules/.bin/wrangler secret put GITHUB_ISSUE_REPO
9-
# (e.g. "first-fluke/contact-inbox")
10-
# 5) ./node_modules/.bin/wrangler deploy
3+
# Required secrets (set via wrangler secret put):
4+
# RESEND_API_KEY — Resend API key (re_...)
115
#
12-
# After deploy, copy the printed URL (e.g. firstfluke-contact.<account>.workers.dev)
13-
# and set it as a GitHub Actions Repository Variable named NEXT_PUBLIC_CONTACT_API_URL.
6+
# Required vars (set in [vars] below or override per environment):
7+
# RESEND_FROM — verified sender, e.g. "FIRST FLUKE <contact@mail.firstfluke.com>"
8+
# CONTACT_TO_EMAIL — destination inbox, e.g. "our.first.fluke@gmail.com"
9+
# ALLOWED_ORIGINS — comma-separated origins for CORS
1410

1511
name = "firstfluke-contact"
1612
main = "src/index.ts"
1713
compatibility_date = "2025-05-01"
1814
workers_dev = true
1915

2016
[vars]
17+
RESEND_FROM = "FIRST FLUKE <contact@mail.firstfluke.com>"
18+
CONTACT_TO_EMAIL = "our.first.fluke@gmail.com"
2119
ALLOWED_ORIGINS = "https://firstfluke.com,https://www.firstfluke.com,http://localhost:3000"

0 commit comments

Comments
 (0)