From 8ae905a2c14739436b272393dfb2bb846407d1bd Mon Sep 17 00:00:00 2001 From: Luis Pereira Date: Fri, 26 Dec 2025 11:18:10 +0000 Subject: [PATCH] feat: add limit extension macros for define-page and define-api --- asteroid.asd | 2 ++ limiter.lisp | 56 ++++++++++++++++++++++++++++++++++++++++++++ template/error.ctml | 57 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 115 insertions(+) create mode 100644 limiter.lisp create mode 100644 template/error.ctml diff --git a/asteroid.asd b/asteroid.asd index 7cae886..117038b 100644 --- a/asteroid.asd +++ b/asteroid.asd @@ -36,6 +36,7 @@ :r-data-model (:interface :auth) (:interface :database) + (:interface :rate) (:interface :user)) :pathname "./" :components ((:file "app-utils") @@ -43,6 +44,7 @@ (:module :config :components ((:file radiance-postgres))) (:file "conditions") + (:file "limiter") (:file "database") (:file "template-utils") (:file "parenscript-utils") diff --git a/limiter.lisp b/limiter.lisp new file mode 100644 index 0000000..84f79e1 --- /dev/null +++ b/limiter.lisp @@ -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)))))) diff --git a/template/error.ctml b/template/error.ctml new file mode 100644 index 0000000..3907fc4 --- /dev/null +++ b/template/error.ctml @@ -0,0 +1,57 @@ + + + + Error - Asteroid Radio + + + + + + + + +
+
+

+ Asteroid + ASTEROID RADIO + Asteroid +

+ +
+
+
+

+ + + + + + ⚠️ Something went wrong with your request! + + +

+

+ + + + + + We seem to be unable to process your request right now. + + +

+ + +

+
+
+
+
+
+ +