E2E Test Suite / Form Interaction Tests (push) Failing after 10m53s
E2E Test Suite / Security Header Tests (push) Failing after 10m39s
E2E Test Suite / Mobile Device Tests (push) Failing after 11m31s
E2E Test Suite / Cross-Browser Regression (webkit) (push) Failing after 12m23s
E2E Test Suite / Cross-Browser Regression (firefox) (push) Failing after 13m16s
E2E Test Suite / Cross-Browser Regression (chromium) (push) Failing after 14m8s
E2E Test Suite / Destructive & Chaos Tests (push) Failing after 15m0s
Ping Search Engines / Notify Search Engines (push) Failing after 14m47s
E2E Test Suite / Smoke Tests (P0) (push) Failing after 21m30s
E2E Test Suite / Critical User Journeys (push) Has been skipped
E2E Test Suite / API Integration Tests (push) Has been skipped
E2E Test Suite / Test Report Summary (push) Has been cancelled
Deploy to Production / Post-Deploy Verification (push) Failing after 14m58s
Deploy to Production / Build & Verify (push) Failing after 19m22s
Deploy to Production / Pre-Deploy Tests (push) Failing after 14m59s
Deploy to Production / Deploy to Railway (push) Failing after 14m58s
Deploy to Production / Deploy to Render (push) Failing after 14m57s
Deploy to Production / Deploy to VPS (PM2) (push) Failing after 14m6s
Deploy to Production / Deploy to Fly.io (push) Failing after 14m5s
Deploy to Production / Notify on Failure (push) Failing after 14m58s
Addresses contact-page audit points 22–27: - Confirmed no '200+ companies' credibility claim; canonical stat set retained - Service dropdown now matches real services, led by ERP/Enterprise + Government IT; removed Cloud Services and AI & ML, added UI/UX Design - Backend allowlist + labels updated to accept 'ui-ux' and reflect real service names - H1 paired with value prop: free 1-page proposal in 24 hours - '< 24h response' promise badge moved above the form - Clarified contradictory hours to 'We reply within 24 business hours, Mon–Fri' - Reduced message textarea height (rows 5→4) and max length (5000→2000) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
291 lines
9.0 KiB
Markdown
291 lines
9.0 KiB
Markdown
# Form Backend Integration
|
||
|
||
**Agent**: backend-specialist
|
||
**Date**: 2026-03-21
|
||
**Status**: Complete ✓
|
||
|
||
---
|
||
|
||
## Overview
|
||
|
||
Two API endpoints handle form submissions for the WorkRoot site:
|
||
|
||
| Endpoint | File | Purpose |
|
||
|----------|------|---------|
|
||
| `POST /api/contact` | `src/pages/api/contact.ts` | Contact form submissions |
|
||
| `POST /api/newsletter` | `src/pages/api/newsletter.ts` | Newsletter subscriptions |
|
||
|
||
Both endpoints include server-side validation, rate limiting, spam protection, and graceful degradation when optional dependencies (SMTP, newsletter providers) are not configured.
|
||
|
||
---
|
||
|
||
## Architecture
|
||
|
||
### Contact Form (`/api/contact`)
|
||
|
||
**Flow:**
|
||
1. Client submits form → `POST /api/contact` (JSON body)
|
||
2. Rate limit check (5 requests / IP / hour, sliding window)
|
||
3. Honeypot field check (silent success if bot detected)
|
||
4. Server-side field validation
|
||
5. Email delivery via SMTP (nodemailer) — logs to console if SMTP not configured
|
||
6. JSON response returned
|
||
|
||
**Fields validated:**
|
||
- `name` — required, 2–100 characters
|
||
- `email` — required, valid format, max 254 chars
|
||
- `phone` — optional; if present, must match phone regex
|
||
- `subject` — required, allowlist enum (erp-solutions, government-systems, web-development, mobile-development, ui-ux, consulting, other; legacy values cloud-services, ai-ml, support still accepted by the API but no longer offered in the contact-page dropdown)
|
||
- `message` — required, 10–2000 characters
|
||
- `website` — honeypot, must be empty
|
||
|
||
**Rate limit:** 5 requests per IP per hour
|
||
|
||
---
|
||
|
||
### Newsletter (`/api/newsletter`)
|
||
|
||
**Flow:**
|
||
1. Client submits email → `POST /api/newsletter` (JSON body)
|
||
2. Rate limit check (3 requests / IP / hour)
|
||
3. Email format validation
|
||
4. Subscribe via configured provider (Mailchimp → ConvertKit → SMTP notification → log-only)
|
||
5. JSON response returned
|
||
|
||
**Rate limit:** 3 requests per IP per hour
|
||
|
||
---
|
||
|
||
## Configuration
|
||
|
||
Set these environment variables (see `.env.example`):
|
||
|
||
### Email (SMTP) — for contact form delivery and newsletter fallback
|
||
```env
|
||
SMTP_HOST=smtp.gmail.com
|
||
SMTP_PORT=587
|
||
SMTP_USER=your_email@example.com
|
||
SMTP_PASS=your_app_password
|
||
CONTACT_EMAIL=hello@workroot.in
|
||
```
|
||
|
||
> **Gmail tip:** Enable 2FA, then create an App Password at https://myaccount.google.com/apppasswords
|
||
|
||
### Newsletter — pick ONE provider
|
||
|
||
**Mailchimp (recommended):**
|
||
```env
|
||
MAILCHIMP_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-us1
|
||
MAILCHIMP_LIST_ID=abc123def456
|
||
MAILCHIMP_DC=us1
|
||
```
|
||
Uses double opt-in (`status: "pending"`). Handles already-subscribed gracefully.
|
||
|
||
**ConvertKit:**
|
||
```env
|
||
CONVERTKIT_API_KEY=your_api_key
|
||
CONVERTKIT_FORM_ID=1234567
|
||
```
|
||
|
||
> If no provider is configured, submissions are logged to server console. Safe for development and staging.
|
||
|
||
---
|
||
|
||
## Dependencies
|
||
|
||
### nodemailer (installed)
|
||
The SMTP functionality requires `nodemailer`. It is installed as a production dependency:
|
||
|
||
```bash
|
||
npm install nodemailer
|
||
npm install --save-dev @types/nodemailer
|
||
```
|
||
|
||
Both are now in `package.json`. The dynamic import (`await import('nodemailer')`) in the API routes handles the case where the module is unavailable with graceful degradation.
|
||
|
||
---
|
||
|
||
## Graceful Degradation Matrix
|
||
|
||
| Config state | Contact form | Newsletter |
|
||
|---|---|---|
|
||
| SMTP not configured | Logs submission to console, returns 200 | Falls through to next provider |
|
||
| SMTP configured, nodemailer unavailable | Warns and logs only, returns 200 | Warns and logs only, returns 200 |
|
||
| No newsletter provider | — | Logs to console, returns 200 |
|
||
| SMTP set, no newsletter provider | Sends email | Falls back to SMTP notification |
|
||
| All configured | Sends HTML email | Sends to Mailchimp/ConvertKit |
|
||
|
||
This prevents the site from breaking in any environment, including when secrets aren't yet configured.
|
||
|
||
---
|
||
|
||
## Security
|
||
|
||
### Spam Protection
|
||
- **Honeypot field**: `website` input hidden from users via CSS (`position: absolute; left: -9999px`). Bots that fill all fields are detected server-side and receive a silent 200 response.
|
||
- **Rate limiting**: In-memory sliding window per IP address. Configurable constants at the top of each API file:
|
||
- Contact: `RATE_LIMIT_MAX_REQUESTS = 5` per hour
|
||
- Newsletter: `RATE_LIMIT_MAX_REQUESTS = 3` per hour
|
||
|
||
### Input Validation
|
||
- All input validated server-side regardless of client-side checks
|
||
- String lengths enforced to prevent DoS via large payloads
|
||
- Subject field uses an allowlist (enum) to prevent injection
|
||
- HTML in email templates uses `escapeHtml()` to prevent XSS in email clients
|
||
|
||
### CORS
|
||
- Production: `Access-Control-Allow-Origin: https://workroot.in`
|
||
- Development: `Access-Control-Allow-Origin: *`
|
||
- Preflight `OPTIONS` handlers implemented on both endpoints
|
||
|
||
### Rate Limit Response Headers
|
||
Both endpoints return standard rate limit headers on every response:
|
||
```
|
||
X-RateLimit-Limit: 5
|
||
X-RateLimit-Remaining: 4
|
||
X-RateLimit-Reset: 1742000000
|
||
```
|
||
|
||
---
|
||
|
||
## Response Format
|
||
|
||
All responses follow a consistent structure:
|
||
|
||
**Success (200):**
|
||
```json
|
||
{ "success": true, "message": "Human-readable success message" }
|
||
```
|
||
|
||
**Validation error (422):**
|
||
```json
|
||
{ "success": false, "error": "Specific validation error" }
|
||
```
|
||
|
||
**Rate limited (429):**
|
||
```json
|
||
{ "success": false, "error": "Too many requests. Please try again later." }
|
||
```
|
||
|
||
**Server error (500):**
|
||
```json
|
||
{ "success": false, "error": "Failed to send message. Please try again or email us directly." }
|
||
```
|
||
|
||
---
|
||
|
||
## Client Integration
|
||
|
||
### Contact form (`src/pages/contact.astro`)
|
||
Posts JSON to `/api/contact`. Handles all response states:
|
||
- Success → toast notification + form reset
|
||
- Validation error → toast with specific message
|
||
- Rate limit (429) → error toast
|
||
- Network error → error toast
|
||
|
||
### Newsletter form (`src/components/Footer.astro`)
|
||
Posts JSON to `/api/newsletter`. Handles all response states:
|
||
- Success → inline green message + form reset
|
||
- Error → inline red message
|
||
- Network error → inline red message
|
||
- Message auto-hides after 5 seconds
|
||
|
||
---
|
||
|
||
## Logging & Monitoring
|
||
|
||
Both endpoints use `src/utils/logger.ts` for structured logging:
|
||
- Development: colored console output
|
||
- Production: JSON output for log aggregators (Logtail, Datadog, etc.)
|
||
|
||
Errors are captured via `src/utils/sentry.ts` (optional — only active when `SENTRY_DSN` env var is set).
|
||
|
||
Each API request is logged with:
|
||
- Method, path, status code
|
||
- IP address
|
||
- Duration in milliseconds
|
||
|
||
---
|
||
|
||
## Testing
|
||
|
||
Test the endpoints manually with curl:
|
||
|
||
```bash
|
||
# Contact form - success case
|
||
curl -X POST http://localhost:4321/api/contact \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"name":"Test User","email":"test@example.com","subject":"consulting","message":"Hello, I need help with a project."}'
|
||
|
||
# Newsletter - success case
|
||
curl -X POST http://localhost:4321/api/newsletter \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"email":"subscriber@example.com"}'
|
||
|
||
# Test honeypot (should return 200 silently)
|
||
curl -X POST http://localhost:4321/api/contact \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"name":"Bot","email":"bot@spam.com","subject":"other","message":"Buy cheap stuff now!","website":"http://spam.com"}'
|
||
|
||
# Test rate limiting (run 4+ times to trigger 429 on newsletter)
|
||
for i in 1 2 3 4; do
|
||
curl -s -o /dev/null -w "%{http_code}\n" \
|
||
-X POST http://localhost:4321/api/newsletter \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"email":"test@example.com"}'
|
||
done
|
||
```
|
||
|
||
Playwright tests are located at:
|
||
- `tests/contact-form.spec.ts` — contact form UI and API
|
||
- `tests/newsletter-subscription.spec.ts` — newsletter UI and API
|
||
- `tests/api-integration.spec.ts` — API endpoint integration tests
|
||
- `tests/e2e-form-interactions.spec.ts` — form interaction flows
|
||
|
||
Run all form tests:
|
||
```bash
|
||
npm run test:forms
|
||
```
|
||
|
||
---
|
||
|
||
## In-Memory Rate Limiter Notes
|
||
|
||
The current rate limiter is in-memory (per process). This means:
|
||
|
||
- ✅ Works correctly for single-instance deployments (VPS, single container)
|
||
- ⚠️ Resets on server restart
|
||
- ⚠️ Not shared across multiple instances (e.g., PM2 cluster mode)
|
||
|
||
For multi-instance deployments, upgrade to Redis-backed rate limiting (see Future Improvements).
|
||
|
||
---
|
||
|
||
## Alternative: FormSpree (No-code option)
|
||
|
||
If you prefer a managed third-party service without SMTP configuration:
|
||
|
||
1. Sign up at https://formspree.io
|
||
2. Create a form and get your form ID (e.g. `xpwzgkab`)
|
||
3. Replace the fetch URL in `contact.astro`:
|
||
```javascript
|
||
const response = await fetch('https://formspree.io/f/xpwzgkab', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
||
body: JSON.stringify(formData),
|
||
});
|
||
```
|
||
|
||
The existing custom API endpoint can remain for future migration back to self-hosted.
|
||
|
||
---
|
||
|
||
## Future Improvements
|
||
|
||
- [ ] Persist rate limit state in Redis for multi-instance PM2 cluster deployments
|
||
- [ ] Add CAPTCHA (hCaptcha or Cloudflare Turnstile) as additional spam layer
|
||
- [ ] Store contact submissions in a database for CRM integration
|
||
- [ ] Add confirmation/welcome email for newsletter signups (double opt-in flow)
|
||
- [ ] Webhook notifications to Slack/Discord on new contact submissions
|
||
- [ ] Admin dashboard to view contact form submissions
|