41 lines
2.4 KiB
Markdown
41 lines
2.4 KiB
Markdown
## Context
|
|
|
|
The alert path already has location-aware throttling + fingerprint blocking. This change adds cost protection and the 2014 paid-gating model using existing schema only (no Stripe). The 2014 code suspended SMS on `is_paid`/late-payment; our equivalent is `orders.status != 'paid'`.
|
|
|
|
## Goals / Non-Goals
|
|
|
|
**Goals:**
|
|
- Non-paid orders ⇒ no alert SMS (record scans only).
|
|
- Per-tag daily alert cap + per-IP hourly rate limit (configurable, env-overridable).
|
|
- Zero new schema, zero external services.
|
|
|
|
**Non-Goals:**
|
|
- Payment gateway (Stripe/Paddle) — later; it will simply set `orders.status`.
|
|
- Plan tiers/entitlements beyond the daily/hourly caps.
|
|
- Admin UI changes (TagResource already shows the order relation).
|
|
|
|
## Decisions
|
|
|
|
1. **Paid-gating in `shouldAlert`**: load the tag's order (via `tag.order_id` + a `GetOrderByID` query); if order exists and `status != 'paid'` → false. No order → proceed (transitional — most tags aren't sales-linked yet).
|
|
2. **Per-tag daily cap**: `CountAlertsByTagSince(tagID, now()-24h)`; `maxAlertsPerTagDay` default 5, env `ALERT_MAX_TAG_DAY`.
|
|
3. **Per-IP hourly limit**: need the scanner IP → `CountAlertsByIPSince(ip, now()-1h)`; but `scans` has no IP column! Options: (a) add `scans.ip TEXT` (idempotent ALTER) and record `r.RemoteAddr` host; (b) skip IP limit. Adding the column is cheap and matches the 2014 model (they tracked `ip_address`). **Decision: add `scans.ip` column** (idempotent) + record the client IP; `maxAlertsPerIPHour` default 10, env `ALERT_MAX_IP_HOUR`.
|
|
4. Ordering of checks in `shouldAlert`: sms_enabled → owner phone → fingerprint block → **paid-gating** → **daily cap** → **IP rate** → 10-min window rules → send.
|
|
5. Client IP: from `X-Forwarded-For` if present (behind Caddy), else `RemoteAddr` host.
|
|
|
|
## Risks / Trade-offs
|
|
|
|
- [Tags without orders keep alerting] → intentional transitional behaviour; documented.
|
|
- [IP capture behind proxy] → X-Forwarded-For parsing (Caddy sets it); falls back to RemoteAddr.
|
|
- [Cap constants too strict/loose] → env-overridable; no schema change to retune.
|
|
|
|
## Migration Plan
|
|
|
|
1. `db/schema.sql`: idempotent `ALTER TABLE scans ADD COLUMN IF NOT EXISTS ip TEXT;` → `make db-up`.
|
|
2. sqlc: `GetOrderByID`, `CountAlertsByTagSince`, `CountAlertsByIPSince` → `make generate`.
|
|
3. Handlers: scan.go gating + IP capture; `InsertScan` gains IP.
|
|
4. Verify with extended suite; commit.
|
|
|
|
## Open Questions
|
|
|
|
- None blocking.
|