This commit is contained in:
badblocks 2026-07-10 00:21:45 -07:00
parent 6e7977997e
commit cad1e68cae
Signed by: badblocks
SSH key fingerprint: SHA256:hEcM6BP4hKm9F7WsomNuXSBpPn2BSDnFzPSl3y1NerQ
21 changed files with 285 additions and 197 deletions

View file

@ -3,13 +3,15 @@ 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 Otp, { verifyOtp, normalizePhone } from "@lib/Otp.ts";
import { createCap } from "@lib/CapAdapter";
import {
OTP_SUPER_SECRET_SALT,
ANDROID_SMS_GATEWAY_RECIPIENT_PHONE,
} from "astro:env/server";
const MAX_OTP_VERIFY_ATTEMPTS = 5;
const isValidMobilePhone: [(data: string) => any, { message: string }] = [
(value: string) =>
validator.isMobilePhone(value, ["en-US", "en-CA"]) &&
@ -39,7 +41,11 @@ const captcha_input = z.string().trim().nonempty();
const sendOtpAction = z.object({
action: z.literal("send_otp"),
name: z.string().trim().min(5).max(32).transform(stripDisallowedCharacters),
name: z
.string()
.trim()
.transform(stripDisallowedCharacters)
.pipe(z.string().min(5).max(32)),
phone: z
.string()
.trim()
@ -47,11 +53,16 @@ const sendOtpAction = z.object({
msg: z
.string()
.trim()
.min(25)
.max(512)
.transform(stripDisallowedCharacters)
.refine(...noYelling)
.refine(...noExcessiveRepetitions),
.pipe(
z
.string()
.min(25)
.max(512)
.refine(...noYelling)
.refine(...noExcessiveRepetitions),
),
captcha: captcha_input,
});
@ -63,6 +74,7 @@ const sendMsgAction = z.object({
const resetAction = z.object({
action: z.literal("reset"),
captcha: captcha_input,
});
const formAction = z.discriminatedUnion("action", [
@ -73,7 +85,10 @@ const formAction = z.discriminatedUnion("action", [
const submitActionDefinition = {
input: formAction,
handler: async (input: any, context: ActionAPIContext) => {
handler: async (
input: z.infer<typeof formAction>,
context: ActionAPIContext,
) => {
if (!OTP_SUPER_SECRET_SALT || !ANDROID_SMS_GATEWAY_RECIPIENT_PHONE) {
throw new ActionError({
code: "INTERNAL_SERVER_ERROR",
@ -83,12 +98,10 @@ const submitActionDefinition = {
const cap = createCap(context.session ?? null);
if (
!(
/^[a-fA-F0-9]{16}:[a-fA-F0-9]{30}$/.test(input.captcha) &&
(await cap.validateToken(input.captcha))
)
) {
if (!(
/^[a-fA-F0-9]{16}:[a-fA-F0-9]{30}$/.test(input.captcha) &&
(await cap.validateToken(input.captcha))
)) {
throw new ActionError({
code: "BAD_REQUEST",
message: "Invalid Captcha Token.",
@ -96,7 +109,19 @@ const submitActionDefinition = {
}
if (input.action === "send_otp") {
const { name, phone, msg } = input;
const { name, msg } = input;
const phone = normalizePhone(input.phone);
if (
Otp.isRateLimitedGlobally() ||
Otp.isRateLimitedForOtp(phone) ||
Otp.isRateLimitedForMsgs(phone)
) {
throw new ActionError({
code: "TOO_MANY_REQUESTS",
message: "Too many requests. Try again later.",
});
}
const otp = Otp.generateOtp(phone, OTP_SUPER_SECRET_SALT);
const stepSeconds = Otp.getOtpStep();
@ -107,6 +132,9 @@ const submitActionDefinition = {
remainingSeconds != 0 ? " " + remainingSeconds + " seconds." : "."
}`;
Otp.recordOtpRequest(phone);
Otp.recordGlobalOtpSend();
const result = await new SmsClient().sendSMS(phone, message);
if (result.success) {
@ -118,38 +146,59 @@ const submitActionDefinition = {
nextAction: "send_msg",
};
} else {
console.error("OTP SMS send failed:", result.message);
throw new ActionError({
code: "SERVICE_UNAVAILABLE",
message: "Verification code failed to send: " + result.message,
message: "Verification code failed to send. 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 raw_phone = await context.session?.get("phone");
const msg = await context.session?.get("msg");
if (!name || !otp || !msg || !phone) {
if (!name || !otp || !msg || !raw_phone) {
throw new ActionError({
code: "BAD_REQUEST",
message: "Missing required fields.",
});
}
const phone = normalizePhone(raw_phone);
if (Otp.isRateLimitedForOtp(phone) || Otp.isRateLimitedForMsgs(phone)) {
throw new ActionError({
code: "TOO_MANY_REQUESTS",
message: "Too many requests. Try again later.",
});
}
const isVerified = verifyOtp(phone, OTP_SUPER_SECRET_SALT, otp);
if (!isVerified) {
const attempts = ((await context.session?.get("otpAttempts")) ?? 0) + 1;
if (attempts >= MAX_OTP_VERIFY_ATTEMPTS) {
context.session?.delete("phone");
context.session?.delete("name");
context.session?.delete("msg");
context.session?.delete("otpAttempts");
return {
nextAction: "send_otp",
error: "Too many incorrect codes. Please start over.",
field: "otp",
};
}
context.session?.set("otpAttempts", attempts);
return {
nextAction: "send_msg",
error: "Invalid or expired verification code.",
field: "otp",
};
// throw new ActionError({
// code: "BAD_REQUEST",
// message: "Invalid or expired verification code.",
// });
}
const message = `Web message from ${name} ( ${phone} ):\n\n${msg}`;
const message = `Web message from ${name} (${phone}):\n\n${msg}`;
Otp.recordMsgSubmission(phone);
const smsClient = new SmsClient();
const result = await smsClient.sendSMS(
@ -158,17 +207,17 @@ const submitActionDefinition = {
);
if (result.success) {
Otp.recordMsgSubmission(phone);
context.session?.delete("phone");
context.session?.delete("name");
context.session?.delete("msg");
context.session?.delete("otpAttempts");
return {
nextAction: "complete",
};
}
console.error("Contact SMS send failed:", result.message);
throw new ActionError({
code: "SERVICE_UNAVAILABLE",
message: "Message failed to send.",
@ -177,6 +226,7 @@ const submitActionDefinition = {
context.session?.delete("phone");
context.session?.delete("name");
context.session?.delete("msg");
context.session?.delete("otpAttempts");
return {
nextAction: "send_otp",