171 lines
4.3 KiB
JavaScript
171 lines
4.3 KiB
JavaScript
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,
|
|
interactionData,
|
|
website,
|
|
} = await readBody(event);
|
|
|
|
let phoneNumber;
|
|
try {
|
|
phoneNumber = normalizeAndValidatePhoneNumber(rawPhoneNumber);
|
|
} catch (error) {
|
|
throw createError({ statusCode: 400, statusMessage: error.message });
|
|
}
|
|
|
|
if (website) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
statusMessage: "Spam detected.",
|
|
});
|
|
}
|
|
|
|
if (interactionData) {
|
|
if (interactionData.timeSpent < 3000) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
statusMessage: "Submission too fast.",
|
|
});
|
|
}
|
|
|
|
if (
|
|
interactionData.mouseActivity === 0 &&
|
|
interactionData.keyboardActivity === 0
|
|
) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
statusMessage: "No user interaction detected.",
|
|
});
|
|
}
|
|
}
|
|
|
|
if (!name || !userMessage || !code) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
statusMessage: "All fields are required.",
|
|
});
|
|
}
|
|
|
|
const nameRegex = /^[a-zA-Z\s'-]{2,50}$/;
|
|
if (!nameRegex.test(name.trim())) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
statusMessage: "Name contains invalid characters or format.",
|
|
});
|
|
}
|
|
|
|
if (isRateLimited(phoneNumber)) {
|
|
throw createError({
|
|
statusCode: 429,
|
|
statusMessage:
|
|
"You have reached the maximum of 3 messages per 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.",
|
|
});
|
|
}
|
|
|
|
if (userMessage.trim().length < 10) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
statusMessage: "Message is too short.",
|
|
});
|
|
}
|
|
|
|
const spamKeywords = [
|
|
"viagra",
|
|
"casino",
|
|
"lottery",
|
|
"winner",
|
|
"click here",
|
|
"free money",
|
|
"urgent",
|
|
"limited time",
|
|
];
|
|
const hasSpamKeywords = spamKeywords.some((keyword) =>
|
|
userMessage.toLowerCase().includes(keyword.toLowerCase()),
|
|
);
|
|
|
|
if (hasSpamKeywords) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
statusMessage: "Message contains inappropriate content.",
|
|
});
|
|
}
|
|
|
|
if (/([a-zA-Z])\1{4,}/.test(userMessage)) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
statusMessage: "Message contains excessive repeated characters.",
|
|
});
|
|
}
|
|
|
|
const uppercaseRatio =
|
|
(userMessage.match(/[A-Z]/g) || []).length / userMessage.length;
|
|
if (uppercaseRatio > 0.7 && userMessage.length > 10) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
statusMessage: "Message contains excessive uppercase text.",
|
|
});
|
|
}
|
|
|
|
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.",
|
|
});
|
|
}
|
|
|
|
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.",
|
|
});
|
|
}
|
|
|
|
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);
|
|
|
|
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.",
|
|
});
|
|
}
|
|
});
|