Skip to content

fix: validate product URLs in offer edits (#137) and reorder email validation before rate-limit (#143)#178

Merged
ralyodio merged 1 commit into
profullstack:masterfrom
nguyenlnp:fix/url-validation-and-rate-limit-order-137-143
May 23, 2026
Merged

fix: validate product URLs in offer edits (#137) and reorder email validation before rate-limit (#143)#178
ralyodio merged 1 commit into
profullstack:masterfrom
nguyenlnp:fix/url-validation-and-rate-limit-order-137-143

Conversation

@nguyenlnp
Copy link
Copy Markdown
Contributor

Summary

Fixes two related validation issues on a single branch.

#137 — Affiliate offer edits accept invalid product URLs

Bug: The PATCH /api/affiliates/offers/[id] endpoint accepted malformed product URLs (missing scheme, ftp://, javascript:, data:, etc.) without validation.

Fix:

  • Import isValidUrl from @/lib/affiliates/validation (already exists)
  • Validate product_url before saving: reject non-http(s) schemes with 400
  • Allow empty string to clear the URL field (sets to null)
  • Trim whitespace before validation

#143 — Referral invite validation reports rate limits for invalid email batches

Bug: POST /api/referrals applied rate-limit checks using emails.length (raw input count) before validating email syntax. Invalid emails counted against throttle limits, producing misleading 429 errors instead of 400 validation errors.

Fix:

  • Move email syntax validation (regex check) before rate-limit checks
  • Only validEmails.length counts toward throttle limits (not raw emails.length)
  • Invalid email batches now correctly return 400 "No valid email addresses provided" instead of 429
  • Updated dedup logic to use newValidEmails (valid + not-already-invited)

Regression Tests

Files Changed

  • src/app/api/affiliates/offers/[id]/route.ts — import isValidUrl, add product_url validation
  • src/app/api/affiliates/offers/[id]/route.test.ts — add PATCH regression tests
  • src/app/api/referrals/route.ts — reorder email validation before rate-limit
  • src/app/api/referrals/route.test.ts — add rate-limit order regression tests

Closes #137
Closes #143

… email validation before rate-limit (profullstack#143)

profullstack#137: Add isValidUrl validation to PATCH /api/affiliates/offers/[id]
- Import isValidUrl from validation module
- Validate product_url before saving, rejecting non-http(s) schemes
- Rejects javascript:, ftp:, data:, and missing-scheme URLs with 400
- Allows empty string to clear the field

profullstack#143: Reorder email validation and rate-limit checks in POST /api/referrals
- Move email syntax validation BEFORE rate-limit checks
- Only valid emails count toward throttle limits
- Invalid email batches now return 400 instead of misleading 429
- Replace duplicate validation with dedup filter for already-invited emails

Both fixes include regression tests.
@ralyodio ralyodio closed this May 23, 2026
@ralyodio ralyodio reopened this May 23, 2026
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 23, 2026

Greptile Summary

This PR fixes two validation gaps: PATCH offers now rejects non-http(s) product_url values using the existing isValidUrl helper, and POST /api/referrals moves email-syntax validation before the rate-limit check so invalid batches get 400 instead of 429. The referrals refactor introduces a normalization regression — validEmails retains original casing/whitespace, causing the already-invited deduplication check to silently fail for mixed-case inputs.

  • offers/[id]/route.ts: Imports isValidUrl, validates product_url on PATCH, allows empty string to clear the field to null.
  • referrals/route.ts: Moves email-regex filter before rate-limit; updates counts and insert list to use validEmails/newValidEmails, but validEmails is not normalized, breaking the alreadyInvited.has() deduplication guard for non-lowercase inputs.
  • Tests: Both route test files add targeted regression cases covering the new validation paths.

Confidence Score: 3/5

The offers URL validation is safe to merge, but the referrals change introduces a normalization regression that breaks the already-invited deduplication guard for mixed-case email inputs.

The referrals refactor moved email validation earlier (the intended fix), but lost the normalization step that the old code applied up-front. validEmails keeps original casing and whitespace, while alreadyInvited holds lowercased DB values. A user already invited as 'user@example.com' can be re-invited by sending 'User@Example.com', bypassing the guard and hitting a DB constraint error at insert time instead of returning a clean 400.

src/app/api/referrals/route.ts — the validEmails filter and the duplicate-check query need to use the same normalized form before the alreadyInvited set lookup.

Important Files Changed

Filename Overview
src/app/api/referrals/route.ts Email validation correctly moved before rate-limit checks; however, validEmails stores original (non-normalized) strings while alreadyInvited holds lowercased DB values, breaking the already-invited guard for mixed-case inputs.
src/app/api/affiliates/offers/[id]/route.ts Imports isValidUrl from the existing validation library and gates product_url on http/https scheme; empty string correctly maps to null. Change is minimal and correct.
src/app/api/affiliates/offers/[id]/route.test.ts Adds PATCH regression tests for javascript:, ftp:, data:, and scheme-less URLs. Mock for isValidUrl correctly mirrors the real implementation.
src/app/api/referrals/route.test.ts New tests verify all-invalid emails return 400 and that only valid emails count toward the rate limit. Tests don't exercise the normalization edge case (mixed-case already-invited emails).

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[POST /api/referrals] --> B{auth?}
    B -- no --> Z1[401 Unauthorized]
    B -- yes --> C{emails array valid?}
    C -- no --> Z2[400 Bad Request]
    C -- yes --> D["Filter valid emails (regex)\nvalidEmails = emails.filter(...)"]
    D --> E{validEmails.length == 0?}
    E -- yes --> Z3[400 No valid emails]
    E -- no --> F[Query hourly count from DB]
    F --> G{hourlyCount + validEmails.length > 10?}
    G -- yes --> Z4[429 Too many invites]
    G -- no --> H[Query daily count from DB]
    H --> I{dailyCount + validEmails.length > 50?}
    I -- yes --> Z5[429 Daily limit]
    I -- no --> J["Query existing invites\nnormalizedEmails = emails.map(normalize)"]
    J --> K["newValidEmails = validEmails.filter(not alreadyInvited)\n⚠️ normalization mismatch here"]
    K --> L{newValidEmails.length == 0?}
    L -- yes --> Z6[400 Already invited]
    L -- no --> M[Insert referral rows]
    M --> N[Send invite emails]
    N --> O[200 Success]
Loading

Comments Outside Diff (1)

  1. src/app/api/referrals/route.ts, line 116-122 (link)

    P2 The normalizedEmails variable is still derived from the raw emails array (including syntactically invalid ones). Since validEmails is now computed first and already contains every valid, normalized address, querying the DB with validEmails instead avoids an unnecessarily wide IN clause and keeps the intent clear.

Reviews (1): Last reviewed commit: "fix: validate product URLs in offer edit..." | Re-trigger Greptile

Comment on lines +74 to +75
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const validEmails = emails.filter((e: string) => typeof e === "string" && emailRegex.test(e.trim().toLowerCase()));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Email normalization mismatch in duplicate check — the filter on line 141 compares original-casing values from validEmails against the lowercased values in alreadyInvited (which come from the DB). For example, if "User@Test.Com" was already invited as "user@test.com", alreadyInvited.has("User@Test.Com") returns false, bypassing the guard and attempting a duplicate insert that will hit a DB constraint error. The old code normalized all emails at the top with .map(e => e.trim().toLowerCase()) before any filtering; the new code lost that normalization for validEmails.

Suggested change
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const validEmails = emails.filter((e: string) => typeof e === "string" && emailRegex.test(e.trim().toLowerCase()));
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const validEmails = emails
.filter((e: string) => typeof e === "string" && emailRegex.test(e.trim().toLowerCase()))
.map((e: string) => e.trim().toLowerCase());

@ralyodio ralyodio merged commit 71fc97c into profullstack:master May 23, 2026
4 checks passed
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.

bug: referral invite validation reports rate limits for invalid email batches bug: affiliate offer edits accept invalid product URLs

2 participants