From f2f42a84b51ea46e8688ed3bd86ba94b8ce42e7f Mon Sep 17 00:00:00 2001 From: badbl0cks <4161747+badbl0cks@users.noreply.github.com> Date: Thu, 22 Jan 2026 12:25:20 -0800 Subject: [PATCH] Add contact actions and migrate contact form to Astro actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement send_otp and send_msg handlers with server-side validation, captcha verification, OTP generation/verification, rate limiting, session-backed multi-step flow, and SMS gateway integration. Replace broken contact endpoint and types with Astro server actions; delete endpoints/contact.ts, types/ContactForm.ts and src/components/ContactForm.astro. Update pages/contact.astro to use astro:actions and improve captcha handling (listen for solve and set hidden input). Adjust OTP timing (step 60s → 300s, VALID_PAST_OTP_STEPS 5 → 1). Add validator, @types/validator, Prettier and prettier-plugin-astro; include .prettierrc.mjs in tsconfig include. Add Prettier config for Astro --- .prettierrc.mjs | 12 + bun.lockb | Bin 213390 -> 215964 bytes package.json | 8 +- src/actions/contact.ts | 206 ++++++++++++++ src/actions/index.ts | 5 + src/components/ContactForm.astro | 160 ----------- src/lib/Otp.ts | 4 +- src/lib/contact.ts | 0 src/pages/contact.astro | 204 ++++++++------ src/pages/endpoints/contact.ts | 451 ------------------------------- src/types/ContactForm.ts | 52 ---- tsconfig.json | 4 +- 12 files changed, 360 insertions(+), 746 deletions(-) create mode 100644 .prettierrc.mjs create mode 100644 src/actions/contact.ts create mode 100644 src/actions/index.ts delete mode 100644 src/components/ContactForm.astro delete mode 100644 src/lib/contact.ts delete mode 100644 src/pages/endpoints/contact.ts delete mode 100644 src/types/ContactForm.ts diff --git a/.prettierrc.mjs b/.prettierrc.mjs new file mode 100644 index 0000000..7e61930 --- /dev/null +++ b/.prettierrc.mjs @@ -0,0 +1,12 @@ +/** @type {import("prettier").Config} */ +export default { + plugins: ["prettier-plugin-astro"], + overrides: [ + { + files: "*.astro", + options: { + parser: "astro", + }, + }, + ], +}; diff --git a/bun.lockb b/bun.lockb index 41a9d0fbd72a4db930404e983641858fe1b1b73a..06bbc8400ccacbc9539c607fa9cf057051396080 100755 GIT binary patch delta 40708 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 RoSwK5s0XADc!V4CQviNM`=I~; delta 39265 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 diff --git a/package.json b/package.json index 95fa7a8..f218011 100644 --- a/package.json +++ b/package.json @@ -31,9 +31,13 @@ "htmx.org": "^2.0.8", "iconify-icon": "^3.0.2", "otplib": "^12.0.1", - "typescript": "^5.9.3" + "typescript": "^5.9.3", + "validator": "^13.15.26" }, "devDependencies": { - "@types/bun": "^1.3.5" + "@types/bun": "^1.3.5", + "@types/validator": "^13.15.10", + "prettier": "^3.8.1", + "prettier-plugin-astro": "^0.14.1" } } diff --git a/src/actions/contact.ts b/src/actions/contact.ts new file mode 100644 index 0000000..b59b9f0 --- /dev/null +++ b/src/actions/contact.ts @@ -0,0 +1,206 @@ +import { defineAction, ActionError } from "astro:actions"; +import { z } from "astro/zod"; +import type { ActionAPIContext } from "astro:actions"; +import validator from "validator"; +import SmsClient from "@lib/SmsGatewayClient.ts"; +import Otp, { verifyOtp } from "@lib/Otp.ts"; +import CapServer from "@lib/CapAdapter"; +import { + OTP_SUPER_SECRET_SALT, + ANDROID_SMS_GATEWAY_RECIPIENT_PHONE, +} from "astro:env/server"; + +const isValidCaptcha: [(data: string) => any, { message: string }] = [ + async (value: string) => + typeof console.log(value) && + /^[a-fA-F0-9]{16}:[a-fA-F0-9]{30}$/.test(value) && + (await CapServer.validateToken(value)), + { + message: "Invalid captcha token.", + }, +]; + +const stripLow = (value: string) => validator.stripLow(value); + +const isMobilePhone: [(data: string) => any, { message: string }] = [ + (value: string) => validator.isMobilePhone(value, ["en-US", "en-CA"]), + { message: "Invalid phone number" }, +]; + +const noYelling: [(data: string) => any, { message: string }] = [ + (value: string) => + (value.match(/\p{Uppercase_Letter}/gv) || []).length / value.length < 0.1, + { message: "No yelling!" }, +]; + +const noExcessiveRepetitions: [(data: string) => any, { message: string }] = [ + (value: string) => !/(.)\1{2,}/.test(value), + { message: "No excessive repetitions!" }, +]; + +const acceptableText: [(data: string) => any, { message: string }] = [ + (value: string) => + /^[\p{Letter}\p{Mark}\p{General_Category=Decimal_Number}\p{General_Category=Punctuation}\p{General_Category=Space_Separator}\p{General_Category=Symbol}\p{RGI_Emoji}]*$/v.test( + value, + ), + { + message: + "Only letters, numbers, punctuation, spaces, symbols, and emojis are allowed.", + }, +]; + +const captcha_input = z + .string() + .trim() + .nonempty() + .refine(...isValidCaptcha); + +const sendOtpAction = z.object({ + action: z.literal("send_otp"), + name: z + .string() + .trim() + .min(5) + .max(32) + .transform(stripLow) + .refine(...acceptableText), + phone: z + .string() + .trim() + .refine(...isMobilePhone), + msg: z + .string() + .trim() + .min(25) + .max(512) + .transform(stripLow) + .refine(...noYelling) + .refine(...noExcessiveRepetitions) + .refine(...acceptableText), + captcha: captcha_input, +}); + +const sendMsgAction = z.object({ + action: z.literal("send_msg"), + otp: z.string().trim().length(6), + captcha: captcha_input, +}); + +const formAction = z.discriminatedUnion("action", [ + sendOtpAction, + sendMsgAction, +]); + +const submitActionDefinition = { + input: formAction, + handler: async (input: any, context: ActionAPIContext) => { + if (!OTP_SUPER_SECRET_SALT || !ANDROID_SMS_GATEWAY_RECIPIENT_PHONE) { + throw new ActionError({ + code: "INTERNAL_SERVER_ERROR", + message: "Server variables are missing.", + }); + } + + if (input.action === "send_otp") { + const { name, phone, msg } = input; + if (!phone || !Otp.validatePhoneNumber(phone)) { + throw new ActionError({ + code: "BAD_REQUEST", + message: "Invalid phone number.", + }); + } + + if (Otp.isRateLimitedForOtp(phone)) { + throw new ActionError({ + code: "TOO_MANY_REQUESTS", + message: "Too many OTP requests. Please try again later.", + }); + } + + if (Otp.isRateLimitedForMsgs(phone)) { + throw new ActionError({ + code: "TOO_MANY_REQUESTS", + message: "Too many message requests. Please try again later.", + }); + } + + const otp = Otp.generateOtp(phone, OTP_SUPER_SECRET_SALT); + const stepSeconds = Otp.getOtpStep(); + const stepMinutes = Math.floor(stepSeconds / 60); + const remainingSeconds = stepSeconds % 60; + + const api = new SmsClient(); + const message = `${otp} is your verification code. This code is valid for ${stepMinutes} minutes${ + remainingSeconds != 0 ? " " + remainingSeconds + " seconds." : "." + }`; + const result = await api.sendSMS(phone, message); + + if (result.success) { + Otp.recordOtpRequest(phone); + + context.session?.set("phone", phone); + context.session?.set("name", name); + context.session?.set("msg", msg); + + return { + nextAction: "send_msg", + }; + } else { + throw new ActionError({ + code: "SERVICE_UNAVAILABLE", + message: "Verification code failed to send. Please try again later.", + }); + } + } else if (input.action === "send_msg") { + const { otp } = input; + const name = await context.session?.get("name"); + const phone = await context.session?.get("phone"); + const msg = await context.session?.get("msg"); + + if (!name || !otp || !msg || !phone) { + throw new ActionError({ + code: "BAD_REQUEST", + message: "Missing required fields.", + }); + } + + const isVerified = verifyOtp(phone, OTP_SUPER_SECRET_SALT, otp); + if (!isVerified) { + throw new ActionError({ + code: "BAD_REQUEST", + message: "Invalid or expired verification code.", + }); + } + + const message = `Web message from ${name} ( ${phone} ):\n\n${msg}`; + + const smsClient = new SmsClient(); + const result = await smsClient.sendSMS( + ANDROID_SMS_GATEWAY_RECIPIENT_PHONE, + message, + ); + + if (result.success) { + Otp.recordMsgSubmission(phone); + + context.session?.delete("phone"); + context.session?.delete("name"); + context.session?.delete("msg"); + + return { + nextAction: "complete", + }; + } + + throw new ActionError({ + code: "SERVICE_UNAVAILABLE", + message: "Message failed to send.", + }); + } + }, +}; + +export const contact = { + submitForm: defineAction({ ...submitActionDefinition, accept: "form" }), + submitJson: defineAction({ ...submitActionDefinition, accept: "json" }), +}; diff --git a/src/actions/index.ts b/src/actions/index.ts new file mode 100644 index 0000000..8e56698 --- /dev/null +++ b/src/actions/index.ts @@ -0,0 +1,5 @@ +import { contact } from "./contact.ts"; + +export const server = { + contact, +}; diff --git a/src/components/ContactForm.astro b/src/components/ContactForm.astro deleted file mode 100644 index 2cdc617..0000000 --- a/src/components/ContactForm.astro +++ /dev/null @@ -1,160 +0,0 @@ ---- -import { POST, generateInitialState } from "@pages/endpoints/contact"; -import * as ContactFormTypes from "../types/ContactForm"; - -async function handlePost(): Promise { - try { - let response = - await (await POST(Astro))?.json(); - - if (!response) { - return generateInitialState("Invalid response."); - } - - return response; - } catch (error) { - let message = "An unexpected error occurred."; - if (error instanceof Error) { - message = "An unexpected error occurred: " + error.message; - } - - return generateInitialState(message); - } -} - -// CANNOT USE SESSION INSIDE AN ASTRO COMPONENT! MUST REVALIDATE FORM FIELDS OR CONVERT TO REGULAR PAGE (preferable as there will never be more than one contact form) - -const state = (Astro.request.method === "POST")? await handlePost() : generateInitialState(); - ---- - - - - -

Contact

-{state.state !== "complete" &&
-
-

Use the below form to shoot me a quick text!

- {state.error &&

{state.error}

} -
-
- - - -
- -
- -
- - -
||

Your message has been sent successfully!

} diff --git a/src/lib/Otp.ts b/src/lib/Otp.ts index 95969f4..a6f3ba5 100644 --- a/src/lib/Otp.ts +++ b/src/lib/Otp.ts @@ -7,8 +7,8 @@ const ONE_WEEK_IN_MS: number = 7 * 24 * 60 * 60 * 1000; const ONE_HOUR_IN_MS: number = 60 * 60 * 1000; const MAX_OTP_REQUESTS_PER_HOUR: number = 3; const MAX_MESSAGES_PER_WEEK: number = 3; -const OTP_STEP_IN_SEC: number = 60; -const VALID_PAST_OTP_STEPS: number = 5; +const OTP_STEP_IN_SEC: number = 300; +const VALID_PAST_OTP_STEPS: number = 1; const VALID_FUTURE_OTP_STEPS: number = 1; const OTP_NUM_DIGITS: number = 6; diff --git a/src/lib/contact.ts b/src/lib/contact.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/pages/contact.astro b/src/pages/contact.astro index 3e753c4..93092c3 100644 --- a/src/pages/contact.astro +++ b/src/pages/contact.astro @@ -1,55 +1,48 @@ --- import Layout from "@layouts/BaseLayout.astro"; -import { POST, generateInitialState } from "@pages/endpoints/contact"; -import * as ContactFormTypes from "../types/ContactForm"; +import { actions, isInputError } from "astro:actions"; export const prerender = false; -async function handlePost(): Promise { - try { - let response = - await (await POST(Astro))?.json(); - - if (!response) { - return generateInitialState("Invalid response."); - } - - return response; - } catch (error) { - let message = "An unexpected error occurred."; - if (error instanceof Error) { - message = "An unexpected error occurred: " + error.message; - } - - return generateInitialState(message); - } -} - -const state = (Astro.request.method === "POST")? await handlePost() : generateInitialState(); +const result = Astro.getActionResult(actions.contact.submitForm); +const nextAction = result?.data?.nextAction || "send_otp"; +const error = isInputError(result?.error) ? result.error.fields : {}; --- + -