@@ -24,6 +24,7 @@ import ( |
| 24 | "github.com/tenseleyFlow/shithub/internal/infra/storage" | 24 | "github.com/tenseleyFlow/shithub/internal/infra/storage" |
| 25 | "github.com/tenseleyFlow/shithub/internal/ratelimit" | 25 | "github.com/tenseleyFlow/shithub/internal/ratelimit" |
| 26 | usersdb "github.com/tenseleyFlow/shithub/internal/users/sqlc" | 26 | usersdb "github.com/tenseleyFlow/shithub/internal/users/sqlc" |
| | 27 | + "github.com/tenseleyFlow/shithub/internal/web/handlers/api/apilimit" |
| 27 | "github.com/tenseleyFlow/shithub/internal/web/middleware" | 28 | "github.com/tenseleyFlow/shithub/internal/web/middleware" |
| 28 | ) | 29 | ) |
| 29 | | 30 | |
@@ -38,6 +39,12 @@ type Deps struct { |
| 38 | RunnerJWT *runnerjwt.Signer | 39 | RunnerJWT *runnerjwt.Signer |
| 39 | SecretBox *secretbox.Box | 40 | SecretBox *secretbox.Box |
| 40 | RateLimiter *ratelimit.Limiter | 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 | // Handlers is the registered API handler set. Construct with New. | 50 | // Handlers is the registered API handler set. Construct with New. |
@@ -77,9 +84,20 @@ const runnerAPIMaxBodyBytes = 768 * 1024 |
| 77 | | 84 | |
| 78 | // Mount registers /api/v1/* on r. Caller is responsible for putting r | 85 | // Mount registers /api/v1/* on r. Caller is responsible for putting r |
| 79 | // in a CSRF-exempt group. | 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 | func (h *Handlers) Mount(r chi.Router) { | 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 | r.Group(func(r chi.Router) { | 98 | r.Group(func(r chi.Router) { |
| 82 | r.Use(middleware.MaxBodySize(runnerAPIMaxBodyBytes)) | 99 | r.Use(middleware.MaxBodySize(runnerAPIMaxBodyBytes)) |
| | 100 | + r.Use(apiLimitMW) |
| 83 | h.mountRunners(r) | 101 | h.mountRunners(r) |
| 84 | }) | 102 | }) |
| 85 | r.Group(func(r chi.Router) { | 103 | r.Group(func(r chi.Router) { |
@@ -88,6 +106,9 @@ func (h *Handlers) Mount(r chi.Router) { |
| 88 | Pool: h.d.Pool, | 106 | Pool: h.d.Pool, |
| 89 | Debouncer: h.d.Debouncer, | 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 | r.Group(func(r chi.Router) { | 112 | r.Group(func(r chi.Router) { |
| 92 | r.Use(middleware.RequireScope(pat.ScopeUserRead)) | 113 | r.Use(middleware.RequireScope(pat.ScopeUserRead)) |
| 93 | r.Get("/api/v1/user", h.userMe) | 114 | r.Get("/api/v1/user", h.userMe) |