feat: enhance contact form security and add animated hero
This commit is contained in:
parent
ea18dcdb8e
commit
8497cd819d
19 changed files with 320 additions and 112 deletions
|
|
@ -18,16 +18,13 @@ export function normalizeAndValidatePhoneNumber(rawPhoneNumber) {
|
|||
throw new Error("Phone number is required.");
|
||||
}
|
||||
|
||||
// 1. Strip all non-digit characters.
|
||||
const digitsOnly = rawPhoneNumber.replace(/\D/g, "");
|
||||
|
||||
// 2. If the number starts with a '1', remove it.
|
||||
let numberToValidate = digitsOnly;
|
||||
if (numberToValidate.startsWith("1")) {
|
||||
numberToValidate = numberToValidate.substring(1);
|
||||
}
|
||||
|
||||
// 3. Check if the resulting number is exactly 10 digits long.
|
||||
if (numberToValidate.length !== 10) {
|
||||
throw new Error("Please provide a valid 10-digit phone number.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,33 +1,99 @@
|
|||
// A shared, in-memory store for tracking submission timestamps.
|
||||
const submissionTimestamps = new Map();
|
||||
|
||||
// The rate-limiting period (1 week in milliseconds).
|
||||
const otpRequestTimestamps = new Map();
|
||||
|
||||
const ONE_WEEK_IN_MS = 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
const ONE_HOUR_IN_MS = 60 * 60 * 1000;
|
||||
|
||||
const MAX_OTP_REQUESTS_PER_HOUR = 3;
|
||||
|
||||
const MAX_MESSAGES_PER_WEEK = 3;
|
||||
|
||||
/**
|
||||
* Checks if a given phone number is currently rate-limited.
|
||||
* A phone number is considered rate-limited if a successful submission
|
||||
* was recorded within the last week.
|
||||
* Checks if a given phone number is currently rate-limited for message submissions.
|
||||
* A phone number is considered rate-limited if it has made 3 or more message submissions
|
||||
* within the last week.
|
||||
*
|
||||
* @param {string} phoneNumber The phone number to check.
|
||||
* @returns {boolean} True if the number is rate-limited, false otherwise.
|
||||
*/
|
||||
export function isRateLimited(phoneNumber) {
|
||||
const lastSubmissionTime = submissionTimestamps.get(phoneNumber);
|
||||
if (!lastSubmissionTime) {
|
||||
return false; // Not in the map, so not rate-limited.
|
||||
const submissionTimestampsArray = submissionTimestamps.get(phoneNumber);
|
||||
if (!submissionTimestampsArray || submissionTimestampsArray.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the time elapsed since the last submission is less than one week.
|
||||
return Date.now() - lastSubmissionTime < ONE_WEEK_IN_MS;
|
||||
const now = Date.now();
|
||||
const recentSubmissions = submissionTimestampsArray.filter(
|
||||
(timestamp) => now - timestamp < ONE_WEEK_IN_MS,
|
||||
);
|
||||
|
||||
if (recentSubmissions.length !== submissionTimestampsArray.length) {
|
||||
submissionTimestamps.set(phoneNumber, recentSubmissions);
|
||||
}
|
||||
|
||||
return recentSubmissions.length >= MAX_MESSAGES_PER_WEEK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a successful submission for a given phone number by setting
|
||||
* the current timestamp. This will start the 1-week rate-limiting period.
|
||||
* Records a successful submission for a given phone number by adding
|
||||
* the current timestamp to the submission history.
|
||||
*
|
||||
* @param {string} phoneNumber The phone number to record the submission for.
|
||||
*/
|
||||
export function recordSubmission(phoneNumber) {
|
||||
submissionTimestamps.set(phoneNumber, Date.now());
|
||||
const now = Date.now();
|
||||
const existingSubmissions = submissionTimestamps.get(phoneNumber) || [];
|
||||
|
||||
const recentSubmissions = existingSubmissions.filter(
|
||||
(timestamp) => now - timestamp < ONE_WEEK_IN_MS,
|
||||
);
|
||||
recentSubmissions.push(now);
|
||||
|
||||
submissionTimestamps.set(phoneNumber, recentSubmissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given phone number is currently rate-limited for OTP requests.
|
||||
* A phone number is considered rate-limited if it has made 3 or more OTP requests
|
||||
* within the last hour.
|
||||
*
|
||||
* @param {string} phoneNumber The phone number to check.
|
||||
* @returns {boolean} True if the number is rate-limited for OTP, false otherwise.
|
||||
*/
|
||||
export function isOtpRateLimited(phoneNumber) {
|
||||
const requestTimestamps = otpRequestTimestamps.get(phoneNumber);
|
||||
if (!requestTimestamps || requestTimestamps.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const recentRequests = requestTimestamps.filter(
|
||||
(timestamp) => now - timestamp < ONE_HOUR_IN_MS,
|
||||
);
|
||||
|
||||
if (recentRequests.length !== requestTimestamps.length) {
|
||||
otpRequestTimestamps.set(phoneNumber, recentRequests);
|
||||
}
|
||||
|
||||
return recentRequests.length >= MAX_OTP_REQUESTS_PER_HOUR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Records an OTP request for a given phone number by adding
|
||||
* the current timestamp to the request history.
|
||||
*
|
||||
* @param {string} phoneNumber The phone number to record the OTP request for.
|
||||
*/
|
||||
export function recordOtpRequest(phoneNumber) {
|
||||
const now = Date.now();
|
||||
const existingRequests = otpRequestTimestamps.get(phoneNumber) || [];
|
||||
|
||||
const recentRequests = existingRequests.filter(
|
||||
(timestamp) => now - timestamp < ONE_HOUR_IN_MS,
|
||||
);
|
||||
recentRequests.push(now);
|
||||
|
||||
otpRequestTimestamps.set(phoneNumber, recentRequests);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import { authenticator } from "otplib";
|
||||
import { createHash } from "crypto";
|
||||
|
||||
// These settings must be consistent between generation and verification.
|
||||
authenticator.options = {
|
||||
step: 60, // OTP is valid for 1 minute per window
|
||||
window: [5, 1], // Allow tokens from 5 previous and 1 future time-steps.
|
||||
step: 60,
|
||||
window: [5, 1],
|
||||
digits: 6,
|
||||
};
|
||||
|
||||
|
|
@ -46,8 +45,6 @@ export function generateTOTP(phoneNumber, salt) {
|
|||
*/
|
||||
export function verifyTOTP(phoneNumber, salt, token) {
|
||||
const userSecret = getUserSecret(phoneNumber, salt);
|
||||
// The `verify` method checks the token against the current and adjacent
|
||||
// time-steps, as configured in the options.
|
||||
return authenticator.verify({ token, secret: userSecret });
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue