Compare commits

...

12 Commits

Author SHA1 Message Date
4d5e05061c Remove tracked env files
All checks were successful
Deploy Jody's App / build-and-deploy (push) Successful in 1m0s
2026-02-18 22:58:19 -06:00
a7b80c8318 fix: added better origin handling for contact form submission
All checks were successful
Deploy Jody's App / build-and-deploy (push) Successful in 34s
2026-02-18 22:39:03 -06:00
0ae4affe20 added different domain types to origin allowed to see if that will work
All checks were successful
Deploy Jody's App / build-and-deploy (push) Successful in 34s
2026-02-18 22:25:00 -06:00
30e6c23bac removed duplicate reference of allowed_origin in env (bruh moment)
All checks were successful
Deploy Jody's App / build-and-deploy (push) Successful in 33s
2026-02-18 22:23:08 -06:00
5f0d87ce2a added origin allowed for contact in env
All checks were successful
Deploy Jody's App / build-and-deploy (push) Successful in 32s
2026-02-18 22:18:15 -06:00
beca15d9bf added env
All checks were successful
Deploy Jody's App / build-and-deploy (push) Successful in 32s
2026-02-18 22:14:16 -06:00
0b99bcb800 removed reference to add turnstile key
All checks were successful
Deploy Jody's App / build-and-deploy (push) Successful in 34s
2026-02-18 22:11:40 -06:00
14333f90d1 commit test 6
All checks were successful
Deploy Jody's App / build-and-deploy (push) Successful in 34s
2026-02-18 22:09:33 -06:00
cfc564ce11 commit test 5
Some checks failed
Deploy Jody's App / build-and-deploy (push) Failing after 32s
2026-02-18 22:07:08 -06:00
0c7d9e3bd2 contact test 4
Some checks failed
Deploy Jody's App / build-and-deploy (push) Failing after 30s
2026-02-18 22:04:05 -06:00
28452a288e contact test 3
Some checks failed
Deploy Jody's App / build-and-deploy (push) Failing after 31s
2026-02-18 22:00:37 -06:00
975254e933 test again
Some checks failed
Deploy Jody's App / build-and-deploy (push) Failing after 29s
2026-02-18 21:55:25 -06:00
6 changed files with 60 additions and 12 deletions

View File

@@ -7,6 +7,8 @@ on:
jobs: jobs:
build-and-deploy: build-and-deploy:
runs-on: vps-host runs-on: vps-host
env:
CONTACT_HEALTH_URL: https://jodyholt.com/api/health
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
@@ -44,8 +46,19 @@ jobs:
- name: Restart Contact API - name: Restart Contact API
run: | run: |
systemctl restart jody-contact-api set -euo pipefail
systemctl is-active --quiet jody-contact-api SYSTEMCTL_BIN="/usr/bin/systemctl"
if [ ! -x "$SYSTEMCTL_BIN" ]; then
SYSTEMCTL_BIN="/bin/systemctl"
fi
sudo -n "$SYSTEMCTL_BIN" restart jody-contact-api
sudo -n "$SYSTEMCTL_BIN" is-active --quiet jody-contact-api
echo "jody-contact-api service is active"
- name: Health Check Contact API - name: Health Check Contact API
run: curl --fail --silent http://127.0.0.1:8787/health run: |
curl --fail --show-error --silent \
--retry 8 \
--retry-delay 2 \
--retry-all-errors \
"$CONTACT_HEALTH_URL"

4
.gitignore vendored
View File

@@ -19,9 +19,7 @@ contact-api/dist/
*.log *.log
# Environment variables # Environment variables
.env
.env.*.local
contact-api/.env
# OS generated # OS generated
.DS_Store .DS_Store

View File

