@@ -24,6 +24,7 @@ import ( |
| 24 | 24 | "github.com/tenseleyFlow/shithub/internal/infra/storage" |
| 25 | 25 | "github.com/tenseleyFlow/shithub/internal/ratelimit" |
| 26 | 26 | usersdb "github.com/tenseleyFlow/shithub/internal/users/sqlc" |
| 27 | + "github.com/tenseleyFlow/shithub/internal/web/handlers/api/apilimit" |
| 27 | 28 | "github.com/tenseleyFlow/shithub/internal/web/middleware" |
| 28 | 29 | ) |
| 29 | 30 | |
@@ -38,6 +39,12 @@ type Deps struct { |
| 38 | 39 | RunnerJWT *runnerjwt.Signer |
| 39 | 40 | SecretBox *secretbox.Box |
| 40 | 41 | RateLimiter *ratelimit.Limiter |
| 42 | + // BaseURL is the public scheme://host prefix used for absolute |
| 43 | + // pagination Link headers. Empty falls back to path-relative URLs. |
| 44 | + BaseURL string |
| 45 | + // APILimit configures the /api/v1/* rate-limit middleware. Zero |
| 46 | + // values inherit apilimit.Middleware's no-op fallback. |
| 47 | + APILimit apilimit.Config |
| 41 | 48 | } |
| 42 | 49 | |
| 43 | 50 | // Handlers is the registered API handler set. Construct with New. |
@@ -77,9 +84,20 @@ const runnerAPIMaxBodyBytes = 768 * 1024 |
| 77 | 84 | |
| 78 | 85 | // Mount registers /api/v1/* on r. Caller is responsible for putting r |
| 79 | 86 | // in a CSRF-exempt group. |
| 87 | +// |
| 88 | +// Outer middleware on every /api/v1/* request: apilimit stamps the |
| 89 | +// X-RateLimit-* headers and refuses over-budget callers with a JSON |
| 90 | +// 429. Inner groups attach body caps, PAT auth, and scope decorators |
| 91 | +// according to the surface they expose. |
| 80 | 92 | func (h *Handlers) Mount(r chi.Router) { |
| 93 | + apiLimitMW := apilimit.Middleware(h.d.RateLimiter, apilimit.Config{ |
| 94 | + AuthedPerHour: h.d.APILimit.AuthedPerHour, |
| 95 | + AnonPerHour: h.d.APILimit.AnonPerHour, |
| 96 | + Logger: h.d.Logger, |
| 97 | + }) |
| 81 | 98 | r.Group(func(r chi.Router) { |
| 82 | 99 | r.Use(middleware.MaxBodySize(runnerAPIMaxBodyBytes)) |
| 100 | + r.Use(apiLimitMW) |
| 83 | 101 | h.mountRunners(r) |
| 84 | 102 | }) |
| 85 | 103 | r.Group(func(r chi.Router) { |
@@ -88,6 +106,9 @@ func (h *Handlers) Mount(r chi.Router) { |
| 88 | 106 | Pool: h.d.Pool, |
| 89 | 107 | Debouncer: h.d.Debouncer, |
| 90 | 108 | })) |
| 109 | + r.Use(apiLimitMW) |
| 110 | + // /meta is capability discovery — no scope required, anon ok. |
| 111 | + h.mountMeta(r) |
| 91 | 112 | r.Group(func(r chi.Router) { |
| 92 | 113 | r.Use(middleware.RequireScope(pat.ScopeUserRead)) |
| 93 | 114 | r.Get("/api/v1/user", h.userMe) |