feat: add contact form with SMS OTP verification

This commit is contained in:
badblocks 2025-07-17 19:34:29 -07:00
parent 91b162fb44
commit 3874443c34
No known key found for this signature in database
14 changed files with 729 additions and 54 deletions

View file

@ -0,0 +1,97 @@
import { verifyTOTP } from "../utils/totp";
import { createSmsGatewayClient } from "../lib/sms-gateway";
import { isRateLimited, recordSubmission } from "../utils/rate-limiter.js";
import { normalizeAndValidatePhoneNumber } from "../utils/phone-validator.js";
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig(event);
const {
name,
message: userMessage,
phoneNumber: rawPhoneNumber,
code,
} = await readBody(event);
let phoneNumber;
try {
phoneNumber = normalizeAndValidatePhoneNumber(rawPhoneNumber);
} catch (error) {
throw createError({ statusCode: 400, statusMessage: error.message });
}
// --- Input Validation ---
if (!name || !userMessage || !code) {
throw createError({
statusCode: 400,
statusMessage: "All fields are required.",
});
}
// Prevent abuse by checking rate limit before doing anything
if (isRateLimited(phoneNumber)) {
throw createError({
statusCode: 429,
statusMessage:
"You have already sent a message within the last week. Please try again later.",
});
}
if (userMessage.length > 140) {
throw createError({
statusCode: 400,
statusMessage: "Message cannot be longer than 140 characters.",
});
}
const printableAsciiRegex = /^[\x20-\x7E\n\r]*$/;
if (!printableAsciiRegex.test(userMessage)) {
throw createError({
statusCode: 400,
statusMessage: "Message contains non-ASCII or non-printable characters.",
});
}
// --- Server Configuration Check ---
if (!config.myPhoneNumber || !config.superSecretSalt) {
console.error(
"Server is not fully configured. MY_PHONE_NUMBER and SUPER_SECRET_SALT are required.",
);
throw createError({
statusCode: 500,
statusMessage: "A server configuration error occurred.",
});
}
// --- Verification ---
const isVerified = verifyTOTP(phoneNumber, config.superSecretSalt, code);
if (!isVerified) {
throw createError({
statusCode: 401,
statusMessage:
"Your verification code is invalid or has expired. Please try again.",
});
}
// --- Send Message ---
try {
const api = createSmsGatewayClient(config);
const finalMessage = `New message from ${name} ( ${phoneNumber} ) via your portfolio:\n\n"${userMessage}"`;
const message = {
phoneNumbers: [config.myPhoneNumber],
message: finalMessage,
};
const state = await api.send(message);
// On success, record the submission time to start the rate-limiting period.
recordSubmission(phoneNumber);
return { success: true, messageId: state.id };
} catch (error) {
console.error("Failed to send message:", error);
throw createError({
statusCode: 500,
statusMessage: "Failed to send message.",
});
}
});