@@ -10,7 +10,9 @@ const boolFromEnv = z
const envSchema = z.object({ const envSchema = z.object({
NODE_ENV: z.enum(["development", "test", "production"]).default("development"), NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
PORT: z.coerce.number().int().positive().default(8787), PORT: z.coerce.number().int().positive().default(8787),
CONTACT_ALLOWED_ORIGIN: z.string().url(), // Comma-separated list of allowed browser origins, e.g.
// https://jodyholt.com,https://www.jodyholt.com
CONTACT_ALLOWED_ORIGIN: z.string().min(1),
TURNSTILE_SECRET_KEY: z.string().min(1), TURNSTILE_SECRET_KEY: z.string().min(1),
TURNSTILE_EXPECTED_HOSTNAME: z.string().min(1), TURNSTILE_EXPECTED_HOSTNAME: z.string().min(1),
TURNSTILE_EXPECTED_ACTION: z.string().min(1).default("contact_form"), TURNSTILE_EXPECTED_ACTION: z.string().min(1).default("contact_form"),

View File

@@ -16,6 +16,25 @@ type ApiErrorResponse = {
const app = express(); const app = express();
app.set("trust proxy", 1); app.set("trust proxy", 1);
const normalizeOrigin = (value: string): string => {
const cleaned = value
.trim()
.replace(/^['"]|['"]$/g, "")
.replace(/\/+$/g, "");
// Some clients can emit explicit default ports. URL.origin normalizes them.
try {
return new URL(cleaned).origin.toLowerCase();
} catch {
return cleaned.toLowerCase();
}
};
const allowedOrigins = config.CONTACT_ALLOWED_ORIGIN
.split(",")
.map(normalizeOrigin)
.filter((value, index, all) => value.length > 0 && all.indexOf(value) === index);
app.use( app.use(
pinoHttp({ pinoHttp({
level: config.NODE_ENV === "production" ? "info" : "debug", level: config.NODE_ENV === "production" ? "info" : "debug",
@@ -30,7 +49,13 @@ app.use(helmet());
app.use( app.use(
cors({ cors({
origin(origin, callback) { origin(origin, callback) {
if (!origin || origin === config.CONTACT_ALLOWED_ORIGIN) { if (!origin) {
callback(null, true);
return;
}
const normalizedOrigin = normalizeOrigin(origin);
if (allowedOrigins.includes(normalizedOrigin)) {
callback(null, true); callback(null, true);
return; return;
} }

View File

@@ -7,6 +7,18 @@ type TurnstileVerifyResponse = {
"error-codes"?: string[]; "error-codes"?: string[];
}; };
const normalizeHostname = (value: string): string =>
value
.trim()
.replace(/^['"]|['"]$/g, "")
.replace(/\.+$/g, "")
.toLowerCase();
const expectedHostnames = config.TURNSTILE_EXPECTED_HOSTNAME
.split(",")
.map(normalizeHostname)
.filter((value, index, all) => value.length > 0 && all.indexOf(value) === index);
export async function verifyTurnstileToken( export async function verifyTurnstileToken(
token: string, token: string,
remoteIp?: string, remoteIp?: string,
@@ -36,7 +48,7 @@ export async function verifyTurnstileToken(
return { ok: false, reason: codes }; return { ok: false, reason: codes };
} }
if (result.hostname !== config.TURNSTILE_EXPECTED_HOSTNAME) { if (!result.hostname || !expectedHostnames.includes(normalizeHostname(result.hostname))) {
return { ok: false, reason: "hostname_mismatch" }; return { ok: false, reason: "hostname_mismatch" };
} }

View File

@@ -297,9 +297,7 @@ export function Contact() {
<div className="space-y-2"> <div className="space-y-2">
<div ref={turnstileContainerRef} className="min-h-[66px]" /> <div ref={turnstileContainerRef} className="min-h-[66px]" />
{!TURNSTILE_SITE_KEY && (
<p className="text-xs text-contrast">Set `VITE_TURNSTILE_SITE_KEY` to enable submissions.</p>
)}
{TURNSTILE_SITE_KEY && !turnstileReady && ( {TURNSTILE_SITE_KEY && !turnstileReady && (
<p className="text-xs text-text/65">Loading human verification...</p> <p className="text-xs text-text/65">Loading human verification...</p>
)} )}