feat: add limit extension macros for define-page and define-api
This commit is contained in:
parent
1a39e0c6d2
commit
8ae905a2c1
|
|
@ -36,6 +36,7 @@
|
||||||
:r-data-model
|
:r-data-model
|
||||||
(:interface :auth)
|
(:interface :auth)
|
||||||
(:interface :database)
|
(:interface :database)
|
||||||
|
(:interface :rate)
|
||||||
(:interface :user))
|
(:interface :user))
|
||||||
:pathname "./"
|
:pathname "./"
|
||||||
:components ((:file "app-utils")
|
:components ((:file "app-utils")
|
||||||
|
|
@ -43,6 +44,7 @@
|
||||||
(:module :config
|
(:module :config
|
||||||
:components ((:file radiance-postgres)))
|
:components ((:file radiance-postgres)))
|
||||||
(:file "conditions")
|
(:file "conditions")
|
||||||
|
(:file "limiter")
|
||||||
(:file "database")
|
(:file "database")
|
||||||
(:file "template-utils")
|
(:file "template-utils")
|
||||||
(:file "parenscript-utils")
|
(:file "parenscript-utils")
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
;;;; limiter.lisp - Rate limiter definitions for the application
|
||||||
|
|
||||||
|
(in-package :asteroid)
|
||||||
|
|
||||||
|
(defun render-rate-limit-error-page()
|
||||||
|
(clip:process-to-string
|
||||||
|
(load-template "error")
|
||||||
|
:error-message "It seems that your acceleration has elevated your orbit out of your designated path."
|
||||||
|
:error-action "Please wait a moment for it to stabilize and try your request again."))
|
||||||
|
|
||||||
|
(defun api-limit-error-output ()
|
||||||
|
(api-output `(("status" . "error")
|
||||||
|
("message" . "It seems that your acceleration has elevated your orbit out of your designated path."))
|
||||||
|
:message "It seems that your acceleration has elevated your orbit out of your designated path."
|
||||||
|
:status 429))
|
||||||
|
|
||||||
|
(defun extract-limit-options (options)
|
||||||
|
"Extracts the rate-limit options and forwards the reamaining radiance route options"
|
||||||
|
(let ((limit (getf options :limit))
|
||||||
|
(timeout (getf options :timeout))
|
||||||
|
(group (getf options :limit-group))
|
||||||
|
(rest (loop for (k v) on options by #'cddr
|
||||||
|
unless (member k '(:limit :timeout :limit-group))
|
||||||
|
append (list k v))))
|
||||||
|
(values limit timeout group rest)))
|
||||||
|
|
||||||
|
|
||||||
|
(defmacro define-page-with-limit (name uri options &body body)
|
||||||
|
"Rate limit for a page route. Defaults to 30 requests per minute."
|
||||||
|
(multiple-value-bind (limit timeout group rest) (extract-limit-options options)
|
||||||
|
(let* ((limit-name (string-upcase (format nil "~a-route-limit" (or group name))))
|
||||||
|
(limit-sym (intern limit-name))
|
||||||
|
(limit (or limit 30))
|
||||||
|
(timeout (or timeout 60)))
|
||||||
|
`(eval-when (:compile-toplevel :load-toplevel :execute)
|
||||||
|
(rate:define-limit ,limit-sym (time-left :limit ,limit :timeout ,timeout)
|
||||||
|
;; (format t "Route limit '~a' hit. Wait ~a seconds and retry.~%" ,(string name) time-left)
|
||||||
|
(render-rate-limit-error-page))
|
||||||
|
(define-page ,name ,uri ,rest
|
||||||
|
(rate:with-limitation (,limit-sym)
|
||||||
|
,@body))))))
|
||||||
|
|
||||||
|
(defmacro define-api-with-limit (name args options &body body)
|
||||||
|
"Rate limit for api routes. Defaults to 60 requests per minute."
|
||||||
|
(multiple-value-bind (limit timeout group rest) (extract-limit-options options)
|
||||||
|
(let* ((limit-name (string-upcase (format nil "~a-api-limit" (or group name))))
|
||||||
|
(limit-sym (intern limit-name))
|
||||||
|
(limit (or limit 60))
|
||||||
|
(timeout (or timeout 60)))
|
||||||
|
`(eval-when (:compile-toplevel :load-toplevel :execute)
|
||||||
|
(rate:define-limit ,limit-sym (time-left :limit ,limit :timeout ,timeout)
|
||||||
|
;; (format t "API Rate limit '~a' hit. Wait ~a seconds and retry.~%" ,(string name) time-left)
|
||||||
|
(api-limit-error-output))
|
||||||
|
(define-api ,name ,args ,rest
|
||||||
|
(rate:with-limitation (,limit-sym)
|
||||||
|
,@body))))))
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Error - Asteroid Radio</title>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="icon" type="image/x-icon" href="/asteroid/static/favicon.ico">
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="/asteroid/static/favicon-32x32.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="/asteroid/static/favicon-16x16.png">
|
||||||
|
<link rel="stylesheet" type="text/css" href="/asteroid/static/asteroid.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1 style="display: flex; align-items: center; justify-content: center; gap: 15px;">
|
||||||
|
<img src="/asteroid/static/asteroid.png" alt="Asteroid" style="height: 50px; width: auto;">
|
||||||
|
<span>ASTEROID RADIO</span>
|
||||||
|
<img src="/asteroid/static/asteroid.png" alt="Asteroid" style="height: 50px; width: auto;">
|
||||||
|
</h1>
|
||||||
|
<nav class="nav">
|
||||||
|
<a href="/asteroid/">Home</a>
|
||||||
|
<a href="/asteroid/player">Player</a>
|
||||||
|
<a href="/asteroid/about">About</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
<main style="max-width: 800px; margin: 0 auto; padding: 20px;">
|
||||||
|
<section style="margin-bottom: 30px;">
|
||||||
|
<h2 style="color: #00ff00; border-bottom: 2px solid #00ff00; padding-bottom: 10px;">
|
||||||
|
<c:if test="error-title">
|
||||||
|
<c:then>
|
||||||
|
<c:splice lquery="(text error-title)"></c:splice>
|
||||||
|
</c:then>
|
||||||
|
<c:else>
|
||||||
|
⚠️ Something went wrong with your request!
|
||||||
|
</c:else>
|
||||||
|
</c:if>
|
||||||
|
</h2>
|
||||||
|
<p style="line-height: 1.6; font-size: 1.2rem;">
|
||||||
|
<c:if test="error-message">
|
||||||
|
<c:then>
|
||||||
|
<c:splice lquery="(text error-message)"></c:splice>
|
||||||
|
</c:then>
|
||||||
|
<c:else>
|
||||||
|
We seem to be unable to process your request right now.
|
||||||
|
</c:else>
|
||||||
|
</c:if>
|
||||||
|
</p>
|
||||||
|
<c:if test="error-action">
|
||||||
|
<c:then>
|
||||||
|
<p style="line-height: 1.6; font-size: 1.2rem;" lquery="(text error-action)"></p>
|
||||||
|
</c:then>
|
||||||
|
</c:if>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in New Issue