mirror of
https://github.com/warmbly/warmbly.git
synced 2026-09-06 00:01:24 +00:00
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.
45 lines
1.5 KiB
Go
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",
|
|
}
|