Add honeypot, spam protection to contact form
This commit is contained in:
+208
-15
@@ -1,20 +1,213 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { connect } from 'node:net';
|
||||
import type { AddressInfo } from 'node:net';
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { svelteTesting } from '@testing-library/svelte/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import { createLogger, type Plugin, type ProxyOptions } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [svelteTesting({ autoCleanup: false }), sveltekit()],
|
||||
server: {
|
||||
proxy: {
|
||||
'/api/submit': {
|
||||
target: 'http://localhost:8000',
|
||||
rewrite: (path) => path.replace(/^\/api\/submit/, '/submit')
|
||||
}
|
||||
}
|
||||
},
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
setupFiles: ['./vitest.setup.ts'],
|
||||
include: ['src/**/*.test.ts']
|
||||
const packageJson = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8')) as {
|
||||
name: string;
|
||||
version: string;
|
||||
devDependencies?: Record<string, string>;
|
||||
};
|
||||
|
||||
const appName = packageJson.name;
|
||||
const appVersion = packageJson.version;
|
||||
const viteVersion = packageJson.devDependencies?.vite ?? 'unknown';
|
||||
const svelteKitVersion = packageJson.devDependencies?.['@sveltejs/kit'] ?? 'unknown';
|
||||
const submitProxyPath = '/api/submit';
|
||||
const submitProxyTarget = 'http://localhost:8000';
|
||||
const submitProxyDestination = `${submitProxyTarget}/submit`;
|
||||
|
||||
function resolvePort(target: URL) {
|
||||
if (target.port) {
|
||||
return Number(target.port);
|
||||
}
|
||||
|
||||
return target.protocol === 'https:' ? 443 : 80;
|
||||
}
|
||||
|
||||
function probeTcpPort(target: string, timeoutMs = 1200) {
|
||||
const url = new URL(target);
|
||||
|
||||
return new Promise<{ ok: true } | { ok: false; error: NodeJS.ErrnoException }>((resolve) => {
|
||||
const socket = connect({
|
||||
host: url.hostname,
|
||||
port: resolvePort(url)
|
||||
});
|
||||
|
||||
let settled = false;
|
||||
|
||||
const finish = (result: { ok: true } | { ok: false; error: NodeJS.ErrnoException }) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
|
||||
settled = true;
|
||||
socket.destroy();
|
||||
resolve(result);
|
||||
};
|
||||
|
||||
socket.once('connect', () => finish({ ok: true }));
|
||||
socket.once('error', (error: NodeJS.ErrnoException) => finish({ ok: false, error }));
|
||||
socket.setTimeout(timeoutMs, () =>
|
||||
finish({
|
||||
ok: false,
|
||||
error: Object.assign(new Error(`Timed out after ${timeoutMs}ms`), { code: 'ETIMEDOUT' })
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function formatProxyError(error: NodeJS.ErrnoException) {
|
||||
const code = error.code ?? 'UNKNOWN';
|
||||
const hint =
|
||||
code === 'ECONNREFUSED'
|
||||
? `Mail API is not reachable at ${submitProxyTarget}. Start the Python API before submitting the form.`
|
||||
: code === 'ETIMEDOUT'
|
||||
? `Mail API at ${submitProxyTarget} did not respond in time.`
|
||||
: 'The booking request could not be forwarded to the Mail API.';
|
||||
|
||||
return {
|
||||
code,
|
||||
hint
|
||||
};
|
||||
}
|
||||
|
||||
function createGoodwalkLogger() {
|
||||
const logger = createLogger();
|
||||
const baseError = logger.error.bind(logger);
|
||||
|
||||
logger.error = (msg, options) => {
|
||||
if (
|
||||
typeof msg === 'string' &&
|
||||
msg.includes('[vite] http proxy error') &&
|
||||
(msg.includes('/submit') || msg.includes(submitProxyPath))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
baseError(msg, options);
|
||||
};
|
||||
|
||||
return logger;
|
||||
}
|
||||
|
||||
function goodwalkDevServerPlugin(logger: ReturnType<typeof createLogger>): Plugin {
|
||||
let printedStartup = false;
|
||||
|
||||
const printStartup = async (serverHost: string | undefined, address: AddressInfo | string | null) => {
|
||||
if (printedStartup) {
|
||||
return;
|
||||
}
|
||||
|
||||
printedStartup = true;
|
||||
|
||||
const localUrls = server.resolvedUrls?.local ?? [];
|
||||
const networkUrls = server.resolvedUrls?.network ?? [];
|
||||
const reachable = await probeTcpPort(submitProxyTarget);
|
||||
const backendStatus = reachable.ok
|
||||
? `ready at ${submitProxyTarget}`
|
||||
: `${formatProxyError(reachable.error).code} at ${submitProxyTarget}`;
|
||||
|
||||
logger.info('');
|
||||
logger.info(`[goodwalk] ${appName} dev server ready`);
|
||||
logger.info(`[goodwalk] Version ${appVersion} | Node ${process.version} | Vite ${viteVersion} | SvelteKit ${svelteKitVersion}`);
|
||||
|
||||
if (localUrls.length) {
|
||||
logger.info(`[goodwalk] Local: ${localUrls.join(', ')}`);
|
||||
}
|
||||
|
||||
if (networkUrls.length) {
|
||||
logger.info(`[goodwalk] Network: ${networkUrls.join(', ')}`);
|
||||
}
|
||||
|
||||
if (!localUrls.length && address && typeof address !== 'string') {
|
||||
const host = serverHost && serverHost !== '0.0.0.0' ? serverHost : 'localhost';
|
||||
logger.info(`[goodwalk] Local: http://${host}:${address.port}/`);
|
||||
}
|
||||
|
||||
logger.info(`[goodwalk] Booking proxy: ${submitProxyPath} -> ${submitProxyDestination}`);
|
||||
|
||||
if (reachable.ok) {
|
||||
logger.info(`[goodwalk] Mail API: ${backendStatus}`);
|
||||
} else {
|
||||
logger.warn(`[goodwalk] Mail API: ${backendStatus}`);
|
||||
logger.warn(`[goodwalk] Hint: ${formatProxyError(reachable.error).hint}`);
|
||||
}
|
||||
|
||||
logger.info('');
|
||||
};
|
||||
|
||||
let server: import('vite').ViteDevServer;
|
||||
|
||||
return {
|
||||
name: 'goodwalk-dev-server-logging',
|
||||
configureServer(devServer) {
|
||||
server = devServer;
|
||||
|
||||
devServer.httpServer?.once('listening', () => {
|
||||
const address = devServer.httpServer?.address() ?? null;
|
||||
const configuredHost =
|
||||
typeof devServer.config.server.host === 'string' ? devServer.config.server.host : undefined;
|
||||
|
||||
void printStartup(configuredHost, address);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default defineConfig(() => {
|
||||
const logger = createGoodwalkLogger();
|
||||
|
||||
const submitProxy: ProxyOptions = {
|
||||
target: submitProxyTarget,
|
||||
rewrite: (path) => path.replace(/^\/api\/submit/, '/submit'),
|
||||
configure(proxy) {
|
||||
proxy.on('error', (error: NodeJS.ErrnoException, req, res) => {
|
||||
const { code, hint } = formatProxyError(error);
|
||||
const requestPath = req.url ?? submitProxyPath;
|
||||
const requestMethod = req.method ?? 'POST';
|
||||
|
||||
logger.error(
|
||||
[
|
||||
'',
|
||||
'[goodwalk] Booking proxy failed',
|
||||
`[goodwalk] Request: ${requestMethod} ${requestPath}`,
|
||||
`[goodwalk] Target: ${submitProxyDestination}`,
|
||||
`[goodwalk] Code: ${code}`,
|
||||
`[goodwalk] Message: ${error.message}`,
|
||||
`[goodwalk] Hint: ${hint}`,
|
||||
''
|
||||
].join('\n')
|
||||
);
|
||||
|
||||
if (res && 'writeHead' in res && !res.headersSent) {
|
||||
res.writeHead(502, { 'Content-Type': 'application/json' });
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
error: 'Mail API unavailable',
|
||||
detail: hint
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
customLogger: logger,
|
||||
plugins: [goodwalkDevServerPlugin(logger), svelteTesting({ autoCleanup: false }), sveltekit()],
|
||||
server: {
|
||||
proxy: {
|
||||
[submitProxyPath]: submitProxy
|
||||
}
|
||||
},
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
setupFiles: ['./vitest.setup.ts'],
|
||||
include: ['src/**/*.test.ts']
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user