Skip to content

Reject malformed emails on trade inquiries (#117) - #121

Merged
spizeck merged 1 commit into
mainfrom
fix/issue-117-trade-email-validation
Sep 23, 2026
Merged

spizeck merged 1 commit into
mainfrom
fix/issue-117-trade-email-validation

Conversation

@spizeck

@spizeck spizeck commented Sep 23, 2026 •

Copy link
Copy Markdown
Owner

Summary

POST /api/trade-inquiry verified that email existed and fit the field-length limit, but never validated its format. A direct request could bypass the browser's type="email" check and submit something like not-an-email — which was persisted to tradeLeads and passed to Resend as replyTo, producing a guaranteed trade_inquiry.notification_failed and monitoring noise while the customer still saw success.

The fix adds a server-side email-format check that returns 400 (Please enter a valid email address.) before the honeypot reply, rate limit, persistence, or any Resend work.

Closes #117

Changes

  • lib/email.ts (new): canonical isValidEmail() — the identical EMAIL_REGEX previously duplicated in app/api/admin/users/route.ts and components/admin-access.tsx is now a single dependency-free module both server routes and client components can import.
  • lib/trade-leads-common.ts: new handleTradeInquiry() holding the whole post-parse pipeline — trim → required fields → field-length bounds → email format → honeypot → rate limit → submit — with injected isRateLimited/submit collaborators, matching the module's established DI-for-testability pattern.
  • app/api/trade-inquiry/route.ts: thin wrapper — JSON parse, then handleTradeInquiry, mapping the {status, body} result to the response. Rate-limiter implementation unchanged.
  • app/api/admin/users/route.ts, components/admin-access.tsx: consume isValidEmail(); behavior identical.
  • tests/lib/email.test.ts (new), tests/lib/trade-leads.test.ts: regression coverage — for each malformed address, asserts 400 and that submit (the single entry point to persist + notify) is never called, and that the rate limiter is never consulted. Valid cases cover customer@example.com, first.last@example.com, customer+trade@example.com, sales@wholesale.example.com, and trimmed whitespace. Missing/oversized email, honeypot, 429, and the generic-500 mapping stay green. Source-level wiring assertions guard that route.ts actually delegates to the tested pipeline (the route module itself can't be imported in node:test — it pulls in server-only modules).
  • docs/TECHNICAL.md: §11 updated for the new pipeline and the email-format check.

Verification

  • npx tsc --noEmit — clean
  • npm run lint — clean (one pre-existing jsx-ast-utils TSSatisfiesExpression notice, also present on main)
  • npm test — 350 tests pass (13 new: 3 in email.test.ts, 10 pipeline/wiring in trade-leads.test.ts)
  • npm run test:rules — 29 pass (Firestore/Storage emulators, Java 21)
  • npm run build — succeeds
  • npx playwright test — 117 smoke tests pass
  • npm run check:md-links — all links resolve
  • npm run check:react-versions — react/react-dom match (19.3.0)
  • npm audit --omit=dev — 0 vulnerabilities
  • Regression proof: with the isValidEmail check removed (pre-fix behavior), not-an-email flows through with a 200 and reaches submit; with the fix it returns 400 and submit is never invoked.

Risk / deployment notes

  • No secrets, credentials, or private data were committed.
  • No schema, rules, env var, or dependency changes. Ordering is preserved: honeypot requests still get fake ok: true; the rate limiter still only counts requests that pass validation.
  • Noted but deliberately out of scope: venueType remains effectively free-text server-side (only presence + ≤64 chars enforced) while the UI constrains it to a fixed <select> list — a candidate for a separate, narrowly scoped follow-up if desired.

Generated with Devin

Summary by Sourcery

Prevent malformed trade-inquiry email addresses from reaching persistence and notification services.

New Features:

  • Add shared email-format validation for trade inquiries and admin invitation flows.

Bug Fixes:

  • Reject malformed trade-inquiry email addresses with a 400 response before rate limiting, persistence, or notification attempts.

Enhancements:

  • Centralize trade-inquiry request validation and submission handling in a reusable, testable pipeline.

Documentation:

  • Update technical documentation to describe the shared email validation and trade-inquiry processing pipeline.

Tests:

  • Add coverage for shared email validation, malformed trade-inquiry rejection, processing order, and route delegation.

POST /api/trade-inquiry checked that email existed and fit the field-length
limit, but never validated its format — a direct request could bypass the
browser's type="email" check, persist a dead tradeLeads record, and hand a
guaranteed-failure replyTo to Resend (trade_inquiry.notification_failed
noise in monitoring).

The post-parse request pipeline now lives in handleTradeInquiry() in
lib/trade-leads-common.ts with injected rate-limit/submit collaborators,
matching the module's established DI-for-testability pattern. It validates
email format via a new shared isValidEmail() (lib/email.ts) — extracted from
the identical EMAIL_REGEX duplicated in admin users route and
admin-access.tsx — and returns 400 before the honeypot, rate limit, or any
persistence/notification work.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @spizeck, this account has used its review budget of 1,500,000 diff characters for the last 7 days.

You can request another review in 39 minutes by commenting @sourcery-ai review. Upgrade to get a review now.

@vercel

vercel Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
deepdivebrewing-web Ready Ready Preview Sep 23, 2026 11:20pm UTC

Request Review

@sourcery-ai

sourcery-ai Bot commented Sep 23, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR adds shared server/client email-format validation and refactors the trade-inquiry endpoint around an injectable validation pipeline, ensuring malformed emails return a 400 before honeypot handling, rate limiting, persistence, or notification while preserving existing behavior and adding focused regression coverage.

Sequence diagram for trade inquiry email validation pipeline

sequenceDiagram
    participant Client
    participant Route as trade-inquiry route
    participant Pipeline as handleTradeInquiry
    participant RateLimiter
    participant Submit as submitTradeInquiry
    participant Storage as Firestore
    participant Resend

    Client->>Route: POST /api/trade-inquiry
    Route->>Pipeline: handleTradeInquiry(body, context, deps)
    Pipeline->>Pipeline: isValidEmail(email)
    alt malformed email
        Pipeline-->>Route: 400 Please enter a valid email address.
        Route-->>Client: 400 response
    else valid email
        Pipeline->>RateLimiter: isRateLimited(clientIp)
        alt rate limit exceeded
            Pipeline-->>Route: 429 response
            Route-->>Client: 429 response
        else allowed
            Pipeline->>Submit: submitTradeInquiry(input, requestId)
            Submit->>Storage: Persist trade lead
            Submit->>Resend: Send notification with replyTo
            Pipeline-->>Route: 200 response
            Route-->>Client: 200 ok
        end
    end
Loading

File-Level Changes

Change Details Files
Centralize pragmatic email-format validation for reuse across server and client code.
  • Added dependency-free isValidEmail() using the existing email policy.
  • Replaced duplicated regex usage in admin invitation validation and the admin users API.
lib/email.ts
app/api/admin/users/route.ts
components/admin-access.tsx
Move trade-inquiry validation and submission orchestration into an injectable, unit-testable pipeline.
  • Added trimming, required-field, length, email-format, honeypot, rate-limit, and submission stages in the documented order.
  • Preserved existing 400, 429, 500, and fake-success responses while ensuring malformed emails stop before rate limiting or submission.
  • Injected rate limiting and submission collaborators for isolated testing.
lib/trade-leads-common.ts
app/api/trade-inquiry/route.ts
Add regression coverage for email validation, pipeline ordering, and route delegation.
  • Covered malformed, valid, trimmed, missing, and oversized email inputs.
  • Verified rejected requests do not consult the limiter or invoke persistence/notification.
  • Added checks for honeypot, rate-limit, submission failure, trimming, and route wiring.
tests/lib/email.test.ts
tests/lib/trade-leads.test.ts
Document the shared email helper and revised trade-inquiry request pipeline.
  • Classified the email helper as client-safe shared logic.
  • Updated the trade-inquiry architecture and validation-order documentation.
docs/TECHNICAL.md

Assessment against linked issues

Issue Objective Addressed Explanation
#117 Reject syntactically malformed email addresses in POST /api/trade-inquiry with a clear 400 response before persistence, notification, honeypot handling, or rate limiting. ✅
#117 Allow valid email addresses, including plus-tagged addresses, dotted local parts, subdomains, and addresses with surrounding whitespace that is trimmed before validation. ✅
#117 Add regression tests covering invalid and valid email formats while preserving existing required-field and length-validation behavior. ✅

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@spizeck
spizeck merged commit e4439e3 into main Sep 23, 2026
4 checks passed
@spizeck
spizeck deleted the fix/issue-117-trade-email-validation branch September 23, 2026 23:29

This branch was successfully deployed

1 active deployment
Preview — 69a7c495 Deployed Sep 23, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Trade inquiry API accepts malformed email addresses

1 participant