Files
warmbly/internal/errx/codes.go
T
Matt b633e326e5 feat(throttle): per-day creation throttles for campaigns/mailboxes/orgs
The HardCap* constants stop "you have 5000 campaigns on this org"; the
throttles in this commit stop "you created 1000 campaigns today on a
fresh unlimited account." Different shape, different abuse, different
mechanism — Redis-backed per-(scope, resource, UTC-day) counters that
reset by key design at midnight UTC, no scheduled job needed.

New service internal/app/dailythrottle:
  - CheckAndIncrement(scope, resource, ceiling) atomically bumps the
    counter and returns errx.TooManyRequests when the post-increment
    value exceeds the ceiling.
  - 25h TTL so the key always expires after the day rolls over even
    if the process restarts before midnight.
  - Fail-open when the cache is absent (jobs/tests) so creation paths
    that haven't been wired with a cache still work.

Caps (config.DailyThrottleNew*):
  - 20 new campaigns/org/day
  - 5  new mailboxes/org/day
  - 3  new workspaces/owner/day

Wired into three creation paths:
  - campaign.Create — scoped on the orgID when present
  - email.OAuthFinish + email.OnboardSMTPIMAP — scoped on the orgID;
    fires only at actual create, not OAuthStart, so retrying a failed
    OAuth flow doesn't burn the day's budget.
  - organization.Create — scoped on the owner uuid (the org doesn't
    exist yet)

Adds errx.TooManyRequests (HTTP 429) since no caller had one before.

emailService gains WireThrottle alongside the existing WireWebhooks
pattern so jobs / tests can build the service without a cache. Same
treatment in main.go.
2026-05-28 13:34:01 +02:00

45 lines
1.5 KiB
Go

package errx
import "net/http"
type Code int
const (
BadRequest Code = http.StatusBadRequest
Unauthorized Code = http.StatusUnauthorized
Forbidden Code = http.StatusForbidden
NotFound Code = http.StatusNotFound
Conflict Code = http.StatusConflict
Unprocessable Code = http.StatusUnprocessableEntity
TooManyRequests Code = http.StatusTooManyRequests
Internal Code = http.StatusInternalServerError
NotImplemented Code = http.StatusNotImplemented
ServiceUnavailable Code = http.StatusServiceUnavailable
)
var codeToHTTP = map[Code]int{
BadRequest: http.StatusBadRequest,
Unauthorized: http.StatusUnauthorized,
Forbidden: http.StatusForbidden,
NotFound: http.StatusNotFound,
Conflict: http.StatusConflict,
Unprocessable: http.StatusUnprocessableEntity,
TooManyRequests: http.StatusTooManyRequests,
Internal: http.StatusInternalServerError,
NotImplemented: http.StatusNotImplemented,
ServiceUnavailable: http.StatusServiceUnavailable,
}
var codeToString = map[Code]string{
BadRequest: "Bad Request",
Unauthorized: "Unauthorized",
Forbidden: "Forbidden",
NotFound: "Not Found",
Conflict: "Conflict",
Unprocessable: "Unprocessable",
TooManyRequests: "Too Many Requests",
Internal: "Internal Server Error",
NotImplemented: "Not Implemented",
ServiceUnavailable: "Service Unavailable",
}