dev work
This commit is contained in:
parent
6e7977997e
commit
cad1e68cae
21 changed files with 285 additions and 197 deletions
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue