Updated security and form logic
This commit is contained in:
@@ -8,7 +8,6 @@ import tailwind from '@astrojs/tailwind';
|
|||||||
import mdx from '@astrojs/mdx';
|
import mdx from '@astrojs/mdx';
|
||||||
import react from '@astrojs/react';
|
import react from '@astrojs/react';
|
||||||
import partytown from '@astrojs/partytown';
|
import partytown from '@astrojs/partytown';
|
||||||
import netlify from '@astrojs/netlify';
|
|
||||||
import icon from 'astro-icon';
|
import icon from 'astro-icon';
|
||||||
import compress from 'astro-compress';
|
import compress from 'astro-compress';
|
||||||
import type { AstroIntegration } from 'astro';
|
import type { AstroIntegration } from 'astro';
|
||||||
@@ -23,21 +22,40 @@ const whenExternalScripts = (items: (() => AstroIntegration) | (() => AstroInteg
|
|||||||
hasExternalScripts ? (Array.isArray(items) ? items.map((item) => item()) : [items()]) : [];
|
hasExternalScripts ? (Array.isArray(items) ? items.map((item) => item()) : [items()]) : [];
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
output: 'server',
|
output: 'static',
|
||||||
adapter: netlify(),
|
site: 'https://www.365devnet.com',
|
||||||
|
|
||||||
i18n: {
|
i18n: {
|
||||||
locales: ["en", "de", "nl", "fr"],
|
locales: ["en", "de", "nl", "fr"],
|
||||||
defaultLocale: "en",
|
defaultLocale: "en",
|
||||||
|
routing: {
|
||||||
|
strategy: 'prefix-always',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
integrations: [
|
integrations: [
|
||||||
react(),
|
react({
|
||||||
|
include: ['**/react/*'],
|
||||||
|
exclude: ['**/react/*.test.{js,jsx,ts,tsx}'],
|
||||||
|
}),
|
||||||
tailwind({
|
tailwind({
|
||||||
applyBaseStyles: false,
|
applyBaseStyles: false,
|
||||||
|
config: {
|
||||||
|
future: {
|
||||||
|
hoverOnlyWhenSupported: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
sitemap({
|
||||||
|
changefreq: 'weekly',
|
||||||
|
priority: 0.7,
|
||||||
|
lastmod: new Date(),
|
||||||
|
}),
|
||||||
|
mdx({
|
||||||
|
drafts: false,
|
||||||
|
syntaxHighlight: 'prism',
|
||||||
|
rehypePlugins: [responsiveTablesRehypePlugin, lazyImagesRehypePlugin],
|
||||||
}),
|
}),
|
||||||
sitemap(),
|
|
||||||
mdx(),
|
|
||||||
icon({
|
icon({
|
||||||
include: {
|
include: {
|
||||||
tabler: ['*'],
|
tabler: ['*'],
|
||||||
@@ -67,10 +85,25 @@ export default defineConfig({
|
|||||||
HTML: {
|
HTML: {
|
||||||
'html-minifier-terser': {
|
'html-minifier-terser': {
|
||||||
removeAttributeQuotes: false,
|
removeAttributeQuotes: false,
|
||||||
|
collapseWhitespace: true,
|
||||||
|
removeComments: true,
|
||||||
|
minifyCSS: true,
|
||||||
|
minifyJS: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Image: {
|
||||||
|
sharp: {
|
||||||
|
quality: 80,
|
||||||
|
progressive: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
JavaScript: {
|
||||||
|
compress: true,
|
||||||
|
mangle: true,
|
||||||
|
format: {
|
||||||
|
comments: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Image: true,
|
|
||||||
JavaScript: true,
|
|
||||||
SVG: false,
|
SVG: false,
|
||||||
Logger: 1,
|
Logger: 1,
|
||||||
}),
|
}),
|
||||||
@@ -82,11 +115,25 @@ export default defineConfig({
|
|||||||
|
|
||||||
image: {
|
image: {
|
||||||
domains: ['cdn.pixabay.com', 'raw.githubusercontent.com'],
|
domains: ['cdn.pixabay.com', 'raw.githubusercontent.com'],
|
||||||
|
service: {
|
||||||
|
entrypoint: 'astro/assets/services/sharp',
|
||||||
|
},
|
||||||
|
remotePatterns: [
|
||||||
|
{
|
||||||
|
protocol: 'https',
|
||||||
|
hostname: 'cdn.pixabay.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
protocol: 'https',
|
||||||
|
hostname: 'raw.githubusercontent.com',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
markdown: {
|
markdown: {
|
||||||
remarkPlugins: [readingTimeRemarkPlugin],
|
remarkPlugins: [readingTimeRemarkPlugin],
|
||||||
rehypePlugins: [responsiveTablesRehypePlugin, lazyImagesRehypePlugin],
|
rehypePlugins: [responsiveTablesRehypePlugin, lazyImagesRehypePlugin],
|
||||||
|
syntaxHighlight: 'prism',
|
||||||
},
|
},
|
||||||
|
|
||||||
vite: {
|
vite: {
|
||||||
@@ -95,5 +142,22 @@ export default defineConfig({
|
|||||||
'~': path.resolve(__dirname, './src'),
|
'~': path.resolve(__dirname, './src'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
build: {
|
||||||
|
target: 'esnext',
|
||||||
|
minify: 'terser',
|
||||||
|
cssMinify: true,
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks: {
|
||||||
|
'react-vendor': ['react', 'react-dom'],
|
||||||
|
'ui-vendor': ['@astrojs/react', '@astrojs/tailwind'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
optimizeDeps: {
|
||||||
|
include: ['react', 'react-dom'],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
2490
package-lock.json
generated
2490
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
21
package.json
21
package.json
@@ -19,10 +19,15 @@
|
|||||||
"check:prettier": "prettier --check .",
|
"check:prettier": "prettier --check .",
|
||||||
"fix": "npm run fix:eslint && npm run fix:prettier",
|
"fix": "npm run fix:eslint && npm run fix:prettier",
|
||||||
"fix:eslint": "eslint --fix .",
|
"fix:eslint": "eslint --fix .",
|
||||||
"fix:prettier": "prettier -w ."
|
"fix:prettier": "prettier -w .",
|
||||||
|
"analyze": "astro build && astro-compress --analyze",
|
||||||
|
"clean": "rimraf dist .astro",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"lint": "eslint . --ext .js,.jsx,.ts,.tsx,.astro",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"prepare": "husky install"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/netlify": "^6.1.0",
|
|
||||||
"@astrojs/react": "^4.2.0",
|
"@astrojs/react": "^4.2.0",
|
||||||
"@astrojs/rss": "^4.0.11",
|
"@astrojs/rss": "^4.0.11",
|
||||||
"@astrojs/sitemap": "^3.2.1",
|
"@astrojs/sitemap": "^3.2.1",
|
||||||
@@ -67,16 +72,28 @@
|
|||||||
"eslint": "^9.18.0",
|
"eslint": "^9.18.0",
|
||||||
"eslint-plugin-astro": "^1.3.1",
|
"eslint-plugin-astro": "^1.3.1",
|
||||||
"globals": "^15.14.0",
|
"globals": "^15.14.0",
|
||||||
|
"husky": "^9.0.11",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
|
"lint-staged": "^15.2.2",
|
||||||
"mdast-util-to-string": "^4.0.0",
|
"mdast-util-to-string": "^4.0.0",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
"prettier-plugin-astro": "^0.14.1",
|
"prettier-plugin-astro": "^0.14.1",
|
||||||
"reading-time": "^1.5.0",
|
"reading-time": "^1.5.0",
|
||||||
|
"rimraf": "^5.0.5",
|
||||||
"sharp": "0.33.5",
|
"sharp": "0.33.5",
|
||||||
"tailwind-merge": "^2.6.0",
|
"tailwind-merge": "^2.6.0",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^5.7.3",
|
||||||
"typescript-eslint": "^8.21.0",
|
"typescript-eslint": "^8.21.0",
|
||||||
"unist-util-visit": "^5.0.0"
|
"unist-util-visit": "^5.0.0"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.{js,jsx,ts,tsx,astro}": [
|
||||||
|
"eslint --fix",
|
||||||
|
"prettier --write"
|
||||||
|
],
|
||||||
|
"*.{json,md,mdx,yaml,yml}": [
|
||||||
|
"prettier --write"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -13,6 +13,42 @@ const { inputs, textarea, disclaimer, button = 'Contact us', description = '' }
|
|||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
background-color: rgba(239, 68, 68, 0.05);
|
background-color: rgba(239, 68, 68, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Honeypot field styling */
|
||||||
|
.hidden-field {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Rate limit message styling */
|
||||||
|
.rate-limit-message {
|
||||||
|
color: #ef4444;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading state */
|
||||||
|
.button-loading {
|
||||||
|
opacity: 0.7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-loading::after {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
border-top-color: transparent;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
@@ -20,8 +56,6 @@ const { inputs, textarea, disclaimer, button = 'Contact us', description = '' }
|
|||||||
name="contact"
|
name="contact"
|
||||||
method="POST"
|
method="POST"
|
||||||
class="needs-validation"
|
class="needs-validation"
|
||||||
data-netlify="true"
|
|
||||||
data-netlify-honeypot="bot-field"
|
|
||||||
novalidate
|
novalidate
|
||||||
>
|
>
|
||||||
<!-- Form status messages -->
|
<!-- Form status messages -->
|
||||||
@@ -33,13 +67,19 @@ const { inputs, textarea, disclaimer, button = 'Contact us', description = '' }
|
|||||||
There was an error sending your message. Please check all fields and try again.
|
There was an error sending your message. Please check all fields and try again.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Netlify form name -->
|
<div id="rate-limit-error" class="hidden mb-6 p-4 bg-yellow-100 border border-yellow-200 text-yellow-700 rounded-lg">
|
||||||
<input type="hidden" name="form-name" value="contact" />
|
Too many attempts. Please try again later.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Honeypot fields -->
|
||||||
|
<div class="hidden-field">
|
||||||
|
<input type="text" name="website" tabindex="-1" autocomplete="off" />
|
||||||
|
<input type="email" name="email_confirm" tabindex="-1" autocomplete="off" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Timestamp field for rate limiting -->
|
||||||
|
<input type="hidden" name="timestamp" id="form-timestamp" />
|
||||||
|
|
||||||
<!-- Honeypot field to prevent spam -->
|
|
||||||
<p class="hidden">
|
|
||||||
<label>Don't fill this out if you're human: <input name="bot-field" /></label>
|
|
||||||
</p>
|
|
||||||
{
|
{
|
||||||
inputs &&
|
inputs &&
|
||||||
inputs.map(
|
inputs.map(
|
||||||
@@ -60,6 +100,9 @@ const { inputs, textarea, disclaimer, button = 'Contact us', description = '' }
|
|||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
class="py-3 px-4 block w-full text-md rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-slate-900"
|
class="py-3 px-4 block w-full text-md rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-slate-900"
|
||||||
required={required}
|
required={required}
|
||||||
|
pattern={type === 'email' ? '[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$' : undefined}
|
||||||
|
minlength={type === 'text' ? '2' : undefined}
|
||||||
|
maxlength={type === 'text' ? '100' : undefined}
|
||||||
/>
|
/>
|
||||||
<div class="invalid-feedback hidden text-red-600 text-sm mt-1" />
|
<div class="invalid-feedback hidden text-red-600 text-sm mt-1" />
|
||||||
</div>
|
</div>
|
||||||
@@ -81,6 +124,8 @@ const { inputs, textarea, disclaimer, button = 'Contact us', description = '' }
|
|||||||
placeholder={textarea.placeholder}
|
placeholder={textarea.placeholder}
|
||||||
class="py-3 px-4 block w-full text-md rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-slate-900"
|
class="py-3 px-4 block w-full text-md rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-slate-900"
|
||||||
required
|
required
|
||||||
|
minlength="10"
|
||||||
|
maxlength="1000"
|
||||||
/>
|
/>
|
||||||
<div class="invalid-feedback hidden text-red-600 text-sm mt-1" />
|
<div class="invalid-feedback hidden text-red-600 text-sm mt-1" />
|
||||||
</div>
|
</div>
|
||||||
@@ -113,7 +158,7 @@ const { inputs, textarea, disclaimer, button = 'Contact us', description = '' }
|
|||||||
{
|
{
|
||||||
button && (
|
button && (
|
||||||
<div class="mt-10 grid">
|
<div class="mt-10 grid">
|
||||||
<Button variant="primary" type="submit">
|
<Button variant="primary" type="submit" id="submit-button">
|
||||||
{button}
|
{button}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -130,55 +175,261 @@ const { inputs, textarea, disclaimer, button = 'Contact us', description = '' }
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// Rate limiting configuration
|
||||||
|
const RATE_LIMIT_WINDOW = 3600000; // 1 hour in milliseconds
|
||||||
|
const MAX_SUBMISSIONS = 3; // Maximum submissions per hour
|
||||||
|
const STORAGE_KEY = 'form_submissions';
|
||||||
|
|
||||||
|
// Form validation and submission handling
|
||||||
const form = document.getElementById('contact-form') as HTMLFormElement;
|
const form = document.getElementById('contact-form') as HTMLFormElement;
|
||||||
|
const submitButton = document.getElementById('submit-button') as HTMLButtonElement;
|
||||||
|
|
||||||
if (form) {
|
if (form) {
|
||||||
|
// Set initial timestamp
|
||||||
|
const timestampInput = document.getElementById('form-timestamp') as HTMLInputElement;
|
||||||
|
if (timestampInput) {
|
||||||
|
timestampInput.value = Date.now().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate form on submit
|
||||||
form.addEventListener('submit', async (event) => {
|
form.addEventListener('submit', async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const formData = new FormData(form);
|
// Disable submit button and show loading state
|
||||||
const data = Object.fromEntries(formData.entries());
|
if (submitButton) {
|
||||||
|
submitButton.disabled = true;
|
||||||
|
submitButton.classList.add('button-loading');
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/.netlify/functions/contact', {
|
// Check rate limit
|
||||||
method: 'POST',
|
if (isRateLimited()) {
|
||||||
body: JSON.stringify(data),
|
showRateLimitError();
|
||||||
});
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (response.ok) {
|
// Validate form
|
||||||
const result = await response.json();
|
if (!validateForm()) {
|
||||||
console.log(result.message); // Log success message
|
return;
|
||||||
const successElement = document.getElementById('form-success');
|
|
||||||
if (successElement) {
|
|
||||||
successElement.classList.remove('hidden');
|
|
||||||
}
|
|
||||||
const errorElement = document.getElementById('form-error');
|
|
||||||
if (errorElement) {
|
|
||||||
errorElement.classList.add('hidden');
|
|
||||||
}
|
|
||||||
form.reset(); // Clear the form
|
|
||||||
} else {
|
|
||||||
console.error('Error:', response.status);
|
|
||||||
const errorElement = document.getElementById('form-error');
|
|
||||||
if (errorElement) {
|
|
||||||
errorElement.classList.remove('hidden');
|
|
||||||
}
|
|
||||||
const successElement = document.getElementById('form-success');
|
|
||||||
if (successElement) {
|
|
||||||
successElement.classList.add('hidden');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get form data
|
||||||
|
const formData = new FormData(form);
|
||||||
|
const name = formData.get('name') as string;
|
||||||
|
const email = formData.get('email') as string;
|
||||||
|
const message = formData.get('message') as string;
|
||||||
|
|
||||||
|
// Check honeypot fields
|
||||||
|
if (formData.get('website') || formData.get('email_confirm')) {
|
||||||
|
showError('Invalid form submission');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for spam
|
||||||
|
if (isSpam(name, email, message)) {
|
||||||
|
showError('Your message was flagged as potential spam. Please revise and try again.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create email body
|
||||||
|
const subject = `Contact Form Submission from ${name}`;
|
||||||
|
const body = `
|
||||||
|
Name: ${name}
|
||||||
|
Email: ${email}
|
||||||
|
|
||||||
|
Message:
|
||||||
|
${message}
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
// Create mailto link
|
||||||
|
const mailtoLink = `mailto:info@365devnet.eu?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
|
||||||
|
|
||||||
|
// Open default email client
|
||||||
|
window.location.href = mailtoLink;
|
||||||
|
|
||||||
|
// Record submission
|
||||||
|
recordSubmission();
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
showSuccess();
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
form.reset();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error:', error);
|
console.error('Form submission error:', error);
|
||||||
const errorElement = document.getElementById('form-error');
|
showError('An unexpected error occurred. Please try again.');
|
||||||
if (errorElement) {
|
} finally {
|
||||||
errorElement.classList.remove('hidden');
|
// Re-enable submit button and remove loading state
|
||||||
}
|
if (submitButton) {
|
||||||
const successElement = document.getElementById('form-success');
|
submitButton.disabled = false;
|
||||||
if (successElement) {
|
submitButton.classList.remove('button-loading');
|
||||||
successElement.classList.add('hidden');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rate limiting functions
|
||||||
|
function isRateLimited() {
|
||||||
|
const submissions = getSubmissions();
|
||||||
|
const now = Date.now();
|
||||||
|
const recentSubmissions = submissions.filter(time => now - time < RATE_LIMIT_WINDOW);
|
||||||
|
return recentSubmissions.length >= MAX_SUBMISSIONS;
|
||||||
|
}
|
||||||
|
|
||||||
|
function recordSubmission() {
|
||||||
|
const submissions = getSubmissions();
|
||||||
|
submissions.push(Date.now());
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(submissions));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSubmissions() {
|
||||||
|
const stored = localStorage.getItem(STORAGE_KEY);
|
||||||
|
return stored ? JSON.parse(stored) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form validation
|
||||||
|
function validateForm() {
|
||||||
|
const name = form.querySelector('input[name="name"]') as HTMLInputElement;
|
||||||
|
const email = form.querySelector('input[name="email"]') as HTMLInputElement;
|
||||||
|
const message = form.querySelector('textarea[name="message"]') as HTMLTextAreaElement;
|
||||||
|
const disclaimer = form.querySelector('input[name="disclaimer"]') as HTMLInputElement;
|
||||||
|
|
||||||
|
let isValid = true;
|
||||||
|
|
||||||
|
// Validate name
|
||||||
|
if (!name.value || name.value.length < 2) {
|
||||||
|
showFieldError(name, 'Name must be at least 2 characters long');
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate email
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
if (!email.value || !emailRegex.test(email.value)) {
|
||||||
|
showFieldError(email, 'Please enter a valid email address');
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate message
|
||||||
|
if (!message.value || message.value.length < 10) {
|
||||||
|
showFieldError(message, 'Message must be at least 10 characters long');
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate disclaimer
|
||||||
|
if (!disclaimer.checked) {
|
||||||
|
showFieldError(disclaimer, 'Please accept the terms');
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced spam detection
|
||||||
|
function isSpam(name: string, email: string, message: string) {
|
||||||
|
const spamPatterns = [
|
||||||
|
'viagra', 'cialis', 'casino', 'lottery', 'prize', 'winner',
|
||||||
|
'free money', 'buy now', 'click here', 'earn money', 'make money',
|
||||||
|
'investment opportunity', 'bitcoin', 'cryptocurrency', 'forex',
|
||||||
|
'weight loss', 'diet pill', 'enlargement', 'cheap medication',
|
||||||
|
'urgent', 'congratulations', 'winner', 'selected', 'lottery',
|
||||||
|
'inheritance', 'bank transfer', 'bank account', 'wire transfer',
|
||||||
|
'nigerian', 'prince', 'royal', 'dear friend', 'dear sir',
|
||||||
|
'dear madam', 'dear beloved', 'dear winner', 'dear beneficiary'
|
||||||
|
];
|
||||||
|
|
||||||
|
const content = `${name} ${email} ${message}`.toLowerCase();
|
||||||
|
|
||||||
|
// Check for spam keywords
|
||||||
|
if (spamPatterns.some(pattern => content.includes(pattern))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for excessive capitalization
|
||||||
|
const uppercaseRatio = (message.match(/[A-Z]/g) || []).length / message.length;
|
||||||
|
if (uppercaseRatio > 0.5 && message.length > 20) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for excessive special characters
|
||||||
|
const specialChars = '!@#$%^&*()_+-=[]{}\\|;:\'",.<>/?';
|
||||||
|
const specialCharCount = [...message].filter(char => specialChars.includes(char)).length;
|
||||||
|
const specialCharRatio = specialCharCount / message.length;
|
||||||
|
if (specialCharRatio > 0.3 && message.length > 20) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for excessive URLs
|
||||||
|
const urlCount = message.split('http').length - 1;
|
||||||
|
if (urlCount > 2) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for suspicious email patterns
|
||||||
|
const suspiciousEmailPatterns = [
|
||||||
|
/[0-9]{10,}@/, // Numbers in email
|
||||||
|
/[a-z]{1,2}[0-9]{5,}@/, // Short name with many numbers
|
||||||
|
/[a-z]{20,}@/, // Very long username
|
||||||
|
/@[a-z]{1,2}\.[a-z]{1,2}$/, // Short domain
|
||||||
|
/@[0-9]+\.[a-z]+$/, // IP-like domain
|
||||||
|
];
|
||||||
|
|
||||||
|
if (suspiciousEmailPatterns.some(pattern => pattern.test(email))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for suspicious name patterns
|
||||||
|
const suspiciousNamePatterns = [
|
||||||
|
/^[a-z]{1,2}[0-9]{5,}$/, // Short name with many numbers
|
||||||
|
/^[0-9]{10,}$/, // All numbers
|
||||||
|
/^[a-z]{20,}$/, // Very long name
|
||||||
|
];
|
||||||
|
|
||||||
|
if (suspiciousNamePatterns.some(pattern => pattern.test(name.toLowerCase()))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI helper functions
|
||||||
|
function showSuccess() {
|
||||||
|
const successElement = document.getElementById('form-success');
|
||||||
|
const errorElement = document.getElementById('form-error');
|
||||||
|
const rateLimitElement = document.getElementById('rate-limit-error');
|
||||||
|
|
||||||
|
if (successElement) successElement.classList.remove('hidden');
|
||||||
|
if (errorElement) errorElement.classList.add('hidden');
|
||||||
|
if (rateLimitElement) rateLimitElement.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showError(message: string) {
|
||||||
|
const errorElement = document.getElementById('form-error');
|
||||||
|
const successElement = document.getElementById('form-success');
|
||||||
|
const rateLimitElement = document.getElementById('rate-limit-error');
|
||||||
|
|
||||||
|
if (errorElement) {
|
||||||
|
errorElement.textContent = message;
|
||||||
|
errorElement.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
if (successElement) successElement.classList.add('hidden');
|
||||||
|
if (rateLimitElement) rateLimitElement.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showRateLimitError() {
|
||||||
|
const rateLimitElement = document.getElementById('rate-limit-error');
|
||||||
|
const successElement = document.getElementById('form-success');
|
||||||
|
const errorElement = document.getElementById('form-error');
|
||||||
|
|
||||||
|
if (rateLimitElement) rateLimitElement.classList.remove('hidden');
|
||||||
|
if (successElement) successElement.classList.add('hidden');
|
||||||
|
if (errorElement) errorElement.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showFieldError(element: HTMLElement, message: string) {
|
||||||
|
const feedbackElement = element.nextElementSibling as HTMLElement;
|
||||||
|
if (feedbackElement) {
|
||||||
|
feedbackElement.textContent = message;
|
||||||
|
feedbackElement.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
Reference in New Issue
Block a user