Files
inter-hub/Application/Helper/ApiRateLimit.hs
Bernd Worsch ce42607fca fix(WP-0014/A2): close remaining pure-param and structural compilation errors
Convert all remaining `<- paramOrNothing / param / paramOrDefault /
currentUserOrNothing` monadic binds to `let` — these functions are pure
(ImplicitParams-based) in IHP v1.5, so `<-` is a type error in an IO
do-block.

Controllers fixed:
  AgentDelegations, AiGovernancePolicies, Annotations, ApiConsumers,
  CollectiveProposals, DecisionRecords, DeploymentRecords,
  HubCapabilityManifests, HubRoutingRules, InstitutionalKnowledge,
  OutcomeCorrelations, RequirementCandidates, TypeRegistries,
  WebhookSubscriptions, Widgets,
  Api/V2/{Annotations,InteractionEvents,Token}

WebhookSubscriptions: remove orphaned `Right () ->` case arm that was
left inside a bare `unless` block (structural parse error).

Also carries forward all in-progress fixes from the working tree:
  helpers (AgentBridge, ApiRateLimit, BottleneckDetector,
            CrossHubPropagation, FrictionScore),
  views (CanSelect instances, HSX lambda extraction, formFor wrappers),
  env/build (envrc GHCi perms, flake.nix Tailwind + GHC resource limits,
             static/app.css additional Tailwind output).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 01:14:08 +00:00

66 lines
2.3 KiB
Haskell

module Application.Helper.ApiRateLimit where
-- Rate limiting and request logging for /api/v2/ endpoints.
-- Called before action dispatch in all ApiV2* controllers.
import Generated.Types
import IHP.Prelude
import IHP.ModelSupport
import IHP.ControllerPrelude
import Web.Routes ()
import Data.Aeson (object, (.=))
import Database.PostgreSQL.Simple (Only(..))
import Web.Controller.Api.V2.Auth (respondWithStatus)
-- | Log a request to api_request_log and enforce rate limit / quota.
-- Returns () on success; calls respondWithStatus and exits on limit exceeded.
checkRateLimitAndLog ::
( ?context :: ControllerContext
, ?modelContext :: ModelContext
, ?respond :: Respond
, ?request :: Request
) =>
ApiConsumer ->
Text -> -- HTTP method
Text -> -- endpoint path
IO ()
checkRateLimitAndLog consumer endpoint method = do
-- Check rate limit: requests in last 60 seconds
rows1 <- sqlQuery
"SELECT COUNT(*) FROM api_request_log \
\WHERE api_consumer_id = ? AND requested_at >= NOW() - INTERVAL '60 seconds'"
(Only consumer.id)
let reqCount = case rows1 of
[Only (n :: Int)] -> n
_ -> 0
when (reqCount >= consumer.rateLimitPerMinute) do
respondWithStatus 429 $ object
[ "error" .= ("Rate limit exceeded" :: Text)
, "code" .= ("rate_limited" :: Text)
, "retry_after" .= (60 :: Int)
]
-- Check daily quota
rows2 <- sqlQuery
"SELECT COUNT(*) FROM api_request_log \
\WHERE api_consumer_id = ? AND requested_at >= ? - INTERVAL '1 day'"
(consumer.id, consumer.quotaResetsAt)
let quotaUsed = case rows2 of
[Only (n :: Int)] -> n
_ -> 0
when (quotaUsed >= consumer.quotaPerDay) do
respondWithStatus 429 $ object
[ "error" .= ("Daily quota exceeded" :: Text)
, "code" .= ("quota_exceeded" :: Text)
, "quota_resets_at" .= consumer.quotaResetsAt
]
-- Log the request (status_code will be 0 here; update after response)
sqlExec
"INSERT INTO api_request_log (id, api_consumer_id, endpoint, method, status_code, requested_at) \
\VALUES (uuid_generate_v4(), ?, ?, ?, 200, NOW())"
(consumer.id, endpoint, method)
pure ()